div点击展开 vue_封装 Vue.js 组件库

本文介绍了如何使用Vue.js开发组件库,包括组件分类、页面间传值、快速原型开发、组件管理(Monorepo、Storybook)、打包与发布。详细讲解了表单组件的实现思路和验证过程,以及如何利用Lerna进行Monorepo管理和发布。
摘要由CSDN通过智能技术生成
文章内容输出来源:拉勾教育大前端高薪训练营

必备知识

组件分类

  • 第三方组件:对界面UI要求不高
  • 基础组件:对界面UI要求高
  • 业务组件:针对特定的业务。譬如财务,入库等

页面中的传值

  1. 依赖注入

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 : 这种方式的数据是非响应式的,同时页面之间的耦合度较高

  1. 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

组件开发-表单组件

实现思路

  1. 内容展示及数据绑定功能
  2. 表单验证功能
  3. 单个表单验证(在文本框输入时验证)
  4. 点击提交时,验证

实现

  • 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 是对你有用的资源。

20637750d9dc3b2229f9b555ea8aacc4.png
// 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

目录结构

240d1d9ab52b2a385fe1e6211d322e1d.png

使用

  1. package 目录放入项目中
  2. 修改 ./.storybook/main.js 文件
// main.js
module.exports = {
  stories: ['../packages/**/*.stories.js'], // 入口
  addons: ['@storybook/addon-actions', '@storybook/addon-links'],
};
  1. 各个组件中index.js
import LgInput from './src/input.vue'

LgInput.install = Vue => {
  Vue.component(LgInput.name, LgInput)
}

export default LgInput
  1. 填写各个组件中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

  1. 配置
  2. 根目录下的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

8103fb0ec45084678493ad2f28128138.png
  • Vue Test Utils

4423e228467eba5c4c94907583f5e0f1.png

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 更合适

打包

  1. 安装依赖
  2. Rollup
  3. rollup-plugin-teser 压缩
  4. rollup-plugin-vue@5.1.9 把单文件组件编译成js
  5. vue-template-compiler
  6. 在 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()
    ]
  }
]
  1. 找到 input 包中的 package.json 的 scripts 配置
"build": "rollup -c"
  1. 运行打包/单个包
yarn workspace lg-input run build

打包多个包

  1. 安装依赖

除上述几个依赖外,还需 yarn add @rollup/plugin-json rollup-plugin-postcss @rollup/plugin-node-resolve -D -W

  1. 项目根目录创建 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
    }
  })
  1. 在每一个包中设置 package.json 中的 main 和 module 字段
"main": "dist/cjs/index.js",
"module": "dist/es/index.js"
  1. 根目录的 package.json 中配置 scripts

"build": "rollup -c" **

设置环境变量

  1. 安装依赖

yarn add cross-env -D -W

  1. 修改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",
  }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值