vue组件封装的流程

前言

前一段时间利用空闲时间学习了一下vue组件的封装,也在工作中进行了实践,将公司常用的一个api抽象成了vue组件,并发布在npm上。之前觉得组件的封装是一件很困难的事情,通过亲身体验之后,发现确实有很多需要注意的地方,但是当自己真正走完了这个过程之后,回头看的时候,其实也不过如此。真正困难的其实不是组件封装的流程与步骤,而是组件的实现思想。但是,对于没有进行过组件封装的同学来说,流程和步骤确实也存在着许多坑,但是一旦你趟过去之后,就会非常轻松。

我的学习渠道主要来源于两个地方,一个是vue官方文档cookbook中一篇介绍组件封装的文章,另一个是饥人谷的一门课程。

我将通过一系列文章去讲一下整个组件封装的过程中我是如何做的,文章会围绕一个简易组件的封装过程去写,这个组件并不具有实际用处,只是一个demo。希望通过几篇文章,给那些想自己封装组件的同学做一个参考。

demo地址

https://github.com/zhuweileo/vue-component-demo

需求分析

我们的需求如下:

  • 写一个button.vue组件

    ps:由于是为了学习封装步骤,所以这里button组件的功能十分简单。

  • 将组件发布至npm

    按说单元测试应该在发布之前进行,但是单元测试比较复杂,为了快点得到成就感,所以先走简单的流程。

  • 对组件进行单元测试

步骤

1.使用webpack打包组件

2.发布到npm

3.单元测试


1.使用webpack打包组件

为什么要打包

可能你会问为什么要对.vue文件进行打包,直接引用.vue文件不可以么?当然可以,但是前提是用户有自己的打包工具可以处理.vue文件,如果用户没有打包工具,你的组件是不是就不能用了呢!

不打包,你只能这么用

import MyComponent from 'my-component';

export default {
  components: {
    MyComponent,
  },
  // rest of the component
}

打包后,你还可以这么用

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Title</title>
</head>
<body>
<div id="app">
  {{text}}
  <m-button>nio</m-button>
</div>

<script src="https://cdn.jsdelivr.net/npm/vue@2.5.17/dist/vue.js"></script>
<script src="../dist/index.js"></script>
<script>
  console.log(MyComponent);
  const {MButton} = MyComponent;
  const app = new Vue({
    el:'#app',
    data:{
      text: 'hello vue!',
    },
    components:{
      'm-button':MButton,
    }
  })
</script>
</body>
</html>

组件的封装肯定离不开打包工具,打包工具大家最熟悉的一定非webpack莫属了。其实,在vue官方文档中的cookbook中,文章的作者推荐使用的 打包工具是Rollup,并附有详细的配置文件,但是我之前对Rollup不熟悉,就没有用,有兴趣的同学可以自己尝试。

webpack版本及文件具体内容

webpack版本:4.17.1 (比较新的版本)

webpack.config.js

var path = require('path');
const VueLoaderPlugin = require('vue-loader/lib/plugin')
var webpack = require('webpack')

module.exports = {
    entry: {
        'index': './src/index.js'
    },
    output: {
        path: path.resolve(__dirname, './dist'),
        filename: '[name].js',
        library: 'MyComponent',
        libraryTarget: 'umd'
    },
    devtool: '#eval-source-map',
    resolve: {
        alias: {
            'vue$': 'vue/dist/vue.esm.js'
        },
        extensions: ['.js', '.vue']
    },
    mode: 'production',
    performance: {
        hints: false
    },
    module: {
        rules: [
            {
                test: /\.js$/,
                exclude: /node_modules/,
                use:{
                    loader: 'babel-loader',
                    options: {
                        presets: ['@babel/preset-env'],
                        "env": {
                            "test": {
                                "plugins": ["istanbul"]
                            }
                        }
                    }
                }
            },
            {
                test: /\.vue$/,
                loader: 'vue-loader',
                exclude: /node_modules/
            },
            {
                test: /\.css$/,
                use: [
                    'vue-style-loader',
                    'css-loader'
                ]
            }
        ]
    },
    plugins: [
        new VueLoaderPlugin()
    ]
};

webpack配置文件详解

入口文件

    entry: {
        'index': './src/index.js'
    },

入口文件配置比较简单,关键在于入口文件的内容。

index.js

export {default as MButton}  from './MButton.vue'

入口文件的意思就是将MButton.vue文件中的默认导出值,重命名为MButton然后再导出。

(对于export语法不理解的同学,推荐查看阮一峰的es6相关教程

输出配置

    output: {
        path: path.resolve(__dirname, './dist'), //输出目录
        filename: '[name].js', //输出文件名
        library: 'MyComponent', //输出的全局变量名称
        libraryTarget: 'umd',//输出规范为umd
    },

前两行配置不解释,解释下后两行

  • library: 'MyComponent'

    MyCompoent是一个全局变量名称(你自定义),当用户直接通过script标签引用你的组件的时候,这个将作为你的组件的命名空间,你的组件的内容会挂载到该全局变量上面,作为它的一个属性,类似于你使用jquery的时候,会有一个全局的$jquery供你引用。

  • libraryTarget: 'umd',

    使用umd(通用模块规范)打包你的模块。umd兼容amd以及cmd模式,并且会导出一个全局变量。这样使打包后的模块可以使用各种规范引用,增强模块的通用性。

    引用webpack官网的解释:

    libraryTarget: 'umd' - This exposes your library under all the module definitions, allowing it to work with CommonJS, AMD and as global variable. Take a look at the UMD Repository to learn more.

    这么设置可以让你的库适应所有的模块标准,允许别人通过CommonJS、AMD和全局变量的形式去使用它。

    具体什么是umd、amd、cmd大家自行百度吧。

模式

mode: 'production'

webpack4 新增的配置参数,意为webpack将认为该打包是为了生产环境,会将一些默认配置设置为生产环境所需要的,例如默认进行代码压缩。

rules

这里是重点,有三个规则

  1. 使用babel处理js,这样你就可以在vue单文件组件中的script标签内放心使用es6语法

     {
         test: /\.js$/,
         exclude: /node_modules/,
         use:{
             loader: 'babel-loader',
             options: {
                 presets: ['@babel/preset-env'],
                 "env": {
                     "test": {
                         "plugins": ["istanbul"]
                     }
                 }
             }
         }
     },
    
  2. 使用vue-loader处理.vue文件。在webpack中每一种文件的处理都需要对应的loader,就像css需要css-loader,js文件需要babel-loader,vue文件也不例外。其实vue-loader就是将你写的单文件组件内的三个标签,转化为原生的js,具体原理查看官方文档

     {
         test: /\.vue$/,
         loader: 'vue-loader',
         exclude: /node_modules/
     },
    
  3. 使用css-loader处理和vue-style-loader处理单文件组件内style便签内的css样式

     {
         test: /\.css$/,
         use: [
             'vue-style-loader',
             'css-loader'
         ]
     }
    

使用vue-loader插件

官方说必须使用VueLoaderPlugin配合vue-loader使用,具体为什么我也不清楚。

  plugins: [
    // make sure to include the plugin for the magic
    new VueLoaderPlugin()
  ]

这就是所有的webpack配置,其实还是挺简单的。


2. 发布到npm上

修改你的package.json文件

   {
      ...
      
         "name": "vue-component-demo",//你的组件的名字
         "version": "0.0.1",//当前版本号
         "description": "vue component demo",//描述
         "main": "dist/index.js",//入口文件
         
         ...
   }
  • 入口参数"main": "dist/index.js",指向的就是我们之前打包好的文件。

    这样当用户向下面这样引入你的组件的时候,打包工具就会直接去"main": "dist/index.js"找文件。

    import {button} from 'vue-component-demo'
    
  • name参数不能和npm上已有的组件名相同,否则发布的时候会报错,如果不幸有人用了这个组件名,你就需要修改一下,再重复这个流程重新发布就好了。

登录npm(需要提前注册一个npm账号)

   /vue-component-demo (master)
   $ npm adduser
   Username: 
   Password:
   Email: (this IS public) 

发布组件

   /vue-component-demo (master)
   $ npm publish

至此,你的组件就已经发布到npm上了,别人就可以通过npm 安装你的组件,然后使用。

   npm install vue-component-demo

更新组件

以上是我们发布的第一个版本,如果之后你有修复组件中的bug,或者增强了组件的功能,你就要更新组件,更新组件也很简单。

  • 更新package.json中的version参数,不能和之前的版本号重复,否则发布不成功。
  • 再执行一次npm publish

3.单元测试

为什么单元测试

单元测试的目的是为了保证组件的的质量(可靠性),毕竟写组件是为了让更多的人使用,发布完之后出现一堆bug总是不好的。单元测试,可以让你每次你修改组件之后,及时发现是否存在bug,保证每次发布的代码存在较少的bug。

单元测试工具

安装工具

安装主要的工具

npm install karma mocha chai @vue/test-utils 

karma 配合chai,mocha等工具时,需要安装对应的一系列插件,插件比较多没有都写出来,具体参考package.json

npm install karma-chai karma-mocha karma-webpack karma-sourcemap-loader...

karma配置

//引入打包用的webpack配置
var webpackConfig = require('./webpack.config.js')

module.exports = function (config) {
    config.set({
        //引入需要使用的工具
        frameworks: ['mocha','sinon-chai','chai-dom','chai',],
        /*
         这个参数决定哪些文件会被放入测试页面,哪些文件的变动会被karma监听,以及以服务的形
         式供给
         */
        files: [
            'test/**/*.spec.js'
        ],
        //测试文件会使用webpack进行预处理
        preprocessors: {
            '**/*.spec.js': ['webpack', 'sourcemap']
        },
        //预处理时webpack的配置
        webpack: webpackConfig,
        //使用哪些工具进行测试报告
        reporters: ['spec','coverage'],
        //通过哪些浏览器进行测试
        browsers: ['Chrome']
    })
}

写测试用例

/test/button.spec.js

import MButton from '../src/MButton'
import {mount} from "@vue/test-utils";
import Syn from 'syn'

describe('MButton.vue',function () {

  it('can set type prop',function () {
    const wrapper = mount(MButton,{
      propsData:{
        type: 'warn',
      }
    });
    const vm = wrapper.vm;
    expect(vm.$el.classList.contains('warn')).to.be.true
  })

  it('can click',function (done) {
    const click = sinon.spy();
    const haha = sinon.spy();
    const wrapper = mount(MButton,{
      propsData:{
        type: 'warn',
      },
      listeners:{
        click,
      }
    });

    Syn.click(wrapper.vm.$el,function () {
      sinon.assert.calledWith(click);
      done();
    });
  })

});

describe,it函数

测试用例中的这两个函数是 mocha 库中提供的

  • 为什么没有import,就可以直接使用?
    还记得karma配置文件中的frameworks: ['mocha','sinon-chai','chai-dom','chai',]吗?
  • 这两个函数有什么用?
    为你的测试用例划分区块,一个describe是一个大区块,一个it是一个小区块,两个函数的第一个参数是对于区块的描述,第二个参数是一个回调函数,指定区块的具体测试内容。

mount函数

mount函数是@vue/test-utils库中的函数

  • 有什么用?

    mount的作用是装载(运行)你的vue组件,相当于如下代码。

    const constructor =  Vue.extend(MButton)
    new constructor().$mount()
    

    只有将你的组件运行起来,才可以测试其功能是否正确。

  • @vue/test-utils是什么?

    是vue组件测试辅助库,使用细节查看@vue/test-utils

expect函数

expect函数来源于chai

  • 有什么用?

    expect期待一个结果 。

    //期待button组件的 dom元素的classList中包含warn是真的
    expect(vm.$el.classList.contains('warn')).to.be.true  
    
  • to,be有什么用?

    没有任何实质性意义,是为了让代码看起来更像一个句子,增强可读性

sinon

  • 有什么用

    可以用来测试事件是否被触发

    //声明一个间谍函数
    const click = sinon.spy();
    
    const wrapper = mount(MButton,{
      propsData:{
        type: 'warn',
      },
      //这里参看  mount 函数的介绍
      listeners:{
        click, //把间谍函数作为click事件的回调函数
      }
    });
    

syn

  • 用什么用

    模拟用户的交互动作(点击、拖拽等)

    //模拟click事件,然后期待sinon生成的click函数被调用
    Syn.click(wrapper.vm.$el,function () {
       sinon.assert.calledWith(click);
       done();
    });
    
    
    

运行测试用例

在package.json中加入如下代码
package.json

 "scripts": {
    "test": "cross-env BABEL_ENV=test karma start --single-run=false", 
    ...
  },

当执行以上脚本之后,karma会自动打开一个浏览器窗口,将测试用例都执行一遍,并告诉你哪个测试通过了,哪个没有通过。如果有测试用例没通过,你就应该检查是你的代码有问题,还是测试用例编写的不正确,并修复问题,直到所有测试用例都通过,之后你就可以发布你的代码了。

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

推荐阅读更多精彩内容

  • 写在前面的话 阅读本文之前,先看下面这个webpack的配置文件,如果每一项你都懂,那本文能带给你的收获也许就比较...
    不忘初心_9a16阅读 3,232评论 0 17
  • ## 框架和库的区别?> 框架(framework):一套完整的软件设计架构和**解决方案**。> > 库(lib...
    Rui_bdad阅读 2,895评论 1 4
  • webpack 是什么? 本质上,webpack 是一个现代 JavaScript 应用程序的静态模块打包器(mo...
    IT老马阅读 3,308评论 2 27
  • 期中考试,大家一听这个词,肯定非常紧张,今天我们迎来了上一年级第一次的正式考试(期中考试),我的心里有点小小...
    董筱萱阅读 283评论 0 0
  • 昨天女儿开学了,在客车上瞌睡了一会儿,笔记本电脑被偷走了。女儿给边哭边给我打电话说情况。我说,宝贝,电脑丢了,你很...
    王瑶燕行阅读 149评论 0 0