如何创建Nx schematic插件?

前言

玩过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,也就是正常一个项目一个仓库。如下图所示:

Polyrepo vs Monorepo

更多关于monorepo的简介,可以阅读以下文章:

  1. Advantages of monorepos
  2. How to develop React apps like Facebook, Microsoft, and Google
  3. Misconceptions about Monorepos: Monorepo != Monolith

关于Nx plugin

先贴一张脑图,一个一个讲解schematic的相关概念:

Nx plugin mindmap

前面提到Nx plugin包括了builder(自动化构建)和schematic(自动化项目代码的增删改查)。一个成型的Nx plugin可以使用Nx内置命令执行。

对于文章要介绍的schematics,可以认为它是自动化代码生成脚本,甚至可以作为脚手架生成整个项目结构。

Schematics要实现的目标

Schematics的出现优化了开发者的体验,提升了效率,主要体现在以下几个方面:

  1. 同步式的开发体验,无需知道内部的异步流程

    Schematics的开发“感觉”上是同步的,也就是说每个操作输入都是同步的,但是输出则可能是异步的,不过开发者可以不用关注这个,直到上一个操作的结果完成前,下一个操作都不会执行。

  2. 开发好的schematics具有高扩展性和高重用性

    一个schematic由很多操作步骤组成,只要“步骤”划分合理,扩展只需要往里面新增步骤即可,或者删除原来的步骤。同时,一个完整的schematic也可以看做是一个大步骤,作为另一个schematic的前置或后置步骤,例如要开发一个生成Application的schematic,就可以复用原来的生成Component的schematic,作为其步骤之一。

  3. 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的名字执行指令外,还可以使用schema1description表示一段可选的描述内容。

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的两个核心概念:TreeRule

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是一个函数,接收TreeContext作为参数,返回一个新的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主函数,我们在函数中返回一个RuleRule的操作是新建一个默认名为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:

QR Code
微信扫一扫,欢迎咨询~

联系我们
武汉格发信息技术有限公司
湖北省武汉市经开区科技园西路6号103孵化器
电话:155-2731-8020 座机:027-59821821
邮件:tanzw@gofarlic.com
Copyright © 2023 Gofarsoft Co.,Ltd. 保留所有权利
遇到许可问题?该如何解决!?
评估许可证实际采购量? 
不清楚软件许可证使用数据? 
收到软件厂商律师函!?  
想要少购买点许可证,节省费用? 
收到软件厂商侵权通告!?  
有正版license,但许可证不够用,需要新购? 
联系方式 155-2731-8020
预留信息,一起解决您的问题
* 姓名:
* 手机:

* 公司名称:

姓名不为空

手机不正确

公司不为空