Nx Vue+Vite 插件开发指南

1,029 阅读7分钟

1. Nx简介

Nx本质是一个构建系统,作用类似webpack和roolup。他主要优先支持monorep项目。

  • Nx并不是一个包管理工具,Nx的使用需要配合常用的包管理工具npm/yarn/pnpm
  • Nx实现monorepo有两种方式:
    • Packaged Monorepo
      • 需要配合npm/yarn/pnpmworkspace配置,本质上为npm/yarn/pnpmworkspace提供缓存和自动化的程序,每一个子项目有自己的node_modules,项目间引用时需要进行构建。
    • Integrated Monorepo
      • 项目不依赖于npm/yarn/pnpmworkspace,所有的子项目可以共享node_modules,子项目引用时可以直接引用源码,无需进行构建。
  • Nx作为一个构建系统,主要解决monorepo项目中的以下一些痛点:
    • 项目间的依赖问题
      • 项目间存在依赖时,需要检查依赖项是否完成构建,同时检查是否是最新的版本,前期需要进行大量的检查和准备工作;Nx可以根据项目的依赖关系,自动执行指定的脚本(如build最新版本)
    • 项目构建时长问题
      • Nx针对每一次的构建提供了一个优秀的缓存机制,当依赖没有更新时,直接从缓存中获取构建结果,极大地节省了构建时间
    • 项目的管理问题
      • Nx为子项目初始化、运行构建命令提供了一系列的工具和插件,避免了繁琐的手工创建、编辑配置文件的操作

2. 生成 Nx Workspace

我们首先使用官方推荐的方式,新建一个nxworkspace

npx create-nx-workspace@latest

过程如下:

  • 定义workspace的名称 image.png

  • 选择技术栈,由于目前没有vue的选项,因此,可以选择TS/JS image.png

  • 选择onorepo的模式 image.png

  • 是否需要针对CI/CD进行加速,Nx也提供了一个云端的缓存,用以加速CI/CD。 image.png

  • 初始化文件和安装必要的依赖 image.png

生成好的Workspace文件结构如下

├──apps          // 可以独立部署并运行的应用,主要用于对外发布
├──libs          // 工具类的包和依赖,用于解决单一的业务问题或功能问题
├──tools  
├──nx.json       // Nx的配置文件
└──package.json

3. 生成Plugin

Nx插件主要包含生成器(generators)和执行器(executors)。 插件的主要作用是为Nx workspace中的子项目提供脚手架工具同时进行自动化的配置。

  • 初始化一个my-plugin的插件
# 安装依赖
npm install @nx/plugin -D

# 使用@nx/plugin插件,调用其中名为plugin的插件,创建一个名字为my-plugin的插件。
nx g @nx/plugin:plugin my-plugin

在上述命令执行完毕之后,在libs文件夹下,会新建一个my-plugin文件夹。

image.png

在初次创建插件时,会同时在根目录下创建一些配置文件。在tsconfig.base.json文件中可以看到,在完成上述命令后,会自动的在配置文件paths中追加一条记录,这条记录用于在其他项目中引入my-plugin时,可以使用路径别名。

{
  "compileOnSave": false,
  "compilerOptions": {
    "rootDir": ".",
    "sourceMap": true,
    "declaration": false,
    "moduleResolution": "node",
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "importHelpers": true,
    "target": "es2015",
    "module": "esnext",
    "lib": ["es2020", "dom"],
    "skipLibCheck": true,
    "skipDefaultLibCheck": true,
    "baseUrl": ".",
    "paths": {
      "@org/my-plugin": ["libs/my-plugin/src/index.ts"]
    }
  },
  "exclude": ["node_modules", "tmp"]
}

新生成的插件文件夹内有一些基础的配置文件。

libs
└── my-plugin
    ├── README.md
    ├── package.json      // npm依赖
    ├── project.json      // 子项目配置文件
    ├── src               // 实际执行的操作
    │   └── index.ts
    ├── tsconfig.json     // ts配置文件
    ├── tsconfig.lib.json // ts配置文件
    └── tsconfig.spec.json

4. 生成Generator

生成器的主要作用是为开发者提供一个脚手架工具,用于组件、库、应用开发时的初始化工作,包括模板和配置的初始化。他为使用Nx的开发者提供了一个中标准化的创建项目的能力。

# 调用 @nx/plugin 插件的 generator 为 my-plugin 项目生成一个名字叫 vue 的生成器
nx g @nx/plugin:generator vue --project=my-plugin

image.png

在上述指令执行完毕之后,可以在my-pluginsrc文件夹下看到一个generators文件夹,该文件夹中包含了一个名为vue的文件夹。

libs
└── my-plugin
    ├── README.md
    ├── generators.json
    ├── jest.config.ts
    ├── package.json
    ├── project.json
    ├── src
    │   ├── generators
    │   │   └── vue
    │   │       ├── files
    │   │       │   └── index.js.template
    │   │       ├── generator.spec.ts
    │   │       ├── generator.ts
    │   │       ├── schema.d.ts
    │   │       └── schema.json
    │   └── index.ts
    ├── tsconfig.json
    ├── tsconfig.lib.json
    └── tsconfig.spec.json
  • generator.ts:生成器运行的入口文件,在运行时,会针对整个项目的文件树进行一些操作。其中tree表示的是nx workspace的整个文件树。在执行nx g my-plugin:vue 时,会运行vueGenerator函数。
import {
  addProjectConfiguration,
  formatFiles,
  generateFiles,
  Tree,
} from '@nx/devkit';
import * as path from 'path';
import { VueGeneratorSchema } from './schema';

export async function vueGenerator(tree: Tree, options: VueGeneratorSchema) {
  const projectRoot = `libs/${options.name}`;
  addProjectConfiguration(tree, options.name, {
    root: projectRoot,
    projectType: 'library',
    sourceRoot: `${projectRoot}/src`,
    targets: {},
  });
  generateFiles(tree, path.join(__dirname, 'files'), projectRoot, options);
  await formatFiles(tree);
}

export default vueGenerator;

  • schema.json:描述了生成器运行时可以接受的参数和一些对生成器本身的描述信息。如下,其中properties字段描述了生成器接受的参数为name,其类型为字符串,是在命令行执行时的第一个参数。
{
  "$schema": "http://json-schema.org/schema",
  "$id": "Vue",
  "title": "",
  "type": "object",
  "properties": {
    "name": {
      "type": "string",
      "description": "",
      "$default": {
        "$source": "argv",
        "index": 0
      },
      "x-prompt": "What name would you like to use?"
    }
  },
  "required": ["name"]
}

生成器最终的使用场景如下

nx g my-plygin:vue <name>

generator.ts文件中:

  • generateFiles :根据模板,去生成一个文件夹和一些文件。
  • formatfiles:根据prettier去格式化生成后的文件

这两个函数都是@nx/devkit提供的工具函数。

5. 定制文件模板

首先明确当前插件的一个目标:

  • 模板:
    • 使用vitevue-ts的模板
    • 使用vite作为构建工具
  • 使用方式:
    • 使用 nx [plugin-name]:[package-type] [package-name]
    • package-type支持可独立部署和发布的应用类型和组件类型
  • 依赖和配置
    • 在使用时,插件能够自动的安装依赖和处理配置信息

files文件夹下创建vue-ts的模板。 也可以使用vite的脚本进行文件的创建

pnpm create vite my-vue-app --template vue-ts

在模板文件创建完毕后,生成器的目录结构应该如下,在执行构建器时,就会把files文件的内容初始化到apps文件夹的<app-name>文件夹下。

.
└── vue
    ├── files
    │   ├── README.md
    │   ├── index.html
    │   ├── package.json
    │   ├── public
    │   │   └── vite.svg
    │   ├── src
    │   │   ├── App.vue
    │   │   ├── assets
    │   │   │   └── vue.svg
    │   │   ├── components
    │   │   │   └── HelloWorld.vue
    │   │   ├── main.ts
    │   │   ├── style.css
    │   │   └── vite-env.d.ts
    │   ├── tsconfig.json
    │   ├── tsconfig.node.json
    │   └── vite.config.ts
    ├── generator.spec.ts
    ├── generator.ts
    ├── schema.d.ts
    └── schema.json

修改生成器初始操作的一些配置

import {
  addProjectConfiguration,
  formatFiles,
  generateFiles,
  Tree,
} from '@nx/devkit';
import * as path from 'path';
import { VueGeneratorSchema } from './schema';

export async function vueGenerator(tree: Tree, options: VueGeneratorSchema) {
-  const projectRoot = `libs/${options.name}`;
+  const projectRoot = `apps/${options.name}`;
  addProjectConfiguration(tree, options.name, {
    root: projectRoot,
-    projectType: 'library',
+    projectType: 'application',
    sourceRoot: `${projectRoot}/src`,
    targets: {},
  });
  generateFiles(tree, path.join(__dirname, 'files'), projectRoot, options);
  await formatFiles(tree);
}

export default vueGenerator;

测试命令

nx g ./libs/my-plugin:vue

image.png

可以看到,当命令执行完毕之后apps文件夹下就出现了test应用的模板文件。但是此时,还需要解决两个问题,命令行参数的导入,和依赖的安装。此时当我们想要运行test项目的dev server时,如

nx dev test

image.png

这是因为test项目的配置文件project.json中没有对应的任务

{
  "name": "test",
  "$schema": "../../node_modules/nx/schemas/project-schema.json",
  "projectType": "application",
  "sourceRoot": "apps/test/src",
  "targets": {}
}

同时package.json文件的name字段也和预期不一致。

接下来,使用@nx/devkit提供的接口addProjectConfiguration对初始化后的project.json文件进行一些配置:

NPM脚本package.json的内容project.json的目标
devvitedev
buildvue-tsc && vite buildbuild
previewvite previewpreview

我们将项目需要执行targets命名为dev,build,preview,其中externalDependencies表示需要使用外部依赖vitecommand字段表示需要执行的内容是一段命令行脚本。dependsOn表示该命令执行时需要其所依赖的模块预先执行build命令。

import {
  addProjectConfiguration,
  formatFiles,
  generateFiles,
  Tree,
} from '@nx/devkit';
import * as path from 'path';
import { VueGeneratorSchema } from './schema';

export async function vueGenerator(tree: Tree, options: VueGeneratorSchema) {
  const projectRoot = `apps/${options.name}`;
  addProjectConfiguration(tree, options.name, {
    root: projectRoot,
    projectType: 'application',
    sourceRoot: `${projectRoot}/src`,
-    targets: {}
+    targets: {
+      dev: {
+        inputs: [
+          {
+            externalDependencies: ['vite'],
+          },
+        ],
+        command: `cd ${projectRoot} && vite`,
+        dependsOn: ['^build'],
+      },
+      build: {
+        inputs: [
+          {
+            externalDependencies: ['vite', 'vue-tsc'],
+          },
+        ],
+        command: `cd ${projectRoot} && vue-tsc && vite build`,
+        dependsOn: ['^build'],
+      },
+      preview: {
+        inputs: [
+          {
+            externalDependencies: ['vite'],
+          },
+        ],
+        command: `cd ${projectRoot} && vite preview`,
+      },
+    },
  });
  generateFiles(tree, path.join(__dirname, 'files'), projectRoot, options);
  await formatFiles(tree);
}

export default vueGenerator;

同时修改package.jsonpackage.json.template,并将name字段修改为

{
    "name": "<%= name %>",
}

再次执行

nx g ./libs/my-plugin:vue test2

apps/test2文件夹下的package.jsonname字段变成了test2,project.json文件中targets字段下也出现了三条指令devbuildpreview

此时运行test2的dev server,会发现项目环境缺少相关的依赖

image.png

这是因为我们使用生成器时只是生成了模板,但是并没有安装相关的依赖,为了避免开发人员手动安装依赖,使用@nx/devkit提供的addDependenciesToPackageJson方法,在项目创建时去自动安装依赖

import {
  addDependenciesToPackageJson,
  addProjectConfiguration,
  formatFiles,
  generateFiles,
  GeneratorCallback,
  runTasksInSerial,
  Tree,
} from '@nx/devkit';
import * as path from 'path';
import { VueGeneratorSchema } from './schema';

+ const DEPENDENCY = {
+  dependencies: {
+    vue: '^3.2.47',
+  },
+  devDependencies: {
+    '@vitejs/plugin-vue': '^4.1.0',
+    typescript: '^5.0.2',
+    vite: '^4.3.9',
+    'vue-tsc': '^1.4.2',
+  },
+ };

export async function vueGenerator(tree: Tree, options: VueGeneratorSchema) {
  const projectRoot = `apps/${options.name}`;
  addProjectConfiguration(tree, options.name, {
    root: projectRoot,
    projectType: 'application',
    sourceRoot: `${projectRoot}/src`,
    targets: {
      dev: {
        inputs: [
          {
            externalDependencies: ['vite'],
          },
        ],
        command: `cd ${projectRoot} && vite`,
        dependsOn: ['^build'],
      },
      build: {
        inputs: [
          {
            externalDependencies: ['vite', 'vue-tsc'],
          },
        ],
        command: `cd ${projectRoot} && vue-tsc && vite build`,
        dependsOn: ['^build'],
      },
      preview: {
        inputs: [
          {
            externalDependencies: ['vite'],
          },
        ],
        command: `cd ${projectRoot} && vite preview`,
      },
    },
  });
  generateFiles(tree, path.join(__dirname, 'files'), projectRoot, options);
  await formatFiles(tree);
+  const tasks: GeneratorCallback[] = [];
+  const installTask = await addDependenciesToPackageJson(
+    tree,
+    DEPENDENCY.dependencies,
+    DEPENDENCY.devDependencies
+  );
+  tasks.push(installTask);

+  return runTasksInSerial(...tasks);
}

export default vueGenerator;

再次初始化一个test3app时,会看到

image.png

在应用初始化的时候,也安装了项目所需的依赖,同时各个项目安装的依赖集中于项目根目录下,这样也避免了各个子项目下重复安装依赖,造成文件冗余。

测试dev server

image.png

可以正常运行

image.png

关于build和preview的命令就不再重复的测试

6. 发布

当我们需要对外提供该插件的能力时,可以使用两种方式:

  • nx的方式
    • nx publish [plugin-name] --ver=1.0.0
  • npm的方式
    • nx build my-plugin
    • cd dist/libs/my-plugin
    • npm publish

注意,在使用模板文件时,涉及到编译的部分,如vue文件、ts文件等会在nx的build过程中被编译,导致预期外的结果,因此需要为模板文件添加后缀.template

7. 使用

CommandDescription
nx g [npm name]:[generator name for app] [app name]在apps文件夹下生成应用模板
nx g [npm name]:[generator name for comp] [comp name]在packages文件夹下生成组件模板

如果需要使用nx的插件去初始化vue3+viteproject欢迎使用nx-vite-vue插件。 该插件使用vitevue-ts模板生成对应的app文件夹和组件文件夹。执行

nx g nx-vite-vue:app app-name

即可在apps文件夹下生成一个名为app-name的应用文件夹。

相关的示例代码,参见:github.com/Arfly/nx-vu…