8.4.3 封装Vue.js组件库

18 篇文章 0 订阅
2 篇文章 0 订阅
本文介绍了Component-Driven Development(CDD)的概念,强调了组件边界情况的处理,如获取根、父和子组件,以及Vue的provide&inject。接着,文章通过ElementUI演示如何快速开发和原型验证,详细讲解了Monorepo、Storybook和yarn workspaces的使用。此外,文章还涉及了lerna工具和组件单元测试的好处,包括使用vue test utils和jest进行测试,并给出了rollup打包配置的示例。
摘要由CSDN通过智能技术生成

本文为拉勾网大前端高薪训练营第一期笔记

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值