前言
在日常開發(fā)過程中,構(gòu)建組件庫是必不可少的一環(huán),此篇文章就是描述如何搭建一個(gè)完整的組件庫,解決組件庫開發(fā)發(fā)布過程中的如下問題:
1.如何在最少的依賴下快速開發(fā)一個(gè)vue組件。
2.如何將所有的包放置在一個(gè)git倉庫內(nèi)。
3.如何將git倉庫內(nèi)的所有包一鍵發(fā)布。
4.如何管理所有包的依賴,減少包的體積。
5.如何快速創(chuàng)建組件示例。
6.如何打包組件,webpack?
快速原型開發(fā)
開發(fā)組件和開發(fā)項(xiàng)目是不一樣的,在開發(fā)組件的時(shí)候,我們希望能夠有一種工具能夠快速針對(duì)某個(gè)vue文件搭建開發(fā)環(huán)境,并且在發(fā)布的時(shí)候能夠?qū)ζ溥M(jìn)行打包編譯,此時(shí)我們可以使用@vue/cli-service-global。
- 全局安裝
此包必須全局安裝:
npm install -g @vue/cli-service-global
- 創(chuàng)建vue文件
在根目錄下創(chuàng)建App.vue文件:
<template>
<h1>Hello!</h1>
</template>
- 啟動(dòng)開發(fā)服務(wù)器
在命令行中執(zhí)行:
vue serve
入口可以是 main.js、index.js、App.vue 或 app.vue 中的一個(gè)。你也可以顯式地指定入口文件:
vue serve App.vue
- 執(zhí)行打包
vue build
打包完成后,打包結(jié)果會(huì)放到dist目錄下, 默認(rèn)情況下,會(huì)打包生成一個(gè)應(yīng)用,該應(yīng)用包含html和資源文件,可以直接部署為靜態(tài)站點(diǎn)。但是通常情況下,我們需要將組件打包成一個(gè)庫,以便發(fā)布后供項(xiàng)目使用。
打包成庫需要指定構(gòu)建目標(biāo):
vue build --target lib
添加構(gòu)建目標(biāo)后,執(zhí)行打包,dist目錄中包含各種規(guī)范的js文件和一個(gè)demo示例html。
目前為止,快速開發(fā)vue組件已經(jīng)完成,我們可以快樂的開發(fā)各種組件,但是,當(dāng)所需開發(fā)的組件慢慢變多之后,文件的組織方式成為我們需要考慮的事情。
可以想到有以下三種方式組織文件結(jié)構(gòu):
1.每一個(gè)組件都是一個(gè)單獨(dú)的倉庫。
2.一個(gè)倉庫中包含多個(gè)組件vue文件,作為一個(gè)包發(fā)布。
3.一個(gè)倉庫中包含多個(gè)組件包,每個(gè)組件包單獨(dú)發(fā)布。
第一種方式,每一個(gè)組件都是一個(gè)單獨(dú)倉庫,雖然有利于組件開發(fā),但是組件維護(hù)起來比較麻煩。組件越多,需要維護(hù)的倉庫也就越多,當(dāng)其中部分組件依賴的如lodash需要升級(jí)時(shí),我們需要一個(gè)個(gè)進(jìn)行升級(jí),比較麻煩。
第二種方式,將所有的組件作為一個(gè)包發(fā)布,雖然維護(hù)比較方便,但是發(fā)布后,別人只想使用其中的一個(gè)組件時(shí),會(huì)需要把整個(gè)組件庫引入,如果不提供按需加載,那么會(huì)造成項(xiàng)目中引入很多不必要的代碼。
第三種方式可參考下文。
monorepo
當(dāng)我們查看 vue3 源碼時(shí),可以看到,倉儲(chǔ)結(jié)構(gòu)如下:
packages
├── compiler-core
├──_tests_ #單元測試
├──src #源文件目錄
├──package.json
├── compiler-dom
├──_tests_ #單元測試
├──src #源文件目錄
├──package.json
package.json
這個(gè)就是典型的monorepo ,monorepo是項(xiàng)目代碼的一種管理方式,指在一個(gè)倉庫中管理多個(gè)模塊/包。
monorepo追求的是在一個(gè)倉庫中管理多個(gè)模塊,每個(gè)模塊有獨(dú)立的package.json管理各自依賴,同時(shí)在項(xiàng)目根目錄下可以通過命令安裝或升級(jí)模塊依賴,并提供了一個(gè)模塊共享的node_modules。
yarn workspace
yarn workspace 是實(shí)現(xiàn)monorepo的一種方式。
使用yarn workspace要求在根目錄的package.json中添加如下屬性:
{
"private": true,
"workspaces": ["packages/*"]
}
private屬性指定根目錄是私有的,不會(huì)被發(fā)布工具發(fā)布到npm上。
workspace屬性指定組件所在文件夾,支持通配符。
修改完package.json之后,按照vue-next的項(xiàng)目結(jié)構(gòu)在packages文件夾下創(chuàng)建input測試組件。
假設(shè),自定義的input組件依賴dayjs包,可以在根目錄下執(zhí)行如下命令安裝:
yarn workspace m-input add dayjs
其中m-input并不是packages下組件文件夾的名稱,而是組件文件夾下package.json中的name屬性值。
安裝完成后,dayjs會(huì)自動(dòng)添加到input組件的package.json下,但是包下載到了根目錄下的node_modules文件夾中,這樣做可以更好的管理多組件包的依賴。如果當(dāng)前組件依賴的包版本和其他組件依賴的包版本不一樣,如其他組件依賴lodash@4,當(dāng)前組件依賴lodash@3, 此時(shí)依賴包會(huì)被下載到當(dāng)前組件文件夾下的node_modules中。
通過yarn workspace可以執(zhí)行某個(gè)組件下的npm scripts,如給input組件添加一個(gè)build命令,可以在根目錄下通過如下命令執(zhí)行build:
yarn workspace m-input run build
對(duì)于build這種命令,幾乎所有組件都需要,那么yarn workspace提供了一個(gè)快捷命令,可以一鍵執(zhí)行所有組件包的build命令:
yarn workspaces run build
storybook
目前為止,倉庫的整體文件結(jié)構(gòu)和組件庫的依賴包管理都已經(jīng)完成了,可以愉快的開發(fā)組件了,當(dāng)組件開發(fā)完成后,一般開發(fā)人員都會(huì)編寫相應(yīng)的使用文檔,文檔中包含相應(yīng)的使用示例.
storybook是可視化的組件管理展示平臺(tái),支持在隔離的開發(fā)環(huán)境中,以交互式的方式展示組件,支持vue,react等。
安裝使用:
npx -p @storybook/cli sb init --type vue
yarn add vue -W
yarn add vue-loader vue-template-compiler --dev -W
修改配置:
安裝完成之后,在根目錄的.storybook文件夾下存放著storybook使用的所有配置文件,修改main.js中stories屬性,將其指向packages所有組件下的.stories.js文件。
"stories": [
"../packages/**/*.stories.mdx",
"../packages/**/*.stories.@(js|jsx|ts|tsx)"
]
添加組件示例:
在input組件包中添加 Input.stories.js 文件:
import MInput from './index'
export default {
title: 'MInput',
component: MInput
};
export const Text = () => ({
components: { MInput },
template: '<m-input />',
});
export const Password = () => ({
components: { MInput },
template: '<m-input type="password" placeholder="請(qǐng)輸入密碼"/>',
});
其中默認(rèn)導(dǎo)出是storybook頁面左側(cè)導(dǎo)航欄,每一個(gè)具名導(dǎo)出都是一個(gè)樣例。
最終執(zhí)行yarn storybook,打開站點(diǎn):

lerna
lerna 是babel團(tuán)隊(duì)開源的用于管理多包倉庫的工具,也可以用于實(shí)現(xiàn)monorepo。
安裝lerna:
npm install lerna -g
初始化lerna:
lerna init
會(huì)在項(xiàng)目根目錄下添加lerna.json配置文件。
可以使用lerna管理項(xiàng)目依賴:
如果當(dāng)前form自定義組件依賴input自定義組件,可以使用:
lerna add input --scope=form
還可以使用import命令導(dǎo)入本地包:
lerna import <path-to-external-repository>
通過exec和run執(zhí)行包里面的相關(guān)命令
lerna run --scope my-component test
通過clean命令一鍵清除所有包的node_modules目錄:
lerna clean
learn最主要的功能是一鍵發(fā)布所有包的npm上:
lerna publish
發(fā)布包到npm需要登錄,可以通過 npm whoami 查看當(dāng)前登錄用戶,通過 npm login 進(jìn)行登錄。
單元測試
單元測試是組件化開發(fā)中必不可少的部分
安裝依賴:
npm i jest @vue/test-utils vue-jest babel-jest -D
1.添加jest配置文件jest.config.js
module.exports = {
"testMatch": ["**/_tests_/**/*.[jt]s?(x)"],
"moduleFileExtensions": [
"js",
"json",
// 告訴 Jest 處理 `*.vue` 文件
"vue"
],
"transform": {
// 用 `vue-jest` 處理 `*.vue` 文件
".*\\.(vue)$": "vue-jest",
// 用 `babel-jest` 處理 js
".*\\.(js)$": "babel-jest"
}
}
1.添加babel配置文件babel.config.js
module.exports = {
presets: [
[
'@babel/preset-env'
]
]
}
1.添加測試命令
"test": "jest"
1.添加測試文件
在組件包的tests文件夾下添加相關(guān)js文件,如input包下面添加input.test.js
import input from '../src/index.js'
import { mount } from '@vue/test-utils'
describe('m-input', () => {
test('input-text', () => {
const wrapper = mount(input)
expect(wrapper.html()).toContain('input type="text"')
})
})
1.執(zhí)行測試命令
yarn test
測試可以在命令行中看到單元測試執(zhí)行結(jié)果:

rollup打包
rollup是一個(gè)基于ESM的模塊打包工具,和webpack相比,其打包結(jié)果更小,因此適合打包框架或者組件庫。
安裝必須的依賴:
npm i rollup rollup-plugin-terser rollup-plugin-vue@5.1.9 vue-template-compiler -D
需要注意的是安裝vue時(shí)需要指定版本,否則會(huì)安裝vue3。
- 單組件打包
1.添加配置文件
在組件中添加rollup.config.js文件,該文件是rollup打包的配置文件,指定起始文件,輸出文件位置及格式,插件。
import { terser } from 'rollup-plugin-terser'
import vue from 'rollup-plugin-vue'
module.exports = {
input: 'src/index.js',
output: [
{
file: 'dist/index.js',
format: 'es'
}
],
plugins: [
vue({
css: true,
compileTemplate: true
}),
terser()
]
}
1.添加可執(zhí)行命令
在package.json文件的scripts屬性下添加打包命令:
"build": "rollup -c"
-c指的是使用當(dāng)前項(xiàng)目目錄下的配置文件rollup.config.js
1.執(zhí)行命令
yarn build
執(zhí)行完畢之后,可以看到打包結(jié)果。
- 多組件打包
雖然可以用上述單組件打包的方式為每一個(gè)組件打包,但是這樣比較麻煩,可以在項(xiàng)目根目錄下通過一個(gè)配置文件打包所有組件。
此時(shí)需要添加額外依賴:
npm i @rollup/plugin-json rollup-plugin-postcss @rollup/plugin-node-resolve cross-env -D
1.為組件指定入口文件
在每個(gè)包下的package.json文件中添加main和module屬性:
"main": "dist/cjs/index.js",
"module": "dist/es/index.js",
1.設(shè)置環(huán)境變量
利用cross-env設(shè)置環(huán)境變量,區(qū)分開發(fā)環(huán)境和生產(chǎn)環(huán)境:
"build:prod": "cross-env NODE_ENV=production rollup -c",
"build:dev": "cross-env NODE_ENV=development rollup -c"
1.添加配置文件
在項(xiàng)目的根目錄下添加rollup.config.js文件,該文件會(huì)遍歷packages文件夾下的所有文件夾并打包:
import fs from 'fs'
import path from 'path'
import json from '@rollup/plugin-json'
import vue from 'rollup-plugin-vue'
import { terser } from 'rollup-plugin-terser'
import postcss from 'rollup-plugin-postcss'
import { nodeResolve } from '@rollup/plugin-node-resolve'
const isDev = process.env.NODE_ENV !== 'production'
// 公共插件配置
const plugins = [
vue({
css: true,
compileTemplate: true
}),
json(),
nodeResolve(),
postcss({
// 把 css 插入到 style 中
// inject: true,
// 把 css 放到和js同一目錄
extract: true
})
]
// 如果不是開發(fā)環(huán)境,開啟壓縮
isDev || plugins.push(terser())
// packages 文件夾路徑
const root = path.resolve(__dirname, 'packages')
module.exports = fs.readdirSync(root)
.filter(item => fs.statSync(path.resolve(root, item)).isDirectory())
.map(item => {
// 獲取每個(gè)包的配置文件
const pkg = require(path.resolve(root, item, 'package.json'))
return {
input: path.resolve(root, item, 'src/index.js'),
output: [
{
exports: 'auto',
file: path.resolve(root, item, pkg.main),
format: 'cjs'
},
{
exports: 'auto',
file: path.join(root, item, pkg.module),
format: 'es'
},
],
plugins: plugins
}
})
此時(shí)執(zhí)行打包命令,可以一次性為所有組件包打包。
現(xiàn)在有個(gè)問題,每次打包的時(shí)候需要?jiǎng)h除上次打包結(jié)果,因此需要添加刪除命令:
安裝依賴包:
npm i -D rimraf
為每個(gè)組件包添加del命令:
"del": "rimraf dist"
在根目錄下添加clean命令:
"clean": "yarn workspaces run del"
此時(shí)執(zhí)行yarn clean 就可以清除所有包的dist目錄。
plop模版
截止到目前為止,項(xiàng)目的整體結(jié)構(gòu)已經(jīng)完成,接下來就是無休止的添加組件了,但是考慮到每個(gè)組件的初始化有很多相同的工作需要手動(dòng)完成,此時(shí)可以通過plop將這部分工作交給機(jī)器。
安裝依賴:
npm i plop -D
1.創(chuàng)建模版文件
在項(xiàng)目中添加plop-template/component文件夾,此文件夾下放置創(chuàng)建組件用的所有模版文件。
1.添加plopfile.js
該文件是plop插件執(zhí)行的入口文件:
module.exports = plop => {
plop.setGenerator('component', {
description: 'create a custom component',
prompts: [
{
type: 'input',
name: 'name',
message: 'component name',
default: 'MyComponent'
}
],
actions: [
{
type: 'add',
path: 'packages/{{name}}/src/{{name}}.vue',
templateFile: 'plop-template/component/src/component.hbs'
}
]
})
}
為plop添加一個(gè)可執(zhí)行的命令,該命令會(huì)詢問用戶組件的名稱,然后將模版中所有的文件拷貝到packages相關(guān)文件夾內(nèi)。
1.添加scripts命令
"plop": "plop"
此時(shí)在命令行中執(zhí)行yarn plop component就可以創(chuàng)建組件了。
