14.1 组件开发基础
CDD 基础
- CDD(Component-Driven Development)
- 自上而下
- 从组件级别开始,到页面级别结束
- 先从相对完的的设计中抽象出来组件,先隔离开发组件然后再开发页面
CDD的好处
- 组件在最大程度被重用
- 并行开发
- 对单个组件的开发使用CDD可以让以页面级开发无法实现的方式在不同团队之间共享任务:开发相对隔离的组件
- 可视化测试
- 通过一些工具可以直接浏览一些组件,而不需要到业务系统中再测试组件,可以对不同组件的状态进行测试
处理组件的边界情况
src/main.js
:在 Vue 的根实例中设置了title
属性,在下面设置了handle
方法打印了title
的值
- $root
src/01-root/
:使用插着表达式把$root.title
打印了出来,点击第一个按钮调用$root.handle
获取title的值并打印到控制台,点击第二个按钮改变title
查看其是否为响应式的- 建议在组件比较少的小型项目中使用,实际开发中如果状态较多会难以维护
- $parent / $children
src/02-parent/
:创建了三个嵌套组件,在chile.vue
中使用$parent.title
获取parent.vue
的title
并显示出来,同时也嵌套了第三个组件grandson.vue
并使用$parent.$parent.title
获取title
显示出来- 通过
$parent
可以操作父组件的成员,它可以替换prop
使用,prop
是不允许修改的,通过$parent
获取的成员可以直接修改。也就是在子组件中可以直接修改父组件中的成员,如果应用复杂的话会导致我们难以维护,而且嵌套过多使用也不方便
- 通过
src/03-child/
:这里定义了三个组件,parent
中使用了children1
和children2
两个组件,当点击按钮分别打印$children
数组并分别获取对应索引的的组件,当拿到子组件后可以访问里面的title
属性以及handle
方法
- $refs
src.04-ref/
:定义了两个组件parent
和myinput
,myinput
中放了一个input
标签,通过v-model
绑定了value
属性,同时设置了ref="txt"
。我们希望点击按钮时让子组件的input
获取焦点,我们定义了focus
方法this.$refs.txt.focus()
来获取input
标签,此处获取的为DOM对象。在parent
组件中使用了myinput
并设置ref="mytxt"
,当点击按钮通过this.$refs.mytxt.focus()
获取自定义组件调用其focus
方法$refs
可以用在两个地方:如果用在普通html标签上,通过$refs
获取到的就是普通DOM对象;如果用在子组件上获取到的就是对应的子组件对象。需要注意的是我们需要等待组件渲染完毕之后在通过$refs
获取子组件
- 依赖注入 provide / inject
src/05-provide&inject
:里面有三个组件和02-parent
中的三个组件一致,我们希望在子组件中访问parent
中的一些成员可以通过provide
先在父组件中提供,再从子组件中通过inject
注入,然后就可以在子组件中通过插值表达式展示- 需要注意的是:应该避免修改
indect
进来的成员,它不是响应式的。可以把依赖注入看做大范围的prop
,父组件的成员在所有子组件、多层嵌套的子组件中都可以使用。依赖注入带来的负面影响是组件之间的耦合变高,子组件依赖父组件使重构变得更加困难
- 需要注意的是:应该避免修改
$attrs / $listeners
如果你需要开发自定义组件的话,你会用到这两个属性
- $ attrs
- 把父组件中非 prop 属性绑定到内部组件
- $ liteners
- 把组件中的 都没DOM对象的原生事件绑定到内部组件
src/06-attrs&listener
:有两个组件,子组件myinput
以及父组件parent
parent
:
<template>
<div>
<myinput
required
placeholder="Enter your username"
class="theme-dark"
data-test="test">
</myinput>
</div>
</template>
<script>
import myinput from './02-myinput'
export default {
components: {
myinput
}
}
</script>
myinput
:
<template>
<!--
1. 从父组件传给自定义子组件的属性,如果没有 prop 接收
会自动设置到子组件内部的最外层标签上
如果是 class 和 style 的话,会合并最外层标签的 class 和 style
-->
<input type="text" class="form-control">
</template>
<script>
export default {
}
</script>
如果设置了父组件中设置属性对应的prop
export default {
props: ['placeholder', 'style', 'class']
}
我们可以发现此时placeholder
没有设置成功,但是class
却设置成功了,而且报了两个错误
它告诉我们class
和style
是保留的属性,不能用在组件的prop
中
现在我们希望从父组件中接收的placeholder
属性能够正常的设置到input
标签上
<template>
<input type="text" class="form-control" :placeholder="placeholder">
</template>
<script>
export default {
props: ['placeholder']
}
</script>
如果设置prop
来接受父组件中传递的属性需要自己在标签上绑定相应的属性,另外prop
不能使用style
和class
属性,如果不使用prop
的话父组件默认传递过来的属性会绑定到template
的根标签上。
如果input
在div
中包裹,此时会把父组件传递的属性设置给template
的根元素,使用$attrs
可以方便处理这种情况
<template>
<div>
<input type="text" class="form-control">
</div>
</template>
<script>
export default {
}
</script>
<template>
<!--
2. 如果子组件中不想继承父组件传入的非 prop 属性,可以使用 inheritAttrs 禁用继承
然后通过 v-bind="$attrs" 把外部传入的非 prop 属性设置给希望的标签上
但是这不会改变 class 和 style
-->
<div>
<input type="text" v-bind="$attrs" class="form-control">
</div>
</template>
<script>
export default {
inheritAttrs: false
}
</script>
我们演示了$attrs
的使用,它可以让我们在子组件中更方便得控制父组件传过来的属性。
接下来来掩饰父组件给子组件传递事件:
parent.vue
:
<template>
<div>
<myinput
required
placeholder="Enter your username"
class="theme-dark"
@focus="onFocus"
@input="onInput"
data-test="test">
</myinput>
<button @click="handle">按钮</button>
</div>
</template>
<script>
import myinput from './02-myinput'
export default {
components: {
myinput
},
methods: {
handle () {
console.log(this.value)
},
onFocus (e) {
console.log(e)
},
onInput (e) {
console.log(e.target.value)
}
}
}
</script>
myinput.vue
:
<template>
<!--
3. 注册事件
-->
<div>
<input
type="text"
v-bind="$attrs"
class="form-control"
@focus="$emit('focus', $event)"
@input="$emit('input', $event)"
>
</div>
</template>
<script>
export default {
inheritAttrs: false
}
</script>
我们希望父组件注册的事件能够被触发
现在假设我们给这个文本框注册的事件很多,这样写就很麻烦。所以接下来我们通过$listeners
来简化这件事
<template>
<!--
4. $listeners
-->
<div>
<input
type="text"
v-bind="$attrs"
class="form-control"
v-on="$listeners"
>
</div>
</template>
<script>
export default {
inheritAttrs: false
}
</script>
这里把注册文本框本身的事件和触发自定义事件换成了v-on="$listeners"
快速原型开发
Vue/cli 提供了快速原型开发的工具,它可以让我们很方便地运行一个单文件组件而不需要关心额外的配置
-
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
<template>
<div>
Hello Vue
</div>
</template>
<script>
export default {
}
</script>
<style>
</style>
vue serve
快速原型开发 - ElementUI
我们除了可以从零开发组件外,还可以在第三方组件的基础上二次开发:比如在ElementUI的基础上开发自己的组件
安装 ElementUI
-
初始化 package.json
-
npm init -y
-
-
安装 ElementUI
-
vue add element
-
-
加载 ElementUI,使用 Vue.use() 安装插件
接下来我们使用 ElementUI 做一个登录的组件:
删除掉src
下多余的文件,此处不需要
在使用 ElementUI 之前,首先导入 ElementUI 注册插件
创建入口文件 main.js
import Vue from 'vue'
import ElementUI from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'
import Login from './src/Login.vue'
Vue.use(ElementUI)
new Vue({
el: '#app',
render: h => h(Login)
})
vue serve
14.2 组件开发
组件分类
- 第三方组件:ElemenUI、iView
- 基础组件:文本框、按钮、表单
- 业务组件:结合特定的行业使用场景,可以根据用户的行为输出特定的界面
如果们要开发的应用对界面的要求不高,我们可以直接使用第三方组件;
如果对组件的样式有比较高的要求,或者有一套自己的使用标准,则需要开发自己的组件库,开发一套方便团队内部使用的基础组件、通用组件;
如果针对特定的行业例如财务、餐饮或者人力系统,会有针对特定业务可以抽象出来的组件,我们可以把它们抽象出来方便未来的重用,开发业务组件一般可以基于现有的组件比如第三方组件,在第三方组件的基础上进行开发。
步骤条组件
- 可以引导用户按照指定的步骤完成任务
src/Steps.vue
<template>
<div class="lg-steps">
<div class="lg-steps-line"></div>
<div
class="lg-step"
v-for="index in count"
:key="index"
:style="{ color: active >= index ? activeColor : defaultColor }"
>
{{ index }}
</div>
</div>
</template>
<script>
import './steps.css'
export default {
name: 'LgSteps',
props: {
count: {
type: Number,
default: 3
},
active: {
type: Number,
default: 1
},
activeColor: {
type: String,
default: 'red'
},
defaultColor: {
type: String,
default: 'green'
}
}
}
</script>
vue serve src/Steps.vue
src/Steps-test.vue
<template>
<div>
<steps :count="count" :active="active"></steps>
<button @click="next">下一步</button>
</div>
</template>
<script>
import Steps from './Steps.vue'
export default {
components: {
Steps
},
data () {
return {
count: 4,
active: 0
}
},
methods: {
next () {
this.active++
}
}
}
</script>
<style>
</style>
vue serve ./src/Steps-test.vue
表单组件
整体结构
- Form
- FormItem
- Input
- Button
src/form/Form.vue
<template>
<form>
<slot></slot>
</form>
</template>
<script>
export default {
name: 'LgForm',
props: {
model: {
type: Object
},
rules: {
type: Object
}
}
}
</script>
src/form/FormItem.vue
<template>
<div>
<label>{{ label }}</label>
<div>
<slot></slot>
<p v-if="errMessage">{{ errMessage }}</p>
</div>
</div>
</template>
<script>
export default {
name: 'LgFormItem',
props: {
label: {
type: String
},
prop: {
type: String
}
},
data () {
return {
errMessage: ''
}
}
}
</script>
src/form/Input.vue
<template>
<div>
<!-- 将父组件传递过来的属性展开给 input -->
<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)
}
}
}
</script>
src/form/Button.vue
<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>
src/Form-test.vue
:拿Login.vue
修改而来,将el-form
修改为lg-from
,并做出一些修改:
<template>
<lg-form class="form" ref="form" :model="user" :rules="rules">
<lg-form-item label="用户名" prop="username">
<!-- <lg-input v-model="user.username"></lg-input> -->
<lg-input :value="user.username" @input="user.username=$event" placeholder="请输入用户名"></lg-input>
</lg-form-item>
<lg-form-item label="密码" prop="password">
<lg-input type="password" v-model="user.password"></lg-input>
</lg-form-item>
<lg-form-item>
<lg-button type="primary" @click="login">登 录</lg-button>
</lg-form-item>
</lg-form>
</template>
<script>
import LgForm from './form/Form'
import LgFormItem from './form/FormItem'
import LgInput from './form/Input'
import LgButton from './form/Button'
export default {
components: {
LgForm,
LgFormItem,
LgInput,
LgButton
},
data () {
return {
user: {
username: '',
password: ''
},
rules: {
username: [
{
required: true,
message: '请输入用户名'
}
],
password: [
{
required: true,
message: '请输入密码'
},
{
min: 6,
max: 12,
message: '请输入6-12位密码'
}
]
}
}
},
methods: {
login () {
console.log('button')
// this.$refs.form.validate(valid => {
// if (valid) {
// alert('验证成功')
// } else {
// alert('验证失败')
// return false
// }
// })
}
}
}
</script>
<style>
.form {
width: 30%;
margin: 150px auto;
}
</style>
表单验证
src/form/Form.vue
<script>
export default {
name: 'LgForm',
provide () {
return {
form: this
}
},
...
}
</script>
src/form/FormItem.vue
<script>
export default {
name: 'LgFormItem',
inject: ['form'],
...
}
</script>
npm install async-validator
src/form/FormItem.vue
<script>
import AsyncValidator from 'async-validator'
export default {
name: 'LgFormItem',
...
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>
Input 组件验证
- Input 组件中触发自定义事件 validate
- FormItem 渲染完毕注册自定义事件 validate
src/form/Input.vue
<script>
export default {
name: 'LgInput',
...
methods: {
handleInput (evt) {
this.$emit('input', evt.target.value)
const friendParent = parent => {
while (parent) {
if (parent.$options.name === 'LgFormItem') {
break
} else {
parent = parent.$parent
}
}
return parent
}
const parent = friendParent(this.$parent)
if (parent) {
parent.$emit('validate')
}
}
}
}
</script>
src/form/FormItem.vue
<script>
...
export default {
name: 'LgFormItem',
...
mounted() {
this.$on('validate', () => {
this.validate()
})
},
...
}
</script>
src/Form-test.vue
<script>
methods: {
login () {
// console.log('button')
this.$refs.form.validate(valid => {
if (valid) {
alert('验证成功')
} else {
alert('验证失败')
return false
}
})
}
}
</script>
src/form/Form.vue
<script>
export default {
name: 'LgForm',
...
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>
14.3 组件库管理
Monorepo
假设我们现在要开发一个组件库,其中有很多组件,当它开发完毕后还会发布到诸如NPM或者私有仓库让其他人去使用。
在使用ElementUI的时候我们可以完整地引用,如果只使用到部分组件,为了减少打包的体积我们会选择按需引用的方式,此时要安装babel
的插件然后再配置比较麻烦。
我们开发的组件库为了让别人使用的方便,我们决定把每一个组件作为一个单独的包发布到NPM上,其他人在使用时可以只下载他所需要的组件。
两种项目的组织方式
- Multirepo(Multiple Repository)
- 每一个包对应一个项目
- Monorepo(Monoltipe Repository)
- 一个项目仓库中管理多个模块/包
包的结构
__test__
:测试代码目录dist
:打包的目录src
:源码目录index.js
:打包入口LICENSE
:版权信息package.json
:包的描述信息README.md
:文档
Storybook
- 可视化的组件展示平台
- 在隔离的开发环境中,以交互式的方式展示组件
- 独立开发组件
- 支持的框架
- React、React Native、Vue、Angular、
- Ember、HTML、Svelte、Mithril、Riot
Storybook 安装
- 自动安装
npx -p @storybook/cli sb init --type vue
yarn add vue
yarn add vue-loader vue-template-compiler --dev
- 手动安装
创建一个空项目lgelement
执行上述操作
yarn storybook
接下来我们将之前的项目应用进来
.storybook/main.js
module.exports = {
stories: ['../packages/**/*.stories.js'],
addons: ['@storybook/addon-actions', '@storybook/addon-links'],
};
我们这里演示表单组件,先给input
写一个简单的stories
:渲染文本框、渲染密码框
input/stories/input.stories.js
import LgInput from '../'
export default {
title: 'LgInput',
component: 'LgInput'
}
export const Text = () => ({
components: { LgInput },
template: '<lg-input v-model="value"></lg-input>',
data (){
return {
value: 'admin'
}
}
})
export const Password = () => ({
components: { LgInput },
template: '<lg-input type="password" v-model="value"></lg-input>',
data (){
return {
value: 'admin'
}
}
})
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VokxlWR8-1614789141659)(https://i.loli.net/2021/03/04/shJ1cIiDbQRf9Wm.png)]
接下来进入fromitem
文件夹,安装async-validator
依赖
yarn add async-validator
form/stories/form.stories.js
import LgForm from '../'
import LgFormItem from '../../formitem'
import LgInput from '../../input'
import LgButton from '../../button'
export default {
title: 'LgForm',
component: LgForm
}
export const Login = () => ({
components: { LgForm, LgFormItem, LgInput, LgButton },
template: `
<lg-form class="form" ref="form" :model="user" :rules="rules">
<lg-form-item label="用户名" prop="username">
<!-- <lg-input v-model="user.username"></lg-input> -->
<lg-input :value="user.username" @input="user.username=$event" placeholder="请输入用户名"></lg-input>
</lg-form-item>
<lg-form-item label="密码" prop="password">
<lg-input type="password" v-model="user.password"></lg-input>
</lg-form-item>
<lg-form-item>
<lg-button type="primary" @click="login">登 录</lg-button>
</lg-form-item>
</lg-form>
`,
data () {
return {
user: {
username: '',
password: ''
},
rules: {
username: [
{
required: true,
message: '请输入用户名'
}
],
password: [
{
required: true,
message: '请输入密码'
},
{
min: 6,
max: 12,
message: '请输入6-12位密码'
}
]
}
}
},
methods: {
login () {
// console.log('button')
this.$refs.form.validate(valid => {
if (valid) {
alert('验证成功')
} else {
alert('验证失败')
return false
}
})
}
}
})
yarn workspaces
开启yarn workspaces
可以让我们在根目录中使用yarn install
给所有的包统一安装依赖
- 如果不同的包引用相同的第三方包只会下载一次并把相同的依赖提升到根目录的
node_modules
中减少重复 - 如果不同的包引用的
lodash
版本不相同只会把相同版本的lodash
提升到根目录的node_modules
中 npm
不支持workspaces
开启 yarn 的工作区
- 项目根目录的
package.json
"private": true,
"workspaces": [
"packages/*"
]
"private": true
:将来提交到github或者发布到npm时禁止把当前根目录的内容进行提交。
yarn workspaces 使用
- 给工作区根目录安装开发依赖
yarn add jest -D -W
- 给指定工作区安装依赖
yarn workspace lg-button add lodash@4
- 给所有工作区安装依赖
yarn install
yarn workspace lg-button add lodash@4
yarn workspace lg-form add lodash@4
yarn workspace lg-input add lodash@3
此时,除了lg-input
的依赖安装在input
包内,其余依赖均提升到了根目录包依赖内
Lerna
Lerna
可以方便我们把项目中的所有包统一发布
Lerna 介绍
- Lerna 是一个优化使用
git
和npm
管理多包仓库的工作流工具 - 用于管理具有多个包的
JavaScript
项目 - 它可以一键把代码提交到
git
和npm
仓库
Lerna 使用
- 全局安装
yarn global add lerna
- 初始化
lerna init
- 发布
lerna publish
初始化完成过后会做几件事情:
1.如果当前项目没有被
git
管理的话会进行git
初始化2.在项目根目录创建
lerna.json
的配置文件3.在
package.json
中添加开发依赖确保别人获取我们的项目也可以正常工作
lerna.json
记录当前项目初始化的版本以及所有管理包的路径
{
"packages": [
"packages/*"
],
"version": "0.0.0"
}
package.json
添加scripts
命令"lerna": "lerna publish"
,然后我们需要把项目推送到仓库中并发布npm
Vue组件的单元测试
组件开发完毕发布之前,我们还应该对组件进行单元测试。
单元测试就是对一个函数的输入和输出进行测试,使用断言的方式,根据输入判断实际的输出和预测的输出是否相同。
使用单元测试的目的是用来发现模块内部可能存在的各种错误。组件的单元测试指的是使用单元测试工具对组件的各种状态和行为进行测试,确保组件发布之后在项目中使用组件的过程中不会导致程序出现错误。
组件单元测试的好处
- 提供描述组件行为的文档
- 节省动手测试的时间
- 减少研发新特性时产生的bug
- 改进设计
- 促进重构
安装依赖
- Vue Test Utils
- Vue 官方提供的组件单元测试的官方库
- Jest
- Vue Test Utils 需要结合该单元测试框架一起使用,它和vue的结合最方便、配置最少
- Vue-jest
- 预处理器,用于把vue的单文件组件编译之后的结果交给js处理,Vue-jest支持单文件组件的大多数功能
- Babel-jest
- 测试中会使用到一些ESModule的语法和一些ES的新特性的语法,需要此插件对测试代码进行降级处理
- 安装
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()
packages/input/__tests__/input.test.js
import input from '../src/input.vue'
import { mount } from '@vue/test-utils'
describe('lg-input', () => {
test('input-text', () => {
const wrapper = mount(input)
expect(wrapper.html()).toContain('input type="text"')
})
test('input-password', () => {
const wrapper = mount(input, {
propsData: {
type: 'password'
}
})
expect(wrapper.html()).toContain('input type="password"')
})
test('input-password', () => {
const wrapper = mount(input, {
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()
})
})
Rollup 打包
Rollup
- Rollup 是一个模块打包器
- Rollup 支持 Tree-shaking
- 打包的结果比 Webpack 要小
- 开发框架/组件库的时候使用 Rollup 更合适
安装依赖
Rollup
rollup-plugin-terser
:对代码进行压缩rollup-plugin-vue@5.1.9
:把单文件组件编译成JS代码vue-template-compiler
安装 Rollup 以及所需的插件
yarn add rollup rollup-plugin-terser rollup-plugin-vue@5.1.9 vue-template-compiler -D -W
Rollup 配置文件
在 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
// 把单文件组件中的样式插入到 HTML 中的 style 标签
css: true,
// Explicitly convert template to render function
// 把组件转换成 render 函数
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
中配置 scripts
"build": "rollup -c"
在每一个包中设置 package.json
中的 main
和 module
字段
"main": "dist/cjs/index.js",
"module": "dist/es/index.js",
yarn build
设置环境变量
cross-env
:跨平台设置环境变量
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"
清理
- 清理所有包中的
node_modules
- 清理所有包中的
dist
根目录的 package.json
中配置 scripts
"clean": "lerna clean"
这里再来使用一个第三方库rimraf
,它可以指定要删除的目录
yarn add rimraf -D -W
在每一个包中设置 package.json
中的 scripts
字段
"del": "rimraf dist"
yarn workspaces run del
基于模板生成组件基本结构
-
到这里我们创建了Monorepo的项目结构,在一个项目中管理多个包,这种方式更适合我们来管理组件库和发布每一个组件
-
然后使用Storybook搭建项目可以让用户快速浏览组件
-
使用yarn workspaces管理所有包的依赖
-
使用Lerna发布项目,它可以帮我们把每一个包发布到NPM上
-
最后还演示了测试和打包
如果我要创建一个新的组件该如何做呢?
我们可以把所有组件相同的部分提取出来制作一个模板,然后通过plop
基于模板快速生成一个新的组件结构,方便后续大规模的组件开发。
安装 plop
yarn add plop -D -W
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"
yarn plop
超连接组件
接下来我们把基于模板生成的组件link
实现以下
packages/link/src/link.vue
<template>
<a
:href="disabled ? null : href"
:class="[disabled && 'disabled', !underline && 'no-underline']">
<slot></slot>
</a>
</template>
<script>
export default {
name: 'LgLink',
props: {
href: {
type: String
},
disabled: {
type: Boolean,
default: false
},
underline: {
type: Boolean,
default: false
}
}
}
</script>
<style scoped>
.disabled {
cursor: not-allowed;
}
.no-underline {
text-decoration: none;
}
</style>
packages/link/stories/link.stories.js
import LgLink from '../src/link.vue'
export default {
title: 'LgLink',
component: LgLink
}
export const Link = _ => ({
components: { LgLink },
template: `
<div>
<lg-link :disabled="true" href="http://www.baidu.com">baidu</lg-link>
</div>
`
})
yarn storybook