好处
main.js
import Vue from 'vue'
import App from './App.vue'
Vue.config.productionTip = false
new Vue({
render: h => h(App),
data: {
title: '根实例 - Root'
},
methods: {
handle () {
console.log(this.title)
}
}
}).$mount('#app')
root.vue
<template>
<div>
$root.title:{{ $root.title }}
<br>
<button @click="$root.handle">获取 titlebutton>
<button @click="$root.title = 'Hello $root'">改变 titlebutton>
div>
template>
<script>
export default {
}
script>
<style>
style>
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>
child.vue
<template>
<div class="child">
child<br>
$parent.title:{{ $parent.title }}<br>
<button @click="$parent.handle">获取 $parent.titlebutton>
<button @click="$parent.title = 'Hello $parent.title'">改变 $parent.titlebutton>
<grandson>grandson>
div>
template>
<script>
import grandson from './03-grandson'
export default {
components: {
grandson
}
}
script>
<style>
.child {
border:paleturquoise 1px solid;
}
style>
grandson.vue
<template>
<div class="grandson">
grandson<br>
$parent.$parent.title:{{ $parent.$parent.title }}<br>
<button @click="$parent.$parent.handle">获取 $parent.$parent.titlebutton>
<button @click="$parent.$parent.title = 'Hello $parent.$parent.title'">改变 $parent.$parent.titlebutton>
div>
template>
<script>
export default {
}
script>
<style>
.grandson {
border:navajowhite 1px solid;
}
style>
parent.vue
<template>
<div>
<children1>children1>
<children2>children2>
<button @click="getChildren">获取子组件button>
div>
template>
<script>
import children1 from './02-children1'
import children2 from './03-children2'
export default {
components: {
children1,
children2
},
methods: {
getChildren () {
console.log(this.$children)
console.log(this.$children[0].title)
console.log(this.$children[1].title)
this.$children[0].handle()
this.$children[1].handle()
}
}
}
script>
<style>
style>
children1.vue
<template>
<div>children1div>
template>
<script>
export default {
data () {
return {
title: 'children1 获取子组件 - title'
}
},
methods: {
handle () {
console.log(this.title)
}
}
}
script>
<style>
style>
children2.vue
<template>
<div>children2div>
template>
<script>
export default {
data () {
return {
title: 'children2 获取子组件 - title'
}
},
methods: {
handle () {
console.log(this.title)
}
}
}
script>
<style>
style>
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>
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>
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>
child.vue
<template>
<div class="child">
child<br>
title:{{ title }}<br>
<button @click="handle">获取 titlebutton>
<button @click="title='xxx'">改变 titlebutton>
<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>
grandson.vue
<template>
<div class="grandson">
grandson<br>
title:{{ title }}<br>
<button @click="handle">获取 titlebutton>
<button @click="title='yyy'">改变 titlebutton>
div>
template>
<script>
export default {
inject: ['title', 'handle']
}
script>
<style>
.grandson {
border:navajowhite 1px solid;
}
style>
如果你需要开发自定义组件的话,你会用到这两个属性
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>
<style>
style>
myinput.vue
<template>
<div>
<input
type="text"
v-bind="$attrs"
class="form-control"
v-on="$listeners"
>
div>
template>
<script>
export default {
// 会报错,不可以设置 class 和 style
// props: ['placeholder', 'style', 'class']
// 修改后
// props: ['placeholder']
inheritAttrs: false
}
script>
<style>
style>
Vue/cli 提供了快速原型开发的工具,它可以让我们很方便地运行一个单文件组件而不需要关心额外的配置
npm install -g @vue/cli-service-global
我们除了可以从零开发组件外,还可以在第三方组件的基础上二次开发:比如在 Element-UI的基础上开发自己的组件
安装 Element-UI
npm init -y
vue add element
接下来我们使用 Element-UI 做一个登录的组件
在使用 Element-UI 之前,首先导入 Element-UI 注册插件
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)
})
<template>
<el-form class="form" ref="form" :model="user" :rules="rules">
<el-form-item label="用户名" prop="username">
<el-input v-model="user.username">el-input>
el-form-item>
<el-form-item label="密码" prop="password">
<el-input type="password" v-model="user.password">el-input>
el-form-item>
<el-form-item>
<el-button type="primary" @click="login">登 录el-button>
el-form-item>
el-form>
template>
<script>
export default {
name: "Login",
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");
return false;
// this.$refs.form.validate(valid => {
// if (valid) {
// alert('验证成功')
// } else {
// alert('验证失败')
// return false
// }
// })
},
},
};
script>
<style>
.form {
width: 30%;
margin: 150px auto;
}
style>
vue serve

如果们要开发的应用对界面的要求不高,我们可以直接使用第三方组件
如果对组件的样式有比较高的要求,或者有一套自己的使用标准,则需要开发自己的组件库,开发一套方便团队内部使用的基础组件、通用组件
如果针对特定的行业例如财务、餐饮或者人力系统,会有针对特定业务可以抽象出来的组件,我们可以把它们抽象出来方便未来的重用,开发业务组件一般可以基于现有的组件比如第三方组件,在第三方组件的基础上进行开发
.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;
}
<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>
vue serve src/Steps.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>

整体结构
Form-test.vue
<template>
<lg-form class="form" ref="form" :model="user" :rules="rules">
<lg-form-item label="用户名" prop="username">
<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
<template>
<form>
<slot>slot>
form>
template>
<script>
export default {
name: "LgForm",
// 用于表单验证
provide() {
return {
form: this,
};
},
props: {
model: {
type: Object,
},
rules: {
type: Object,
},
},
data() {
return {};
},
created() {},
mounted() {},
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 scoped lang="less">
style>
src/form/FormItem.vue
<template>
<div>
<label>{{ label }}label>
<div>
<slot>slot>
<p v-if="errMessage">{{ errMessage }}p>
div>
div>
template>
<script>
// 用于表单验证的插件,需要手动安装(elementui使用的就是这个插件)
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: "",
};
},
created() {},
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 scoped lang="less">
style>
src/form/Input.vue
<template>
<div>
<input :type="type" :value="value" @input="handleInput" v-bind="$attrs" />
div>
template>
<script>
export default {
name: "LgInput",
inheritAttrs: false,
props: {
value: {
type: String,
},
type: {
type: String,
default: "text",
},
},
data() {
return {};
},
created() {},
mounted() {},
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 scoped lang="less">
style>
src/form/Button.vue
<template>
<div>
<button @click="handleClick">
<slot>slot>
button>
div>
template>
<script>
export default {
name: "LgButton",
data() {
return {};
},
created() {},
mounted() {},
methods: {
handleClick(evt) {
this.$emit("click", evt);
evt.preventDefault();
},
},
};
script>
<style scoped lang="less">
style>
两种项目的组织方式:
目录结构

__test__:测试代码目录dist:打包的目录src:源码目录index.js:打包入口LICENSE:版权信息package.json:包的描述信息README.md:文档Storybook 安装
演示:

lgelement 执行上述操作yarn storybook

module.exports = {
stories: ['../packages/**/*.stories.js'],
addons: ['@storybook/addon-actions', '@storybook/addon-links'],
};
packages/input/stories/input.stories.js
import LgInput from '../'
export default {
title: 'LgInput',
component: LgInput
}
export const Text = () => ({
components: { LgInput },
template: ' ',
data () {
return {
value: 'admin'
}
}
})
export const Password = () => ({
components: { LgInput },
template: ' ',
data () {
return {
value: 'admin'
}
}
})
packages/input/index.js
import LgInput from './src/input.vue'
LgInput.install = Vue => {
Vue.component(LgInput.name, LgInput)
}
export default LgInput
yarn add async-validator
packages/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: `
登 录
`,
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
}
})
}
}
})
packages/form/index.js
import LgForm from './src/form.vue'
LgForm.install = Vue => {
Vue.component(LgForm.name, LgForm)
}
console.log('test')
export default LgForm
开启 yarn workspaces 可以让我们在根目录中使用 yarn install 给所有的包统一安装依赖
项目依赖

node_modules 中减少重复lodash 版本不相同只会把相同版本的 lodash 提升到根目录的node_modules 中npm 不支持 workspaces开启 yarn 的工作区
package.json{
"private": true,
"workspaces": [
"packages/*"
]
...
}
yarn workspaces 使用
演示
yarn workspace lg-button add lodash@4
yarn workspace lg-form add lodash@4
yarn workspace lg-input add lodash@3
node_modules ,运行 yarn install 看效果
Lerna 可以方便我们把项目中的所有包统一发布
Lerna 介绍
Lerna 使用
初始化完成过后会做几件事情
组件单元测试的好处
安装依赖
配置测试脚本 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
Vue Test Utils 常用 API
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()
})
})
测试
yarn test
安装依赖
安装 Rollup 以及所需的插件
yarn add rollup rollup-plugin-terser rollup-plugin-vue@5.1.9 vue-template-compiler -D -W
Rollup 配置文件
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, // Dynamically inject css as a