出品 | 滴滴技术
做者 | 张伦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年加入滴滴。酷爱编程,伪全周期工程师。点子王,爱折腾,喜欢用技术解决问题。梦想作一棵大树,静看时间流逝。