前端组件化开发

本文详细介绍了前端组件化开发的实践,包括Vue组件的边界情况处理、基于Element-UI的快速原型开发、组件分类与开发、Monorepo的使用、Storybook和Lerna+yarn workspaces的集成、Vue组件的单元测试以及Rollup打包。文章旨在帮助开发者掌握前端组件化的完整流程和最佳实践。

一、前言

开源组件库

  • Element-UI
  • iView

CDD

组件优先的开发方式,组件驱动开发。

  • 自上而下
  • 从组件级别开始,到页面级别结束

CDD的好处:

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

二、组件开发相关Vue知识点

处理组件的边界情况

  • $root:访问到Vue的根实例,操作根实例中的成员,可以在Vue根实例中存储共享数据,但还是推荐使用Vuex管理状态。在这里插入图片描述
  • p a r e n t / parent/ parent/children:获取父组件/子组件,并操作其中成员。大多数情况下,不推荐使用。
    在这里插入图片描述
  • $refs:可以访问子组件
    在这里插入图片描述
  • 依赖注入 provide/inject
    在这里插入图片描述
  • a t t r s / attrs/ attrs/listeners
    • 把父组件中非prop属性绑定到内部组件
    • 把父组件中的DOM对象的原生事件绑定到内部组件
      在这里插入图片描述

三、快速原型开发

  • VueCli中提供了一个插件可以进行原型快速开发
  • 需要先额为安装一个全局的扩展
    npm install -g @vue/cli-service-global
  • 使用vue serve快速查看组件的运行效果

vue serve

  • vue serve如果不指定参数默认会在当前目录找以下的入口文件
    main.js、index.js、App.vue、app.vue
  • 可以指定哟啊加载的组件
    vue serve ./src/Login.vue

基于Element-UI组件开发

  • 初始化package.json
    npm init -y
  • 安装Element-UI
    vue add element
  • 加载Element-UI,使用Vue.use()安装插件

四、组件开发

组件分类

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

组件开发-表单组件在这里插入图片描述

整体结构

  • Form
<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) {
      const tasks = this.$children
        .filter(child => child.prop)
        .map(child => child.validate())
      Promise.all(tasks)
        .then(() => cb(true))
        .catch(() => cb(false))
    }
  }
}
</script>

<style>
</style>
  • FormItem
<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
<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
<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

两种项目的组织方式:

  • Multirepo:每一个包对应一个项目
  • Monorepo:一个项目仓库中管理多个模块/包

目录结构:在这里插入图片描述

六、Storybook

官网地址: https://storybook.js.org/

介绍

  • 可视化的组件展示平台
  • 在隔离的开发环境中,以交互式的方式展示组件
  • 独立开发组件
  • 支持的框架
    • React、React Native、Vue、Angular
    • Ember、HTML、Svelte、Mithril、Riot

Storybook安装

  • 自动安装
    npx -p @storybook/cli sb init --type vue
    yarn add vue
    vue yarn add vue-loader vue-template-compiler --dev
    注:npx如果安装失败,可以这样解决:去掉空格 在这里插入图片描述
  • 手动安装
  • 运行 在这里插入图片描述

七、Lerna+yarn workspaces

yarn workplaces

可以解决项目组重复安装依赖的问题:
在这里插入图片描述

开启yarn的工作区

项目根目录的package.json中添加:

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

yarn workplaces使用

  • 给工作区根目录安装开发依赖
    yarn add jest -D -W
  • 给指定工作区安装依赖
    yarn workspaces violet-button add lodash@4
  • 给所有的工作区安装依赖
    yarn install

Lerna

介绍

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

使用

  • 全局安装
    yarn global add lerna
  • 初始化
    lerna init
  • 发布
    lerna publish

八、Vue组件的单元测试

组件单元测试好处

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

安装依赖

  • Vue Test Utils
  • Jest
  • vue-jest
  • babel-jest

安装:
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桥接

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()

组件测试示例在这里插入图片描述

九、Rollup打包

介绍

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

安装依赖

  • 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 配置文件

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 <style> tag
        css: true, 
        // Explicitly convert template to render function
        compileTemplate: true
      }),
      terser()
    ]
  }
]

配置 build 脚本并运行

找到 button 包中的 package.json 的 scripts 配置

"build": "rollup -c"

运行打包

yarn workspace lg-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({
    // 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"

设置环境变量

清理/删除无用的内容

  • stories文件夹
  • storybook-static

设置环境变量

  • 安装依赖:跨平台设置环境变量
    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"
  }
  • 运行:yarn build:prod
    prod与dev区别:prod模式打包后的js是压缩过的。

清理

清理所有包中的node_modules

"scripts": {
    "clean": "lerna clean"
  }

运行:yarn clean

清理所有包中的dist

  • 安装依赖:rimraf
    yarn add rimraf -D -W
  • 为packages包中每个组件的package.json文件配置del命令
"scripts": {
    "del": "rimraf dist"
  }
  • 运行:yarn workspaces run del(会执行所有组件中的del命令)

十、基于模板生成包的结构

  • 安装plop:yarn add plop -D -W
  • 写模板在这里插入图片描述
  • 写plop配置文件
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'
      }
    ]
  })
}
  • 添加脚本-package.json
"scripts": {
    "plop": "plop"
  }
  • 运行
    yarn plop
    在这里插入图片描述
    会根据模板生成link组件:
    在这里插入图片描述
  • 发布组件到npm仓库
    • 打包:yarn build:prod
    • 登录npm账号 在这里插入图片描述
    • 发布: yarn lerna
    • 检查 在这里插入图片描述
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值