手摸手教你封裝跨項目複用的 Vue 組件

在前端項目的開發中,每每會根據業務需求,沉澱出一些項目內的UI組件/功能模塊(如下通稱組件) 等;這些組件初期只在同一個項目中被維護,並被該項目中的不一樣頁面或模塊複用,此時的組件逐步被完善,是一個只聚焦於功能和健壯性的成長期。javascript

隨着業務的發展,原來的項目可能不得不產生裂變,變成幾個類似但各有不一樣的項目 -- 好比在初始項目中積累經驗後,須要推廣到類似的業態上或根據不一樣大客戶的需求進行定製,這種狀況下每每很難理想化的保持各項目大版本或者後續發展進度的同步,只能逐漸各自發展。這時那些在一開始顯得八面玲瓏的「可複用組件」,每每就須要手忙腳亂的在各個項目中分頭維護,或是出現了意想不到的問題,須要從新規劃了。css

本文以 Vue 技術棧的前端項目爲例,嘗試簡單的探討一種抽象提取跨項目可複用組件的方法。html

可複用組件的常見現狀

  • 組件的複用侷限在單個項目中
  • 一次開發,n 次復
  • 項目的裂變讓問題成倍放大,每一個修正/改動要同步 n 次
  • 兄弟項目的依賴庫可能類似但不一樣,或版本差距較大
  • 單元測試環境或版本的不一樣也讓組件的複用帶來問題

關於同一組件在不一樣項目中的區別方面,以一個二次封裝 element-ui 中 el-date-picker 的 DateRange.vue 組件舉例:前端

所在項目 基礎組件庫 發現的表明性問題
A element-ui@v1.x
  • picker 的結果顯示,在底層 dom 中由單個 input 實現,異於最新版本組件庫
  • el-date-picker 尚不支持 value-format 屬性
B
C
D
E element-ui@v2.x
  • 內部組件的 v-model 不能正確觸發回調,須要用 watch 修正
  • 這幾個項目中,還須要添加一些附加的樣式
F

因爲種種緣由,幾個項目依賴的 UI 庫類似但並不相同;且項目體量過大、維護的團隊不一樣等等,都讓統一基礎組件庫變得🐔乎不可能~ 🐔 你太美,這就很尷尬了嘛~vue

如何收斂維護點?

  • 僅以例子中的幾個項目來講,維護點就在 6 個,工做量×6
  • 若是 收斂到一個統一的庫 中,則維護點變爲 2 個,僅需區分基礎版本庫的差異
  • 而大部分較簡單的組件,基礎組件庫的版本不一樣並不會形成差別的,或是根本沒有引用 element-ui 組件庫的簡單組件,則維護點直接能縮減到 1 個

什麼樣的組件是通用的?

  • 足夠抽象,不包含業務邏輯,或擴展性足夠好
  • 儘可能不包含 $t$router 等和項目環境有關的依賴
  • 有覆蓋率足夠高的單元測試
  • 有必要的文檔,或經過單元測試描述了足夠完整的功能
  • 最好也提供可運行的例子

發佈到 npm

在某一個具體項目內,對組件只需引用其源碼便可;java

對於跨項目的通用組件庫,一種方法是在各項目內部維護一個指向組件庫源碼的子模塊(git 的 subtree 或 submodule),但這種方法維護比較麻煩,故不經常使用。node

另外一種咱們比較習慣的方式是經過 npm 安裝後直接引用組件的註冊名稱(package.json 中的 name)。webpack

固然若是本身的組件多少仍是關乎業務邏輯、對外部的項目其實也沒那麼通用,而公司內部又維護有 npm 的鏡像,那麼選擇將其發佈到這個內部環境中也是能夠的。git

發佈 npm 組件的主要步驟:

在 npmjs.com 上註冊用戶,或經過命令行:web

npm adduser
複製代碼

發佈前確認登陸:

npm login
複製代碼

發佈前手動更改 package.json ,或用命令行更新項目版本號,注意每次發佈的版本號不能相同:

npm version x.x.x
複製代碼

執行發佈:

npm publish
複製代碼

直接在命令行中打開項目主頁查看:

npm home [name]
複製代碼

更多的命令參見官方的完整文檔: docs.npmjs.com/cli-documen…

另外須要注意的是,正確配置 package.json 裏的 repository 字段,能夠在組件的 npm 主頁上顯示代碼倉庫的連接。

用 rollup 而不是 webpack 打包組件

本例中選擇了 rollup 做爲打包工具:

  • webpack 雖然功能強大,但配置複雜、生成的代碼冗餘較多
  • rollup 更適用於庫、組件等類型源碼的編譯
  • rollup 基於插件擴展打包功能,且配置相對簡單
  • rollup 的配置項和 webpack 高度類似,便於遷移和適應

一套基本的配置

假設組件庫結構規劃以下:

├─.babelrc
├─.eslintignore
├─.eslintrc.js
├─.gitignore
├─CHANGELOG.md
├─jest.config.js
├─package.json
├─README.md
├─postcss.config.js
├─rollup.config.js
├─dist/
├─example/
├─node_modules/
├─src/
├─__mocks__/
└─__tests__/
複製代碼

最小化的 npm scripts 以下:

// package.json

"scripts": {
  "build": "rollup --config"
},
複製代碼

較基礎的 rollup 配置以下:

// rollup.config.js

import path from 'path';
import json from 'rollup-plugin-json';
import { uglify } from 'rollup-plugin-uglify';
import alias from 'rollup-plugin-alias';
import vue from 'rollup-plugin-vue';
import babel from 'rollup-plugin-babel';
import commonjs from 'rollup-plugin-commonjs';
import resolve from 'rollup-plugin-node-resolve';
import nodeGlobals from 'rollup-plugin-node-globals';
import bundleSize from 'rollup-plugin-filesize';
import { eslint } from 'rollup-plugin-eslint';
import pkg from './package.json';

const pathResolve = p => path.resolve(__dirname, p);

const extensions = ['.js', '.vue'];

module.exports = {
  input: 'src/index.js',
  output: {
    file: 'dist/bundle.min.js',
    format: 'umd',
    name: 'MyComponents',
    globals: {
      vue: 'Vue',
      echarts: 'echarts',
      lodash: 'lodash'
    },
    sourcemap: true
  },
  external: Object.keys(pkg.dependencies),
  plugins: [
    resolve({
      extensions,
      browser: true
    }),
    eslint({
      extensions,
      exclude: ['**/*.json'],
      cache: true,
      throwOnError: true
    }),
    bundleSize(),
    commonjs(),
    nodeGlobals(),
    vue({
      template: {
        isProduction: !process.env.ROLLUP_WATCH,
        compilerOptions: { preserveWhitespace: false }
      },
      css: true
    }),
    babel({
      exclude: 'node_modules/**'
    }),
    alias({
      '@': pathResolve('src')
    }),
    json(),
    uglify()
  ]
};
複製代碼

關於該配置,簡要說明以下:

  • 上例中插件的順序是重要的
  • node-globals 插件會將 process 等變量注入打包後的文件
  • eslint 插件會在打包以前檢查語法,而且基本能複用平時項目中的 .eslintrc.js 配置文件
  • bundleSize 插件用來在打包後顯示目標文件的體積
  • vue 插件中的 css 字段,表示是否將內嵌樣式打包到目標 js 中
  • 繼續使用 babel,而不是也常常和 rollup 搭配的更輕量的 buble 來編譯 ES6 代碼,目的也是和 jest 複用
  • json 組件解決源碼中可能會直接導入 json 文件的狀況
  • external 配置的意思是:package.json 中 dependencies 包含的依賴,都不被打包到組件中,而是須要在具體項目中安裝

相關的語法轉換和語法檢查配置:

// .babelrc

{
  "presets": [["env", { "modules": false }]],
  "env": {
    "test": {
      "presets": [["env", { "targets": { "node": "current" } }]]
    }
  }
}
複製代碼
// .eslintignore

__tests__/*
*.css
複製代碼
// .eslintrc.js

module.exports = {
  extends: [
    "airbnb-base",
    'plugin:vue/essential'
  ],
  rules: {
    'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off',
    'no-debugger': 'error',
    'space-before-function-paren': 'off',
    'no-underscore-dangle': 'off',
    'no-param-reassign': 'off',
    'func-names': 'off',
    'no-bitwise': 'off',
    'prefer-rest-params': 'off',
    'no-trailing-spaces': 'off',
    'comma-dangle': 'off',
    'quote-props': 'off',
    'consistent-return': 'off',
    'no-plusplus': 'off',
    'prefer-spread': 'warn',
    semi: 'warn',
    indent: 'warn',
    'no-tabs': 'warn',
    'no-unused-vars': 'warn',
    quotes: 'warn',
    'no-void': 'off',
    'no-nested-ternary': 'off',
    'import/no-unresolved': 'off',
    'no-return-assign': 'warn',
    'linebreak-style': 'off',
    'prefer-destructuring': 'off',
    'no-restricted-syntax': 'warn'
  },
  parserOptions: {
    parser: 'babel-eslint'
  }
}
複製代碼

配置單元測試環境

維護點收斂到了一個庫中,須要注意的是,相應的風險也高度集中了,可謂一損俱損一榮俱榮🐶。

因此單元測試也愈發重要起來,庫裏的組件或模塊,凡有條件的(好比 Vue 中的 directives 就沒那麼好作單元測試,但 filters 純函數很容易),想要讓各個項目的開發者小夥伴們放心大膽的統一引用,就應該無條件的買一送一,搭配完善的單元測試。

這裏以 jest 爲例,列舉其主要配置:

// jest.config.js

module.exports = {
  modulePaths: [
    '<rootDir>/src/'
  ],
  moduleFileExtensions: [
    'js',
    'json',
    'vue'
  ],
  transform: {
    '^.+\\.vue$': 'vue-jest',
    '^.+\\.jsx?$': 'babel-jest'
  },
  transformIgnorePatterns: [
    '/node_modules/'
  ],
  moduleNameMapper: {
    '^@/(.*)$': '<rootDir>/src/$1',
    '\\.(css|less|scss)$': '<rootDir>/__mocks__/emptyMock.js'
  },
  snapshotSerializers: [
    'jest-serializer-vue'
  ],
  collectCoverage: true,
  collectCoverageFrom: [
    '<rootDir>/src/**/*.{js,vue}',
    '!**/node_modules/**'
  ],
  coveragePathIgnorePatterns: [
    '<rootDir>/__tests__/'
  ],
  coverageReporters: [
    'text'
  ]
};
複製代碼

其中 emptyMock.js 是用來在用例中忽略對樣式的引用的:

// __mocks__/emptyMock.js

module.exports = {};
複製代碼

對應的 npm scripts:

"scripts": {
  // ...
  "test": "jest"
},
"pre-commit": [
  "test"
],
複製代碼

這裏用 pre-commit 包實現了提交前先進行單元測試的鉤子功能。

關於 Vue 單元測試的更多內容請參考這篇文章

預覽組件實際效果

光說不練假把式,雖然靜態語法也檢查了、單元測試也跑通了,仍是眼見爲實比較踏實,對其餘開發者也比較直觀;藉助 rollup-plugin-serve 等插件,能夠運行起一個最小配置的瀏覽器運行環境,人肉看看組件的實際表現。

在 npm scripts 中設置環境參數,分別對徹底通用的組件,及適用於特定類型項目的組件啓動 demo 頁面服務:

"scripts": {
  // ...
  "dev:common": "rollup --watch --config rollup.config.dev.js --environment PROJ_ENV:common",
  "dev:A": "rollup --watch --config rollup.config.dev.js --environment PROJ_ENV:A",
  "dev:B": "rollup --watch --config rollup.config.dev.js --environment PROJ_ENV:B",
},
複製代碼

適當修改 rollup 原配置,增長單獨的 rollup.config.dev.js,根據環境參數啓動服務:

import serve from 'rollup-plugin-serve';
import postcss from 'rollup-plugin-postcss';
import baseConfig, {
  pathResolve,
  browserGlobals
} from './rollup.config.base';

...

const PORT = 3001;
const PROJECT = process.env.PROJ_ENV;

export default {
  input: pathResolve(`example/${PROJECT}/main.js`),
  output: {
    file: pathResolve(`example/${PROJECT}/dist/example.bundle.${PROJECT}.js`),
    format: 'umd',
    name: 'exampleApp',
    globals: browserGlobals,
    sourcemap: false
  },
  plugins: [
    postcss(),
    ...baseConfig.plugins,
    serve({
      port: PORT,
      contentBase: [
        pathResolve(`example/${PROJECT}`)
      ]
    })
  ]
};
複製代碼

這裏假設簡單粗暴的都把組件引用到 App.vue 中,暫不考慮分路由等狀況,對應的 example 目錄的結構可能以下:

+---A
|   |   App.vue
|   |   index.html
|   |   main.js
|   |
|   \---dist
|           example.bundle.A.js
|
+---B
|   |   App.vue
|   |   index.html
|   |   foo.css
|   |   main.js
|   |
|   \---dist
|           example.bundle.B.js
|
\---common
    |   App.vue
    |   index.html
    |   main.js
    |
    +---dist
    |       example.bundle.common.js
    |
    \---fonts
複製代碼

總結

同時維護幾個同質化的前端項目時,不可避免的涉及到一些較通用的 UI組件/功能模塊 的狀況,將其集結後發佈到 npm 上,並輔以完善的單元測試和可運行的 demo 展現、必要的文檔,就能將維護組件的工做量大大減輕。



--End--

搜索 fewelife 關注公衆號

轉載請註明出處

相關文章
相關標籤/搜索