Angular脚手架开发的实现步骤


Posted in Javascript onApril 09, 2019

简介

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

前提

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

antd脚手架

克隆项目

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

开始

打开项目

Angular脚手架开发的实现步骤

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

Angular脚手架开发的实现步骤

一共定了了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本地化啥的

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

Angular脚手架开发的实现步骤

Angular脚手架开发的实现步骤

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

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:https://www.kevinschuchard.com/blog/2018-07-17-jest-schematic/
Schematic:https://brianflove.com/2018/12/11/angular-schematics-tutorial/
Ng add:https://brianflove.com/2018/12/15/ng-add-schematic/

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
js验证模型自我实现的具体方法
Jun 21 Javascript
javascript 树形导航菜单实例代码
Aug 13 Javascript
jquery自定义滚动条插件示例分享
Feb 21 Javascript
聊一聊JS中this的指向问题
Jun 17 Javascript
jQuery查找节点并获取节点属性的方法
Sep 09 Javascript
javascript 删除数组元素和清空数组的简单方法
Feb 24 Javascript
解决浏览器会自动填充密码的问题
Apr 28 Javascript
十个免费的web前端开发工具详细整理
Sep 18 Javascript
解决element UI 自定义传参的问题
Aug 22 Javascript
vue elementUI table表格数据 滚动懒加载的实现方法
Apr 04 Javascript
ElementUI多个子组件表单的校验管理实现
Nov 07 Javascript
webpack优化之代码分割与公共代码提取详解
Nov 22 Javascript
详解vue 自定义marquee无缝滚动组件
Apr 09 #Javascript
javascript实现手动点赞效果
Apr 09 #Javascript
实例分析Array.from(arr)与[...arr]到底有何不同
Apr 09 #Javascript
浅谈Vue.js组件(二)
Apr 09 #Javascript
4 种滚动吸顶实现方式的比较
Apr 09 #Javascript
vue响应式系统之observe、watcher、dep的源码解析
Apr 09 #Javascript
浅谈发布订阅模式与观察者模式
Apr 09 #Javascript
You might like
基于PHP与XML的PDF文档生成技术
2006/10/09 PHP
PHP fgetcsv 定义和用法(附windows与linux下兼容问题)
2012/05/29 PHP
PHP+jQuery 注册模块的改进(三):更新到Smarty3.1
2014/10/14 PHP
PHP简单实现无限级分类的方法
2016/05/13 PHP
总结PHP代码规范、流程规范、git规范
2018/06/18 PHP
PHP使用JpGraph绘制折线图操作示例【附源码下载】
2019/10/18 PHP
jQuery find和children方法使用
2011/01/31 Javascript
16个最流行的JavaScript框架[推荐]
2011/05/29 Javascript
javascript 闭包
2011/09/15 Javascript
jQuery(非HTML5)可编辑表格实现代码
2012/12/11 Javascript
jquery表格内容筛选实现思路及代码
2013/04/16 Javascript
node.js中的http.response.setHeader方法使用说明
2014/12/14 Javascript
jQuery Dialog 取消右上角删除按钮事件
2016/09/07 Javascript
AngularJS 与百度地图的结合实例
2016/10/20 Javascript
Jquery删除css属性的简单方法
2016/12/04 Javascript
浅谈JavaScript中的apply/call/bind和this的使用
2017/02/26 Javascript
jqueryUI tab标签页代码分享
2017/10/09 jQuery
解决vue+webpack打包路径的问题
2018/03/06 Javascript
基于angular6.0实现的一个组件懒加载功能示例
2018/04/12 Javascript
今天,小程序正式支持 SVG
2019/04/20 Javascript
async/await让异步操作同步执行的方法详解
2019/11/01 Javascript
flexible.js实现移动端rem适配方案
2020/04/07 Javascript
vue实现简单图片上传
2020/06/30 Javascript
[02:47]3.19DOTA2发布会 国服成长历程回顾
2014/03/25 DOTA
使用FastCGI部署Python的Django应用的教程
2015/07/22 Python
Python logging管理不同级别log打印和存储实例
2018/01/19 Python
python3实现域名查询和whois查询功能
2018/06/21 Python
医院护士的求职信范文
2013/12/26 职场文书
物业公司采购员岗位职责
2013/12/31 职场文书
综合实践活动方案
2014/02/14 职场文书
环保宣传标语
2014/06/12 职场文书
关于调整工作时间的通知
2015/04/24 职场文书
院系推荐意见
2015/06/05 职场文书
2015重阳节座谈会主持词
2015/07/30 职场文书
致男子1500米运动员的广播稿
2019/11/08 职场文书
pytorch中的model.eval()和BN层的使用
2021/05/22 Python