当前位置: 移动技术网 > IT编程>脚本编程>AngularJs > Angular脚手架开发的实现步骤

Angular脚手架开发的实现步骤

2019年07月18日  | 移动技术网IT编程  | 我要评论

简介

写一份自定义的angular脚手架吧
写之前我们先解析一下antd的脚手架

前提

先把 angular schematic这篇文章读一遍,确保了解了collection等基础

antd脚手架

克隆项目

git clone https://github.com/ng-zorro/ng-zorro-antd.git

开始

打开项目

在schematics下的collection.json为入口,查看内容

一共定了了4个schematic,每个schema分别指向了各文件夹的子schema.json,factory指向了函数入口,index.ts

ng-add/schema.json

{
 // 指定schema.json的验证模式
 "$schema": "http://json-schema.org/schema",
 "id": "nz-ng-add",
 "title": "ant design of angular(ng-zorro) ng-add schematic",
 "type": "object",
 // 包含的属性
 "properties": {
  "project": {
   "type": "string",
   "description": "name of the project.",
   "$default": {
    "$source": "projectname"
   }
  },
  // 是否跳过package.json的安装属性
  "skippackagejson": {
  // 类型为布尔
   "type": "boolean",
   // 默认值为false
   "default": false,
   // 这是个描述,可以看到,如果在ng add ng-zorro-antd时不希望自动安装可以加入--skippackagejson配置项
   "description": "do not add ng-zorro-antd dependencies to package.json (e.g., --skippackagejson)"
  },
  // 开始页面
  "bootpage": {
  // 布尔
   "type": "boolean",
   // 默认为true
   "default": true,
   // 不指定--bootpage=false的话,你的app.html将会被覆盖成antd的图标页
   "description": "set up boot page."
  },
  // 图标配置
  "dynamicicon": {
   "type": "boolean",
   "default": false,
   "description": "whether icon assets should be add.",
   "x-prompt": "add icon assets [ detail: https://ng.ant.design/components/icon/en ]"
  },
  // 主题配置
  "theme": {
   "type": "boolean",
   "default": false,
   "description": "whether custom theme file should be set up.",
   "x-prompt": "set up custom theme file [ detail: https://ng.ant.design/docs/customize-theme/en ]"
  },
  // i18n配置,当你ng add ng-antd-zorro 的时候有没有让你选择这个选项呢?
  "i18n": {
   "type": "string",
   "default": "en_us",
   "enum": [
    "ar_eg",
    "bg_bg",
    "ca_es",
    "cs_cz",
    "da_dk",
    "de_de",
    "el_gr",
    "en_gb",
    "en_us",
    "es_es",
    "et_ee",
    "fa_ir",
    "fi_fi",
    "fr_be",
    "fr_fr",
    "is_is",
    "it_it",
    "ja_jp",
    "ko_kr",
    "nb_no",
    "nl_be",
    "nl_nl",
    "pl_pl",
    "pt_br",
    "pt_pt",
    "sk_sk",
    "sr_rs",
    "sv_se",
    "th_th",
    "tr_tr",
    "ru_ru",
    "uk_ua",
    "vi_vn",
    "zh_cn",
    "zh_tw"
   ],
   "description": "add locale code to module (e.g., --locale=en_us)"
  },
  "locale": {
   "type": "string",
   "description": "add locale code to module (e.g., --locale=en_us)",
   "default": "en_us",
   "x-prompt": {
    "message": "choose your locale code:",
    "type": "list",
    "items": [
     "en_us",
     "zh_cn",
     "ar_eg",
     "bg_bg",
     "ca_es",
     "cs_cz",
     "de_de",
     "el_gr",
     "en_gb",
     "es_es",
     "et_ee",
     "fa_ir",
     "fi_fi",
     "fr_be",
     "fr_fr",
     "is_is",
     "it_it",
     "ja_jp",
     "ko_kr",
     "nb_no",
     "nl_be",
     "nl_nl",
     "pl_pl",
     "pt_br",
     "pt_pt",
     "sk_sk",
     "sr_rs",
     "sv_se",
     "th_th",
     "tr_tr",
     "ru_ru",
     "uk_ua",
     "vi_vn",
     "zh_tw"
    ]
   }
  },
  "gestures": {
   "type": "boolean",
   "default": false,
   "description": "whether gesture support should be set up."
  },
  "animations": {
   "type": "boolean",
   "default": true,
   "description": "whether angular browser animations should be set up."
  }
 },
 "required": []
}

schema.ts

当你进入index.ts时首先看到的是一个带options:schema的函数,options指向的类型是schema interface,而这个interface 恰好是schema.json中的properties,也就是cli的传入参数类.

我们可以通过自定义传入参数类来完成我们需要的操作.

export type locale =
 | 'ar_eg'
 | 'bg_bg'
 | 'ca_es'
 | 'cs_cz'
 | 'da_dk'
 | 'de_de'
 | 'el_gr'
 | 'en_gb'
 | 'en_us'
 | 'es_es'
 | 'et_ee'
 | 'fa_ir'
 | 'fi_fi'
 | 'fr_be'
 | 'fr_fr'
 | 'is_is'
 | 'it_it'
 | 'ja_jp'
 | 'ko_kr'
 | 'nb_no'
 | 'nl_be'
 | 'nl_nl'
 | 'pl_pl'
 | 'pt_br'
 | 'pt_pt'
 | 'sk_sk'
 | 'sr_rs'
 | 'sv_se'
 | 'th_th'
 | 'tr_tr'
 | 'ru_ru'
 | 'uk_ua'
 | 'vi_vn'
 | 'zh_cn'
 | 'zh_tw';

export interface schema {
 bootpage?: boolean;
 /** name of the project to target. */
 project?: string;
 /** whether to skip package.json install. */
 skippackagejson?: boolean;
 dynamicicon?: boolean;
 theme?: boolean;
 gestures?: boolean;
 animations?: boolean;
 locale?: locale;
 i18n?: locale;
}

ng-add/index.ts

import { rule, schematiccontext, tree } from '@angular-devkit/schematics';
import { nodepackageinstalltask, runschematictask } from '@angular-devkit/schematics/tasks';
import { addpackagetopackagejson } from '../utils/package-config';
import { hammerjsversion, zorroversion } from '../utils/version-names';
import { schema } from './schema';
// factory指向的index.ts必须实现这个函数,一行一行看代码
// 我们的函数是一个更高阶的函数,这意味着它接受或返回一个函数引用。
// 在这种情况下,我们的函数返回一个接受tree和schematiccontext对象的函数。
// options:schema上面提到了
export default function(options: schema): rule {
// tree:虚拟文件系统:用于更改的暂存区域,包含原始文件系统以及要应用于其的更改列表。
// rule:a rule是一个将动作应用于tree给定的函数schematiccontext。
 return (host: tree, context: schematiccontext) => {
  // 如果需要安装包,也就是--skippackagejson=false
  if (!options.skippackagejson) {
   // 调用addpackagetopackagejson,传入,tree文件树,包名,包版本
   addpackagetopackagejson(host, 'ng-zorro-antd', zorroversion);
   // hmr模式包
   if (options.gestures) {
    addpackagetopackagejson(host, 'hammerjs', hammerjsversion);
   }
  }

  
  const installtaskid = context.addtask(new nodepackageinstalltask());

  context.addtask(new runschematictask('ng-add-setup-project', options), [installtaskid]);

  if (options.bootpage) {
   context.addtask(new runschematictask('boot-page', options));
  }
 };
}

addpackagetopackagejson

// 看function名字就知道这是下载依赖的函数
// @host:tree 文件树
// @pkg:string 包名
// @vserion:string 包版本
// @return tree 返回了一个修改完成后的文件树
export function addpackagetopackagejson(host: tree, pkg: string, version: string): tree {
  // 如果文件树里包含package.json文件
 if (host.exists('package.json')) {
  // 读取package.json的内容用utf-8编码
  const sourcetext = host.read('package.json').tostring('utf-8');
  // 然后把package.json转化为对象,转为对象,转为对象
  const json = json.parse(sourcetext);
  // 如果package.json对象里没有dependencies属性
  if (!json.dependencies) {
    // 给package对象加入dependencies属性
   json.dependencies = {};
  }
  // 如果package对象中没有 pkg(包名),也就是说:如果当前项目没有安装antd
  if (!json.dependencies[pkg]) {
    // 那么package的dependencies属性中加入 antd:version
   json.dependencies[pkg] = version;
   // 排个序
   json.dependencies = sortobjectbykeys(json.dependencies);
  }
  // 重写tree下的package.json内容为(刚才不是有package.json对象吗,现在在转回去)
  host.overwrite('package.json', json.stringify(json, null, 2));
 }
  // 把操作好的tree返回给上一级函数
 return host;
}

现在在回过头去看 ng-add/index.ts

// 给context对象增加一个安装包的任务,然后拿到了任务id
const installtaskid = context.addtask(new nodepackageinstalltask());
// context增加另一个任务,然后传入了一个runschematictask对象,和一个id集合
  context.addtask(new runschematictask('ng-add-setup-project', options), [installtaskid]);

runschematictask('ng-add-setup-project')

任务ng-add-setup-project定义在了schematic最外层的collection.json里,记住如下4个schematic,后文不再提及

{
 "$schema": "./node_modules/@angular-devkit/schematics/collection-schema.json",
 "schematics": {
  "ng-add": {
   "description": "add ng-zorro",
   "factory": "./ng-add/index",
   "schema": "./ng-add/schema.json"
  },
  // 在这里
  "ng-add-setup-project": {
   "description": "sets up the specified project after the ng-add dependencies have been installed.",
   "private": true,
   // 这个任务的函数指向
   "factory": "./ng-add/setup-project/index",
   // 任务配置项
   "schema": "./ng-add/schema.json"
  },
  "boot-page": {
   "description": "set up boot page",
   "private": true,
   "factory": "./ng-generate/boot-page/index",
   "schema": "./ng-generate/boot-page/schema.json"
  },
  "add-icon-assets": {
   "description": "add icon assets into cli config",
   "factory": "./ng-add/setup-project/add-icon-assets#addicontoassets",
   "schema": "./ng-generate/boot-page/schema.json",
   "aliases": ["fix-icon"]
  }
 }
}

ng-add/setup-project

// 刚才的index一样,实现了一个函数
export default function (options: schema): rule {
 // 这里其实就是调用各种函数的一个集合.options是上面的index.ts中传过来的,配置项在上文有提及
 return chain([
  addrequiredmodules(options),
  addanimationsmodule(options),
  registerlocale(options),
  addthemetoappstyles(options),
  options.dynamicicon ? addicontoassets(options) : noop(),
  options.gestures ? hammerjsimport(options) : noop()
 ]);
}

addrequiredmodules

// 模块字典
const modulesmap = {
 ngzorroantdmodule: 'ng-zorro-antd',
 formsmodule   : '@angular/forms',
 httpclientmodule : '@angular/common/http'
};
// 加入必须依赖模块
export function addrequiredmodules(options: schema): rule {
 return (host: tree) => {
  // 获取tree下的工作目录
  const workspace = getworkspace(host);
  // 获取项目
  const project = getprojectfromworkspace(workspace, options.project);
  // 获取app.module的路径
  const appmodulepath = getappmodulepath(host, getprojectmainfile(project));
  // 循环字典
  for (const module in modulesmap) {
  // 调用下面的函数,意思就是:给appmodule引一些模块,好吧,传入了tree,字典key(模块名称),字典value(模块所在包),project对象,appmodule的路径,schema配置项
   addmoduleimporttoapptmodule(host, module, modulesmap[ module ],
    project, appmodulepath, options);
  }
  // 将构建好的tree返回给上层函数
  return host;
 };
}

function addmoduleimporttoapptmodule(host: tree, modulename: string, src: string,
                   project: workspaceproject, appmodulepath: string,
                   options: schema): void {
  // 如果app.module引入了ngzorroantdmodule等字典中的模块
 if (hasngmoduleimport(host, appmodulepath, modulename)) {
  // 来个提示
  console.log(chalk.yellow(`could not set up "${chalk.blue(modulename)}" ` +
   `because "${chalk.blue(modulename)}" is already imported. please manually ` +
   `check "${chalk.blue(appmodulepath)}" file.`));
  return;
 }
 //如果没有引入过就直接引入
 addmoduleimporttorootmodule(host, modulename, src, project);
}

addanimationsmodule 内容差不多,略过

registerlocale

不怕多,一点一点看,这里主要做的工作就是i18n本地化啥的

先上一张图片,记得脑子里哦

接下来的函数都是为了做上面这个工作

export function registerlocale(options: schema): rule {
 return (host: tree) => {
  // 获取路径
  const workspace = getworkspace(host);
  const project = getprojectfromworkspace(workspace, options.project);
  const appmodulepath = getappmodulepath(host, getprojectmainfile(project));
  const modulesource = getsourcefile(host, appmodulepath);
  // 获取add 时选择的zh_cn,en_us啥的就是一个字符串
  const locale = getcompatiblelocal(options);
  // 拿到 zh en这种
  const localeprefix = locale.split('_')[ 0 ];
  // recorder可以理解成?快照,一个目录下多个文件组成的文件快照,re coder
  // 为什么要beginupdate,实际上我的理解是拿appmodulepath文件建立了快照
  // 直到后文 host.commitupdate(recorder);才会把快照作出的修改提交到tree上面
  // 也可以理解成你的项目有git控制,在你commit之前你操作的是快照,理解理解
  const recorder = host.beginupdate(appmodulepath);
  // 对快照的操作列表
  // insertimport = import {xxx} from 'xxx'这种
  // 结合代码看一下app.module.ts上面的import内容(上面图片)
  const changes = [
   insertimport(modulesource, appmodulepath, 'nz_i18n',
    'ng-zorro-antd'),
   insertimport(modulesource, appmodulepath, locale,
    'ng-zorro-antd'),
   insertimport(modulesource, appmodulepath, 'registerlocaledata',
    '@angular/common'),
   insertimport(modulesource, appmodulepath, localeprefix,
    `@angular/common/locales/${localeprefix}`, true),
   registerlocaledata(modulesource, appmodulepath, localeprefix),
   // 这个函数特殊,看下面
   ...inserti18ntokenprovide(modulesource, appmodulepath, locale)
  ];

  // 循环变更列表如果是insertchange(import)那么引入
  changes.foreach((change) => {
   if (change instanceof insertchange) {
    recorder.insertleft(change.pos, change.toadd);
   }
  });
  // 提交变更到tree
  host.commitupdate(recorder);
  // 返回tree给上一级函数
  return host;
 };
}

//上面说了,就是那个zh_cn/en_us
function getcompatiblelocal(options: schema): string {
 const defaultlocal = 'en_us';
 if (options.locale === options.i18n) {
  return options.locale;
 } else if (options.locale === defaultlocal) {

  console.log();
  console.log(`${chalk.bgyellow('warn')} ${chalk.cyan('--i18n')} option will be deprecated, ` +
   `use ${chalk.cyan('--locale')} instead`);

  return options.i18n;
 } else {
  return options.locale || defaultlocal;
 }
}

// 这个函数主要是为了生成调用angular本地化的代码registerlocaledata(zh);
function registerlocaledata(modulesource: ts.sourcefile, modulepath: string, locale: string): change {
 ...

 if (registerlocaledatafun.length === 0) {
  // 最核心的要在app.module中加入registerlocaledata(zh);才能把本地化做到angular上面
  return insertafterlastoccurrence(allimports, `\n\nregisterlocaledata(${locale});`,
   modulepath, 0) as insertchange;
 } 
...
}


 * 这个change在change列表略特殊
 * @param modulesource module文件
 * @param modulepath module路径
 * @param locale zh
 */
function inserti18ntokenprovide(modulesource: ts.sourcefile, modulepath: string, locale: string): change[] {
 const metadatafield = 'providers';
 // 获取app.module中ngmodule注释的内容
 //{
 //  declarations: [
 //   appcomponent
 //  ],
 //  imports: [
 //   browsermodule,
 //   approutingmodule,
 //   ngzorroantdmodule,
 //   formsmodule,
 //   httpclientmodule,
 //   browseranimationsmodule
 //  ],
 //  providers: [{ provide: nz_i18n, usevalue: zh_cn }],
 //  bootstrap: [appcomponent]
 // }
 const nodes = getdecoratormetadata(modulesource, 'ngmodule', '@angular/core');
 // 生成一个provide到app.module中的ngmodule注释中,生成到providers数组中 **的操作**(只是生成一个动作)还没应用到文件上
 const addprovide = addsymboltongmodulemetadata(modulesource, modulepath, 'providers',
  `{ provide: nz_i18n, usevalue: ${locale} }`, null);
 let node: any = nodes[ 0 ]; // tslint:disable-line:no-any
// 然后下面开始做了一堆校验工作
 if (!node) {
  return [];
 }

 const matchingproperties: ts.objectliteralelement[] =
     (node as ts.objectliteralexpression).properties
     .filter(prop => prop.kind === ts.syntaxkind.propertyassignment)
     .filter((prop: ts.propertyassignment) => {
      const name = prop.name;
      switch (name.kind) {
       case ts.syntaxkind.identifier:
        return (name as ts.identifier).gettext(modulesource) === metadatafield;
       case ts.syntaxkind.stringliteral:
        return (name as ts.stringliteral).text === metadatafield;
      }

      return false;
     });

 if (!matchingproperties) {
  return [];
 }

 if (matchingproperties.length) {
  const assignment = matchingproperties[ 0 ] as ts.propertyassignment;
  if (assignment.initializer.kind !== ts.syntaxkind.arrayliteralexpression) {
   return [];
  }
  const arrliteral = assignment.initializer as ts.arrayliteralexpression;
  if (arrliteral.elements.length === 0) {
   return addprovide;
  } else {
   node = arrliteral.elements.filter(e => e.gettext && e.gettext().includes('nz_i18n'));
   if (node.length === 0) {
    return addprovide;
   } else {
    console.log();
    console.log(chalk.yellow(`could not provide the locale token to your app.module file (${chalk.blue(modulepath)}).` +
     `because there is already a locale token in provides.`));
    console.log(chalk.yellow(`please manually add the following code to your provides:`));
    console.log(chalk.cyan(`{ provide: nz_i18n, usevalue: ${locale} }`));
    return [];
   }
  }
 } else {
  // 如果都没什么大问题,则把增加provide的动作返回到changes列表,等待commit然后作出更改动作
  return addprovide;
 }
}

参考文章

ast:
schematic:
ng add:

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持移动技术网。

如对本文有疑问, 点击进行留言回复!!

相关文章:

验证码:
移动技术网