欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

Angular脚手架开发的实现步骤

程序员文章站 2022-06-24 17:37:12
简介 写一份自定义的angular脚手架吧 写之前我们先解析一下antd的脚手架 前提 先把 angular schematic这篇文章读一遍,确保了解了colle...

简介

写一份自定义的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:
schematic:
ng add:

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

相关标签: Angular 脚手架