本文为拉勾网大前端高薪训练营第一期笔记
CDD
(Component-Driven Development)
- 自下而上
- 从组件级别开始,到页面级别结束
CDC的好处
- 组件在最大程度被复用
- 并行开发
- 可视化测试
组件的边界情况
root: 获取根组件
parent: 获取父组件
children: 获取所有子组件
ref:拿到组件或者dom
provide&inject: 类似于react的context provider,上层定义一个provide的一些变量,下层任意一层都能inject获取之前provide提供的变量,但是变量不是响应式的,只能获取,不能修改
attrs-listeners:
- $attrs: 把父组件中非prop属性绑定到内部组件
- https://vuejs.org/v2/api/#inheritAttrs
- 一般来说如果props没有去接收父组件传来的参数,会给子组件template最外层传这些参数,如果想传到最外层以外的位置,可以inheritAttrs: false,然后给想接收的位置加v-bind="$attrs",参数中class和style除外,class和style还是会和子组件根节点的class style合并
- $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>
//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
使用vue serve快速查看组件的运行效果
vue serve如果不指定参数的话,默认会在当前目录找以下的入口文件
- main.js index.js App.vue app.vue
可以指定要加载的组件
vue serve ./src/login.vue
以ElementUI为例
npm init -y
vue add element
//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)
})
步骤条例子
组件分类
- 第三方组件
- 基础组件
- 业务组件
//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: 0
},
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;
}
//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>
表单组件例子
//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 (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>
表单验证使用import AsyncValidator from ‘async-validator’
Form里传rules,通过provide传下去
provide () {
return {
form: this
}
},
然后FormItem里使用inject拿到Form,从而拿到rules
inject: ['form'],
Monorepo
两种项目的组织方式
- Multirepo(Multiple Repository)
- 每一个包对应一个项目
- Monorepo(Monolithic Repository)
- 一个项目仓库管理多个模块/包
- 一般在根目录下有个文件夹packages,里面放所有的模块
Storybook
- 可视化的组件展示平台
- 在隔离的开发环境中,以交互式的方式展示组件
- 独立开发组件
- 支持的框架
- React, React Native, Vue, Angular
- Ember, HTML, Svelte, Mithril, Riot
自动安装
npx -p @storybook/cli sb init --type vue
yarn add vue
vue yarn add vue-loader vue-template-compiler --dev
https://www.learnstorybook.com/intro-to-storybook/react/en/get-started/
yarn workspaces
开启yarn工作区
//package.json
"private": true, //将来提交npm和git禁止把当前根目录内容进行提交
"workspaces": [
"./packages/*"
]
给工作区根目录安装开发依赖
yarn add jest -D -W
-D开发依赖 -W根目录
给指定工作区安装依赖
yarn workspace lg-button add lodash@4
给所有工作区安装依赖
删除子包的node_modules,然后yarn install
会将大部分子包共享的依赖提升到根目录,有版本冲突的子包的依赖会单独安装在子包下的node_modules
lerna介绍
lerna是一个优化使用git和npm管理多包仓库的工作流工具
用于管理具有多个包的javascript项目
可以一键把代码提交到git和npm仓库
使用
yarn global add lerna
lerna init
lerna publish
组件单元测试好处
- 提供描述组件行为的文档
- 节省手动测试的时间
- 减少研发新特性时产生的bug
- 改进设计
- 促进重构
安装依赖
vue test utils
jest
vue-jest
babel-jest
yarn add jest @vue/test-utils vue-jest babel-jest -D -W
//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.config.js
module.exports = {
presets: [
[
'@babel/preset-env'
]
]
}
需要注意的是vue-test依赖的babel6,项目里的babel7,这时候需要安装
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()
例如
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()
})
})
第一次会生成快照,yarn test -u会重新生成快照
rollup
- 模块打包器
- 支持tree-shaking
- 打包的结果比webpack小
- 开发框架/组件库的时候使用rollup更合适
安装依赖
- rollup
- rollup-plugin-terser 对代码进行压缩
- rollup-plugin-vue@5.1.9 是把vue2.x的组件编译成js代码
- vue-template-compiler 把template转换成render函数
举例
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()
]
}
]
对workspace下某一个执行npm run build
yarn workspace lg-button run build
如果想一次性打包所有插件
安装依赖
yarn add @rollup/plugin-json rollup-plugin-postcss @rollup/plugin-node-resolve -D -W
@rollup/plugin-node-resolve 把依赖的第三方库打包进来
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
}
})
设置环境变量
yarn add cross-env -D -W
-W是给根目录下安装
“build:prod”: “cross-env NODE_ENV=production rollup -c”
“build:dev”: “cross-env NODE_ENV=development rollup -c”
清理组件里的文件
清理node_modules
根目录下的package.json
scripts: {clean: “lerna clean”}
清理dist
yarn add rimraf -D -W
每个包的package.json里都加
scripts: {del: “rimraf dist”}
然后yarn workspaces run del
根据模板生成组件基本结构
yarn add plop -W -D
写好组件的模板,然后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'
}
]
})
}
npm发布前检测用户npm whoami