文章
问答
冒泡
使用Vue3+Vite在 monorepo 模式下搭建组件库

基于 pnpm 搭建 monorepo 工程目录结构

💡 Tips:现代的前端工程越来越多的都会选择 Monorepo 的方式进行开发,比如 Vue、React 等知名开源项目都采用的 Monorepo 的方式进行管理。

我们选择创建自己的Vue3组件库的时候可以参考Vue3的经典组件库 Element-plus 作为参考,一个monorepo模式的项目目录为以下模式。

├── packages
|   ├── pkg1
|   |   ├── package.json
|   ├── pkg2
|   |   ├── package.json
├── package.json

# 为 a 包安装 lodash
pnpm --filter a i -S lodash
pnpm --filter a i -D lodash
# 指定 a 模块依赖于 b 模块
pnpm --filter a i -S b
# 发布所有包名为 @a/ 开头的包
pnpm --filter @a/* publish
# 为 a 以及 a 的所有依赖项执行测试脚本
pnpm --filter a... run test
# 为 b 以及依赖 b 的所有包执行测试脚本
pnpm --filter ...b run test

# 找出自 origin/master 提交以来所有变更涉及的包
# 为这些包以及依赖它们的所有包执行构建脚本
# README.md 的变更不会触发此机制
pnpm --filter="...{packages/**}[origin/master]"
  --changed-files-ignore-pattern="**/README.md" run build

# 找出自上次 commit 以来所有变更涉及的包
pnpm --filter "...[HEAD~1]" run build



pnpm的使用可以查阅官方文档 https://pnpm.io/zh/motivation

初始化一个monorepo项目

先创建一个工程文件夹,并且初始化一下。

mkdir my-ui
cd my-ui
pnpm init

项目根目录会生成一个 packages.json文件,这里会作为整个monorepo项目的管理中枢,只保留name字段,其他的根据我们后续需求自定义。


之后我们在根目录创建一个 pnpm-workspace.yaml文件,这个文件会让pnpm使用monorepo模式管理这个项目,我们在这个文件里写入以下内容。告诉pnpm哪些目录是独立的模块,这些独立模块叫做 workspace(工作空间)

packages:
  # 根目录下的 docs 是一个独立的文档应用,应该被划分为一个模块
  - docs
  # packages 目录下的每一个目录都作为一个独立的模块
  - packages/*
  # 简单使用时,可以只指定一个模块
  - demo

我们先按照以下的目录结构创建好文件。

📦my-ui
 ┣ 📂docs
 ┃ ┗ 📜package.json
 ┣ 📂packages
 ┃ ┣ 📂button
 ┃ ┃ ┗ 📜package.json
 ┃ ┣ 📂input
 ┃ ┃ ┗ 📜package.json
 ┃ ┗ 📂shared
 ┃   ┗ 📜package.json
 ┣ 📜package.json
 ┣ 📜pnpm-workspace.yaml
 ┗ 📜README.md

设置好每个子包的 package.json,以 button 组件的文件为例,可以为每一个组件设置好package.json。

{
  "name": "@myui/button",
  "version": "0.0.0",
  "description": "",
  "keywords": [
    "vue",
    "ui",
  ],
  "author": "xxx",
  "scripts": {

  },
  "dependencies": {
  }
}

以上项目雏形就已经建立完毕了。

Vite集成

为了集成vite使用组件库可以成功构建,我我们至少要完成以下步骤:

  1. 编写源码。

  2. 配置好vite.config文件。

  3. package.josn中设置好脚本命令。

目录结构可以参考以下方式:

📦my-ui
 ┣ 📂docs
 ┃ ┗ 📜package.json
 ┣ 📂demo               # 展示组件效果的 Web 应用
 ┃ ┣ 📂node_modules
 ┃ ┣ 📂dist
 ┃ ┣ 📂public
 ┃ ┣ 📂src
 ┃ ┣ 📜index.html
 ┃ ┣ 📜vite.config.ts
 ┃ ┣ 📜tsconfig.json
 ┃ ┗ 📜package.json
 ┣ 📂packages
 ┃ ┣ 📂button
 ┃ ┃ ┣ 📂node_modules
 ┃ ┃ ┣ 📂dist           # 组件产物目录
 ┃ ┃ ┣ 📂src            # 组件源码目录
 ┃ ┃ ┃ ┣ 📜Button.vue
 ┃ ┃ ┃ ┗ 📜index.ts
 ┃ ┃ ┣ 📜package.json
 ┃ ┃ ┗ 📜vite.config.ts
 ┃ ┣ 📂input
 ┃ ┃ ┗ 📜...
 ┃ ┣ 📂shared
 ┃ ┃ ┗ 📜...
 ┃ ┗ 📂ui               # 组件库主包,各组件的统一出口
 ┃   ┗ 📜...
 ┣ 📜package.json
 ┣ 📜tsconfig.json
 ┣ 📜tsconfig.base.json
 ┣ 📜tsconfig.node.json
 ┣ 📜tsconfig.src.json
 ┣ 📜pnpm-workspace.yaml
 ┗ 📜README.md

公共方法组件

我们把工具方法都放在一个包下统一编写,并且演示一下外部依赖的能力

// 模块源码目录
📦shared
 ┣ ...
 ┣ 📂src
 ┃ ┣ 📜hello.ts
 ┃ ┣ 📜index.ts
 ┃ ┗ 📜useLodash.ts
 ┣ ...

安装一下外部依赖:

# 为 shared 包安装 lodash 相关依赖
pnpm --filter @myui/shared i -S lodash @types/lodash


Vue组件

我们先编写button组件的基础代码,并且调用公共模块的方法,目录设计如下:

📦button
 ┣ ...
 ┣ 📂src
 ┃ ┣ 📜button.vue
 ┃ ┗ 📜index.ts
 ┣ ...

先在子模块下的 package.json 中按照 workspace 协议 手动声明内部依赖,然后通过 pnpm -w i 执行全局安装。

// packages/button/package.json
{
  // ...
  "dependencies": {
+   "@myui/shared": "workspace:^"
  }
}

# 或者
pnpm --filter @myui/button i -S @myui/shared


编写 button.vue index.ts文件:

// button.vue
<script setup lang="ts">
import { hello } from '@myui/shared';

const props = withDefaults(
  defineProps<{
    text?: string;
  }>(),
  {
    text: 'World',
  },
);

function clickHandler() {
  hello(props.text);
}
</script>

<template>
  <button class="openx-button" @click="clickHandler">
    <slot></slot>
  </button>
</template>

//index.ts

import Button from './button.vue';

export { Button };


编写好 button/input 组件之后,我们把 ui 模块统一作为各个组件的出口,需要先在package.json 中声明组件的依赖关系。

{

  "dependencies": {
    "@openxui/button": "workspace:^",
    "@openxui/input": "workspace:^",
    "@openxui/shared": "workspace:^"
  },
}


完成声明后,执行 <font style="background-color:rgb(255, 245, 245);">pnpm -w i</font> 更新依赖。


之后在 <font style="background-color:rgb(255, 245, 245);">src/index.ts</font> 文件中将各个模块的内容导出。

// packages/ui/src/index.ts
export * from '@myui/button';
export * from '@myui/input';
export * from '@myui/shared';


构建各个模块

我们以构建公共方法shared 模块为例,添加vite.config.ts 配置文件。参考文档:https://cn.vitejs.dev/config/#config-intellisense


并且把package.json中的build脚本修改成vite指令来进行构建。注意设置rollupOptions参数来声明不想打包的外部依赖。

import { defineConfig } from 'vite';

export default defineConfig({
  build: {
    // 产物输出目录,默认值就是 dist。我们使用默认值,注释掉此字段。
    // outDir: 'dist',

    // 参考:https://cn.vitejs.dev/config/build-options.html#build-lib
    lib: {
      // 构建的入口文件
      entry: './src/index.ts',

      // 产物的生成格式,默认为 ['es', 'umd']。我们使用默认值,注释掉此字段。
      // formats: ['es', 'umd'],

      // 当产物为 umd、iife 格式时,该模块暴露的全局变量名称
      name: 'MyuiShared',
      // 产物文件名称
      fileName: 'myui-shared',
    },
    // 为了方便学习,查看构建产物,将此置为 false,不要混淆产物代码
    minify: false,
    rollupOptions: {
      // 确保外部化处理那些你不想打包进库的依赖
      external: [/lodash.*/],

      output: {
        // 在 UMD 构建模式下为这些外部化的依赖提供一个全局变量。即使不设置,构建工具也会为我们自动生成。个人倾向于不设置
        /*
          globals: {
            lodash: 'lodash'
          }
          */
      },
    },
  },
});


执行构建命令:

pnpm --filter @myui/shared run build


我们再构建Vue组件模块:


Vite提供了针对vue组件的官方插件来执行编译任务:https://github.com/vitejs/vite-plugin-vue/tree/main/packages/plugin-vue

import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';

export default defineConfig({
  // 增加插件的使用
  plugins: [vue()],
  build: {
    lib: {
      entry: './src/index.ts',
      name: 'MyuiButton',
      fileName: 'myui-button',
    },
    minify: false,
    rollupOptions: {
      external: [
        // 除了 @myui/shared,未来可能还会依赖其他内部模块,不如用正则表达式将 myui 开头的依赖项一起处理掉
        /@myui.*/,
        'vue',
      ],
    },
  },
});


然后我们修改一下产物的路径依赖

{
  "name": "@myui/button",
  "version": "0.0.0",
  "description": "",
  "keywords": [
    "vue",
    "ui",
    "component library"
  ],
  "author": "gzx",
  "main": "./dist/myui-button.umd.js",
  "module": "./dist/myui-button.mjs",
  "exports": {
    ".": {
      "require": "./dist/myui-button.umd.js",
      "module": "./dist/myui-button.mjs"
    }
  },
  "scripts": {
    "build": "vite build",
    "test": "echo test"
  },
  "dependencies": {
    "@myui/shared": "workspace:^"
  }
}

参照以上配置我们可以把ui模块也一起配置好构建命令。


我们可以通过路径过滤器选择packages目录下的所有包开始构建,执行整体构建命令

pnpm --filter "./packages/**" run build

#或者
pnpm --filter @myui/ui... run build



可以把构建命令写入启动脚本:

// package.json
{
  // ...
  "scripts": {
   "build:ui": "pnpm --filter ./packages/** run build"
  },
}


创建网站demo使用各模块

我们接下来搭建一个网站应用demo演示我们的组件包效果

📦my-ui
 ┣ 📂...
 ┣ 📂demo
 ┃ ┣ 📂node_modules
 ┃ ┣ 📂dist
 ┃ ┣ 📂public
 ┃ ┣ 📂src
 ┃ ┃ ┣ 📂main.ts
 ┃ ┃ ┗ 📜App.vue
 ┃ ┣ 📜index.html
 ┃ ┣ 📜vite.config.ts
 ┃ ┣ 📜tsconfig.json
 ┃ ┗ 📜package.json


import { createApp } from 'vue';
import App from './app.vue';

createApp(App).mount('#app');


import { createApp } from 'vue';
import App from './app.vue';

createApp(App).mount('#app');


启动命令验证效果:

pnpm --filter @myui/demo run dev

TypeScript的集成

Vite 本质上是双引擎架构——内部除了 Rollup 之外,还集成了另一个构建工具Esbuild 有着超快的编译速度,它在其中负责第三方库构建和 TS/JSX 语法编译。无论是构建模式还是开发服务器模式,**Vite**** 都通过 **Esbuild** 来将 **ts** 文件转译为 ****js**

规划TS分治

对于每个 TypeScript 项目而言,编译选项 compilerOptions 大部分都是重复的,因此我们需要建立一个基础配置文件 tsconfig.base.json,供其他配置文件继承。


对重复的基础配置进行统一编写:

{
  "compilerOptions": {
    // 项目的根目录
    "rootDir": ".",
    // 项目基础目录
    "baseUrl": ".",
    // tsc 编译产物输出目录
    "outDir": "dist",
    // 编译目标 js 的版本
    "target": "es2022",
    // 
    "module": "esnext",
    // 模块解析策略
    "moduleResolution": "node",
    // 是否生成辅助 debug 的 .map.js 文件。 
    "sourceMap": false,
    // 产物不消除注释
    "removeComments": false,
    // 严格模式类型检查,建议开启
    "strict": true,
    // 不允许有未使用的变量
    "noUnusedLocals": true,
    // 允许引入 .json 模块
    "resolveJsonModule": true,

    // 与 esModuleInterop: true 配合允许从 commonjs 的依赖中直接按 import XX from 'xxx' 的方式导出 default 模块。
    "allowSyntheticDefaultImports": true,
    "esModuleInterop": true,

    // 在使用 const enum 或隐式类型导入时受到 TypeScript 的警告
    "isolatedModules": true,
    // 检查类型时是否跳过类型声明文件,一般在上游依赖存在类型问题时置为 true。
    "skipLibCheck": true,
    // 引入 ES 的功能库
    "lib": [],
    // 默认引入的模块类型声明
    "types": [],
    // 路径别名设置
    "paths": {
      "@myui/*": ["packages/*/src"]
    }
  }
}

把node环境下执行的脚本进行配置:

{
  "extends": "./tsconfig.base.json",
  "compilerOptions": {
    "composite": true,
    "lib": ["ESNext"],
    "types": ["node"],
    "allowJs": true
  },
  "include": [
    // 目前项目中暂时只有配置文件,以后会逐步增加
    "**/*.config.*",
    "scripts"
  ],
  "exclude": [
    "**/dist",
    "**/node_modules"
  ]
}

对于所有模块src目录的源文件的ts配置:

{
  "extends": "./tsconfig.base.json",
  "compilerOptions": {
    "composite": true,
    // 组件库依赖浏览器的 DOM API
    "lib": ["ESNext", "DOM", "DOM.Iterable"],
    "types": ["node"],
  },
  "include": [
    "typings/env.d.ts",
    "packages/**/src"
  ],
}


最后建立总的ts配置:

{
  "compilerOptions": {
    "target": "es2022",
    "moduleResolution": "node",
    // vite 会读取到这个 tsconfig 文件(位于工作空间根目录),按照其推荐配置这两个选项
    // https://cn.vitejs.dev/guide/features.html#typescript-compiler-options
    "isolatedModules": true,
    "useDefineForClassFields": true,
  },
  "files": [],
  "references": [
    // 聚合 ts project
    { "path": "./tsconfig.src.json" },
    { "path": "./tsconfig.node.json" }
  ],
}


由于tsc是定位到的源码文件,而vite定位到的是构建产物,这样我们对源码模块进行修改的时候无法同步,必须先对子模块打包才能避免报错,所以为demo配置的ts命中源码而非产物,让vite的理解与tsc一致,就需要配置别名alias了:

import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import { join } from 'node:path';

export default defineConfig({
  plugins: [vue()],
  resolve: {
    alias: [
      {
        find: /^@myui\/(.+)$/,
        replacement: join(__dirname, '..', 'packages', '$1', 'src'),
      },
    ],
  },
});


TS类型检查

通过tsc命令可以对文件进行类型检查,可以通过以下方式实现:

pnpm i -wD vue-tsc

npx vue-tsc -p tsconfig.src.json --noEmit --composite false


在package.json中更新命令:

"scripts": {
  "type:node": "tsc -p tsconfig.node.json --noEmit --composite false",
  "type:src": "vue-tsc -p tsconfig.src.json --noEmit --composite false",
  "build:ui": "pnpm --filter ./packages/** run build"
},

生成 d.ts类型声明

在vite中,类型声明可以使用 vue-tsc实现。为了在每一个子包都生成对应的d.ts,我们创建script文件夹,并且创建dts-mv 脚本。

import { join } from 'node:path';
import { readdir, cp } from 'node:fs/promises';

/** 以根目录为基础解析路径 */
const fromRoot = (...paths: string[]) => join(__dirname, '..', ...paths);

/** 包的 d.ts 产物目录 */
const PKGS_DTS_DIR = fromRoot('dist/packages');

/** 包的目录 */
const PKGS_DIR = fromRoot('packages');

/** 单个包的 d.ts 产物相对目录 */
const PKG_DTS_RELATIVE_DIR = 'dist';

/** 包的代码入口相对目录 */
const PKG_ENTRY_RELATIVE_DIR = 'src';

async function main() {
  const pkgs = await match();
  const tasks = pkgs.map(resolve);
  await Promise.all(tasks);
}

/** 寻找所有需要移动 dts 的包 */
async function match() {
  const res = await readdir(PKGS_DTS_DIR, { withFileTypes: true });
  return res.filter((item) => item.isDirectory()).map((item) => item.name);
}

/** 
 * 处理单个包的 dts 移动
 * @param pkgName 包名
 */
async function resolve(pkgName: string) {
  try {
    const sourceDir = join(PKGS_DTS_DIR, pkgName, PKG_ENTRY_RELATIVE_DIR);
    const targetDir = join(PKGS_DIR, pkgName, PKG_DTS_RELATIVE_DIR);
    const sourceFiles = await readdir(sourceDir);
    const cpTasks = sourceFiles.map((file) => {
      const source = join(sourceDir, file);
      const target = join(targetDir, file);
      console.log(`[${pkgName}]: moving: ${source} => ${target}`);
      return cp(source, target, {
        force: true,
        recursive: true,
      })
    })
    await Promise.all(cpTasks);
    console.log(`[${pkgName}]: moved successfully!`);  
  } catch (e) {
    console.log(`[${pkgName}]: failed to move!`);
  }
}

main().catch((e) => {
  console.error(e);
  process.exit(1);
})


安装tsx用于执行ts命令和rimraf用于清空产物目录。

pnpm i -wD tsx

pnpm i -wD rimraf


然后配置相应的命令:

{
  "name": "myui",
  "private": true,
  "scripts": {
    "clean:type": "rimraf ./dist",
    "type:node": "tsc -p tsconfig.node.json --noEmit --composite false",
    "type:src": "pnpm run clean:type && vue-tsc -p tsconfig.src.json --composite false --declaration --emitDeclarationOnly",
    "mv-type": "tsx ./scripts/dts-mv.ts",
    "build:ui": "pnpm run type:src && pnpm --filter ./packages/** run build && pnpm run mv-type"
  },
  "devDependencies": {
    "@types/node": "^20.17.9",
    "@vitejs/plugin-vue": "^5.2.1",
    "rimraf": "^6.0.1",
    "sass": "^1.82.0",
    "tsx": "^4.19.2",
    "typescript": "^5.7.2",
    "vite": "^6.0.3",
    "vue-tsc": "^2.1.10"
  },
  "dependencies": {
    "vue": "^3.5.13"
  }
}

然后要记得给所有子包补上声明文件的入口字段:

{
  "name": "@myui/button",
  ...
  "types": "./dist/index.d.ts",
  "exports": {
    ".": {
      "require": "./dist/myui-button.umd.js",
      "module": "./dist/myui-button.mjs",
      "types": "./dist/index.d.ts"
    }
  },
...
}

代码规范工具的集成

ESLint

关于eslint的配置,推荐安装以下工具 (注意:eslint尽量使用8版本,9版本更新较大容易踩坑,过一段时间社区踩坑案例丰富以后再统一升级9)

# 版本号8任选一个 加个@+版本号就好
pnpm i -wD eslint
# 由于我们要具备解析 TypeScript 的能力,所以要安装 typescript-eslint 系列工具。同理,为了能够解析 Vue 语法,也要安装 vue-eslint-parser。
pnpm i -wD @typescript-eslint/parser @typescript-eslint/eslint-plugin vue-eslint-parser
# import 模块引入相关的规则、Vue 相关规则并不包含在默认规则集、typescript-eslint 规则集以及 Airbnb 规则集中,所以我们要额外安装对应的 plugin,引入这些规则集。
pnpm i -wD eslint-plugin-import eslint-plugin-vue
# 之后安装 Airbnb 规则集,便于我们一键继承。
pnpm i -wD eslint-config-airbnb-base eslint-config-airbnb-typescript
# 这个库能够让在我们编写 .eslintrc.js 配置文件时,提供完善的类型支持,大幅度提升体验。
pnpm i -wD eslint-define-config


编写配置文件

const { defineConfig } = require('eslint-define-config');
const path = require('path');

module.exports = defineConfig({
  // 指定此配置为根级配置,eslint 不会继续向上层寻找
  root: true,

  // 将浏览器 API、ES API 和 Node API 看做全局变量,不会被特定的规则(如 no-undef)限制。
  env: {
    browser: true,
    es2022: true,
    node: true,
  },

  // 设置自定义全局变量,不会被特定的规则(如 no-undef)限制。
  globals: {
    // 假如我们希望 jquery 的全局变量不被限制,就按照如下方式声明。
    // $: 'readonly',
  },

  // 集成 Airbnb 规则集以及 vue 相关规则
  extends: [
    'airbnb-base',
    'airbnb-typescript/base',
    'plugin:vue/vue3-recommended',
  ],

  // 指定 vue 解析器
  parser: 'vue-eslint-parser',
  parserOptions: {
    // 配置 TypeScript 解析器
    parser: '@typescript-eslint/parser',

    // 通过 tsconfig 文件确定解析范围,这里需要绝对路径,否则子模块中 eslint 会出现异常
    project: path.resolve(__dirname, 'tsconfig.eslint.json'),

    // 支持的 ecmaVersion 版本
    ecmaVersion: 13,

    // 我们主要使用 esm,设置为 module
    sourceType: 'module',

    // TypeScript 解析器也要负责 vue 文件的 <script>
    extraFileExtensions: ['.vue'],
  },

  // 在已有规则及基础上微调修改
  rules: {
    'import/no-extraneous-dependencies': 'off',
    'import/prefer-default-export': 'off',
    'import/no-unresolved': 'off',
    'import/no-relative-packages': 'off',

    // vue 允许单单词组件名
    'vue/multi-word-component-names': 'off',

    '@typescript-eslint/no-use-before-define': [
      'error',
      {
        functions: false,
      },
    ],

    'operator-linebreak': ['error', 'after'],
    'class-methods-use-this': 'off',

    // 允许使用 ++
    'no-plusplus': 'off',

    'no-spaced-func': 'off',

    // 换行符不作约束
    'linebreak-style': 'off',

    'no-await-in-loop': 'off',
  },

  // 文件级别的重写
  overrides: [
    // 对于 vite 和 vitest 的配置文件,不对 console.log 进行错误提示
    {
      files: [
        '**/vite.config.*',
        '**/vitest.config.*',
        'scripts/**',
      ],
      rules: {
        'import/no-relative-packages': 'off',
        'no-console': 'off',
      },
    },
  ],
});


对于eslint的相关ts配合设置,monorepo推荐以下方案:

{
  // eslint 检查专用,不要包含到 tsconfig.json 中
  "extends": "./tsconfig.base.json",
  "compilerOptions": {
    // 参考 https://typescript-eslint.io/linting/typed-linting/monorepos
    "noEmit": true
  },
  // 只检查,不构建,因此要包含所有需要检查的文件
  "include": [
    "**/*",
    // .xxx.js 文件需要单独声明,例如 .eslintrc.js
    "**/.*.*"
  ],
  "exclude": [
    // 排除产物目录
    "**/dist",
    "**/node_modules"
  ]
}


Stylelint

pnpm i -wD stylelint
pnpm i -wD stylelint-config-standard-scss stylelint-config-recommended-vue stylelint-config-recess-order stylelint-stylistic


对于stylelint进行相关配置:

 module.exports = {
  // 继承的预设,这些预设包含了规则集插件
  extends: [
    // 代码风格规则
    'stylelint-stylistic/config',
    // 基本 scss 规则
    'stylelint-config-standard-scss',
    // scss vue 规则
    'stylelint-config-recommended-vue/scss',
    // 样式属性顺序规则
    'stylelint-config-recess-order',
  ],
  rules: {
    // 自定义规则集的启用 / 禁用
    // 'stylistic/max-line-length': null,
    'stylistic/max-line-length': 100,
  },
};


Prettier

安装prettier并使用,可以借助ide来读取项目的prettier配置文件来帮助我们完成自动格式化。

pnpm i -wD prettier


module.exports = {
  // 一行最多字符
  printWidth: 100,
  // 使用 2 个空格缩进
  tabWidth: 2,
  // 不使用缩进符,而使用空格
  useTabs: false,
  // 行尾需要有分号
  semi: true,
  // 使用单引号
  singleQuote: true,
  // 末尾需要有逗号
  trailingComma: 'all',
  // 大括号内的首尾需要空格
  bracketSpacing: true,
  // 标签闭合不换行
  bracketSameLine: true,
  // 箭头函数尽量简写
  arrowParens: 'avoid',
  // 行位换行符
  endOfLine: 'lf',
};


以上几项目配置都需要配置相应的忽略文件:

node_modules
dist


commitlint

我们可以通过配置commitlint来进行提交检查。

pnpm i -wD @commitlint/config-conventional @commitlint/cli


module.exports = {
  extends: ['@commitlint/config-conventional'],
};

对于commitlint的一些提交规范示例

#### 功能开发
feat/版本/功能名(或任务ID)
feat/2024R1/translate

#### 修复bug
fix/bug号(或bug内容)

fix/bug-1386

fix/auto-flow


通过husky集成git hooks

pnpm i -wD husky

npx husky install


打包体系

为了消除大量重复代码,集中的维护构建配置,我们探究一种比较适合自己的打包方案体系。


正常的打包我们利用pnpm自己的能力,是可以实现的

pnpm --filter ./packages/** run build


我们可以计划在 vite.config 中调用公共的 generateConfig 方法直接生成完善的打包配置,通过 vite build 的 CLI 命令去读取配置,启动构建进程。届时,vite.config 配置大幅简化变成类似下面的形式。

import { generateConfig } from '@myui/build'

export default generateConfig(/** ... */);


样式方案

集成UnoCSS

UnoCSS是非常优秀的css原子化方案: https://unocss.nodejs.cn/

pnpm i -wD unocss


注意引入unocss之后要配置一下 uno.config.ts文件,我们可以使用自带的预设文件。

import { defineConfig, presetUno } from 'unocss';

export default defineConfig({
  presets: [presetUno()],
});


在vite中导入该插件(注意需要用到UnoCss的工程最好都要把这个导入进来)

export default defineConfig({
  plugins: [
    vue(),
    unocss(),
  ],
});
// 之后在index.ts导入样式
import 'virtual:uno.css';


UnoCSS 会自动向上寻找最近的 uno.config.ts 配置文件,在 packages/button 没有配置文件的情况下,根目录的 uno.config.ts 文件会生效。

创建组件库文档

初始化文档

我们从最开始的目录创建,给文档留了一个位置。可以选择 VitePress来搭建一个组件库文档。https://vitejs.cn/vitepress/guide/what-is-vitepress 。首先可以安装vitepress

pnpm --filter @myui/docs i -D vitepress


目录结构我们需要这样设计:

📦docs
 ┣ 📂.vitepress
 ┃ ┗ config.mts       # VitePress 主要配置文件
 ┣ 📂public           # 静态资源目录 
 ┃ ┣ 📜favicon.ico
 ┃ ┗ 📜logo.png
 ┣ 📜index.md         # 首页文件
 ┗ 📜package.json


docs/作为根目录,让VitePress读取docs/.vitepress/config.mts 作为配置文件

规划文档路由

规划文档路由:https://vitepress.dev/zh/guide/routing,我们可以使用vitepress依据markdown文件的目录,自动生成路由。

📦docs
 ┣ 📂...
 ┣ 📂guide                  # 组件使用指南
 ┃ ┣ 📜index.md             # 介绍,路由:/guide/
 ┃ ┣ 📜quick-start.md       # 快速开始。路由:/guide/quick-start.html
 ┃ ┗ 📜...  
 ┣ 📂components             # 组件用例文档,路由:/components/xxx
 ┃ ┣ 📜index.md             # 组件用例首页,路由:/components/
 ┃ ┣ 📜button.md            # 按钮组件用例。路由:/components/button.html
 ┃ ┣ 📜input.md            # 输入组件用例。路由:/components/input.html
 ┃ ┗ 📜...
 ┣ 📜index.md               # 首页文件,路由:/
 ┗ 📜package.json


具体的md编写语法可以参考:https://vitepress.dev/zh/reference/default-theme-layout


同时去配置好导航栏路由

import { defineConfig } from 'vitepress'

export default defineConfig({
  themeConfig: {
    nav: [
      { text: '指南', link: '/guide/' },
      { text: '组件', link: '/components/' },
    ],
  }
})


引入组件实例

我们可以选择引入插件https://github.com/xinlei3166/vitepress-theme-demoblock 来把vue实例添加到文档中。


先把组件引入到项目中来。

pnpm --filter @myui/docs i -S @myui/ui


为了实现热更新,注意更改配置文件定位到组件源码:

import { defineConfig } from 'vite';
import { join } from 'node:path';
import unocss from 'unocss/vite';

export default defineConfig({
  plugins: [
    // 应用组件库的 unocss 预设,配置文件在根目录的 uno.config.ts
    // 集成 UnoCss 方便我们编写组件用例,或者实现 VitePress 专用组件
    unocss(),
  ],
  resolve: {
    alias: [
      {
        // 将内部依赖定位到源码路径
        find: /^@myui\/(.+)$/,
        replacement: join(__dirname, '..', 'packages', '$1', 'src'),
      },
    ],
  },
});



VitePress是可以直接使用组件实例的,写法演示如下:

<script setup>
  import demo1 from '../demo/button/demo1.vue'
</script>

# Button 按钮

常用的操作按钮。

## 基础用法

基础的按钮用法。

<!-- 展示组件 -->
<demo1></demo1>

<!-- 展示源码 -->
<<< ../demo/button/demo1.vue

## Button API


查看运行效果,我们看到无论是用例的渲染还是源码都能够正确展示,并且用例渲染还能响应组件源码的修改做到热更新。如果对展示的体验要求不高的话,其实这种程度就已经可以达到基本需求了。

vue3
vite

关于作者

却黑
社恐
获得点赞
文章被阅读