Vue3 + Vite 搭建 UI 组件库
Vue3.0 和 Vite 发布都已经有很长的一段时间了,为了后续在工作中能更快的从 Vue 2.x 切换到 Vue 3, 有必要提前学习掌握,所以将使用 Vue3 + Vite 从 0 搭建一个简单的 UI 组件库。学习完你将了解以下内容
- 组件库开发环境搭建工作
- vite 如何打包整个组件库及单个组件
- 如何搭建一个组件库文档
- vue3.0 如何开发一个强大的组件
一、组件库初始化
组件库采用 monorepo
策略,将使用 Lerna
来管理该项目。首先使用 lerna 进行项目初始化
(PS: 关于什么是 monorepo
策略,请参考: All in one:项目级 monorepo 策略最佳实践)
1.1、monorepo 项目初始化
yarn global add lerna
mkdir gt-ui
cd gt-ui
lerna init
初始化后,会在根目录生成 lerna.json
配置文件,同时为了组件可以相互引用,以及避免重复安装包,减少体积等,还添加其它配置
{
"packages": ["packages/*"],
"version": "0.0.0",
"npmClient": "yarn", // 使用yarn管理
"useWorkspaces": true // 使用workspace,需要配置 package.json
}
在 package.json
文件中,也需要设置 workspaces
属性
{
"name": "gt-ui",
"private": true,
"devDependencies": {
"lerna": "^4.0.0"
},
"workspaces": ["packages/*"]
}
二、组件初始化
2.1、lerna 初始化组件
接着,会对单个组件进行初始化,每个组件相当于是一个子项目,子项目命名统一以 @<repo_name>/
开头,这是一种社区最佳实践。Lerna
提供了 lerna create
命令来初始化子项目
$: lerna create button
lerna notice cli v4.0.0
lerna WARN ENOREMOTE No git remote found, skipping repository property
package name: (button) @gt-ui/button
version: (0.0.0)
description:
keywords:
homepage:
license: (ISC)
entry point: (lib/button.js)
git repository:
About to write to /Users/yangjay/learn/playground/packages/button/package.json:
{
"name": "@gt-ui/button",
"version": "0.0.0",
"description": "> TODO: description",
"author": "yangjay <yj960203only@gmail.com>",
"homepage": "",
"license": "ISC",
"main": "lib/button.js",
"directories": {
"lib": "lib",
"test": "__tests__"
},
"files": [
"lib"
],
"publishConfig": {
"registry": "https://registry.npm.taobao.org/"
},
"scripts": {
"test": "echo \"Error: run tests from root\" && exit 1"
}
}
Is this OK? (yes)
lerna success create New package @gt-ui/button created at ./packages/button
此时,整个项目目录结果如下
2.2、引入 vue3、typescript
yarn add vue@next typescript -W
npx tsc --init
此时,会在根目录生成 tsconfig.json
配置文件, 我们进行如下如下设置
{
"compilerOptions": {
"target": "ESNext", // 打包的目标语法
"module": "ESNext", // 模块转化后的格式
"esModuleInterop": true, // 支持模块转化
"skipLibCheck": true, // 跳过类库检测
"forceConsistentCasingInFileNames": true, // 强制区分大小写
"moduleResolution": "node", // 模块解析方式
"jsx": "preserve", // 不转化jsx
"declaration": true, // 生成声明文件
"sourceMap": true // 生成映射文件
}
}
2.3、设置组件出入口
设置 Button
组件出入口,以及对应的 install
方法
packages/button/src/index.vue
文件
<template>
<button>按钮</button>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
name: 'MnButton',
})
</script>
packages/button/index.ts
文件
import { App } from 'vue'
import Button from './src/index.vue'
Button.install = (app: App): void => {
app.component(Button.name, Button)
}
export default Button
此时,项目会报错,是因为默认无法解析.vue
文件后缀的文件,需要增加typings
根目录创建 typings/vue-shim.d.ts
文件
declare module '*.vue' {
import {App,defineComponent} from 'vue';
const component: ReturnType<typeof defineComponent> & {
install(app:App):void
};;
export default component
}
2.4、整合所有组件
当初始化 Button 组件后,在项目根目录执行 yarn install
, 此时,在 node_modules
目录下会包含了我们的子项目
.
├── package.json
└── node_modules/
├── @gt-ui/button/
│ ├── index.js
│ └── package.json
├── └── src/
│ └── index.vue
因此,可以通过各自模块引入机制引入其它的子项目代码。
使用 lerna create gt-ui
创建一个子项目作为组件库的总入口,汇总所有组件。
package/gt-ui/index.ts
文件
import Button from '@gt-ui/button'
import { App } from 'vue'
const components = [Button]
const install = (app: App): void => {
components.forEach((component) => {
if (component.install) {
app.use(component)
} else if (component.name) {
app.component(component.name, component)
}
})
}
export default {
install,
}
(ps: 后续 index.ts
文件需通过脚本自动生成,避免手动引入 )
三、搭建展示文档
用户对组件库的第一印象就是看组件文档是否好看,齐全等,可选择现成的 VitePress
进行搭建,也可自己动手从 0 搭建。
根目录运行 yarn create @vitejs/app website --template vue
命令初始化文档项目,项目目录结构如下
.
├── package.json
└── website/
├── package.json
├── index.html
├── vite.config.js
├── src/
│ ├── main.js
│ └── App.vue
index.html
作为文档项目入口,接下来会测试 mono-ui
组件库是否能正常渲染
website/src/main.js
文件
import { createApp } from 'vue'
import GtUI from 'gt-ui'
import App from './App.vue'
createApp(App).use(GtUI).mount('#app')
website/src/App.vue
文件
<template>
<gt-button></gt-button>
</template>
<script setup></script>
根目录 package.json
文件中增加 script
脚本
{
"scripts": {
"docs:dev": "vite build --config ./website/vite.config.js"
}
}
运行 yarn docs:dev
就看看到 gt-button
组件正常渲染了。
四、组件库打包
我们将使用 vite 来打包组件库,需要先安装一些依赖
yarn add @vitejs/plugin-vue rollup-plugin-typescript2 -D -W
4.1、打包整个组件库
新建 build/vite.config.build.js
文件
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import typescript from 'rollup-plugin-typescript2'
const path = require('path')
export default defineConfig({
plugins: [
vue(),
{
...typescript({
tsconfigOverride: {
include: ['packages/**/*', 'typings/vue-shim.d.ts'],
exclude: ['node_modules', 'packages/**/__tests__/*', 'website'],
},
}), // 默认会调用 tsconfig.json, 帮助我们生成声明文件
apply: 'build', // 仅作用与 build 阶段
},
],
build: {
rollupOptions: {
// 请确保外部化那些你的库中不需要的依赖
external: ['vue'],
output: {
// 在 UMD 构建模式下为这些外部化的依赖提供一个全局变量
globals: {
vue: 'Vue',
},
},
},
lib: {
entry: path.resolve(__dirname, '../packages/index.ts'),
name: 'gt-ui',
formats: ['es', 'umd'],
},
},
})
根目录 package.json
文件中增加 script
脚本
{
"scripts": {
"build": "vite build -c ./build/vite.config.build.js"
}
}
运行命令 yarn build
默认会在 dist
目录下输出打包文件,根据配置,也就是 gt-ui.es.js
和 gt-ui.umd.js
两个文件,同时,还会调用 tsconfig.json
, 帮助我们生成声明文件
dist
├── button
│ ├── index.d.ts
│ └── src
│ └── index.vue.d.ts
├── gt-ui
│ └── index.d.ts
├── gt-ui.es.js
└── gt-ui.umd.js
可在 website/src/main.js
文件中,以相对路径的方式,导入组件库,测试组件能否正常使用
4.2、打包每个单独组件
每个组件都是一个子项目,子项名称约定为 @mono-ui/xxx
, 所以首先需要筛选中所以的组件,并且此打包任务存在多个 entry
新建 build/vite.config.build.disperse.js
文件
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import typescript from 'rollup-plugin-typescript2'
import { getPackagesSync } from '@lerna/project'
const path = require('path')
// 筛选出所有的组件
const inputs = getPackagesSync()
.map((pkg) => pkg.name)
.filter((name) => name.includes('@gt-ui'))
export default defineConfig({
plugins: [
vue({
target: 'browser',
}),
{
...typescript({
tsconfigOverride: {
compilerOptions: {
declaration: false, // 无需再次生成 ts声明文件
},
exclude: ['node_modules'],
},
}), // 默认会调用 tsconfig.json, 帮助我们生成声明文件
apply: 'build',
},
],
build: {
emptyOutDir: false,
rollupOptions: {
external: ['vue'],
input: inputs.map((name) => {
const pkgName = name.split('@gt-ui/')[1]
return path.resolve(__dirname, `../packages/${pkgName}/index.ts`)
}),
output: inputs.map((name) => {
const pkgName = name.split('@gt-ui/')[1]
return {
dir: path.resolve(__dirname, `../dist/${pkgName}/`),
entryFileNames: 'index.js',
format: 'es',
}
}),
},
lib: {
entry: path.resolve(__dirname, '../packages/**/index.ts'),
name: 'index',
},
},
})
根目录 package.json 文件中增加 script 脚本
{
"scripts": {
"build": "vite build -c ./build/vite.config.build.js && build:disperse",
"build:disperse": "vite build -c ./build/vite.config.build.disperse.js"
}
}
此时运行命令 yarn build
, 会先打包组件库整体,再打包每个单独的组件,dist
目录结果如下
dist
├── button
│ ├── index.d.ts
│ ├── index.js
│ └── src
│ └── button.vue.d.ts
├── gt-ui
│ └── index.d.ts
├── gt-ui.es.js
└── gt-ui.umd.js
也可在 website/src/main.js
文件中,以相对路径的方式,导入单个组件,测试能否正常渲染
五、组件库样式处理
5.1、样式库初始化
在 monorepo
策略下,组件库样式也可作为子项目存在,组件的样式将与逻辑进行拆分, 通过 lerna create theme
创建样式项目, 将使用 sass
预处理器来编写样式。
整个项目目录结构如下
theme
├── README.md
├── package.json
└── src
├── commons
│ ├── config.scss
│ └── variables.scss
├── fonts
│ ├── iconfont.css
│ ├── iconfont.ttf
│ ├── iconfont.woff
│ └── iconfont.woff2
├── index.scss
├── mixins
│ ├── bem-mixins.scss
│ └── index.scss
└── packages
├── gt-button.scss
└── index.scss
采用 BEM
命名规范来定义组件的类名,首先需要定义好一些分隔符
src/commons/config.scss
文件
$namespace: 'gt'; // scss 变量,命名空间 gt-button
$element-separator: '__'; //元素分隔符 gt-button__label
$modifier-separator: '--'; // 类型修饰 gt-button--mini
$state-prefix: 'is-'; // 状态 is-plain
src/mixins/bem-mixins.scss
文件
@import '../commons/config.scss';
// .gt-button{}
@mixin b($block) {
$B: $namespace + '-' + $block;
.#{$B} {
@content;
}
}
// &__label => .gt-button__label {}
@mixin e($element) {
@at-root {
#{& + $element-separator + $element} {
@content;
}
}
}
// &--mini => .gt-button--mini {}
@mixin m($modifier) {
@at-root {
#{& + $modifier-separator + $modifier} {
@content;
}
}
}
// .gt-button.is-xxx
@mixin when($state) {
@at-root {
$S: $state-prefix + $state;
&.#{$S} {
@content;
}
}
}
当定义好这些 mixin 函数后,编写组件的样式就可以少写很多重复代码
@import '../mixins/index.scss';
@include b(button) {
display: inline-block;
padding: 12px 20px;
font-size: 14px;
@include m(mini) {
padding: 7px 15px;
font-size: 12px;
}
@include when(disabled) {
color: #c0c4cc;
cursor: not-allowed;
}
}
5.2、样式库打包处理
样式库打包只需要将样式文件拷贝到输出目录即可, build/generate-theme.js
const path = require('path')
const fs = require('fs-extra')
const tasks = []
tasks.push(
fs.copy(
path.resolve(__dirname, '../packages/theme/src'),
path.resolve(__dirname, '../dist/theme')
)
)
Promise.all(tasks).then((res) => {
// logger.success(`copy 成功`);
})
根目录 package.json 文件中增加 script 脚本
{
"scripts": {
"build": "vite build -c ./build/vite.config.build.js && build:disperse && generate:theme",
"build:disperse": "vite build -c ./build/vite.config.build.disperse.js",
"generate:theme": "node ./build/generate-theme.js"
}
}
此时,运行 yarn build
就会一次性打包整个组件库、每个单独的组件以及样式库