开发属于你的 vue3 UI 组件库

1,327 阅读3分钟

技术栈

脚手架: 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

项目搭建流程

  1. init
mkdir project
cd project
yarn init 
  1. 安装依赖
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 各种插件
  1. 配置文档站
文档站这里经过各种调研,选择了 storybook 已经支持了 vue3,具体用法就不具体写了,参考 https://storybook.js.org/blog/storybook-vue3/

值得注意的是, storybook 使用了 webpack5,笔者做的时候 vue3 的某些插件还没有支持 webpack5 所以需要在 package.json 里面手动改一下 webpack 的版本。
  1. 配置 webpack
新建 webpack.config.js
基本的配置就不细讲了,后面说一下组件库额外支持的一些功能
  1. 按需引入

按需引入的原理其实很简单,使用方按需引入是借助 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,
    }
}
  1. 编写组件 以 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>

好,现在一个基本的组件逻辑已经写完了,下面进行编写单元测试和本地自测

  1. 单元测试 单元测试框架使用的是 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,这样我们的一个单测就完成了

  1. 本地测试 虽说文档站能看到组件的效果,但是还是要在本地项目实际测试一下的嘛

那么我们新建一个 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 正常渲染了

  1. npm 发布

在每次发布之前,添加一些勾子

"prepublish": "npm run unit & npm run build"

发布

yarn publish

ok,这样我们的组件库就发布到线上啦,大功告成