一、前言
开源组件库
- Element-UI
- iView
CDD
组件优先的开发方式,组件驱动开发。
- 自上而下
- 从组件级别开始,到页面级别结束
CDD的好处:
- 组件在最大程度被重用
- 并行开发
- 可视化测试
二、组件开发相关Vue知识点
处理组件的边界情况
- $root:访问到Vue的根实例,操作根实例中的成员,可以在Vue根实例中存储共享数据,但还是推荐使用Vuex管理状态。

-
p
a
r
e
n
t
/
parent/
parent/children:获取父组件/子组件,并操作其中成员。大多数情况下,不推荐使用。

- $refs:可以访问子组件

- 依赖注入 provide/inject

-
a
t
t
r
s
/
attrs/
attrs/listeners
- 把父组件中非prop属性绑定到内部组件
- 把父组件中的DOM对象的原生事件绑定到内部组件

三、快速原型开发
- VueCli中提供了一个插件可以进行原型快速开发
- 需要先额为安装一个全局的扩展
npm install -g @vue/cli-service-global - 使用vue serve快速查看组件的运行效果
vue serve
- vue serve如果不指定参数默认会在当前目录找以下的入口文件
main.js、index.js、App.vue、app.vue - 可以指定哟啊加载的组件
vue serve ./src/Login.vue
基于Element-UI组件开发
- 初始化package.json
npm init -y - 安装Element-UI
vue add element - 加载Element-UI,使用Vue.use()安装插件
四、组件开发
组件分类
- 第三方组件
- 基础组件
- 业务组件
组件开发-表单组件
整体结构
- Form
<template>
<form>
<slot></slot>
</form>
</template>
<script>
export default {
name: 'LgForm',
provide() {
return {
form: this
}
},
props: {
model: {
type: Object
},
rules: {
type: Object
}
},
methods: {
validate(cb) {
const tasks = this.$children
.filter(child => child.prop)
.map(child => child.validate())
Promise.all(tasks)
.then(() => cb(true))
.catch(() => cb(false))
}
}
}
</script>
<style>
</style>
- FormItem
<template>
<div>
<label>{{ label }}</label>
<div>
<slot></slot>
<p v-if="errMessage">{{ errMessage }}</p>
</div>
</div>
</template>
<script>
import AsyncValidator from 'async-validator'
export default {
name: 'LgFormItem',
inject: ['form'],
props: {
label: {
type: String
},
prop: {
type: String
}
},
mounted() {
this.$on('validate', () => {
this.validate()
})
},
data() {
return {
errMessage: ''
}
},
methods: {
validate() {
if (!this.prop) return
const value = this.form.model[this.prop]
const rules = this.form.rules[this.prop]
const descriptor = { [this.prop]: rules }
const validator = new AsyncValidator(descriptor)
return validator.validate({ [this.prop]: value }, errors => {
if (errors) {
this.errMessage = errors[0].message
} else {
this.errMessage = ''
}
})
}
}
}
</script>
<style>
</style>
- Input
<template>
<div>
<input v-bind="$attrs" :type="type" :value="value" @input="handleInput" />
</div>
</template>
<script>
export default {
name: 'LgInput',
inheritAttrs: false,
props: {
value: {
type: String
},
type: {
type: String,
default: 'text'
}
},
methods: {
handleInput(evt) {
this.$emit('input', evt.target.value)
const findParent = parent => {
while (parent) {
if (parent.$options.name === 'LgFormItem') {
break
} else {
parent = parent.$parent
}
}
return parent
}
const parent = findParent(this.$parent)
if (parent) {
parent.$emit('validate')
}
}
}
}
</script>
<style>
</style>
- Button
<template>
<div>
<button @click="handleClick">
<slot></slot>
</button>
</div>
</template>
<script>
export default {
name: 'LgButton',
methods: {
handleClick(evt) {
this.$emit('click', evt)
evt.preventDefault()
}
}
}
</script>
<style>
</style>
五、Monorepo
两种项目的组织方式:
- Multirepo:每一个包对应一个项目
- Monorepo:一个项目仓库中管理多个模块/包
目录结构:
六、Storybook
官网地址: https://storybook.js.org/
介绍
- 可视化的组件展示平台
- 在隔离的开发环境中,以交互式的方式展示组件
- 独立开发组件
- 支持的框架
- React、React Native、Vue、Angular
- Ember、HTML、Svelte、Mithril、Riot
Storybook安装
- 自动安装
npx -p @storybook/cli sb init --type vue
yarn add vue
vue yarn add vue-loader vue-template-compiler --dev
注:npx如果安装失败,可以这样解决:去掉空格
- 手动安装
- 运行

七、Lerna+yarn workspaces
yarn workplaces
可以解决项目组重复安装依赖的问题:

开启yarn的工作区
项目根目录的package.json中添加:
"private": true,
"workspaces": [
"packages/*"
]
yarn workplaces使用
- 给工作区根目录安装开发依赖
yarn add jest -D -W - 给指定工作区安装依赖
yarn workspaces violet-button add lodash@4 - 给所有的工作区安装依赖
yarn install
Lerna
介绍
- Lerna是一个优化使用git和npm管理多包仓库的工作流工具
- 用于管理具有多个包的JavaScript项目
- 它可以一键把代码提交到git和npm仓库
使用
- 全局安装
yarn global add lerna - 初始化
lerna init - 发布
lerna publish
八、Vue组件的单元测试
组件单元测试好处
- 提供描述组件行为的文档
- 节省手动测试的时间
- 减少研发新特性时产生的bug
- 改进设计
- 促进重构
安装依赖
- Vue Test Utils
- Jest
- vue-jest
- babel-jest
安装:
yarn add jest @vue/test-utils vue-jest babel-jest -D -W
配置测试脚本
package.json:
"scripts": {
"test": "jest",
}
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) 断言
-
匹配器
- toBe(value) 判断值是否相等
- toEqual(obj) 判断对象是否相等
- toContain(value) 判断数组或字符串中是否包含
- …
-
快照
- toMatchSnapshot()
Vue Test Utils常用API
-
mount()
创建一个包含被挂载和渲染的Vue组件的Wrapper -
Wrapper
- vm wrapper包裹的组件实例
- props() 返回Vue实例选项中的props对象
- html() 组件生产的HTML标签
- find() 通过选择器返回匹配到的组件中的DOM元素
- trigger() 触发DOM原生事件,自定义事件wrapper.vm.$emit()
- …
组件测试示例
九、Rollup打包
介绍
- Rollup是一个模块打包器
- Rollup支持Tree-shaking
- 打包的结果比Webpack要小
- 开发框架/组件库的时候使用Rollup更合适
安装依赖
- Rollup
- rollup-plugin-terser
- rollup-plugin-vue@5.1.9
- vue-template-compiler
yarn add rollup rollup-plugin-terser rollup-plugin-vue@5.1.9 vue-template-compiler -D -W
Rollup 配置文件
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()
]
}
]
配置 build 脚本并运行
找到 button 包中的 package.json 的 scripts 配置
"build": "rollup -c"
运行打包
yarn workspace lg-button run build
打包所有组件
安装依赖
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"
设置环境变量
清理/删除无用的内容
- stories文件夹
- storybook-static
设置环境变量
- 安装依赖:跨平台设置环境变量
yarn add cross-env -D -W - 配置package.json
"scripts": {
"build:prod": "cross-env NODE_ENV=production rollup -c",
"build:dev": "cross-env NODE_ENV=development rollup -c"
}
- 运行:yarn build:prod
prod与dev区别:prod模式打包后的js是压缩过的。
清理
清理所有包中的node_modules
"scripts": {
"clean": "lerna clean"
}
运行:yarn clean
清理所有包中的dist
- 安装依赖:rimraf
yarn add rimraf -D -W - 为packages包中每个组件的package.json文件配置del命令
"scripts": {
"del": "rimraf dist"
}
- 运行:yarn workspaces run del(会执行所有组件中的del命令)
十、基于模板生成包的结构
- 安装plop:yarn add plop -D -W
- 写模板

- 写plop配置文件
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"
}
- 运行
yarn plop

会根据模板生成link组件:

- 发布组件到npm仓库
- 打包:yarn build:prod
- 登录npm账号

- 发布: yarn lerna
- 检查

本文详细介绍了前端组件化开发的实践,包括Vue组件的边界情况处理、基于Element-UI的快速原型开发、组件分类与开发、Monorepo的使用、Storybook和Lerna+yarn workspaces的集成、Vue组件的单元测试以及Rollup打包。文章旨在帮助开发者掌握前端组件化的完整流程和最佳实践。

545

被折叠的 条评论
为什么被折叠?



