Vue組件庫搭建實踐與探索

在之前傳統的前端頁面開發方式時,存在協同困難,可複用性差的問題,致使開發和維護都不是一件簡單的事。而組件化思想的提出,以及Vue、React等MV*框架的快速流行,讓咱們開始嘗試用組件化的思想去開發。因爲筆者最近在研究組件庫的搭建,故撰文記之。

前言

組件化思想讓咱們把頁面劃分爲一個個組件,組件內部維護本身的UI展現、交互邏輯,這樣將能夠大大提升代碼的複用性以及可維護性。css

本文將着重介紹組件庫搭建過程當中的準備工做,包括定義合理的項目結構、組件庫打包構建、實現按需加載以及組件庫所須要完善的其餘工做等,但願對讀者有所幫助~(本文將以vue組件庫爲案例敘述)前端

一、定義項目結構

首先你須要爲你的組件庫定義一個合理的項目結構,合理的項目結構對後期的代碼維護和管理十分有幫助。你也能夠先使用vue-cli初始化一個項目結構,這裏筆者爲了對組件庫單獨分割出來管理,在根目錄下定義一個components文件夾,組件樣式統一放置於該文件夾下的theme-chalk下,項目主要結構以下:vue

image.png

能夠看到,文件結構中有兩個關鍵點,一個是組件庫入口,一個是單組件入口,組件庫入口須要對項目組件進行註冊,同時暴露對象中要含有install方法用於被Vue.use的時候調用。組件庫入口文件以下:node

import helloworld from "../components/helloworld/index.js";
import test from "../components/test/index.js";
import { version } from '../package.json';
const components = {
    helloworld,
    test
}
const install = function (Vue) {
    if (install.installed) return;
    Object.keys(components).forEach(key => {
      Vue.component(components[key].name, components[key]);
    })
    install.installed = true;
  };
if (typeof window !== 'undefined' && window.Vue) {
    install(window.Vue);
}
export {
    helloworld,
    test
}
export default {
    version,
    install
};

其實作的事情很簡單,把組件讀取進來後,進行統一的Vue.component註冊,以後暴露install方法便可。同理,componentA文件夾下的index.js,僅針對A組件作處理,以下:webpack

import helloworld from "./src/main.vue"
helloworld.install = function(Vue) {
  Vue.component(helloworld.name, helloworld);
};
export default helloworld;

聰明的你可能已經察覺到了,前面講樣式單獨分離出來到theme-chalk下管理,是爲何?爲何這裏又沒有引入樣式呢?git

一、不嵌套在vue組件的style標籤中書寫樣式,方便對組件樣式的單獨打包,對於按需加載十分有意義;github

二、單獨分離出樣式組件進行管理,方便後續對組件進行換膚;web

三、單獨分離出樣式文件,方便統一管理。vue-cli

所以,若是你也要對本身的組件提取爲組件庫的話,強烈建議將樣式單獨分離出來處理。npm

二、打包構建配置

項目結構和入口文件都定義好了,接下來就要考慮打包構建,讓咱們的項目跑起來了。

開發環境

開發環境只須要配置好webpack,利用devserver即可以開始調試,若是你是用vue-cli初始化項目的話,直接npm run serve便可。

正式環境

在正式發佈部署的時候,問題開始逐漸變得複雜起來,不一樣於以往的項目,你只須要配置好一份webpack.config.js打包文件,區分開發和發佈環境,就能夠進行打包構建了,可是組件庫或者第三方庫不同,你須要將打包構建的結果提供給開發者使用,做爲一個貼心的庫提供者,你須要考慮如下問題:

一、你可能須要提供不一樣模塊類型的包:commonjs、umd、es模塊;

二、你須要對各組件單獨打包處理,方便用戶按需加載;

三、因爲須要實現按需加載,不避免的,你須要對樣式也單獨打包處理;

四、提供打包壓縮後的.min.js文件

因爲這是一個第三方庫,而且這是一個組件庫,使得咱們對打包這件事變得束手束腳,咱們須要針對不一樣的狀況進行打包,提供豐富的打包產物給用戶,讓用戶想要啥有啥。爲了明確咱們打包構建所須要的產物,筆者畫了一張示意圖:

image.png

或許這樣,你會更明白咱們的打包任務是什麼,相比以往的webpack一把梭,已經梭不了了,在咱們的打包產物中,存在es模塊,而webpack自己打包不支持導出es模塊,因此最終的打包構建咱們只能藉助於rollup了。(你可能爲問,爲何咱們要執着於打包出es模塊的包?其實,大部分的第三庫都已經利用rollup進行打包,除了es模塊將成爲將來的緣由之外,es模塊對於tree-shaking有極大意義,後文將介紹利用其進行按需加載)接下里咱們剖析一下每一個任務要怎麼完成(按照圖中所標序號):

一、構建任務一:導出組件庫總包,利用rollup。若是你不稀罕es模塊的導出的話,請選擇webpack一把梭,這裏爲了提供es模塊的導出,向主流看齊,咱們選擇用rollup進行打包。

二、構建任務二:對各組件單獨打包,利用webpack。還記得咱們在第一節中爲每一個組件預留了一個index.js的入口嗎?沒錯,這就是爲了按需加載埋下的伏筆,本覺得咱們能夠用rollup就進行打包的,可是rollup的entry只支持string形式,若是我有100個組件,難道要執行一百次rollup指令嗎...不要緊,對於單獨打包,仍是webpack香,經過webpack的entry配置hash對象,即可以對各個組件進行單獨打包。(這個時候就沒法導出es模塊了,可是不要緊,畢竟只是單個組件的引入,不treeshaking了唄hh)

三、構建任務三與構建任務四:對樣式統一打包和單獨打包,利用gulp。因爲咱們須要對css文件單獨打包,不管是rollup仍是webpack都不能把打包入口指定爲css文件,因此咱們只能藉助gulp來打包css了。

最後打包文件是如下幾個:

└── build
    ├── gulp.css.js // 針對css的文件處理打包
    ├── rollup.config.js  // 利用rollup進行最終產物打包
    ├── webpack.dev.js  // 開發模式配置 本地起dev server
    └── webpack.component.js // 利用webpack對各組件單獨打包

webpack.component.js配置以下:

...
// 讀取components文件夾下的全部文件
const fs = require('fs');
const items = fs.readdirSync('./components');
const dirs = items.filter(item => {
  return fs.statSync(path.resolve('./components', item)).isDirectory()
})
const entryHash = {}
if(dirs.length > 0){
  dirs.forEach(ele=>{
    //   css不做處理
    if(ele !== "theme-chalk"){
        entryHash[ele] = `./components/${ele}/index.js`
    }
  })
}
// 不打包第三方模塊內容
var externals = [Object.assign({
  vue: 'vue'
}), nodeExternals()];
module.exports = {
      ...
      entry: entryHash,
      ...
      externals:externals,
      ...
      optimization: {
        minimize: false,
      }
}

大部分是常規的配置,可是這裏注意一點,在實踐過程當中,筆者對一個簡單邏輯的componentA進行單獨打包,結果打包出4000+行的代碼,一度感受到生命的絕望,實際上是由於沒有設置external,把第三庫的內容也打包進去了。因爲咱們約定components路徑下存放組件,因此直接經過讀取文件夾下的文件名來建立hash對象,而不須要本身手動維護,可謂一勞永逸!

其餘都是常規的配置,針對js和css單獨打包,配置好rollup和gulp的配置文件,因爲篇幅有限,這裏再也不展現rollup、gulpfile的配置。有興趣的能夠戳個人github看看配置。https://github.com/handsomeguy/oleiwa-demo

最後,咱們的package.json中是這樣的配置的:

"gulp": "npx gulp css && npx gulp all",
    "rollupbuild:es": "npx rollup --config ./build/rollup.config.js",
    "rollupbuild:umd": "format=umd npx rollup --config ./build/rollup.config.js",
    "rollupbuild:min": "minify=true npx rollup --config ./build/rollup.config.js",
    "build:comp": "npx webpack --config ./build/webpack.component.js",
    "build": "npm run gulp && npm run rollupbuild:es && npm run rollupbuild:umd && npm run rollupbuild:min && npm run build:comp",
    "serve": "vue-cli-service serve",

後編譯

後編譯指的是在發佈npm依賴包的時候,不進行編譯構建,跟隨npm包把源碼也一塊兒發出去,以後讓用戶直接引用未編譯的源文件,自行打包編譯。業界提倡後編譯的典範即是cube-ui

後編譯帶來的既有好處也有壞處。

優勢:

一、共用公共依賴。

二、bebal轉碼只有一次,減小代碼量。

三、方便換膚功能實現。(直接針對源碼sass編譯)

缺點:

一、用戶的打包配置要兼容,甚至須要額外作配置。

二、配置頗有多是侵入式的,對於用戶的接入成本過大。

筆者我的認爲,大部分人都會傾向於選擇易於接入的組件庫,我的並不推薦後編譯,可是後編譯其實仍是有它的做用的,例如後面咱們要介紹的換膚功能,其實本質就是一種後編譯,只不過咱們只針對css文件作了後編譯,你須要暴露出一個源碼的scss文件入口。

三、全量加載與按需加載

全量加載

跟大部分的組件庫同樣,咱們只須要這樣,即可以全量引入咱們的組件庫:(注意樣式文件單獨引入)

import oleiwa from "@tencent/oleiwa";
import "@tencent/oleiwa/dist/css/index.css";
Vue.use(oleiwa)

按需引入

按需引入這個問題,其實不單出如今組件庫中,大部分的第三方庫都會面臨這個問題,用戶只須要其中一部分的功能,你要怎麼幫助他剔除無用的模塊,做爲庫提供者,你須要作的就是細分模塊,讓用戶能只引入本身須要的功能模塊。工具庫中的lodash即是一個很好的栗子。回到正題,咱們要怎麼讓咱們組件庫可以按需引入?

一、指定組件路徑

最直接、最粗暴的方式,即是直接指定組件路徑和樣式路徑。

import helloworld from "@tencent/oleiwa/dist/helloworld.js"
import "@tencent/oleiwa/dist/css/base.css"
import "@tencent/oleiwa/dist/css/helloworld.css"
Vue.use(helloworld)

能夠看到,經過直接指定路徑的方式,咱們須要再手動引入css樣式,並且還不能落了base.css這個樣式文件。

二、藉助plugin實現

第一種方式,簡單粗暴,可是你必定不但願你的用戶寫着又臭又長的路徑,嘴裏一邊咒罵:這哪一個XX寫的組件庫。因此還有一種hack的方式,幫助咱們來實現按需引入,利用plugin的方式,對引入路徑作替換,幫助咱們引入須要的組件以及樣式。

目前業界的處理方式:

其本質都是在編譯階段,針對引用路徑作替換。例如:

import { Button } from 'components'

將被替換成如下代碼:

var button = require('components/lib/button')
require('components/lib/button/style.css')

固然,路徑並非固定的,以babel-plugin-component爲例,它容許咱們對lib和樣式文件地址進行配置。

可是因爲配置能力有限,咱們的組件必須放置在lib路徑下,(實際咱們打包在dist路徑下,路徑可修改)可是組件不容許再嵌套一層路徑了,因此咱們須要把咱們的打包配置稍做修改,將每一個組件的打包結果都導出到dist路徑下。

最後,用戶使用起來的時候,只須要安裝好plugin依賴,配置babel.config.js文件以下,即可以實現按需引入:

module.exports = {
  presets: ["@vue/app"],
  "plugins": [["component", {
     libraryName: "@tencent/oleiwa",
     libDir:"dist",
     styleLibraryName:"css",
  }]]
};

使用以下(注意:按需引入狀況下,插件自己會默認幫你導入base.css,因此你也不用擔憂base文件的問題):

import {helloworld} from "@tencent/oleiwa";
Vue.use(helloworld)

三、藉助sideEffects和tree-shaking

sideEffects是webpack4新增的一個特性,須要咱們在package.json中進行配置,其主要做用是告訴webpack咱們這個包有沒有反作用。什麼是反作用,簡單的說就是其導出的模塊是否對其之外的模塊或變量形成影響。例如是否修改了window上的屬性,是否複寫了原生對象 Array, Object 方法,是否修改了其自己所導出的其餘模塊等,若是你還想了解更多,能夠戳這裏。

其實早些時候,還有這樣一篇文章,你的treeshaking並無什麼卵用 [](https://zhuanlan.zhihu.com/p/...

有興趣能夠點進去看一下,主要講的是因爲babel轉碼的緣由,致使最後編譯後的代碼存在了反作用(getter和setter致使),最後致使咱們不能對第三方庫有效的tree-shaking,最後做者提出的方案是在業務中先進行tree-shaking以後再進行轉碼,同時提供了相關插件。這也是咱們爲何最初要用rollup來打包一個es模塊的文件,爲了方便tree-shaking時判斷哪些變量或模塊能夠直接剔除,除此之外,藉助import和export可以更好的發揮tree-shaking的功效。(注:webpack2.X開始和rollup都會感應package.json配置文件中的module屬性,來優先加載es模塊的包,所以你首先須要爲你的包配置此字段)

接下來咱們嘗試對oleiwa包進行sideEffect的配置:

{
   ...
  "main": "dist/oleiwa.umd.js",
  "module": "dist/oleiwa.es.js",
  "sideEffects": false
}

 因爲咱們使用的時候是經過import {componentA} from "@tencent/oleiwa"的形式引入的,因此咱們的入口文件處還設置了各個組件的export,即export { componentA,componentB ,...}

原本開開心心的配置完,按照import {helloworld} from "@tencent/oleiwa"來引用,按理說應該生效了,可是最後打包的結果卻沒有剔除其餘組件?問題出在了哪裏?讓咱們回顧一下剛剛的index.js入口文件:

.. //code
const install = function(Vue){
  .. //code
}
if (typeof window !== 'undefined' && window.Vue) {
    install(window.Vue);
}
export {
    helloworld,
    test
}
export default {
    version,
    install
};

咱們對組件單獨暴露一個屬性,同時export default裏附帶install方法,安裝所有組件,最初筆者覺得多是install的執行邏輯致使webpack不敢對其tree-shaking,因而把if部分的判斷去掉了,可是最後發現,仍是把test組件打包進來了。其實執行的時候window爲undefined,install就已經沒有執行了,因此並不會影響到shaking,關鍵的問題在於最後的export default,筆者刪掉export default 的代碼後,最後實現了按需引入,打包出來的bundle剔除了test組件。緣由是tree-shaking能夠針對單獨的export作處理,可是對export default裏export出來的對象沒法進行shaking,因此若是你要使用tree-shaking,請使用export的方式暴露你的變量。

簡單總結一下,若是你要利用sideEffects和tree-shaking來實現按需加載,須要確保如下幾點:

一、利用rollup打包,導出es模塊;

二、配置package.json文件,若是你確保模塊沒有反作用,可直接把sideEffects設置爲false,同時,指定module入口;

三、導出時使用export,而非export default;

四、用戶在實際開發中須要使用webpack4.x 或 rollup進行打包。

四、其餘工做

前面介紹了項目結構初始化、打包構建以及如何實現按需加載等,大體的組件庫架子已經搭好了,你已經能夠開始愉快開心的開發你的組件庫了,接下里要介紹的是組件庫還能夠進行完善的其餘工做,包括換膚功能的實現、組件庫的類型定義以及組件庫的單元測試。

4.一、換膚功能的實現

大部分的組件庫,element-ui、cube-ui、iview等都容許你對UI主題進行定製,其原理十分簡單,還記得咱們最開始爲咱們的樣式文件定義了一個index.scss的總入口嗎,只須要把這個入口暴露給用戶,讓用戶再進行額外的設置便可。換膚功能的實現本質即是一種後編譯,經過將編譯前的源碼暴露給用戶,讓用戶在開發過程當中去編譯。

用戶只須要安裝好sass-loader,自定義一個user.scss文件,引入咱們的總入口文件便可:

@import '@tencent/oleiwa/components/theme-chalk/index.scss';
// Here are the variables to cover, such as:
@primary-color: #8c0776;

(注:你能夠將變量的定義放置於base.css中,供全部組件css共用)

4.二、類型定義

沒有作類型定義的組件庫,是沒有格調的組件庫,爲了你的用戶可以開心愉快地使用你的組件庫,你要爲你的各個組件定義好類型,方便用戶使用。在這以前你須要在package.json裏定義好類型校驗的入口:

"typings": "types/index.d.ts",

參照Element-ui的實現,咱們能夠這樣設計類型定義文件的結構:

└── types
    ├── index.d.ts // 類型定義總入口
    ├── oleiwa-ui.d.ts  // 類型定義入口,在這裏import其餘的組件定義
    ├── component.d.ts  // 定義組件基類
    └── helloworld.d.ts //  helloworld組件的類型定義

 4.三、單元測試

因爲咱們的組件庫並不是直接的業務組件,因此咱們須要更多關注的是組件交互和渲染的UI測試,而組件庫須要提供給用戶使用,因此完備的單元測試頗有必要,針對組件的單元測試主要能夠細分一下幾類:

一、組件渲染,快照對比

二、props傳遞

三、回調函數執行

四、document.createEvent模擬事件觸發,檢測核心交互邏輯

一個簡單的栗子:

import { expect } from "chai";
import { shallowMount } from "@vue/test-utils";
import HelloWorld from "@/components/HelloWorld.vue";
describe("HelloWorld.vue", () => {
  it("renders props.msg when passed", () => {
    const msg = "new message";
    const wrapper = shallowMount(HelloWorld, {
      propsData: { msg }
    });
    expect(wrapper.text()).to.include(msg);
  });
});

接下來,咱們只須要爲每一個組件寫好單元測試,放置在tests/unit文件夾下統一管理便可。執行單元測試:

npm run test:unit

五、組件庫文檔

開發完了你的組件庫,怎麼也得教你的用戶怎麼使用吧,若是你想偷懶的話,能夠直接用vuese,快速根據你的組件,生成API文檔,其本質是經過AST分析你的文件,提取props、events等參數。

具體使用:安裝好vuese後,配置.vueserc以下:

{
  "include": [
    "./components/**/*.vue"
  ],
  "title": "oleiwa-doc",
  "genType": "docute"
}

執行npx vuese gen便可,簡直方便到爆炸。

固然,若是你不知足於這個的話,可使用markdown-it來書寫本身的文檔,業界最廣泛的方式都是基於此。同時,爲了不demo和code分離,維護兩份代碼,你能夠實現本身的demo-block組件,將本身的 vue 組件插入文檔中,有興趣的話能夠戳如下連接:

最後

最後,一個組件庫的架子,就被咱們這樣手把手的搭起來了。回顧一下咱們學習了啥:

一、爲咱們的組件庫定義好項目結構,以及定義入口文件;

二、因爲組件庫不一樣於普通的應用,因此在打包構建上咱們要針對性的處理,統一打包和單獨打包,css和js各自單獨打包,導出的js文件打包要提供umd、es模塊支持;

三、在前面的項目結構,以及咱們的打包構建基礎上,讓咱們爲組件庫實現按需加載成爲了可能,而且討論了按需加載實現的幾種方式;

四、關於組件庫所須要完善的其餘工做,包括換膚、類型定義以及爲你的組件庫作單元測試;

五、生成組件庫文檔,可使用vuese一把梭,也能夠和業界同樣,採用markdown-it來書寫文檔。

至此,一個組件庫的搭建工做到此結束,可是這只是第一步而已,接下來你須要豐富你的組件庫組件,實現更多的功能,相似於動畫、內置icon等!組件庫之路,道阻且長。

相關文章
相關標籤/搜索