简介
写一份自定义的 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 {
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);
}
未完待续