组件测试及发布

单元测试

在组件开发完成并发布之前,需要对组件进行单元测试,单元测试是使用断言的方式判断实际的输出与预测的输出是否相同,目的是发现可能存在的问题;组件的单元测试是指使用单元测试工具对组件的各种状态及行为进行测试,确保组件发布后在使用过程中不会出现错误。

Vue组件的单元测试

  • 组件的单元测试有很多好处:

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

    官方文档

    首先需要安装 Jest 和 Vue Test Utils

    yarn add jest @vue/test-utils -D -W
    

    然后需要在 package.json 中定义一个单元测试的脚本

    // package.json
    {
      "scripts": {
        "test": "jest" // 修改test为jest
      }
    }
    

    为了告诉 Jest 如何处理 *.vue 文件,需要安装和配置 vue-jest 预处理器:

    yarn add vue-jest -D -W
    

    创建jext.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-jest处理es6语法

    yarn add babel-jest -D -W
    

    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-jest常用API

    中文文档

    • mount() 创建一个包含被挂载和渲染的Vue组件的Wrapper
    • Wrapper
      • vm wrapper包裹的组件实例
      • props() 返回Vue实例选项中的props对象
      • html() 组件生成的HTML标签
      • find() 通过选择器返回匹配的组件中的DOM元素
      • trigger() 触发DOM原生事件,自定义事件wrapper.vm.$emit()
      • ...
  • 创建packages/input/__tests__/input.test.js文件

    @vue/test-utils提供API用于挂载组件,Jest不需要导入因为测试文件是被jest加载执行的

    import input from '../src/input.vue'
    import { mount } from '@vue/test-utils'
    
    // 创建代码块 将input相关测试都添加到这里
    describe('wang-input', () => {
      test('input-text', () => {
        // 挂载组件 只是内存中的挂载 返回一个包裹器
        const wrapper = mount(input)
        // 测试生成的html中是否包含type=text
        expect(wrapper.html()).toContain('input type="text"')
      })
    })
    
    
  • 使用yarn test测试

    image-20210412082210659.png
  • 添加更多测试

    ...
    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',
          },
        })
        // 快照 第一次运行会将wrapper.vm.$el的内容存储在./__snapshots__/input.test.js.snap中
        expect(wrapper.vm.$el).toMatchSnapshot()
      })
    ...
    

    如果以后生成的快照和第一次生成的不同,会测试失败,如下所示:

    image-20210412082559674.png

可以通过yarn test -u删除旧的快照文件,并重新生成

Rollup打包
  • 特点

    • Rollup是一个模块打包器
    • Rollup支持Tree-shaking
    • 打包结果比webpack小
    • 开发框架/组件库使用Rollup更合适,如Vue、React等
  • 安装

    • Rollup
    • rollup-plugin-terser 对代码进行压缩
    • Rollup-plugin-vue@5.1.9 将单文件组件编译成js代码,最新版本适用于vue3.x,内部需要使用到vue-template-compiler
    • vue-template-compiler
  • 设置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({
            // Dynamically inject css as a <style> tag
            css: true, 
            // Explicitly convert template to render function
            compileTemplate: true
          }),
          terser()
        ]
      }
    ]
    
  • 单独打包button组件

    在button组件中添加rollup.config.js

    配置package.json添加脚本

    "build": "rollup -c"
    

    使用yarn workspace执行所有包中的命令

    yarn workspace wang-button run build
    

    打包完成,可以看到dist中生成的js文件

    image-20210412083944432.png
  • 打包所有组件

    需要额外安装的插件

    @rollup/plugin-json 让rollup可以把json文件作为模块加载

    rollup-plugin-postcss

    @rollup/plugin-node-resolve 打包过程会将依赖的第三方包进行打包

    执行命令

    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添加build命令

  • 对每个packages中的package.json设置mainmodule,打包的出口,使用包的入口

    ...
    "main": "dist/cjs/index.js",
    "module": "dist/es/index.js",
    ...
    
  • 使用yarn build进行打包

    打包完成,可以看到在packages中每个组件都生成了cjs和es模块

    image-20210412084909815.png
  • 设置环境变量

    yarn add cross-env -D -W 
    

    配置package.json

    "build:prod": "cross-env NODE_ENV=production rollup -c",
    "build:dev": "cross-env NODE_ENV=development rollup -c"
    

    分别执行不同的打包模式,dev模式下是不会进行代码压缩的

  • 清理所有包中的node_modules和dist

    删除node_module直接使用lerna clean即可

    image-20210412085521460.png

删除dist需要使用第三方包rimraf

安装

yarn add rimraf -D -W

在每个组件的package.json中添加del命令

"del": "rimraf dist"

执行yarn workspaces run del

发布

使用yarn plop新增link组件

  • 发布之前需要先添加测试文件,测试组件所有状态和对外公布方法

    import { mount } from '@vue/test-utils'
    import link from '../src/link.vue'
    
    describe('Wang-Link', () => {
      test('link-disabled-underlined', () => {
        // 挂载组件 只是内存中的挂载 返回一个包裹器
        const wrapper = mount(link, {
          propsData: {
            disabled: true,
          },
        })
        expect(wrapper.html()).toContain('class="disabled underlined"')
      })
      test('link-disabled', () => {
        const wrapper = mount(link, {
          propsData: {
            disabled: true,
            underlined: true,
          },
        })
        expect(wrapper.html()).toContain('class="disabled"')
      })
      test('link-a', () => {
        const wrapper = mount(link, {
          propsData: {
            href: 'www.baidu.com',
          },
        })
        expect(wrapper.props('href')).toBe('www.baidu.com')
      })
      test('link-snapshot', () => {
        const wrapper = mount(link, {
          propsData: {
            href: 'www.baidu.com',
            disabled: true,
          },
        })
        // 快照 第一次运行会将wrapper.vm.$el的内容存储在./__snapshots__/link.test.js.snap中
        expect(wrapper.vm.$el).toMatchSnapshot()
      })
    })
    
  • 运行生成环境打包

  • 发布之前检查npm登陆状态

    npm whoami
    
  • 使用yarn lerna进行发布

    发布完成,打开npm

    image-20210412094537562.png

项目地址

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,366评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,521评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,689评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,925评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,942评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,727评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,447评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,349评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,820评论 1 317
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,990评论 3 337
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,127评论 1 351
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,812评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,471评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,017评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,142评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,388评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,066评论 2 355

推荐阅读更多精彩内容