Vue进阶之 组件库开发
重新识$attrs / $listeners
- $attrs
把父组件中非 prop 属性绑定到内部组件 利用 v-bind="$attrs" - $listeners
把父组件中的 DOM 对象的原生事件绑定到内部组件 利用 v-on="$listeners"
父组件
...
<template>
<div>
<myinput
required
placeholder="Enter your username"
class="theme-dark"
@focus="onFocus"
@input="onInput"
data-test="test">
</myinput>
</div>
</template>
...
子组件
<template>
<!--
1. 从父组件传给自定义子组件的属性,如果没有 prop 接收
会自动设置到子组件内部的最外层标签上
如果是 class 和 style 的话,会合并最外层标签的 class 和 style
-->
<!-- <input type="text" class="form-control" :placeholder="placeholder"> -->
<!--
2. 如果子组件中不想继承父组件传入的非 prop 属性,可以使用 inheritAttrs 禁用继承
然后通过 v-bind="$attrs" 把外部传入的非 prop 属性设置给希望的标签上
但是这不会改变 class 和 style
-->
<!-- <div>
<input type="text" v-bind="$attrs" class="form-control">
</div> -->
<!--
3. 注册事件
-->
<!-- <div>
<input
type="text"
v-bind="$attrs"
class="form-control"
@focus="$emit('focus', $event)"
@input="$emit('input', $event)"
>
</div> -->
<!--
4. $listeners
-->
<div>
<input
type="text"
v-bind="$attrs"
class="form-control"
v-on="$listeners"
>
</div>
</template>
<script>
export default {
// props: ['placeholder', 'style', 'class']
// props: ['placeholder']
inheritAttrs: false
}
</script>
快速开发组件
组件库结构
- Monorepo (Monolithic Repository) 推荐使用这种模式
一个项目仓库中管理多个模块/包
目录结构
package
├── button #各个组件,每个组件可单独发布
│ ├── _test_ #测测试相关
│ ├── dist #打包
│ ├── index.js #入口
│ └── src #源码
│ └── README.md #文档
│ └── package.json #配置
├── form # form
├── input # input
└── steps # steps
工具推荐
Storybook 可以参考 Storybook
yarn Workspaces ,yarn的工作区,将内部的公共第三方插件提升至公共的 node_modules 中(注意npm不支持这个)
使用,在项目根目录的package.json中添加
"private": true,
"workspaces": [
"packages/*" //公共目录
],
- 给工作区根目录安装开发依赖
yarn add jest-d-w - 给指定工作区安装依赖
yarn Workspace Ig-button add lodash@4 - 给所有的工作区安装依赖
yarn install
Lerna 介绍
- Lerna 是一个优化使用 git 和 npm 管理多包仓库的工作流工具
- 用于管理具有多个包的 Javascript 项目
- 它可以一把代码提交到 git 和 npm 仓库
单元测试
使用 jest 测试 可参考 官方文档 JSET
安装测试依赖
yarn add jest @vue/test-utils vue-jest babel-jest -D -W
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"
}
}
Babel 的配置
babel.config.js
module.exports = {
presets: [
[
'@babel/preset-env'
]
]
}
Babel 的桥接
yarn add babel-core@bridge -D -W
Jest 常用 API
- 全局函数
describe (name, fn)把相关测试组合在一起
test (name, fn)测试方法 测试函数
expect (value)断言 函数返回值,将次值可以和其他值进行比较和判断 - 匹配器
toBel(value) 判断值是否相等
toEqual(obj) 判断对象是否相等
toContain(value) 判断数组或者字符串中是否包含
… - 快照
tomatchsnapshot() 将一次结果生成快照,再下一次测试时进场前后对比
Vue Test Utils 常用 API
- mount()
创建一个包含被挂载和渲染的 ue 组件的 Wrapper - Wrapper
vm Wrapper 包裹的组件实例
props() 返回 Vue 实例选项中的 props 对象
html() 组件生成的 HTML 标签
find() 通过选择器返回匹配到的组件中的 DOM 元素
trigger() 触发 DOM 原生事件, 自定义事件 wrapper.vm.$emit()
…
单元测试文件
__tests__文件夹下的 input.test.js 文件
import input from '../src/input.vue' //引入组件
import { mount } from '@vue/test-utils' //vue的jest测试插件,用来挂载组件
describe('lg-input', () => { //测试块儿
test('input-text', () => { //测试函数
const wrapper = mount(input)
expect(wrapper.html()) //wrapper.html() input组件的返回值
test('input-password', () => {
const wrapper = mount(input, {
propsData: {
type: 'password'
}
})
expect(wrapper.html()).toContain('input type="password"') //wrapper.html() input组件的返回值是否包含'input type="password"'
})
test('input-password', () => {
const wrapper = mount(input, { //Vue实例传入配置
propsData: {
type: 'password',
value: 'admin'
}
})
expect(wrapper.props('value')).toBe('admin')
})
test('input-snapshot', () => {
const wrapper = mount(input, {
propsData: {
type: 'text',
value: 'admin'
}
})
expect(wrapper.vm.$el).toMatchSnapshot() //生成快照,就是当前目录的 __snapshots__ 文件夹中
})
})
打包及发布
利用 Rollup 工具 打包单个组件
安装配置
- Rollup
- rollup-plugin-terser // 对代码进行压缩
- rollup-plugin-vue@5. 1. 9 // 编译Vue文件, 最新版支持Vue3.0
- vue-template-compiler // 模板编译器
yarn add rollup rollup-plugin-terser rollup-plugin-vue@5.1.9 vue-template-compiler -D -W
配置文件 在 button 目录中创建 rollup.config.js
import { terser } from 'rollup-plugin-terser'
import vue from 'rollup-plugin-vue'
module.exports = [
{
input: 'index.js',
output: [
{
file: 'dist/index.js',
format: 'es'
}
],
plugins: [
vue({
// Dynamically inject css as a <style> tag
css: true,
// Explicitly convert template to render function
compileTemplate: true
}),
terser()
]
}
]
package.json中配置命令
"scripts": {
"build": "rollup -c"
}
运行打包
yarn workspace lg-button run build
打包所有组件
安装依赖
- @rollup/plugin-json // 将json文件变为模块加载
- rollup-plugin-postcss // postcss
- @rollup/plugin-node-resolve // 打包第三方包儿
yarn add @rollup/plugin-json rollup-plugin-postcss @rollup/plugin-node-resolve -D -W
配置文件
项目根目录创建 rollup.config.js
import fs from 'fs'
import path from 'path'
import json from '@rollup/plugin-json'
import vue from 'rollup-plugin-vue'
import postcss from 'rollup-plugin-postcss'
import { terser } from 'rollup-plugin-terser'
import { nodeResolve } from '@rollup/plugin-node-resolve'
const isDev = process.env.NODE_ENV !== 'production' //环境设置
// 公共插件配置
const plugins = [
vue({
// Dynamically inject css as a <style> tag
css: true,
// Explicitly convert template to render function
compileTemplate: true
}),
json(),
nodeResolve(),
postcss({
// 把 css 插入到 style 中
// inject: true,
// 把 css 放到和js同一目录
extract: true
})
]
// 如果不是开发环境,开启压缩
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 => {
const pkg = require(path.resolve(root, item, 'package.json'))
return {
input: path.resolve(root, item, '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
}
})
在每一个包中设置 package.json 中的 main 和 module 字段
每个模块标明入口
"main": "dist/cjs/index.js",
"module": "dist/es/index.js"
根目录的 package.json 中配置 scripts
"build": "rollup -c"
rollup配置完成
设置环境变量
利用 cross-env 插件
安装后配置 package.json 中配置 scripts
"build:prod": "cross-env NODE_ENV=production rollup -c",
"build:dev": "cross-env NODE_ENV=development rollup -c"
清除打包文件夹
利用插件 rimraf 安装后配置 package.json 中配置 scripts
"del": "rimraf dist"
删除node_modules package.json 中配置 scripts
"clean": "lerna clean",
利用模板生成组件基本结构
利用 plop 插件 安装后编写配置和模板文件
模板可在 plop-template/component文件夹下
根目录配置 plopfile.js
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'
},
{
type: 'add',
path: 'packages/{{name}}/__tests__/{{name}}.test.js',
templateFile: 'plop-template/component/__tests__/component.test.hbs'
},
{
type: 'add',
path: 'packages/{{name}}/stories/{{name}}.stories.js',
templateFile: 'plop-template/component/stories/component.stories.hbs'
},
{
type: 'add',
path: 'packages/{{name}}/index.js',
templateFile: 'plop-template/component/index.hbs'
},
{
type: 'add',
path: 'packages/{{name}}/LICENSE',
templateFile: 'plop-template/component/LICENSE'
},
{
type: 'add',
path: 'packages/{{name}}/package.json',
templateFile: 'plop-template/component/package.hbs'
},
{
type: 'add',
path: 'packages/{{name}}/README.md',
templateFile: 'plop-template/component/README.hbs'
}
]
})
}
配置命令 package.json 中配置 scripts
"plop": "plop"
运行
npm run plop
#或者
yarn plop