如何从零开始搭建一个 Vue3 组件库?
最近有一些小伙伴问雨声:想要自己做一个组件库,但不知道如何开始?看到别人那些组件库各种各样、五花八门的配置文件和文件结构,根本不知道该从何下手。
别慌,其实再复杂的结构与配置,也是从一开始非常简单的项目,在开发过程中根据需要,经历不断的迭代、重构与完善后才逐步形成的。
我们想要开始的时候,其实最重要的第一步就是行动起来,先开一个最简单的项目,写一个最简单的组件,完成一次最简单的打包。
开始?!
创建项目
首先是包管理工具,推荐大家选用 pnpm
,相较于其他包管理,它更快、更节约空间,还有一个很重要的优势就是能非常方便地创建和管理 monorepo,安装可自行查阅 官网。
然后就是脚手架的选择,既然是 Vue3 组件库,那 Vite 必然是不二之选了。首先它几乎可以说是 Vue3 的官配,其次在做库项目方面,Vite 在打包时没 Webpack 这么麻烦,在开发时也比 Rollup 更容易搭建开发服务。
我们直接使用 Vite 官方提供的 vue-ts
模版来快速创建一个原始项目:
pnpm create vite demo-ui --template vue-ts
之后我们把 Git 初始化一下:
git init
接着把
package.json
中 vue-tsc
的内容去掉,因为类型错误的检查我们可以直接借助编辑器的 Volar 插件实时进行。package.json
{
"name": "demo-ui",
"version": "0.0.0",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"vue": "^3.2.25"
},
"devDependencies": {
"@vitejs/plugin-vue": "^2.3.3",
"typescript": "^4.5.4",
"vite": "^2.9.9"
}
}
最后安装一下依赖,项目创建就 OK 了。
pnpm i
第一个组件
有了项目之后,我们不管其他乱七八糟的,先来个最简单的组件。
我们先对目录结构做简单的改造,使其适应 Lib 型项目。
这是我们原始创建后的结构:
文章图片
我们把
public
、src/assets
、src/components/HelloWorld.vue
都删掉,添加 src/index.ts
、src/components/button.vue
:文章图片
其中的
src/App.vue
和 src/main.ts
留着用于开发组件的时候随时看效果。接着我们快快地写一个按钮组件,不管其他的,页面上能看到就行。
src/components/button.vue
src/App.vue
一个按钮
然后我们启动开发服务:
pnpm run dev
打开浏览器:
文章图片
平平无奇,没有任何样式的按钮组件出来了。
不过通常的组件库应该是以
app.use(DemoUI)
的形式来安装的,我们再来改装一下,导出一个具有 install
方法的对象。src/index.ts
import DButton from './components/button.vue'import type { App } from 'vue'const components = [
DButton
]export function install(app: App) {
components.forEach(component => {
app.component(component.name, component)
})
}export default {
install
}export {
DButton
}
src/main.ts
import { createApp } from 'vue'
import App from './App.vue'
import DemoUI from './index'createApp(App).use(DemoUI).mount('#app')
src/App.vue
一个按钮
打包组件
下一步我们就来完成我们的组件打包。
借助
vite
的 build.lib
配置快速完成库打包,注意打包的时候要排除 Vue 本身。vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'export default defineConfig({
build: {
lib: {
entry: 'src/index.ts',
formats: ['cjs', 'es']
},
rollupOptions: {
external: ['vue']
}
},
plugins: [vue()]
})
然后我们执行打包命令:
pnpm run build
文章图片
最后我们为
package.json
添加上 main
和 module
字段,那么一个可发布的组件库雏形就出来了(注意把 private
去掉或者改为 false
)。package.json
{
"name": "demo-ui",
"version": "0.0.0",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"main": "dist/demo-ui.cjs.js",
"module": "dist/demo-ui.es.js",
"dependencies": {
"vue": "^3.2.25"
},
"devDependencies": {
"@vitejs/plugin-vue": "^2.3.3",
"typescript": "^4.5.4",
"vite": "^2.9.9"
}
}
这个时候如果你执行
pnpm publish
那它就真的可以发布出去了(版本号要更新),不过它还有点单薄,还是先不要着急。添加样式
经过上面的步骤,我们得到一个最简单的组件库,不过是光溜溜的,我们还需要为这个组件添加样式。
这里我们选用
sass
作为 css
的预处理语言,当然你也可以选择你喜欢的方案,不过雨声认为 sass
与 css
的过渡足够平滑,而且功能也足够强大,所以比较喜欢用。pnpm i -D sass
然后我们在
src
下创建一个 style
目录,用来专门存放样式文件,并添加 index.scss
和 button.scss
:文章图片
接着我们给
button.scss
写上一些按钮的样式,并在 index.scss
中导出。src/style/button.scss
.d-button {
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
height: 32px;
padding: 0 14px;
color: #fff;
background-color: #339af0;
border: 1px solid #339af0;
border-radius: 4px;
outline: 0;
}
src/style/index.scss
@use './button.scss';
随后我们在
src/index.ts
中将样式也引入进来。src/index.ts
import './style/index.scss'import DButton from './components/button.vue'// 省略下面
这时我们就得到一个具有自定义样式的按钮组件了:
文章图片
再执行
pnpm run build
就可以看到打包后的文件有了 style.css
的样式文件。类型声明
作为一个 Vue3 + TypeScript 的项目,自然不能少了自动生成类型声明文件。
【我想写一个 Vue3 组件库,我该怎么开始()】这里我们借助
vite-plugin-dts
插件来实现打包时自动生成类型声明文件。pnpm i -D vite-plugin-dts
然后在 Vite 配置文件中添加插件:
vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import dts from 'vite-plugin-dts'export default defineConfig({
build: {
lib: {
entry: 'src/index.ts',
formats: ['cjs', 'es']
},
rollupOptions: {
external: ['vue']
}
},
plugins: [
vue(),
dts({ insertTypesEntry: true, copyDtsFiles: false })
]
})
我们再一次执行
pnpm run build
进行打包,可以看到打包后的文件也包含类型声明文件了:文章图片
最后在
package.json
中添加 types
字段:package.json
{
// ...
"main": "dist/demo-ui.cjs.js",
"module": "dist/demo-ui.es.js",
"types": "dist/index.d.ts",
// ...
}
Lint 工具
就和常规项目一样,组件库的项目也需要像是 ESLint 和 Stylelint 这样的来规范统一代码风格的工具的。
在项目的搭建上,雨声觉得到了这一步才是最有趣的,就好像拼乐高一样,有了主体之后,就可以开始往上拼上各种各样的模块,然后看着项目变得丰富起来,非常有意思。我们先来安装 Eslint 全家桶,包含 Vue 和 TypeScript 相关的:
pnpm i -D eslint eslint-plugin-import eslint-plugin-n eslint-plugin-node eslint-plugin-promise
pnpm i -D eslint-plugin-vue @vue/eslint-config-typescript vue-eslint-parser
这里我们基础 ESLint 配置采用
eslint-config-standard
作为基础,你可以选择你喜欢的:pnpm i -D eslint-config-standard
接着在根目录创建
.eslintrc.js
和 .eslintignore
两个文件,配置大部分来自继承,在这基础上将一小部分不太适合组件库情况的做一些调整。.eslintrc.js
module.exports = {
root: true,
env: {
node: true
},
parser: 'vue-eslint-parser',
extends: [
'plugin:vue/vue3-essential',
'plugin:vue/vue3-strongly-recommended',
'plugin:vue/vue3-recommended',
'standard',
'@vue/typescript/recommended'
],
parserOptions: {
ecmaVersion: 'latest'
},
rules: {
'no-console':
process.env.NODE_ENV === 'production'
? [
'error',
{
allow: ['warn', 'error']
}
]
: 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
'space-before-function-paren': [
'error',
{
anonymous: 'always',
named: 'never',
asyncArrow: 'always'
}
],
'vue/match-component-file-name': 'off',
'vue/html-self-closing': 'off'
}
}
.eslintignore
dist/
node_modules/.*rc.js
*.config.js
*.css
*.pcss
*.scss
*.json
同时记得在编辑器上安装 ESLint 相关的插件,然后回过头看看先前的代码有没有报错,有的话就调整一下。
下一个是 Stylelint 全家桶~
当然了,如果你喜欢 Free Style 的话,完全可以不装 Stylelint 系列。
pnpm i -D postcss postcss-html postcss-preset-env
pnpm i -D stylelint stylelint-config-html stylelint-config-recess-order stylelint-config-recommended-vue
pnpm i -D stylelint-config-standard stylelint-config-standard-scss stylelint-order
在根目录创建
.stylelintrc.js
和 .stylelintignore
两个文件,Stylelint 为了适应 sass
的书写习惯,这里做了比较多的定制化,有兴趣深入了解的可以自行拓展阅读。.stylelintrc.js
module.exports = {
defaultSeverity: 'error',
extends: [
'stylelint-config-standard',
'stylelint-config-standard-scss',
'stylelint-config-html',
'stylelint-config-recommended-vue',
'stylelint-config-recess-order'
],
plugins: ['stylelint-order'],
rules: {
'no-empty-source': process.env.NODE_ENV === 'production' ? true : null,
'block-no-empty': process.env.NODE_ENV === 'production' ? true : null,
'string-quotes': 'single',
'at-rule-no-unknown': null,
'at-rule-no-vendor-prefix': true,
'declaration-property-value-disallowed-list': {
'/^transition/': ['/all/'],
'/^background/': ['http:', 'https:'],
'/^border/': ['none'],
'/.+/': ['initial']
},
'media-feature-name-no-vendor-prefix': true,
'property-no-vendor-prefix': true,
'selector-no-vendor-prefix': true,
'value-no-vendor-prefix': true,
'at-rule-empty-line-before': [
'always',
{
except: ['first-nested'],
ignore: [
'after-comment',
'blockless-after-same-name-blockless',
'blockless-after-blockless'
],
ignoreAtRules: ['else']
}
],
'no-descending-specificity': null,
'custom-property-empty-line-before': null,
'selector-class-pattern': [
'^([#a-z][$#{}a-z0-9]*)((-{1,2}|_{2})[$#{}a-z0-9]+)*$',
{
message: 'Expected class selector to be kebab-case'
}
],
'keyframes-name-pattern': [
'^([#a-z][$#{}a-z0-9]*)((-{1,2}|_{2})[$#{}a-z0-9]+)*$',
{
message: 'Expected keyframe name to be kebab-case'
}
],
'color-function-notation': null,
'scss/at-import-partial-extension': 'always',
'function-no-unknown': null,
'alpha-value-notation': 'percentage',
'scss/dollar-variable-empty-line-before': null,
'scss/operator-no-newline-after': null
},
ignoreFiles: [
/* see .stylelintignore */
]
}
.stylelintignore
dist/
node_modules/*.js
*.ts
*.tsx
*.svg
*.gif
*.md
一样别忘了在编辑器上安装 Stylelint 相关的插件,以获得编辑器的错误提示,之前的样式如果有报错那也跟着调整一下。
至此,我们就把最关键的两个 Lint 工具初始化了。
提交
提交 Message 的规范化和提交时的自动格式化代码,我们采用 Commitlint、lint-staged 和 husky 配合完成。
虽然 Commitlint 也属于 Lint 工具,不过因为涉及到 Git 提交,就专门放到这个章节一起讲。老样子,我们先来安装相关的依赖:
pnpm i -D @commitlint/cli @commitlint/config-conventional
pnpm i -D husky is-ci lint-staged && pnpm dlx husky install# 在 windows 系统下只需要把 && 改为 ;
即可拼接多条命令
pnpm i -D husky is-ci lint-staged;
pnpm dlx husky install
其中
pnpm dlx husky install
会帮助我们自动完成 husky 的初始化。之后我们在
package.json
的 scripts
下找到 prepare
这个命令,并做一些小调整:package.json
{
"scripts": {
"prepare": "is-ci || husky install"
}
}
安装完成后,我们开始配置 Commitlint,在根目录创建
.commitlintrc.js
,看到我们上面安装 Commitlint 的时候还多安装了一个配置,我们就继承这个配置再做一些拓展:.commitlintrc.js
module.exports = {
extends: ['@commitlint/config-conventional'],
rules: {
'body-leading-blank': [2, 'always'],
'footer-leading-blank': [1, 'always'],
'header-max-length': [2, 'always', 108],
'subject-empty': [2, 'never'],
'type-empty': [2, 'never'],
'type-enum': [
2,
'always',
[
'feat',
'fix',
'perf',
'style',
'docs',
'test',
'refactor',
'build',
'ci',
'chore',
'revert',
'wip',
'workflow',
'types',
'release'
]
]
}
}
这里可以稍微锁一下
type-enum
里面的内容,可以看到有很多的提交类型,这些类型来自于 Augular 提交规范,并在其基础上做了一些拓展,是目前开源社区里比较流行的方案,感兴趣的小伙伴可以自行拓展阅读。接着我们开始配置 husky,它可以让我们方便地创建一些 Git 钩子脚本,使的我们可以在提交过程的各个阶段执行一些命令,其中就包括调用各种 Lint 工具来检查代码和提交 Message。
先前我们执行了
pnpm dlx husky install
这个命令后,可以看到根目录下多了一个 .husky
的文件夹,这里面就是放置相关配置文件的地方。我们使用 husky 提供的命令创建
common
、commit-msg
和 pre-commit
三个脚本文件,或者直接在 .husky
目录下手动创建也可以:pnpm dlx husky add .husky/common
pnpm dlx husky add .husky/commit-msg
pnpm dlx husky add .husky/pre-commit
接着分别调整里面的内容:
.husky/common
command_exists () {
command -v "$1" >/dev/null 2>&1
}# Workaround for Windows 10, Git Bash and Yarn
if command_exists winpty && test -t 1;
then
exec < /dev/tty
fi
.husky/commit-msg
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
. "$(dirname "$0")/common"npx --no-install commitlint --edit "$1"
.husky/pre-commit
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
. "$(dirname "$0")/common"[ -n "$CI" ] && exit 0pnpm run precommit
husky 这样就算是配置好了,除去
common
是一个补丁脚本外,另外两个脚本会分别在我们进行提交 Message 处理时,以及即将开始提交时执行,我们也分别放入了调用 Commitlint 检查提交 Message 和一条 pnpm run precommit
的命令,不过这条命令在我们的项目里暂时还没有。下一步,我们来配置 lint-staged 的同时把这条命令补全。
lint-staged 的配置文件为
.lintstagedrc
,雨声习惯会把它一同放入 .husky
便于管理,你也可以放到自己喜欢的位置,比如根目录下。.husky/.lintstagedrc
{
"*.{js,jsx,ts,tsx}": [
"eslint --fix"
],
"*.{css,scss,html}": [
"stylelint --fix"
],
"*.vue": [
"eslint --fix",
"stylelint --fix"
]
}
然后我们在
package.json
中添加 precommit
命令:{
"scripts": {
"precommit": "lint-staged -c ./.husky/.lintstagedrc -q"
}
}
这样一来,在每次
pre-commit
脚本执行时,就会调用 lint-staged 对提交的文件执行 Lint 相关的命令,并且我们想要手动检查要提交的文件时也可以直接执行这条命令。事不宜迟,我们马上来进行我们的第一次提交~
git add .
为了验证一下 Commitlint 是否能正常工作,我们先来一个不符合规范的提交 Message:
git commit -m "init"
可以看到控制台输出了一些错误信息,并且提交失败了:
文章图片
确认了 Commitlint 可以正常工作后我们就来一次符合规范的提交:
git commit -m "chore: init"
提交成功,大功告成。
现在,我们的项目结构长这个样子:
文章图片
告一段落
本篇介绍了从零开始搭建一个 Vue3 组件库的第一步,完成了一个最简单的组件库项目,并且能够打包。
同时还介绍了三个 Lint 工具的装配,以及如何实现提交规范化及自动格式化代码。
尽管我们没有真的发布到 npm 上,不过我们如果想试试的话,可以使用 link 协议在本地的其他项目上试一试。
同样用
pnpm create vite
创建一个项目,在 package.json
的 dependencies
上手动添加 link 协议的依赖:{
"dependencies": {
"demo-ui": "link:到 demo-ui 的根目录的物理路径"
// 如 "link:D:/ui-library/demo-ui"
}
}
执行
pnpm i
后就可以像使用其他库一样,来使用本地的 demo-ui
了。未完待续
组件库项目的搭建还远不止这些,随着深入我们还会遇到诸如:
- 组件的开发工作流
- 组件库的发布流程设计
- 组件的单元测试
- 组件的样式的管理
- css 变量设计与主题
- 实现直接的按需加载
- 组件库文档的构建
- 组件库 Playground 的实现
- 使用 monorepo 管理组件库的多个模块
- 等等...
如果有不同的见解或建议,欢迎在评论区理性讨论,或者如果上面列举的点中有哪些更想知道的也可以留言哦。
最后,推荐一下雨声的开源组件库 Vexip-UI - GitHub(小伙伴们赏一个)
现正招募小伙伴来尝试使用或者维护与发展这个项目。
如果你对写组件感兴趣的话,非常欢迎来试一试,还有许多组件的功能等着你来开发与完善,你可以随时回复感兴趣的 issue 参与讨论或发起一个新的 issue。
与大家共勉,共同学习进步~
推荐阅读
- 7个 Vue3 中的组件通信方式
- Vue3中实现路由跳转的过渡动画(一)
- Vue3中插槽的概念和用法
- 关于Vue3的defineProps用法
- elementUI Tree树形控件如何设置默认树节点
- vue2 生命周期详解
- element-uishow-overflow-tooltip 使用
- 深入理解 v-model 的原理
- Vue.js 设计与实现 - 权衡的艺术