出品 | 滴滴技術
做者 | 張倫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 選擇器,經過編譯將新的樣式文件和源文件合併,全部的樣式打包輸出。
這種方式有諸多好處:
說到前端的編譯打包,天然想到 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樣式是最好的時機,爲何呢?有兩點:
編譯過程修改成以下圖所示:
▍開發一個 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 };
你覺得這樣就結束了?不,上述邏輯有兩個問題還需優化:
這兩個問題的解法以下:
這樣一來,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,已經完成了項目的定製化。這種方案的幾個特色:
▍END
2015年正式開始職業生涯,2017年加入滴滴。酷愛編程,僞全週期工程師。點子王,愛折騰,喜歡用技術解決問題。夢想作一棵大樹,靜看時間流逝。