桔妹导读:随着前端技术的发展,Web 应用变得复杂。为解决开发的复杂度,前端开发也有了模块化的概念。使用 Webpack 完成 模块化的打包构建的方案,可谓尽人皆知。但是利用 Webpack 能做的事情远不止如此。这篇文章从一个独特的角度,利用 Webpack 的特点实现了定制化需求,希望能够对大家有一些启发。
▍ 背景
有这样的需求:项目交付的给客户时,需要支持针对客户定制产品的 LOGO、登录界面的背景。
▍ 简单分析
手动替换图片文件再编译的方法肯定是无法接受的。
如果你说采用分支的方式来实现这种需求,我觉得也是不太现实。毕竟,这并不是分支的使用场景。
项目在交付时需要避免交付的代码中包含其他客户的资源和信息。这意味着,通过配置文件等在运行时加载的方式是行不通。
想来想去,问题的本质其实是解决项目编译输出时 CSS 可以使用我们指定的图片文件,而我们需要将这个过程自动化。
▍ 第一种方案
先来一种简单而又直接的方案:直接替换。其步骤如下:
-
将图片资源放入指定的目录中,按项目 ( 客户 ) 区分。
-
执行替换图片资源的脚本,使用指定的资源替换。
-
执行项目的编译命令。
1// pre-packaging.js 2 3const path = require("path"); 4const fs = require("fs"); 5const project = process.argv[2]; 6const distPath = path.resolve("./src/static/images"); // 源代码目录 7const resourcePath = path.resolve("./resources", project); // 项目静态文件目录 8 9function 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 37copyDir(resourcePath, distPath);
执行脚本
1node ./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 配置为例:
1module.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 配置文件可以修改为:
1const { getOptions } = require('loader-utils'); 2 3module.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 的逻辑修改如下:
1module.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 工具库 来优化代码
1const fs = require('fs'); 2const path = require('path'); 3const loaderUtils = require('loader-utils'); 4const validateOptions = require('schema-utils'); 5 6const 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 20module.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年加入滴滴。酷爱编程,伪全周期工程师。点子王,爱折腾,喜欢用技术解决问题。梦想做一棵大树,静看时间流逝。
如果同学们有其他的实现方式
欢迎在本文留言交流
江义旺:滴滴出行安卓端 finalize time out 的解决方案
胡海洋:Hive Metastore Federation 在滴滴的实践
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- 使用gulp实现定制化需求
- 张伦:巧用 webpack loader 实现项目的定制化
- iOS:轻量可定制的防键盘遮挡textField实现总结
- 怎样的 Flutter Engine 定制流程,才能实现真正“开箱即用”?
- 用canal监控binlog并实现mysql定制同步数据的功能
- Android从零撸美团(三) - Android多标签tab滑动切换 - 自定义View快速实现高度定制封装
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Types and Programming Languages
Benjamin C. Pierce / The MIT Press / 2002-2-1 / USD 95.00
A type system is a syntactic method for automatically checking the absence of certain erroneous behaviors by classifying program phrases according to the kinds of values they compute. The study of typ......一起来看看 《Types and Programming Languages》 这本书的介绍吧!