概述
封装Vue.js组件库
文章内容输出来源:大前端高薪训练营
一、组件库介绍
1. 开源组件库
- Element-UI
- IView
2. 组件开发方式CDD
- 自下而上
- 从组件级别开始,到页面级别结束
3. CDD的好处
- 组件在最大程度上被重用
- 并行开发
- 可视化测试
二、处理组件边界情况
vue中处理组件边界情况的API
1. $root
01-root.vue
<template>
<div>
<!--
小型应用中可以在 vue 根实例里存储共享数据
组件中可以通过 $root 访问根实例
-->
$root.title:{{ $root.title }}
<br>
<button @click="$root.handle">获取 title</button>
<button @click="$root.title = 'Hello $root'">改变 title</button>
</div>
</template>
<script>
export default {
}
</script>
<style>
</style>
2. $parent / $children
$parent
01-parent.vue
<template>
<div class="parent">
parent
<child></child>
</div>
</template>
<script>
import child from './02-child'
export default {
components: {
child
},
data () {
return {
title: '获取父组件实例'
}
},
methods: {
handle () {
console.log(this.title)
}
}
}
</script>
<style>
.parent {
border: palegreen 1px solid;
}
</style>
02-child.vue
<template>
<div class="child">
child<br>
$parent.title:{{ $parent.title }}<br>
<button @click="$parent.handle">获取 $parent.title</button>
<button @click="$parent.title = 'Hello $parent.title'">改变 $parent.title</button>
<grandson></grandson>
</div>
</template>
<script>
import grandson from './03-grandson'
export default {
components: {
grandson
}
}
</script>
<style>
.child {
border:paleturquoise 1px solid;
}
</style>
03-grandson.vue
<template>
<div class="grandson">
grandson<br>
$parent.$parent.title:{{ $parent.$parent.title }}<br>
<button @click="$parent.$parent.handle">获取 $parent.$parent.title</button>
<button @click="$parent.$parent.title = 'Hello $parent.$parent.title'">改变 $parent.$parent.title</button>
</div>
</template>
<script>
export default {
}
</script>
<style>
.grandson {
border:navajowhite 1px solid;
}
</style>
-
$children
通过数组索引获取对应的children
3. $ref
01-parent.vue
<template>
<div>
<myinput ref="mytxt"></myinput>
<button @click="focus">获取焦点</button>
</div>
</template>
<script>
import myinput from './02-myinput'
export default {
components: {
myinput
},
methods: {
focus () {
this.$refs.mytxt.focus()
this.$refs.mytxt.value = 'hello'
}
}
// mounted () {
// this.$refs.mytxt.focus()
// }
}
</script>
<style>
</style>
02-myinput.vue
<template>
<div>
<input v-model="value" type="text" ref="txt">
</div>
</template>
<script>
export default {
data () {
return {
value: 'default'
}
},
methods: {
focus () {
this.$refs.txt.focus()
}
}
}
</script>
<style>
</style>
4. 依赖注入provide/inject
注意:inject进来的数据是非响应式的。
01-parent.vue
<template>
<div class="parent">
parent
<child></child>
</div>
</template>
<script>
import child from './02-child'
export default {
components: {
child
},
provide () {
return {
title: this.title,
handle: this.handle
}
},
data () {
return {
title: '父组件 provide'
}
},
methods: {
handle () {
console.log(this.title)
}
}
}
</script>
<style>
.parent {
border: palegreen 1px solid;
}
</style>
02-child.vue
<template>
<div class="child">
child<br>
title:{{ title }}<br>
<button @click="handle">获取 title</button>
<button @click="title='xxx'">改变 title</button>
<grandson></grandson>
</div>
</template>
<script>
import grandson from './03-grandson'
export default {
components: {
grandson
},
inject: ['title', 'handle']
}
</script>
<style>
.child {
border:paleturquoise 1px solid;
}
</style>
03-grandson.vue
<template>
<div class="grandson">
grandson<br>
title:{{ title }}<br>
<button @click="handle">获取 title</button>
<button @click="title='yyy'">改变 title</button>
</div>
</template>
<script>
export default {
inject: ['title', 'handle']
}
</script>
<style>
.grandson {
border:navajowhite 1px solid;
}
</style>
三、$attrs
/ $listeners
$attrs
:把父组件中非prop属性绑定到内部组件
$listeners
:把父组件中的的DOM对象的原生事件绑定到内部组件
01-parent.vue
<template>
<div>
<!-- <myinput
required
placeholder="Enter your username"
class="theme-dark"
data-test="test">
</myinput> -->
<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>
<style>
</style>
02-myinput.vue
<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>
<style>
</style>
四、快速原型开发
- VueCLI中提供了一个插件可以进行原型快速开发
- 需要先额外安装一个全局的扩展:
npm install -g @vue/cli-service-global
1. Vue serve
- Vue serve如果不指定参数默认会在当前目录找一下的入口文件
- main.js、index.js、App.vue、app.vue
- 可以指定要架子啊的组件
vue serve ./src/login.vue
写一个vue组件,App.vue
<template>
<div>
Hello vue
</div>
</template>
<script>
export default {
}
</script>
<style>
</style>
然后执行vue serve
启动了一个服务,打开终端给出的地址,就可以看到这个组件的页面了。
2. ElementUI
安装ElementUI
- 初始化package.json:
npm init -y
- 安装ElementUI:
vue add element
- 加载ElementUI,使用Vue.use()安装插件
五、组件开发
1. 步骤条组件
- 第三方组件
- 基础组件
- 业务组件
Steps-test.vue
<template>
<div>
<Steps
:count="count"
:active="active"
></Steps>
<button @click="next">下一步</button>
</div>
</template>
<script>
import Steps from './Steps'
export default {
components: {
Steps
},
data () {
return {
count: 4,
active: 0
}
},
methods: {
next () {
this.active++
}
}
}
</script>
<style>
</style>
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>
<style>
</style>
steps.css
.lg-steps {
position: relative;
display: flex;
justify-content: space-between;
}
.lg-steps-line {
position: absolute;
height: 2px;
top: 50%;
left: 24px;
right: 24px;
transform: translateY(-50%);
z-index: 1;
background: rgb(223, 231, 239);
}
.lg-step {
border: 2px solid;
border-radius: 50%;
height: 32px;
width: 32px;
display: flex;
justify-content: center;
align-items: center;
font-weight: 700;
z-index: 2;
background-color: white;
box-sizing: border-box;
}
2. 表单组件
整体结构
- Form
- FormItem
- Input
- Button
Input 组件验证
- Input组件中触发自定义事件validate
- FormItem渲染完毕注册自定义事件validate
Form-test.vue
<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>
form/Form.vue
<template>
<div>
<form>
<slot></slot>
</form>
</div>
</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>
form/FormItem.vue
<template>
<div>
<label :for="prop">{{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('validator', () => {
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.props]: 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>
form/Button.vue
<template>
<div>
<button @click="handleClick">
<slot></slot>
</button>
</div>
</template>
<script>
export default {
name: 'LgFButton',
methods: {
handleClick (event) {
this.$emit('click', event)
event.preventDefault()
}
}
}
</script>
<style>
</style>
form/Input.vue
<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 (event) {
this.$emit('input', event.target.value)
const findParent = parent => {
while (parent) {
if (parent.$options.name === 'LgFormItem') {
break
}
parent = parent.$parent
}
return parent
}
const parent = findParent(this.$parent)
if (parent) {
parent.$emit('validator')
}
}
}
}
</script>
<style>
</style>
六、Monorepo
1. 两种项目的组织方式
- Multirepo(Multiple Repository) 每一个包对应一个项目
- Monorepo(Monolithic Repository) 一个项目仓库中管理多个模块/包
2. Monorepo结构
七、Storybook
- 可视化的组件展示平台
- 在格力的开发环境中,以交互的方式展示组件
- 独立开发组件
- 支持的框架
- React、React Native、Vue、Angular
- Ember、HTML、Svelte、Mithril、Riot
1. Storybook安装
- 自动安装
npx -p @storybook/cli sb init --type vue
yarn add vue
使用yarn来安装依赖,因为后面会用到yarn的工作区yarn add vue-loader vue-template-compiler --dev
- 手动安装
自动安装完成之后,执行yarn storybook
启动项目
还可以执行yarn build storybook
进行打包,生成storybook-static静态文件目录
2. 使用storybook写组件
八、yarn workspaces
1. 开启yarn 的工作区
-
项目根目录的package.json
"private": true, "workspaces": [ "./packages/*" ]
2. yarn workspaces 使用
- 给工作区根目录安装开发依赖:
yarn add jest -D -W
- 给指定的工作区安装依赖:
yarn workspace lg-button add lodash@4
- 给所有的工作区安装依赖:
yarn install
Monorepo项目都会结合workspaces来使用,workspaces可以方便管理依赖,将每个工作区中的依赖提升到根目录中的node_modules中。workspaces还可以管理scripts命令。
九、Lerna
1. Lerna介绍
- Lerna 是一个优化使用git和npm管理多宝仓库的工作流工具
- 用于管理具有多个包的JavaScript项目
- 它可以一键把代码提交到Git和npm仓库
2. Lerna使用
- 全局安装:
yarn global add lerna
- 初始化:
lerna init
- 发布:
lerna publish
先执行:yarn global add lerna
然后在项目中执行lerna init
然后在package.json中scripts里增加: "lerna": "lerna publish"
然后创建一个.gitignore
,提交git初始化
echo node_modules > .gitignore
git add .
git commit -m"init"
然后在GitHub创建一个空仓库,我已经创建好了,然后执行:
git remote add origin git@github.com:2604150210/lg-element.git
git push -u origin master
使用npm whoami
查看当前登录npm的用户名
jiailing
使用npm config get registry
查看npm镜像源
https://registry.npm.taobao.org/
发现是淘宝镜像,那要改回来:
npm config set registry http://registry.npmjs.org/
执行yarn lerna
去npm上查看有没有发布成功:
https://www.npmjs.com/settings/jiailing/packages
发现没有发布成功,居然失败了。。。是因为lg-xxx这个名字已经被占用了,改个名字再次尝试一下,将每个组件里的package.json里面的name中的lg-xxx改为jal-xxx再试一次
重新Git提交,再执行yarn lerna
401未授权,那就根据命令行提示,输入npm whoami看看
need auth You need to authorize this machine using
npm adduser
使用npm adduser登录:
完成登录后再次执行yarn lerna发布
显示没有包发布上去,是因为刚才发布时没授权,所以要重新走一遍流程。
不要放弃,再改一下名字试试,把jal改为jiailing,然后再试一次:
git add .
git commit -m"xx"
yarn lerna
提示发布了5个包,真不容易啊
十、Vue组件的单元测试
使用单元测试工具对组件的状态和行为进行测试,确保组件发布之后,在项目中使用组件过程中不会出现错误。
1. 组件单元测试的好处
- 提供描述组件行为的文档
- 节省手动测试的时间
- 减少研发新特性时产生的bug
- 改进设计
- 促进重构
2. 安装依赖
- Vue Test Utils
- Jest
- vue-jest
- babel-jest
- 安装
yarn add jest @vue/test-utils vue-jest babel-jest -D -W
- -D是开发依赖,-W是安装在项目根目录下
3. 配置测试脚本
-
packge.json
"scripts": { "test": "jest" }
4. 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" } }
5. Babel配置文件
-
babel.config.js
module.exports = { presets: [ [ '@babel/preset-env' ] ] }
Babel桥接
yarn add babel-core@bridge -D -W
十一、Vue组件单元测试–Jest
1. Jest常见API
- 全局函数
- describe(name, fn) 把相关测试组合在一起
- test(name, fn) 测试方法
- expect(value) 断言
- 匹配器
- toBe(value) 判断值是否相等
- toEqual(obj) 判断对象是否相等
- toContain(value) 判断数组或者字符串是否包含
- 快照
- toMatchSnapshot()
2. Vue Test Utils 常用API
- mount() 创建一个包含被挂载和渲染的Vue组件的Wrapper
- Wrapper
- vm : wrapper 包裹的组件实例
- props() : 返回Vue实例选项中的props对象
- html() : 组件生成的HTML标签
- find() : 通过选择器返回匹配到的组件中的DOM元素
- trigger() : 触发DOM原生事件,自定义事件 wrapper.vm.$emit()
执行yarn test
进行测试
测试通过的情况:
测试不通过的情况:
增加密码框测试:
属性测试:
快照测试:
先生成快照
生成的快照会存到同级目录的__snapshots__/input.test.js.snap
文件中
然后修改34行的type为text
再重新执行yarn test
,此时将会进行快照对比
执行yarn test -u
可以把快照文件删掉重新生成一个快照
之前的快照是type=“password”,现在就变成了type="password"了。
十二、Rollup打包
1. Rollup
- Rollup是一个模块打包器
- Rollup支持Tree-Shaking
- 打包的结果比Webpack要小
- 开发框架/组件库的时候使用Rollup更合适
2. 安装依赖
- 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.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({
css: true,
compileTemplate: true
}),
terser()
]
}
]
然后在每个组件的package.json中配置脚本命令"build": "rollup -c"
执行:
yarn workspace jiailing-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({
css: true,
compileTemplate: true
}),
json(),
nodeResolve(),
postcss({
// 把css插入到style中
// inject: true,
// 把css放到和js同一级目录
extract: true
})
]
// 如果不是开发环境,开启压缩
isDev || plugins.push(terser())
// pacakges 文件夹路径
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),// 读取package.json中的main属性
format: 'cjs'
},
{
exports: 'auto',
file: path.resolve(root, item, pkg.module), // 读取package.json中的module属性
format: 'es'
}
],
plugins: plugins
}
})
在package.json中配置脚本命令"build": "rollup -c"
在每个组件的package.json里配置main和module属性
"main": "dist/cjs/index.js",
"module": "dist/es/index.js",
执行yarn build
每个组件里的dist路径下生成了es文件夹和cjs文件夹
十三、设置环境变量
安装cross-env,可以跨平台配置环境变量
yarn add cross-env -D -W
修改package.json中的打包命令
"build:prod": "cross-env NODE_ENV=production rollup -c",
"build:dev": "cross-env NODE_ENV=development rollup -c"
执行yarn build:prod
生成的代码是压缩过的
执行yarn build:dev
生成的代码是没有压缩过的
十四、清理
在package.json中配置命令"clean": "lerna clean"
可以删除组件中的node_modules
现在要来安装rimraf,来删除指定的目录,dist
yarn add rimraf -D -W
在每个组件的package.json中配置命令:
“del”: “rimraf dist”
在终端中执行yarn workspaces run del
来执行每个组件中的del
命令
十五、基于模板生成组件基本结构
安装plop
yarn add plop -W -D
十六、发布
yarn build:prod
npm whoami
git add .
git commit -m"最后发布"
yarn lerna
备注:执行yarn lerna之前必须先commit才会发布成功,否则被视为代码没有更新,则不发布包
最后
以上就是悲凉火龙果为你收集整理的大前端学习笔记 -- 封装Vue.js组件库的全部内容,希望文章能够帮你解决大前端学习笔记 -- 封装Vue.js组件库所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复