技术栈
脚手架: webpack4
文档站: storybook 参考 https://storybook.js.org/blog/storybook-vue3/
单元测试:jest
项目结构目录
|- .storybook
|- dist
|- node_modules
|- packages # 组件目录
|- |- button
|- | |- __tests__
|- | | |- Button.spec.ts
|- | |- button.vue
|- | |- index.ts
|- |- index.ts
|- stories # 文档目录
|- .babelrc
|- components.json
|- index.ts
|- jest.config.ts
|- package.json
|- README.md
|- tsconfig.json
|- webpack.config.js
|- yarn.lock
项目搭建流程
- init
mkdir project
cd project
yarn init
- 安装依赖
yarn add vue@next -D // vue3 不要装到 dep 里去,会和引用方的冲突
yarn add webpack@4 webpack-cli webpack-dev-server webpack-merge -dev // webpack 相关
yarn add html-webpack-plugin clean-webpack-plugin terser-webpack-plugin optimize-css-assets-webpack-plugin --dev // webpack 各种插件
- 配置文档站
文档站这里经过各种调研,选择了 storybook 已经支持了 vue3,具体用法就不具体写了,参考 https://storybook.js.org/blog/storybook-vue3/
值得注意的是, storybook 使用了 webpack5,笔者做的时候 vue3 的某些插件还没有支持 webpack5 所以需要在 package.json 里面手动改一下 webpack 的版本。
- 配置 webpack
新建 webpack.config.js
基本的配置就不细讲了,后面说一下组件库额外支持的一些功能
- 按需引入
按需引入的原理其实很简单,使用方按需引入是借助 babel-plugin-component 实现的,那么我们只要保证我们打包出来的文件符合 babel-plugin-component 的要求就可以了 其实也就是 多入口 多出口
新建 components.json
// components.json
{
"Button": "./packages/button/index.ts",
"index": "./packages/index.ts",
"Carousel": "./packages/carousel/index.ts"
}
更改 webpack.config.js 里面的入口文件,新增:
// webpack.config.js
const components = require('./components.json') // 引入 components.json
const entrys = {}
Object.keys(components).forEach(item => {
entrys[item] = components[item]
})
const config = {
...
entry: entrys // 多入口
output: {
path: path.join(__dirname, 'your path'),
publicPath: "/",
libraryTarget: 'umd',
library: 'your name',
globalObject: 'typeof self !== \'undefined\' ? self : this',
filename: "[name].js",
umdNamedDefine: true,
}
}
- 编写组件 以 button 组件为例 在 packages 下面新增文件夹 button,结构如下
|- button
|- |- button.vue
|- |- index.ts
然后在 components.json 下增加 button
// components.json
{
...
"Button": "./packages/button/index.ts"
}
index.ts 暴露 button 组件的 install 方法,只有暴露了 install 方法才是 vue 的一个插件
// index.ts
import Button from './button.vue'
import { App } from 'vue'
const install = (app: App) => {
app.component(Button.name, Button)
}
export default {
install,
}
button.vue 是 button 组件的逻辑
<template>
<button :class="styleClass" class="button" @click="handleClick">
<slot></slot>
</button>
</template>
<script lang="ts">
import { reactive, computed, defineComponent } from 'vue'
import type { PropType } from 'vue'
type IButtonType = PropType<'primary' | 'success' | 'warn' | 'danger' | 'info'>
export default defineComponent({
name: 'z-button',
emits: ['click'],
props: {
type: {
type: String as IButtonType,
default: 'primary'
},
disabled: {
type: Boolean,
default: false
}
},
setup(props, ctx) {
props = reactive(props)
const styleClass = computed(() => {
return [
`button-${props.type}`,
props.disabled ? 'not-allowed' : ''
]
})
const handleClick = args => {
ctx.emit('click', args)
}
return {
handleClick,
styleClass
}
}
})
</script>
<style>
.button{
color: #fff;
outline: none;
border: none;
border-radius: 2px;
padding: 6px 12px;
cursor: pointer;
margin: 4px;
display: inline;
}
.button:hover{
opacity: .9;
}
.not-allowed{
cursor: not-allowed;
opacity: .6;
}
.button-primary{
color: #fff;
background-color: #409eff;
border-color: #409eff;
}
.button-success{
color: #fff;
background-color: #5cb85c;
border-color: #4cae4c;
}
.button-info{
color: #fff;
background-color: #5bc0de;
border-color: #46b8da;
}
.button-danger{
color: #fff;
background-color: #d9534f;
border-color: #d43f3a;
}
.button-warn{
color: #fff;
background-color: #e6a23c;
border-color: #e6a23c;
}
</style>
好,现在一个基本的组件逻辑已经写完了,下面进行编写单元测试和本地自测
- 单元测试 单元测试框架使用的是 jest,首先安装 jest
yarn add -D jest babel-jest ts-jest vue-jest
新建 jest 配置文件 jest.config.ts
// jest.config.ts
module.exports = {
// 转义
transform: {
'^.+\\.vue$': 'vue-jest',
'^.+\\js$': 'babel-jest',
"^.+\\.(t|j)sx?$": "ts-jest"
},
moduleFileExtensions: ['vue', 'js', 'json', 'jsx', 'ts', 'tsx', 'node'],
modulePathIgnorePatterns: ['output', 'dist']
}
在 package.json 中添加 jest 的启动命令
// package.json
{
scripts: {
"unit": "jest",
}
}
这样你就可以通过运行 yarn unit 来进行单元测试了
下面我们在 button 文件夹下新增 tests 目录,然后在 tests 目录下新建 Button.spec.ts,填入以下内容
// Button.spec.ts
import Button from '../button.vue'
import { mount } from '@vue/test-utils'
const text = 'Huang.small is the best girl'
describe('button.vue', () => {
test('create', () => {
const wrapper = mount(Button, {
props: { type: 'primary'}
})
expect(wrapper.classes()).toContain('button-primary')
})
test('render text', () => {
const wrapper = mount(Button, {
slots: {
default: text
}
})
expect(wrapper.text()).toEqual(text)
})
test('handle click', () => {
const wrapper = mount(Button)
wrapper.trigger('click')
expect(wrapper.emitted()).toBeDefined()
})
test('disabled', () => {
const wrapper = mount(Button, {
props: {
disabled: true
}
})
expect(wrapper.classes()).toContain('not-allowed')
})
})
然后运行 yarn unit Button 可以看到 packages/button/tests/Button.spec.ts 中的测试被运行了,可以在终端看到测试运行结果
button.vue
✓ create (21 ms)
✓ render text (5 ms)
✓ handle click (5 ms)
✓ disabled (2 ms)
ok,这样我们的一个单测就完成了
- 本地测试 虽说文档站能看到组件的效果,但是还是要在本地项目实际测试一下的嘛
那么我们新建一个 vue3 项目,然后将组件库 link 到本地
// 在组件库所在目录运行
npm link
在本地用作试验的 vue3 项目中将刚才 link 到本地的 npm 包装上
yarn add {your package name}
在项目中引入
// main.js
import ZhengUI from 'zheng-ui-next'
createApp(App).use(ZhengUI).mount('#app')
// App.vue
<z-button type="primary">
Button
</z-button>
然后启动项目,查看,发现 Button 并不符合我们的预期,Button 的内容没有渲染出来,wtf ?遇到问题不要慌,开始排查
打开控制台,看到有个 warn
# Invalid VNODE type: Symbol(Fragment)
把错误信息谷歌一下,原来是因为项目里有两个 Vue 实例,很显然,一个 Vue 实例是项目本身的,另一个是我们的组件库带来的,检查我们的组件库
果然,发现 Vue 被我们错误放在了 dependencies 里,改一下,放到 devDependencies 里,重新打包
完美,现在可以看到我们的 Button 正常渲染了
- npm 发布
在每次发布之前,添加一些勾子
"prepublish": "npm run unit & npm run build"
发布
yarn publish
ok,这样我们的组件库就发布到线上啦,大功告成