前言
玩过Angular的同学都知道Angular作为一个Framework,拥有一套完备的生态,还集成了强大的CLI。而React则仅仅是一个轻量级的Library,官方社区只定义了一套组件的周期规则,而周边社区可以基于此规则实现自己的组件,React并不会提供给你一套开箱即用的方案,而需要自己在第三方市场挑选满意的组件形成“全家桶”,这也是React社区活跃的原因之一。
最近工作中在考虑使用monorepo对项目进行管理,发现了一套dev toolkit叫做Nx,Nx使用monorepo的方式对项目进行管理,其核心开发者vsavkin同时也是Angular项目的早期核心成员之一,他把Angular CLI这套东西拿到Nx,使其不仅可以支持Angular项目的开发,现在还支持React项目。
Nx支持开发自己的plugin,一个plugin包括schematics和builders(这两个概念也分别来自Angular的schematics以及cli-builders),schematics按字面意思理解就是“纲要”的意思,也就是可以基于一些模板自动化生成所需的文件;而builders就是可以自定义构建流程。
今天要讲的就是如何开发一个属于自己的Nx plugin (包含schematics),我会使用它来自动化创建一个页面组件,同时更新router配置,自动将其加入react router的config。
关于Monorepo
这篇文章不会详细介绍什么是monorepo,mono有“单个”的意思,也就是单个仓库(所有项目放在一个仓库下管理),对应的就是polyrepo,也就是正常一个项目一个仓库。如下图所示:
更多关于monorepo的简介,可以阅读以下文章:
- Advantages of monorepos
- How to develop React apps like Facebook, Microsoft, and Google
- Misconceptions about Monorepos: Monorepo != Monolith
关于Nx plugin
先贴一张脑图,一个一个讲解schematic的相关概念:
前面提到Nx plugin包括了builder(自动化构建)和schematic(自动化项目代码的增删改查)。一个成型的Nx plugin可以使用Nx内置命令执行。
对于文章要介绍的schematics,可以认为它是自动化代码生成脚本,甚至可以作为脚手架生成整个项目结构。
Schematics要实现的目标
Schematics的出现优化了开发者的体验,提升了效率,主要体现在以下几个方面:
-
同步式的开发体验,无需知道内部的异步流程
Schematics的开发“感觉”上是同步的,也就是说每个操作输入都是同步的,但是输出则可能是异步的,不过开发者可以不用关注这个,直到上一个操作的结果完成前,下一个操作都不会执行。
-
开发好的schematics具有高扩展性和高重用性
一个schematic由很多操作步骤组成,只要“步骤”划分合理,扩展只需要往里面新增步骤即可,或者删除原来的步骤。同时,一个完整的schematic也可以看做是一个大步骤,作为另一个schematic的前置或后置步骤,例如要开发一个生成Application的schematic,就可以复用原来的生成Component的schematic,作为其步骤之一。
-
schematic是原子操作
传统的一些脚本,当其中一个步骤发生错误,由于之前步骤的更改已经应用到文件系统上,会造成许多“副作用”,需要我们手动FIX。但是schematic对于每项操作都是记录在运行内存中,当其中一项步骤确认无误后,也只会更新其内部创建的一个虚拟文件系统,只有当所有步骤确认无误后,才会一次性更新文件系统,而当其中之一有误时,会撤销之前所做的所有更改,对文件系统不会有“副作用”。
接下来我们了解下和schematic有关的概念。
Schematics的相关概念
在了解相关概念前,先看看Nx生成的初始plugin目录:
your-plugin
|--.eslintrc
|--builders.json
|--collection.json
|--jest.config.js
|--package.json
|--tsconfig.json
|--tsconfig.lib.json
|--tsconfig.spec.json
|--README.md
|--src
|--builders
|--schematics
|--your-schema
|--your-schema.ts
|--your-schema.spec.ts
|--schema.json
|--schema.d.ts
Collection
Collection包含了一组Schematics,定义在plugin主目录下的collection.json
:
{
"$schema": "../../node_modules/@angular-devkit/schematics/collection-schema.json",
"name": "your-plugin",
"version": "0.0.1",
"schematics": {
"your-schema": {
"factory": "./src/schematics/your-schema/your-schema",
"schema": "./src/schematics/your-schema/schema.json",
"aliases": ["schema1"],
"description": "Create foo"
}
}
}
上面的json文件使用@angular-devkit/schematics下的collection schema来校验格式,其中最重要的是schematics
字段,在这里面定义所有自己写的schematics,比如这里定义了一个叫做"your-schema"的schematic,每个schematic下需要声明一个rule factory(关于rule
之后介绍),该factory指向一个文件中的默认导出函数,如果不使用默认导出,还可以使用your-schema#foo
的格式指定当前文件中导出的foo
函数。
aliases
声明了当前schematic的别名,除了使用your-schema
的名字执行指令外,还可以使用schema1
。description
表示一段可选的描述内容。
schema
定义了当前schematic的schema json定义,nx执行该schematic指令时可以读取里面设置的默认选项,进行终端交互提示等等,下面是一份schema.json
:
{
"$schema": "http://json-schema.org/schema",
"id": "your-schema",
"title": "Create foo",
"examples": [
{
"command": "g your-schema --project=my-app my-foo",
"description": "Generate foo in apps/my-app/src/my-foo"
}
],
"type": "object",
"properties": {
"project": {
"type": "string",
"description": "The name of the project.",
"alias": "p",
"$default": {
"$source": "projectName"
},
"x-prompt": "What is the name of the project for this foo?"
},
"name": {
"type": "string",
"description": "The name of the schema.",
"$default": {
"$source": "argv",
"index": 0
},
"x-prompt": "What name would you like to use for the schema?"
},
"prop3": {
"type": "boolean",
"description": "prop3 description",
"default": true
}
},
"required": ["name", "project"]
}
properties
表示schematic指令执行时的选项,第一个选项project
表示项目名,别名p
,使用$default
表示Angular内置的一些操作,例如$source: projectName
则表示如果没有声明project
,会使用Angular workspaceSchema
(nx中为workspace.json
)中的defaultProject
选项,而第二个选项的$default
则表明使用命令时的第一个参数作为name
。
x-prompt
会在用户不键入选项值时的交互,用来提示用户输入,用户可以不用预先知道所有选项也能完成操作,更复杂的x-prompt
配置请查阅官网。
说了这么多,以下是几个直观交互的例子,帮助大家理解:
nx使用generate
选项来调用plugin中的schematic或者builder,和Angular的ng generate
一致:
# 表示在 apps/app1/src/ 下生成一个名为bar的文件
$ nx g your-plugin:your-schema bar -p=app1
# 或者
$ nx g your-plugin:your-schema -name=bar -project app1
如果使用交互(不键入选项)
# 表示在 apps/app1/src/ 下生成一个名为bar的文件
$ nx g your-plugin:your-schema
? What is the name of the project for this foo?
$ app1
? What name would you like to use for the schema?
$ bar
接下来看看Schematics的两个核心概念:Tree和Rule
Tree
根据官方对Tree
的介绍:
The virtual file system is represented by a Tree. The Tree data structure contains a base (a set of files that already exists) and a staging area (a list of changes to be applied to the base). When making modifications, you don't actually change the base, but add those modifications to the staging area.
Tree
这一结构包含了两个部分:VFS和Staging area,VFS是当前文件系统的一个虚拟结构,Staging area则存放schematics中所做的更改。值得注意的是,当做出更改时,并不是对文件系统的及时更改,而只是将这些操作放在Staging area,之后会把更改逐步同步到VFS,知道确认无误后,才会一次性对文件系统做出变更。
Rule
A Rule object defines a function that takes a Tree, applies transformations, and returns a new Tree. The main file for a schematic, index.ts, defines a set of rules that implement the schematic's logic.
Rule
是一个函数,接收Tree
和Context
作为参数,返回一个新的Tree
,在schematics的主文件index.ts
中,可以定义一系列的Rule
,最后将这些Rule
作为一个综合的Rule
在主函数中返回,就完成了一个schematic。下面是Tree
的完整定义:
export declare type Rule = (tree: Tree, context: SchematicContext) => Tree | Observable<Tree> | Rule | Promise<void> | Promise<Rule> | void;
来看看一个简单的schematic主函数,我们在函数中返回一个Rule
,Rule
的操作是新建一个默认名为hello
的文件,文件中包含一个字符串world
,最后将这个Tree返回。
// src/schematics/your-schema/index.ts
import { Rule, SchematicContext, Tree } from '@angular-devkit/schematics';
// You don't have to export the function as default. You can also have more than one rule factory
// per file.
export function myComponent(options: any): Rule {
return (tree: Tree, _context: SchematicContext) => {
tree.create(options.name || 'hello', 'world');
return tree;
};
}
Context
最后是Context
,上面已经提到过,对于Schematics,是在一个名叫SchematicContext
的Context下执行,其中包含了一些默认的工具,例如context.logger
,我们可以使用其打印一些终端信息。
如何开发一个Nx Schematic?
下面的所有代码均可以在我的GitHub里下载查看,觉得不错的话,欢迎大家star。
接下来进入正题,我们开发一个nx plugin schematic,使用它来创建我们的页面组件,同时更新路由配置。
假设我们的项目目录结构如下:
apps
|...
|--my-blog
|...
|--src
|--components
|--pages
|--home
|--index.ts
|--index.scss
|--about
|--routers
|--config.ts
|--index.ts
|...
router/config.ts
文件内容如下:
export const routers = {
// 首页
'/': 'home',
// 个人主页
'/about': 'about'
};
现在我们要新增一个博客页,不少同学可能就直接新建一个目录,复制首页代码,最后手动添加一条路由配置,对于这个例子倒是还好,但是如果需要更改的地方很多,就很浪费时间了,学习了Nx plugin schematics,这一切都可以用Schematic实现。
搭建Nx环境并使用Nx默认的Schematic创建一个plugin
如果之前已经有了Nx项目,则直接在项目根目录下使用以下命令创建一个plugin: