前言
前一段時間利用空閑時間學習了一下vue組件的封裝,也在工作中進行了實踐,將公司常用的一個api抽象成了vue組件,并發布在npm上。之前覺得組件的封裝是一件很困難的事情,通過親身體驗之后,發現確實有很多需要注意的地方,但是當自己真正走完了這個過程之后,回頭看的時候,其實也不過如此。真正困難的其實不是組件封裝的流程與步驟,而是組件的實現思想。但是,對于沒有進行過組件封裝的同學來說,流程和步驟確實也存在著許多坑,但是一旦你趟過去之后,就會非常輕松。
我的學習渠道主要來源于兩個地方,一個是vue官方文檔cookbook中一篇介紹組件封裝的文章,另一個是饑人谷的一門課程。
我將通過一系列文章去講一下整個組件封裝的過程中我是如何做的,文章會圍繞一個簡易組件的封裝過程去寫,這個組件并不具有實際用處,只是一個demo。希望通過幾篇文章,給那些想自己封裝組件的同學做一個參考。
demo地址
https://github.com/zhuweileo/vue-component-demo
需求分析
我們的需求如下:
-
寫一個button.vue組件
ps:由于是為了學習封裝步驟,所以這里button組件的功能十分簡單。
-
將組件發布至npm
按說單元測試應該在發布之前進行,但是單元測試比較復雜,為了快點得到成就感,所以先走簡單的流程。
對組件進行單元測試
步驟
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
這里是重點,有三個規則
-
使用babel處理js,這樣你就可以在vue單文件組件中的
script
標簽內放心使用es6語法{ test: /\.js$/, exclude: /node_modules/, use:{ loader: 'babel-loader', options: { presets: ['@babel/preset-env'], "env": { "test": { "plugins": ["istanbul"] } } } } },
-
使用vue-loader處理.vue文件。在webpack中每一種文件的處理都需要對應的loader,就像css需要css-loader,js文件需要babel-loader,vue文件也不例外。其實vue-loader就是將你寫的單文件組件內的三個標簽,轉化為原生的js,具體原理查看官方文檔。
{ test: /\.vue$/, loader: 'vue-loader', exclude: /node_modules/ },
-
使用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。
單元測試工具
-
karma:啟動測試流程
-
mocha:劃分測試模塊
-
chai:測試斷言庫
-
vue-test-utils:輔助測試vue組件
安裝工具
安裝主要的工具
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']
})
}
寫測試用例
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會自動打開一個瀏覽器窗口,將測試用例都執行一遍,并告訴你哪個測試通過了,哪個沒有通過。如果有測試用例沒通過,你就應該檢查是你的代碼有問題,還是測試用例編寫的不正確,并修復問題,直到所有測試用例都通過,之后你就可以發布你的代碼了。