文章内容输出来源:拉勾教育大前端高薪训练营
必备知识
组件分类
- 第三方组件:对界面UI要求不高
- 基础组件:对界面UI要求高
- 业务组件:针对特定的业务。譬如财务,入库等
页面中的传值
- 依赖注入
provide / inject
// parent.vue
<template>
</template>
<script>
export default {
data() {
return {
title: '标题'
};
},
provide() {
return {
title:this.title,
handle:this.handle
}
},
methods: {
handle() {
console.log(this.title)
}
}
}
</script>
// child.vue
<template>
</template>
<script>
export default {
data() {
return {};
},
inject: ['title','handle'],
methods: {}
}
</script>
<style lang='scss' scoped>
</style>
tip : 这种方式的数据是非响应式的,同时页面之间的耦合度较高
- attrs-listeners
$attr/$listeners
- $attr 把父组件中非 prop 属性绑定到内部组件
- $listeners 把父组件中的 DOM 对象的原生事件绑定到内部组件
// 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>
// child.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
- 使用 vue serve 快速查看组件的运行效果
- vue serve 如果不指定参数默认会在当前目录查找以下入口文件
- main.js、inde.js、App.vue、app.vue
- 可以指定要加载的组件
- vue serve ./src/login.vue
组件开发-表单组件
实现思路
- 内容展示及数据绑定功能
- 表单验证功能
- 单个表单验证(在文本框输入时验证)
- 点击提交时,验证
实现
- Form-test.vue
- 结构
- 该组件是根据ElementUI修改来的
- 第4行,将v-model语法糖展开,便于理解
- 验证
- 填写验证规则 34行
- 触发Form组件的验证函数 58行
<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
- 结构
- 根据 Form-test.vue 结构,<lg-form> 标签中的内容是变化的,所以需要使用到插槽
- 验证
- 通过依赖注入,传递数据 10行
- 当子组件有 prop 属性时,才进行验证 24行
<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) {
// 这里用的是简化版,默认form-item是form组件的直接子组件(没有嵌套)
// TODOS:嵌套情况实现假象
// 1. 遍历$children,$options.name === 'LgFormItem',时返回
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
- 结构
- label 标签用于显示标题
- p 标签用于显示错误文本
- props 中 prop 用于验证规则
- 验证
- 获取依赖注入的值 15行
- 验证规则 35行
- 注册validate事件,在Input组件中使用$emit触发 25行
<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.vue
- 结构
- $attrs 使用时,记得要添加 第10行代码(禁用父组件传过来的属性)
- 验证(23行开始)
- 这里不使用依赖注入的原因是:
- 依赖注入是强依赖
- input 组件应该在任何地方都能使用,不一定要在form-item组件下
<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.vue
- 由于button在表单中,提交时会刷新页面。所以我们需要preventDefault
<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管理
- 每个包都可以单独测试,单独发布
- 适合管理组件库,和发布每个组件
目录结构
tip: src目录下文件应该小写。
package.json中的license : 这是非常重要但经常被忽略的属性。license 字段使我们可以定义适用于 package.json 所描述代码的许可证。同样,在将项目发布到 NPM 注册表时,这非常重要,因为许可证可能会限制某些开发人员或组织对软件的使用。拥有清晰的许可证有助于明确定义该软件可以使用的术语。 license 字段的值通常是许可证的标识符代码——例如 MIT 或 ISC 之类的字符串,它们代表MIT 许可证和 ISC 许可证。如果你不想提供许可证,或者明确不想授予使用私有或未发布的软件包的权限,则可以将 UNLICENSED 作为许可证。如果你不确定要使用哪个许可证, Choose a License 是对你有用的资源。
// index.js
import LgButton from './src/button.vue'
LgButton.install = Vue => {
Vue.component(LgButton.name, LgButton)
}
export default LgButton
Storybook可视化
- 可视化的组件展示平台
- 在隔离的开发环境中,以交互的方式展示组件
- 独立的开发组件
- 让用户快速预览组件
安装
npx -p @storybook/cli sb init --type vue
yarn add vue
yarn add vue-loader vue-template-compiler --dev
目录结构
使用
- 将 package 目录放入项目中
- 修改 ./.storybook/main.js 文件
// main.js
module.exports = {
stories: ['../packages/**/*.stories.js'], // 入口
addons: ['@storybook/addon-actions', '@storybook/addon-links'],
};
- 各个组件中index.js
import LgInput from './src/input.vue'
LgInput.install = Vue => {
Vue.component(LgInput.name, LgInput)
}
export default LgInput
- 填写各个组件中stories文件:譬如packages/input/stories/index.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'
}
}
})
yarn workspaces依赖
管理所有包的依赖
// 根目录下的package.json
"private": true,// 提交到npm上时,禁止把当前根目录提交
"workspaces": [
"./packages/*"// 管理所有包的路径
]
- 给工作区根目录安装开发依赖
yarn add jest -D -W
- 给指定工作区安装依赖
yarn workspace lg-button add lodash@4
- 这里的 lg-button 指的是,每个包的报名。即各个包中的:package.json中的name
- 给所有的工作区安装依赖
yarn install
- 执行所有工作区的命令
yarn workspace run del
Lerna发布
- 是一个优化使用 git 和 npm 管理多包仓库的工作流工具
- 用于管理具有多个包的 JavaScript 项目
- 它可以一键把代码提交到git和npm仓库
- 全局安装
- yarn global add lerna
- 初始化
- lerna init
- 删除node_modules
- lerna clean
- 发布
- 前提:关联了git仓库,登录了npm,npm镜像源
npm whoami
确定npm账号npm config get registry
npm镜像源lerna publish
单元测试
https://vue-test-utils.vuejs.org/zh/guides/ 好处
- 提供描述组件行为的文档
- 节省手动测试的时间
- 减少研发新特性时产生的bug
- 改进设计
- 促进重构
- 安装
yarn add jest @vue/test-utils vue-jest babel-jest -D -W
- 配置
- 根目录下的package.json
// package.json
"scripts": {
"test": "jest"
...
}
- 根目录下的 jest.config.js
// jest.config
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.config.js
module.exports = {
presets: [
[
'@babel/preset-env'
]
]
}
- 最后,当运行时,会提示找不到 babel。
- 原因:vue-test依赖的是babel6,当前是babel7
- Babel 桥接
yarn add babel-core@bridge -D -W
- 基本API
- JEST
- Vue Test Utils
input实例:tests/input.test.js
// inpu.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"')
})
// 设置type为password,同时测试
test('input-password', () => {
const wrapper = mount(input, {
propsData: {
type: 'password'
}
})
expect(wrapper.html()).toContain('input type="password"')
})
// 测试value是否为admin
test('input-password', () => {
const wrapper = mount(input, {
propsData: {
type: 'password',
value: 'admin'
}
})
expect(wrapper.props('value')).toBe('admin')
})
// 快照
// 第一次运行:将文本内容存到特定的文件中./__snapshots__
// 第二次运行:与第一次对比,相同则成功
// yarn test -u命令, 将快照删除重新生成
test('input-snapshot', () => {
const wrapper = mount(input, {
propsData: {
type: 'text',
value: 'admin'
}
})
expect(wrapper.vm.$el).toMatchSnapshot()
})
})
Rollup
- Rollup 是一个模块打包器
- Rollup 支持 Tree-shaking
- 打包的结果比Webpack小
- 开发框架/组件库的时候使用 Rollup 更合适
打包
- 安装依赖
- Rollup
- rollup-plugin-teser 压缩
- rollup-plugin-vue@5.1.9 把单文件组件编译成js
- vue-template-compiler
- 在 input 目录中创建 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'//es表示es6,cjs表示Commonjs
}
],
plugins: [
vue({
css: true,
compileTemplate: true
}),
terser()
]
}
]
- 找到 input 包中的 package.json 的 scripts 配置
"build": "rollup -c"
- 运行打包/单个包
yarn workspace lg-input 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"
**
设置环境变量
- 安装依赖
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",
"clean": "lerna clean",
"plop": "plop"
}
清理
删除指定目录 yarn add rimraf -D -W
"scripts": {
"del": "rimraf dist"
}
"scripts": {
"clean": "lerna clean",
}