張倫:巧用 webpack loader 實現項目的定製化

出品 | 滴滴技術
做者 | 張倫
圖片描述css

前言:隨着前端技術的發展,Web 應用變得複雜。爲解決開發的複雜度,前端開發也有了模塊化的概念。使用 Webpack 完成 模塊化的打包構建的方案,可謂盡人皆知。可是利用 Webpack 能作的事情遠不止如此。這篇文章從一個獨特的角度,利用 Webpack 的特色實現了定製化需求,但願可以對你們有一些啓發。前端

▍背景vue

有這樣的需求:項目交付的給客戶時,須要支持針對客戶定製產品的 LOGO、登陸界面的背景。node

▍簡單分析webpack

手動替換圖片文件再編譯的方法確定是沒法接受的。web

若是你說採用分支的方式來實現這種需求,我以爲也是不太現實。畢竟,這並非分支的使用場景。編程

項目在交付時須要避免交付的代碼中包含其餘客戶的資源和信息。這意味着,經過配置文件等在運行時加載的方式是行不通。less

想來想去,問題的本質實際上是解決項目編譯輸出時 CSS 可使用咱們指定的圖片文件,而咱們須要將這個過程自動化。前端構建

▍第一種方案模塊化

先來一種簡單而又直接的方案:直接替換。其步驟以下:

  • 將圖片資源放入指定的目錄中,按項目 ( 客戶 ) 區分。
  • 執行替換圖片資源的腳本,使用指定的資源替換。
  • 執行項目的編譯命令。
1 // pre-packaging.js
 2
 3 const path = require("path");
 4 const fs = require("fs");
 5 const project = process.argv[2];
 6 const distPath = path.resolve("./src/static/images"); // 源代碼目錄
 7 const resourcePath = path.resolve("./resources", project); // 項目靜態文件目錄
 8
 9 function copyDir(src, dist) {
10  try {
11    fs.accessSync(dist, fs.constants.R_OK | fs.constants.W_OK);
12  } catch (err) {
13    fs.mkdirSync(dist);
14  }
15
16  const copyFile = (src, dist) => {
17    fs.createReadStream(src).pipe(fs.createWriteStream(dist));
18  };
19
20  const dirList = fs.readdirSync(src);
21
22  dirList.forEach(item => {
23    const currentPath = path.resolve(src, item);
24    const currentDistPath = path.resolve(dist, item);
25
26    if (fs.statSync(currentPath).isDirectory()) {
27      copyDir(currentPath, currentDistPath);
28    } else {
29      const src = currentPath;
30      const dist = currentDistPath;
31
32      copyFile(src, dist);
33    }
34  });
35 }
36
37 copyDir(resourcePath, distPath);

執行腳本

1 node ./pre-packaging.js projectname

看起來咱們的問題已經獲得解決。可是你仔細想一想,便會發現,這種方案存在多個不足之處:

  • 侵入性強。每次自定義版本構建以後都修改目錄中的圖片資源,這些修改很容易被同步到遠端。
  • 拓展性差。自定義的圖片資源必須嚴格按照源碼中的約定,好比圖片格式,圖片尺寸。每一張圖片都須要在代碼中提供相應的插槽。
  • 功能單一。只能修改圖片的引用,當其餘的樣式須要調整時便無能爲力。
  • 體驗性差。將構建過程拆分爲準備靜態資源和編譯兩個過程。

▍第二種方案

是否有更好的方案?此時咱們回到問題:如何實現同一個項目針對不一樣客戶定製界面的Logo和登陸背景?

咱們須要修改的是什麼?CSS!

既想修改 CSS 樣式,又想不對源碼進行修改,那只有採用 CSS 樣式具備的覆蓋規則來實現。源文件中設置默認樣式,約定使用的 CSS 選擇器,經過編譯將新的樣式文件和源文件合併,全部的樣式打包輸出。

這種方式有諸多好處:

  • 侵入性弱。只須要在項目倉庫中維護對應的資源,不影響源代碼,交付時也不會包含多餘的資源。
  • 拓展性強。自定義的圖片資源不在依賴源碼,可使用任意的圖片格式。
  • 功能豐富。能夠額外增長自定義樣式,不限於需求中的 Logo 和背景。
  • 體驗好。在編譯階段加載指定的樣式,一步到位。

說到前端的編譯打包,天然想到 Webpack。能夠從 Webpack Loader 入手,實現上述過程。

▍Webpack Loader

在 Webpack 的生態中,Loader 用於對模塊的源代碼進行轉換。Loader 可使你在 import 或"加載"模塊時預處理文件。所以,Loader 相似於其餘構建工具中「任務(task)」,並提供了處理前端構建步驟的強大方法。Loader 能夠將文件從不一樣的語言(如 TypeScript)轉換爲 JavaScript,或將內聯圖像轉換爲 data URL。

Webpack Loader 的編寫可參考官方文檔,有很是詳細的說明。

以常見的一段 Webpack 配置爲例:

1 module.exports = {
 2  entry: [...],
 3  output: {...},
 4  module: {
 5    rules: [
 6      ...,
 7      {
 8        test: /\.less$/,
 9        use: [
10          {
11            loader: 'style-loader',
12          },
13          {
14            loader: 'css-loader',
15          },
16          {
17            loader: 'less-loader',
18          }
19        ];
20      }
21      ...,
22    ],
23  },
24 };

上述配置在執行過程當中,less文件的編譯會按照以下順序 (Webpack Loader 執行順序):

圖片描述

在整個編譯過程當中,咱們能夠在每個Loader的開始前和結束後合併咱們自定義樣式,以下圖所示:

圖片描述

在less-loader以前加入自定義的CSS樣式是最好的時機,爲何呢?有兩點:

  • 同時支持 CSS 和 Less 兩種文件。
  • 在整個編譯開始以前加入,對編譯的整個過程沒有影響。新增的樣式一樣享受完整編譯過程。

編譯過程修改成以下圖所示:

圖片描述

▍開發一個 merge-loader

在目前的場景中,merge-loader 只須要一個參數:自定義樣式的文件路徑。因此 Webpack 配置文件能夠修改成:

1 const { getOptions } = require('loader-utils');
 2
 3 module.exports = function (source) {
 4  const options = getOptions(this);
 5  const { style } = options;
 6
 7  // 讀取樣式文件,返回字符串
 8  const string = fs.readFileSync(style);
 9
10  // 合併到原始文件,返回給下一個loader
11  source += string;
12
13  return source;
14 };

你覺得這樣就結束了?不,上述邏輯有兩個問題還需優化:

  • 當樣式中存在圖片的引用時,以字符串形式拼接在源碼樣式中會遇到圖片路徑錯誤的問題。
  • 只要文件經過了規則/.less&/的匹配,就會執行一次合併的操做。含有<style lang="less"></style>
    的vue文件也會觸發這個規則(雖然重複引用不會增長代碼量)。

這兩個問題的解法以下:

  • 使用 @import "path/of/style" 方式合併樣式文件。其餘的處理交給後面的Loader,保證文件和圖片路徑引用正確。
  • 增長一個參數target,指定一個文件做爲 merge 的對象。

這樣一來,merge-loader 的邏輯修改以下:

1 module.exports = {
 2  entry: [...],
 3  output: {...},
 4  module: {
 5    rules: [
 6      ...,
 7      {
 8        test: /\.less$/,
 9        use: [
10          {
11            loader: 'style-loader',
12          },
13          {
14            loader: 'css-loader',
15          },
16          {
17            loader: 'less-loader',
18          },
19          {
20            loader: path.resolve(__dirname, './loader/merge-less.js'), // 自定義loader文件的路徑
21            options: {
22              style: path.resolve(root, 'client/statics/projects/it/style.less'),
23            },
24          }
25        ];
26      }
27      ...,
28    ],
29  },
30 };

▍優化 Loader

最後利用 Loader 工具庫 來優化代碼

1 const fs = require('fs');
 2 const path = require('path');
 3 const loaderUtils = require('loader-utils');
 4 const validateOptions = require('schema-utils');
 5
 6 const schema = {
 7  type: 'object',
 8  properties: {
 9    style: {
10      type: 'string',
11    },
12    target: {
13      type: 'string',
14    },
15  },
16  required: [ 'style', 'target' ],
17 };
18
19
20 module.exports = function (source, meta) {
21  const options = loaderUtils.getOptions(this);
22
23  // 驗證 options 參數
24  validateOptions(schema, options, 'Loader options');
25
26  let { style, target } = options;
27
28   /*
29   * Loader 原則之一:不要在模塊代碼中插入絕對路徑,由於當項目根路徑變化時,文件絕對路徑也會變化
30   * 使用 stringifyReques 將絕對路徑轉換成相對路徑
31   */
32  style = loaderUtils.stringifyRequest(this, style);
33
34  if (meta) {
35    const { file, sourceRoot } = meta;
36
37    if (target === path.join(sourceRoot, file)) {
38      const string = `\n @import ${style};\n`;
39
40      source += string;
41    }
42  }
43
44  return source;
45 }

▍結束

藉助 Webpack Loader,已經完成了項目的定製化。這種方案的幾個特色:

  • 侵入性弱。只須要在項目倉庫中維護對應的資源,不影響源代碼,交付時也不會包含多餘的資源。
  • 拓展性強。自定義的圖片資源不在依賴源碼,可使用任意的圖片格式。
  • 功能豐富。能夠額外增長自定義樣式,不限於需求中的 Logo 和背景。
  • 體驗好。在編譯階段加載指定的樣式,一步到位。

▍END

圖片描述

圖片描述

2015年正式開始職業生涯,2017年加入滴滴。酷愛編程,僞全週期工程師。點子王,愛折騰,喜歡用技術解決問題。夢想作一棵大樹,靜看時間流逝。

圖片描述

相關文章
相關標籤/搜索