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 相关文章推荐
推荐一些非常不错的javascript学习资源站点
Aug 29 Javascript
AJAX的跨域与JSONP(为文章自动添加短址的功能)
Jan 17 Javascript
如何使用json在前后台进行数据传输实例介绍
Apr 11 Javascript
JS 屏蔽按键效果与改变按键效果的示例代码
Dec 24 Javascript
js中replace的用法总结
Dec 27 Javascript
js 设置缓存及获取设置的缓存
May 08 Javascript
js 求时间差的实现代码
Apr 26 Javascript
静态页面html中跳转传值的JS处理技巧
Jun 22 Javascript
关于react-router的几种配置方式详解
Jul 24 Javascript
如何理解Vue的v-model指令的使用方法
Jul 19 Javascript
layer.open提交子页面的form和layedit文本编辑内容的方法
Sep 27 Javascript
解决vue更新路由router-view复用组件内容不刷新的问题
Nov 04 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
MySQL时间字段究竟使用INT还是DateTime的说明
2012/02/27 PHP
php对csv文件的读取,写入,输出下载操作详解
2013/08/10 PHP
php遍历目录输出目录及其下的所有文件示例
2014/01/27 PHP
PHP入门教程之正则表达式基本用法实例详解(正则匹配,搜索,分割等)
2016/09/11 PHP
Laravel接收前端ajax传来的数据的实例代码
2017/07/20 PHP
通过判断JavaScript的版本实现执行不同的代码
2010/05/11 Javascript
JavaScript 获取当前时间戳的代码
2010/08/05 Javascript
JavaScript中使用ActiveXObject操作本地文件夹的方法
2014/03/28 Javascript
jquery实现点击变换导航样式的方法
2015/08/31 Javascript
javascript高级选择器querySelector和querySelectorAll全面解析
2016/04/07 Javascript
用jmSlip编写移动端顶部日历选择控件
2016/10/24 Javascript
Javascript实现动态时钟效果
2018/11/17 Javascript
postman自定义函数实现 时间函数的思路详解
2019/04/17 Javascript
解决小程序无法触发SESSION问题
2020/02/03 Javascript
原生JS实现留言板功能
2020/02/08 Javascript
javascript设计模式 ? 装饰模式原理与应用实例分析
2020/04/14 Javascript
JavaScript逻辑运算符相关总结
2020/09/04 Javascript
Python脚本暴力破解栅栏密码
2015/10/19 Python
python数据处理实战(必看篇)
2017/06/11 Python
Python之re操作方法(详解)
2017/06/14 Python
Python supervisor强大的进程管理工具的使用
2019/04/24 Python
django 读取图片到页面实例
2020/03/27 Python
Django中的模型类设计及展示示例详解
2020/05/29 Python
python 写函数在一定条件下需要调用自身时的写法说明
2020/06/01 Python
用python实现一个简单的验证码
2020/12/09 Python
Rag & Bone官网:瑞格布恩高级成衣
2018/04/19 全球购物
软件测试题目
2013/02/27 面试题
医学检验专业个人求职信范文
2013/12/04 职场文书
怎样写演讲稿
2014/01/04 职场文书
新书发布会策划方案
2014/06/09 职场文书
美术课外活动总结
2014/07/08 职场文书
党的群众路线教育实践活动心得体会(企业)
2014/11/03 职场文书
如何将JavaScript将数组转为树形结构
2021/06/02 Javascript
Python中的xlrd模块使用整理
2021/06/15 Python
Oracle以逗号分隔的字符串拆分为多行数据实例详解
2021/07/16 Oracle
用PYTHON去计算88键钢琴的琴键频率和音高
2022/04/10 Python