Angular脚手架开发的实现步骤
简介
写一份自定义的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:
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。