我想写一个 Vue3 组件库,我该怎么开始()

如何从零开始搭建一个 Vue3 组件库? 最近有一些小伙伴问雨声:想要自己做一个组件库,但不知道如何开始?看到别人那些组件库各种各样、五花八门的配置文件和文件结构,根本不知道该从何下手。
别慌,其实再复杂的结构与配置,也是从一开始非常简单的项目,在开发过程中根据需要,经历不断的迭代、重构与完善后才逐步形成的。
我们想要开始的时候,其实最重要的第一步就是行动起来,先开一个最简单的项目,写一个最简单的组件,完成一次最简单的打包。
开始?!
创建项目
首先是包管理工具,推荐大家选用 pnpm,相较于其他包管理,它更快、更节约空间,还有一个很重要的优势就是能非常方便地创建和管理 monorepo,安装可自行查阅 官网。
然后就是脚手架的选择,既然是 Vue3 组件库,那 Vite 必然是不二之选了。首先它几乎可以说是 Vue3 的官配,其次在做库项目方面,Vite 在打包时没 Webpack 这么麻烦,在开发时也比 Rollup 更容易搭建开发服务。
我们直接使用 Vite 官方提供的 vue-ts 模版来快速创建一个原始项目:

pnpm create vite demo-ui --template vue-ts

之后我们把 Git 初始化一下:
git init

接着把 package.jsonvue-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 型项目。
这是我们原始创建后的结构:
我想写一个 Vue3 组件库,我该怎么开始()
文章图片

我们把 publicsrc/assetssrc/components/HelloWorld.vue 都删掉,添加 src/index.tssrc/components/button.vue
我想写一个 Vue3 组件库,我该怎么开始()
文章图片

其中的 src/App.vuesrc/main.ts 留着用于开发组件的时候随时看效果。
接着我们快快地写一个按钮组件,不管其他的,页面上能看到就行。
src/components/button.vue

src/App.vue

然后我们启动开发服务:
pnpm run dev

打开浏览器:
我想写一个 Vue3 组件库,我该怎么开始()
文章图片

平平无奇,没有任何样式的按钮组件出来了。
不过通常的组件库应该是以 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

打包组件
下一步我们就来完成我们的组件打包。
借助 vitebuild.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

我想写一个 Vue3 组件库,我该怎么开始()
文章图片

最后我们为 package.json 添加上 mainmodule 字段,那么一个可发布的组件库雏形就出来了(注意把 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 的预处理语言,当然你也可以选择你喜欢的方案,不过雨声认为 sasscss 的过渡足够平滑,而且功能也足够强大,所以比较喜欢用。
pnpm i -D sass

然后我们在 src 下创建一个 style 目录,用来专门存放样式文件,并添加 index.scssbutton.scss
我想写一个 Vue3 组件库,我该怎么开始()
文章图片

接着我们给 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'// 省略下面

这时我们就得到一个具有自定义样式的按钮组件了:
我想写一个 Vue3 组件库,我该怎么开始()
文章图片

再执行 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 进行打包,可以看到打包后的文件也包含类型声明文件了:
我想写一个 Vue3 组件库,我该怎么开始()
文章图片

最后在 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.jsonscripts 下找到 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 提供的命令创建 commoncommit-msgpre-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"

可以看到控制台输出了一些错误信息,并且提交失败了:
我想写一个 Vue3 组件库,我该怎么开始()
文章图片

确认了 Commitlint 可以正常工作后我们就来一次符合规范的提交:
git commit -m "chore: init"

提交成功,大功告成。
现在,我们的项目结构长这个样子:
我想写一个 Vue3 组件库,我该怎么开始()
文章图片

告一段落
本篇介绍了从零开始搭建一个 Vue3 组件库的第一步,完成了一个最简单的组件库项目,并且能够打包。
同时还介绍了三个 Lint 工具的装配,以及如何实现提交规范化及自动格式化代码。
尽管我们没有真的发布到 npm 上,不过我们如果想试试的话,可以使用 link 协议在本地的其他项目上试一试。
同样用 pnpm create vite 创建一个项目,在 package.jsondependencies 上手动添加 link 协议的依赖:
{ "dependencies": { "demo-ui": "link:到 demo-ui 的根目录的物理路径" // 如 "link:D:/ui-library/demo-ui" } }

执行 pnpm i 后就可以像使用其他库一样,来使用本地的 demo-ui 了。
未完待续
组件库项目的搭建还远不止这些,随着深入我们还会遇到诸如:
  • 组件的开发工作流
  • 组件库的发布流程设计
  • 组件的单元测试
  • 组件的样式的管理
  • css 变量设计与主题
  • 实现直接的按需加载
  • 组件库文档的构建
  • 组件库 Playground 的实现
  • 使用 monorepo 管理组件库的多个模块
  • 等等...
下一篇,我们继续围绕着从零开始搭建一个 Vue3 组件库会遇到的问题,继续讲讲如何完善组件库项目的配套设施。
如果有不同的见解或建议,欢迎在评论区理性讨论,或者如果上面列举的点中有哪些更想知道的也可以留言哦。
最后,推荐一下雨声的开源组件库 Vexip-UI - GitHub(小伙伴们赏一个)
现正招募小伙伴来尝试使用或者维护与发展这个项目。
如果你对写组件感兴趣的话,非常欢迎来试一试,还有许多组件的功能等着你来开发与完善,你可以随时回复感兴趣的 issue 参与讨论或发起一个新的 issue。
与大家共勉,共同学习进步~

    推荐阅读