封装 Vue.js 组件库

文章前言

笔记来源:拉勾教育 大前端高薪训练营
阅读建议:内容较多,建议通过左侧导航栏进行阅读

课程目标

开源组件库

  • Element-UI
  • iView

CDD

基本介绍

  • CDD(Component-Driven Development) :组件驱动开发
    • 自上而下
    • 从组件级别开始,到页面级别结束

CDD 的好处

  • 组件在最大程度被重用
  • 并行开发
  • 可视化测试

基础回顾

处理组件的边界情况

  • $root

  • $parent / $children

  • $refs

    封装 Vue.js 组件库_第1张图片

  • 依赖注入 provide / inject

    • 方便获取最外层的成员
    • 通过依赖注入的成员,不是响应式的,应该避免修改注入(inject)进来的成员

$attrs / $listeners

  • $attrs
    • 把父组件中非 prop 属性绑定到内部组件
  • $listeners
    • 把父组件中的 DOM 对象的原生事件绑定到内部组件

快速原型开发

  • VueCLI 中提供了一个插件可以进行原型快速开发

  • 需要先额外安装一个全局的插件 @vue/cli-service-global

    npm install -g @vue/cli-service-global
    
  • 使用 vue serve 快速查看组件的运行效果

vue serve

  • vue serve 如果不指定参数,默认会在当前目录找以下的入口

    • main.jsindex.jsApp.vueapp.vue
  • 可以指定要加载的组件

    # vue serve 组件路径
    vue serve ./src/login.vue
    

ElementUI

  • 初始化 package.json

    npm init -y
    
  • 安装 ElementUI

    vue add element
    
  • 加载 ElementUI,使用 Vue.use() 安装插件

    import ElemnetUI from 'element-ui'
    import 'element-ui/lib/theme-chalk/index.css'
    
    Vue.use(ElemnetUI)
    

组件开发

组件分类

  • 第三方组件
  • 基础组件
  • 业务组件

步骤条组件

在这里插入图片描述

  • 样式文件 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.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: 'blue'
                }
            }
        }
    script>
    
  • 测试文件(父组件)Steps-test.vue

    <template>
        <div>
            <steps :count="count" :active="active">steps>
            <hr />
            <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 < this.count && this.active++
                }
            }
        }
    script>
    
  • 使用 vue serve 快速查看组件的运行效果

    vue serve src/Steps-test.vue
    
  • 访问 http://localhost:8080,查看组件的使用效果,如图所示:

    封装 Vue.js 组件库_第2张图片

表单组件

展示功能

整体结构

封装 Vue.js 组件库_第3张图片

展示效果

封装 Vue.js 组件库_第4张图片

代码实现

  • 模拟 el-form 组件,文件:Form.vue

    <template>
        <form>
            <slot />
        form>
    template>
    
    <script>
        export default {
            
            name: 'LgForm',
            provide () {
            
                return {
            
                    form: this
                }
            },
            props: {
            
                model: {
            
                    type: Object
                },
                rules: {
            
                    type: Object
                }
            },
        }
    script>
    
  • 模拟 el-form-item 组件,文件:FormItem.vue

    <template>
        <div>
            <label>{
          { label }}label>
            <div>
                <slot />
                <p v-if="errMessage">{
          { errMessage }}p>
            div>
        div>
    template>
    
    <script>
        export default {
            
            name: 'LgFormItem',
            inject: ['form'],
            props: {
            
                label: {
            
                    type: String
                },
                prop: {
            
                    type: String
                }
            },
            data () {
            
                return {
            
                    errMessage: ''
                }
            },
        }
    script>
    
  • 模拟 el-input 组件,文件:Input.vue

    <template>
      <div>
          <input v-bind="$attrs" :type="type" :value="value">
      div>
    template>
    
    <script>
    export default {
            
        name: 'LgInput',
        inheritAttrs: false, // 禁用继承父组件中传入的属性
        props: {
            
            value: {
            
                type: String
            },
            type: {
            
                type: String,
                default: 'text'
            }
        },
    }
    script>
    
  • 模拟 el-button 组件,文件:Button.vue

    <template>
        <div>
            <button :type="type" @click="handleClick">
                <slot />
            button>
        div>
    template>
    
    <script>
        export default {
            
            name: 'LgButton',
            methods: {
            
                handleClick (evt) {
            
                    this.$emit('click', evt)
                    evt.preventDefault()
                }
            }
        }
    script>
    

表单验证

ElementUI 中的表单验证,是使用了 async-validator 模块

  • 安装 async-validator

    npm i async-validator
    

Input 组件验证

  • Input 组件中触发自定义事件 validate

    • Input.vue
    <input v-bind="$attrs" :type="type" :value="value" @input="handleInput">
    
    <script>
        export default {
            
            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>
    
  • FormItem 渲染完毕注册自定义事件 validate

    • FormItem.vue
    <script>
    import AsyncValidator from 'async-validator'
    export default {
            
        mounted () {
            
            this.$on('validate', () => {
            
                this.validate()
            })
        },
        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>
    
  • Form 定义事件 validate

    • Form.vue
    methods: {
           
        validate (cb) {
           
            const tasks = this.$children
                .filter(child => child.prop)
                .map(child => child.validate())
    
            Promise.all(tasks)
                .then(() => cb(true))
                .catch(() => cb(false))
        }
    }
    

Monorepo – 组件管理方式之一

两种项目的组织方式

  • Multirepo(Multiple Repository)
    • 每一个包对应一个项目
  • Monorepo(Monolithic Repository)
    • 一个项目仓库中管理多个模块/包

Monorepo 目录结构

封装 Vue.js 组件库_第5张图片

  • packages 文件夹:存放所有要开发的组件,每一个组件对应一个文件夹,每一个文件夹就是一个包,它可以单独发布到 npm 中

  • button 文件夹:创建的组件名

  • __test__ 文件夹:存放测试的文件

  • dist 文件夹:打包目录

  • src 文件夹:存放源码,*.vue 文件存放的位置

  • index.js 文件:打包的入口,使用组件,并将组件导出

    import Button from './src/button.vue'
    // 安装插件时,进行调用
    Button.install = Vue => {
           
        Vue.component(Button.name, Button)
    }
    
    export default Button
    
  • LICENSE 文件:存放版权信息(本次采用 MIT 协议)

  • package.json 文件:包的描述信息,包的名称和版本

  • README.md 文件:文档

Storybook – UI组件的开发环境

基本介绍

  • 可视化的组件展示平台
  • 在隔离的开发环境中,以交互式的方式展示组件
  • 在主程序之外运行,独立开发组件
  • 支持的框架
    • React、React Native、Vue、Angular
    • Ember、HTML、Svelte、Mithril、Riot
  • 官网地址:https://storybook.js.org/
  • GitHub 地址:https://github.com/storybookjs/storybook

基本使用

  • 自动安装

    npx -p @storybook/cli sb init --type vue
    yarn add vue
    yarn add vue-loader vue-template-compiler --dev
    

    --type 标志来指示 Storybook 根据该标志进行自身配置。

  • 启动服务,并访问

      npm run storybook
      # yarn storybook
    

    封装 Vue.js 组件库_第6张图片

  • 项目构建

      npm run build-storybook
      # yarn build-storybook
    

    封装 Vue.js 组件库_第7张图片

基本案例

  • 1,在根目录创建 packages 文件夹,用于存放组件,一个文件夹代表一个组件,目录结构,如下图所示:

    封装 Vue.js 组件库_第8张图片

  • 2,组件的 *.vue 存放在 src 目录下,如图所示:

    封装 Vue.js 组件库_第9张图片

  • 3,在每一个组件中,分别创建一个 stories/*.stories.js 文件,用于存放 默认导出 描述组件,以及 命名出口 描述 storybook

    • package/input/stories/input.stories.js
    import LgInput from '../'
    
    // Storybook lists your stories and provides information used by addons
    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'
            }
        }
    })
    
    • package/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
                    }
                })
            }
        }
    })
    

    注意:

    ​ 此处引入了 formitem 组件,由于 formitem 组件依赖于 async-validator 包,因此,需要在 formitem 组件中安装 async-validator 包,否则会报错

  • 4,修改 storybook 的配置文件 ./storybook/main.js

    module.exports = {
           
        "stories": ["../packages/**/*.stories.js"],
        "addons": [
            "@storybook/addon-links",
            "@storybook/addon-essentials"
        ]
    }
    
  • 5,启动服务,测试组件

    npm run storybook
    # yarn storybook
    
  • 6,访问网址,如图所示:

    封装 Vue.js 组件库_第10张图片

yarn workspace

项目依赖

封装 Vue.js 组件库_第11张图片

基本介绍

Yarn Workspaces 允许用户在单个根 package.json 文件的子文件夹中从多个package.json 文件中安装依赖。

通过防止 Workspaces 中依赖包的重复,使原生 WorkspacesYarn 可以实现更快更轻松的依赖安装。Yarn 还可以在依赖于彼此的 Workspaces 之间创建软链接,并确保所有目录的一致性和正确性。

开启 yarn 的工作区

  • 项目根目录的 package.json

    {
           
        "private": true,
        "workspaces": ["packages/*"],
    }
    

    "private": true:将项目提交到 GitHub 或 NPM 的时候,禁止把当前根目录的内容进行提交

    "workspaces": []:存放所有要管理的包路径

基本使用

  • 给工作区根目录安装开发依赖

    yarn add jest-D -W
    
    • jest:FaceBook 发布的单元测试工具
    • -D:开发依赖
    • -W:工作区,指的是安装到工作区的根目录
  • 给指定工作区安装依赖

    yarn workspace lg-button add lodash@4
    

    lg-button:包名,即在 package.json 中设置的 name 属性

  • 给所有的工作区安装依赖

    yarn install
    

总结

封装 Vue.js 组件库_第12张图片
重复的依赖包会提升到根目录下的 node_modules 目录中,单独的依赖会安装到对应的组件的 node_modules 目录中,方便管理依赖。

Lerna

基本介绍

  • Lerna 是一个优化使用 git 和 npm 管理多包仓库的工作流工具
  • 用于管理具有多个包的 JavaScript 项目
  • 它可以一键把代码提交到git和npm仓库

基本使用

  • 全局安装

    yarn global add lerna
    
  • 初始化

    lerna init
    
  • 发布

    lerna publish
    
  • 清理项目中的 node_modules

    lerna clean
    

    注意:

    • 在发布之前,需要建立远端 Git 仓库,并且连接本地项目
  • 在发布之前,还需要 注册 / 登录 npm

    # 注册
    npm adduser
    
    # 登录
    npm login
    

    登录结果,如图所示:

    在这里插入图片描述

    查看登录账户,如下所示:

    npm whoami
    

    在这里插入图片描述

  • 查看当前的镜像源

    npm config get registry
    

    查看是否是npm官网,如果不是的话,需要将镜像源修改回来

    在这里插入图片描述

Vue 组件的单元测试

组件单元测试的好处

  • 提供描述组件行为的文档
  • 节省手动测试的时间
  • 减少研发新特性时产生的bug
  • 改进设计
  • 促进重构

安装依赖

  • Vue Test Utils:Vue提供的组件单元测试官方库,需要结合单元测试框架一起使用

  • Jest:FaceBook 开发的单元测试框架

  • vue-jest:预处理器

  • babel-jest:对测试代码进行降级处理,即将 ES6 语法转换为 ES5等

  • 安装

    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 桥接

  • 安装 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()

编写测试文件

  • __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: "password",
                    value: 'admin'
                }
            })
            expect(wrapper.vm.$el).toMatchSnapshot()
        })
    })
    

    执行 yarn test,测试结果,如图所示:

    封装 Vue.js 组件库_第13张图片

    生成的快照文件,如图所示:

    封装 Vue.js 组件库_第14张图片

Rollup

基本介绍

  • Rollup 是一个模块打包器
  • Rollup 支持 Tree-shaking
  • 打包的结果比 Webpack 要小
  • 开发框架/组件库的时候使用 Rollup 更合适

安装依赖

  • Rollup

  • rollup-plugin-terser:对代码进行压缩

  • [email protected]:把单文件组件编译成 js 代码,注意:一定要指定版本

  • vue-template-compiler:编译器

  • 安装

    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