基于 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使用组件库可以成功构建,我我们至少要完成以下步骤:
编写源码。
配置好
vite.config
文件。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
查看运行效果,我们看到无论是用例的渲染还是源码都能够正确展示,并且用例渲染还能响应组件源码的修改做到热更新。如果对展示的体验要求不高的话,其实这种程度就已经可以达到基本需求了。