前言

玩过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的简介,能够浏览以下文章:

  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包含了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.tsimport { 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:

$ nx g @nrwl/nx-plugin:plugin [pluginName]

如果是刚应用Nx,也能够应用上面的命令疾速新建一个我的项目,并主动增加一个plugin:

$ npx create-nx-plugin my-org --pluginName my-plugin

设置好Schematic选项定义

当初Nx为咱们创立了一个默认的plugin,首先更改packages/plugin/collection.json,为schema取名叫做“page”

{  "$schema": "../../node_modules/@angular-devkit/schematics/collection-schema.json",  "name": "plugin",  "version": "0.0.1",  "schematics": {    "page": {      "factory": "./src/schematics/page/page",      "schema": "./src/schematics/page/schema.json",      "description": "Create page component"    }  }}

接下来定义咱们提供的schema option,这里须要批改src/schematics/page/schema.jsonsrc/schematics/page/schema.d.ts,前者作为JSON Schema被Nx plugin应用,后者作为类型定义,开发时用到。

对于page,咱们须要提供两个必须选项:name和对应的project,两个可选选项:connect(是否connect to redux)、classComponent(应用类组件还是函数组件)。

上面别离是schema.jsonschema.d.ts

{  "$schema": "http://json-schema.org/draft-07/schema",  "id": "page",  "title": "Create page component",  "type": "object",  "properties": {    "name": {      "type": "string",      "description": "The name of the page component",      "$default": {        "$source": "argv",        "index": 0      },      "x-prompt": "What name would you like to use?"    },    "project": {      "type": "string",      "description": "The project of the page component",      "$default": {        "$source": "projectName"      },      "alias": "p",      "x-prompt": "Which projcet would you like to add to?"    },    "classComponent": {      "type": "boolean",      "alias": "C",      "description": "Use class components instead of functional component.",      "default": false    },    "connect": {      "type": "boolean",      "alias": "c",      "description": "Create a connected redux component",      "default": false    }  },  "required": ["name", "project"]}
export interface PageSchematicSchema {  name: string;  project: string;  classComponent: boolean;  connected: boolean;}

开发Schematic

创立所需模板文件

模板文件就是通过一些模板变量来生成真正的文件。每一个页面默认有两个文件,index.tsindex.scss,因而创立模板文件如下:

index.ts.template

<% if (classComponent) { %>import React, { Component } from 'react';<% } else { %>import React from 'react';<% } %><% if (connect) { %>import { connect } from 'react-redux';import { IRootState, Dispatch } from '../../store';<% } %>import { RouteComponentProps } from 'react-router-dom';import './index.scss';<% if (connect) { %>type StateProps = ReturnType<typeof mapState>;type DispatchProps = ReturnType<typeof mapDispatch>;type Props = StateProps & DispatchProps & RouteComponentProps;<% } else { %>type Props = RouteComponentProps;<% } %><% if (classComponent) { %>class <%= componentName %> extends Component<Props> {  render() {    return (      <div className="<% className %>">        Welcome to <%= componentName %>!      </div>    );  }}<% } else { %>const <%= componentName %> = (props: Props) => {  return (    <div className="<%= className %>">      Welcome to <%= componentName %>!    </div>  );};<% } %><% if (connect) { %>function mapState(state: IRootState) {  return {  }}function mapDispatch(dispatch: Dispatch) {  return {  }}<% } %><% if (connect) { %>export default connect<StateProps, DispatchProps, {}>(mapState, mapDispatch)(<%= componentName %>);<% } else { %>export default <%= componentName %>;<% } %>

index.scss.template

.<%= className %> {    }

咱们将模板文件放到src/schematics/page/files/下。

基于模板文件创建所需文件和目录

咱们一共须要做四件事:

  1. 格式化选项(把schematic默认的选项进行加工,加工成咱们所需的全副选项)。
  2. 基于模板文件创建所需文件和目录。
  3. 更新app/src/routers/config.ts
  4. 应用eslint格式化排版。

先来实现1和2:

page.ts:

import { PageSchematicSchema } from './schema';import { names } from '@nrwl/workspace';import { getProjectConfig } from '@nrwl/workspace/src/utils/ast-utils';interface NormalizedSchema extends PageSchematicSchema {  /** element className */  className: string;  componentName: string;  fileName: string;  projectSourceRoot: Path;}/** 加工选项 */function normalizeOptions(  host: Tree,  options: PageSchematicSchema): NormalizedSchema {  const { name, project } = options;  const { sourceRoot: projectSourceRoot } = getProjectConfig(host, project);  // kebab-case fileName and UpperCamelCase className  const { fileName, className } = names(name);  return {    ...options,    // element className    className: `${project}-${fileName}`,    projectSourceRoot,    componentName: className,    fileName,  };}

接下来应用模板文件:

page.ts

import { join } from '@angular-devkit/core';import {  Rule,  SchematicContext,  mergeWith,  apply,  url,  move,  applyTemplates,} from '@angular-devkit/schematics';import { PageSchematicSchema } from './schema';import { names } from '@nrwl/workspace';import { getProjectConfig } from '@nrwl/workspace/src/utils/ast-utils';interface NormalizedSchema extends PageSchematicSchema {  /** element className */  className: string;  componentName: string;  fileName: string;  projectSourceRoot: Path;}/** 基于模板创立文件 */function createFiles(options: NormalizedSchema): Rule {  const { projectSourceRoot, fileName } = options;  const targetDir = join(projectSourceRoot, pagePath, fileName);  return mergeWith(    apply(url('./files'), [applyTemplates(options), move(targetDir)])  );}

原理是应用Angular Schematics自带的mergeWithRule,接管一个SourceSource的定义如下:

A source is a function that generates a Tree from a specific context.

也就是说Source()会生成一棵新的Tree。而后将其和原来的Tree合并。

因为咱们须要从模板文件中加载,首先须要应用url加载文件,url接管文件或文件夹的绝对地址,返回一个Source,而后咱们应用applyurl加载模板文件后的Source进行加工,apply接管一个Source和一个Rule的数组,将Rule[]利用后返回一个新的Source

这里咱们须要进行两种“加工”,首先应用options替换模板文件中的变量,最初将这些文件应用move挪动到对应的目录下即可。

更新router config

来到了最重要也是比拟难的一个步骤,咱们还须要批改src/routers/config.ts中的routers变量,在外面减少咱们刚加上的page component。

因为这里是TS文件,所以须要剖析TS的AST (Abstract Syntax Tree),而后批改AST,最初应用批改的AST对原来内容进行笼罩即可。

批改AST能够应用TS官网的Compiler API联合TypeScript AST Viewer进行。不过因为AST的简单构造,TS Compiler API也不太敌对,间接应用API对AST进行操作十分艰难。例如AST的每个节点都有position信息,做一个新的插入时,还须要对position进行计算,API并没有人性化的操作形式。

因为下面的起因,我最终抉择了ts-morph,ts-morph以前也叫做ts-simple-ast,它封装了TS Compiler API,让操作AST变得简略易懂。

看代码之前,咱们先应用TS AST Viewer剖析一下routers/config.ts这段代码的AST:

export const routers = {  // 首页  '/': 'home',  // 第二页  '/about': 'about'};

AST如下(只含根节点信息):

咱们来层层剖析:

  1. 从申明到赋值,整段语句作为Variable Statement
  2. 因为routers是被导出的,蕴含了ExportKeyword
  3. routers = xxx作为VariableDeclarationList中的惟一一个VariableDeclaration
  4. 最初是Identifier“routers”,再到字面量表达式作为它的value。
  5. ...

因为上面代码用到了Initializer,上述的对象字面量表达式ObjectLiteralExpression就是routers这个VariableDeclarationInitializer

看懂AST后,更新router后的代码就容易了解了:

import { join, Path } from '@angular-devkit/core';import {  Rule,  Tree,  chain,  SchematicContext,  mergeWith,  apply,  url,  move,  applyTemplates,} from '@angular-devkit/schematics';import { PageSchematicSchema } from './schema';import { formatFiles, names } from '@nrwl/workspace';import { getProjectConfig } from '@nrwl/workspace/src/utils/ast-utils';import { Project } from 'ts-morph';/** 更新路由配置 */function updateRouterConfig(options: NormalizedSchema): Rule {  return (host: Tree, context: SchematicContext) => {    const { projectSourceRoot, fileName } = options;    const filePath = join(projectSourceRoot, routerConfigPath);    const srcContent = host.read(filePath).toString('utf-8');        // 应用ts-morph的project对AST进行操作    const project = new Project();    const srcFile = project.createSourceFile(filePath, srcContent, {      overwrite: true,    });    try {      // 依据变量标识符拿到对应的VariableDeclaration      const decl = srcFile.getVariableDeclarationOrThrow(        routerConfigVariableName      );      // 获取initializer并转换成string      const initializer = decl.getInitializer().getText();      // 应用正则匹配对象字面量的最初一部分并做插入      const newInitializer = initializer.replace(        /,?\s*}$/,        `,'/${fileName}': '${fileName}' }`      );      // 更新initializer      decl.setInitializer(newInitializer);      // 获取最新的TS文件内容对源文件进行笼罩      host.overwrite(filePath, srcFile.getFullText());    } catch (e) {      context.logger.error(e.message);    }  };}

在如何对Initializer进行操作时,我最开始想到的是将其应用JSON.parse()转换成对象字面量,而后进行简略追加,前面发现这段内容里还可能蕴含正文,所以只能通过正则匹配确定字面量的“尾部局部”,而后进行匹配追加。

应用eslint做好排版

操作实现后咱们能够应用Nx workspace提供的formatFiles将所有文件排版有序。最初咱们只须要在默认导出函数里将上述Rule通过chain这个Rule进行汇总。来看看最终代码:

import { join, Path } from '@angular-devkit/core';import {  Rule,  Tree,  chain,  SchematicContext,  mergeWith,  apply,  url,  move,  applyTemplates,} from '@angular-devkit/schematics';import { PageSchematicSchema } from './schema';import { formatFiles, names } from '@nrwl/workspace';import { getProjectConfig } from '@nrwl/workspace/src/utils/ast-utils';import { Project } from 'ts-morph';interface NormalizedSchema extends PageSchematicSchema {  /** element className */  className: string;  componentName: string;  fileName: string;  projectSourceRoot: Path;}// 页面组件目录const pagePath = 'pages';// 路由配置目录const routerConfigPath = 'routers/config.ts';// 路由配置文件中须要批改的变量名const routerConfigVariableName = 'routers';/** 加工选项 */function normalizeOptions(  host: Tree,  options: PageSchematicSchema): NormalizedSchema {  const { name, project } = options;  const { sourceRoot: projectSourceRoot } = getProjectConfig(host, project);  // kebab-case fileName and UpperCamelCase className  const { fileName, className } = names(name);  return {    ...options,    // element className    className: `${project}-${fileName}`,    projectSourceRoot,    componentName: className,    fileName,  };}/** 基于模板创立文件 */function createFiles(options: NormalizedSchema): Rule {  const { projectSourceRoot, fileName } = options;  const targetDir = join(projectSourceRoot, pagePath, fileName);  return mergeWith(    apply(url('./files'), [applyTemplates(options), move(targetDir)])  );}/** 更新路由配置 */function updateRouterConfig(options: NormalizedSchema): Rule {  return (host: Tree, context: SchematicContext) => {    const { projectSourceRoot, fileName } = options;    const filePath = join(projectSourceRoot, routerConfigPath);    const srcContent = host.read(filePath).toString('utf-8');    const project = new Project();    const srcFile = project.createSourceFile(filePath, srcContent, {      overwrite: true,    });    try {      const decl = srcFile.getVariableDeclarationOrThrow(        routerConfigVariableName      );      const initializer = decl.getInitializer().getText();      const newInitializer = initializer.replace(        /,?\s*}$/,        `,'/${fileName}': '${fileName}' }`      );      decl.setInitializer(newInitializer);      host.overwrite(filePath, srcFile.getFullText());    } catch (e) {      context.logger.error(e.message);    }  };}// 默认的rule factoryexport default function (schema: PageSchematicSchema): Rule {  return function (host: Tree, context: SchematicContext) {    const options = normalizeOptions(host, schema);    return chain([      createFiles(options),      updateRouterConfig(options),      formatFiles({ skipFormat: false }),    ]);  };}

测试

写好了schematic,别忘了进行测试,测试代码如下:

page.spec.ts

import { Tree, Rule } from '@angular-devkit/schematics';import { SchematicTestRunner } from '@angular-devkit/schematics/testing';import { createEmptyWorkspace } from '@nrwl/workspace/testing';import { join } from 'path';import { PageSchematicSchema } from './schema';import { updateWorkspace, names } from '@nrwl/workspace';const testRunner = new SchematicTestRunner(  '@plugindemo/plugin',  join(__dirname, '../../../collection.json'));export function callRule(rule: Rule, tree: Tree) {  return testRunner.callRule(rule, tree).toPromise();}export async function createFakeApp(tree: Tree, appName: string): Promise<Tree> {  const { fileName } = names(appName);  const appTree = await callRule(    updateWorkspace((workspace) => {      workspace.projects.add({        name: fileName,        root: `apps/${fileName}`,        projectType: 'application',        sourceRoot: `apps/${fileName}/src`,        targets: {},      });    }),    tree  );  appTree.create(    'apps/app1/src/routers/config.ts',    `    export const routers = {      // 首页      '/': 'home',      // 个人主页      '/about': 'about'    };  `  );  return Promise.resolve(appTree);}describe('plugin schematic', () => {  let appTree: Tree;  const options: PageSchematicSchema = { name: 'myPage', project: 'app1' };  beforeEach(async () => {    appTree = createEmptyWorkspace(Tree.empty());    appTree = await createFakeApp(appTree, 'app1');  });  it('should run successfully', async () => {    const tree = await testRunner.runSchematicAsync('page', options, appTree).toPromise();    // file exist    expect(      tree.exists('apps/app1/src/pages/my-page/index.tsx')    ).toBeTruthy();    expect(      tree.exists('apps/app1/src/pages/my-page/index.scss')    ).toBeTruthy();    // router modified correctly    const configContent = tree.readContent('apps/app1/src/routers/config.ts');    expect(configContent).toMatch(/,\s*'\/my-page': 'my-page'/);  });});

测试这块能用的轮子也比拟多,我这里简略创立了一个假的App(合乎下面说的目录构造),而后进行了一下简略测试。测试能够应用如下指令对plugin中的单个schematic进行测试:

$ nx test plugin --testFile page.spec.ts

如果写的plugin比较复杂,倡议再进行一遍end2end测试,Nx对e2e的反对也很好。

公布

最初到了公布环节,应用Nx build之后便能够自行公布了。

$ nx build plugin

上述所有代码均能够在我的GitHub里下载查看,同时代码里还减少了一个实在开发环境下的App-demo,外面将plugin引入了失常的开发流程,更能感触到其带来的便捷性。感觉不错的话,欢送大家star。

总结

其实要写好这类对文件系统“增删改查”的工具,要害还是要了解文件内容,比方下面的难点就在于了解TS文件的AST。应用ts-morph还能够做很多事件,比方咱们每减少一个文件,可能须要在进口index.ts中导出一次,应用ts-morph就一句话的事件:

const exportDeclaration = sourceFile.addExportDeclaration({    namedExports: ["MyClass"],    moduleSpecifier: "./file",});

当然,Nx和Angular提供了这一套生态,能用的工具和办法十分多,然而也须要咱们急躁查阅,正当应用。目前来说Nx封装的办法没有具体的文档,可能用起来须要间接查阅d.ts文件,没那么不便。

工欲善其事,必先利其器。Happy Coding!