《写出自己的loader》

优质
小牛编辑
132浏览
2023-12-01

目标

写出一个loader,实现在html文件内容前面添加个人签名、以及自动替换掉敏感词汇的功能,当对应的词汇文件修改时,页面会自动刷新。该loader需能够协作其他loader,实现链式调用。

挑战

写出一个loader,要求每个模块文件依赖于各不相同的敏感词汇json文件。

知识点

1、node module:一个loader就是一个npm包,输出一个function;
2、npm publish:发布npm包;
3、loader API:通过在loader函数中使用this可以调用loader的API;
4、loader-utils:推荐使用 loader-utils 来处理loader被调用时传递进来的参数;
5、从webpack2.0开始,webpack默认支持对json文件的解析,而不需要引入json-loader了;
6、异步解析:根据项目实际需要,解析文件时可能需要依赖于其他文件,直到该文件异步读取完毕之后再对原文件做解析。

课程内容

前言

自定义一个webpack loader是比较简单的事情,要使用到的API也很少,非常容易上手。
我们知道,前端开发时数据和代码是要分开管理的,比如说我司前端用的是vm模版引擎,流程是后台java渲染数据之后再返回到前端,为了在本地开发时方便,我们的做法是在本地生成模拟的json数据,通过webpack loader引入渲染之后直接在本地展现出来。
一般每个模块的数据类型和数据量都是各不相同的,所以把每个模块的数据分别保存到独立的json文件中进行管理是比较合理的做法。在这里我抛砖引玉,本次课程会讲单个数据文件的引入,在课程挑战中由读者独立思考并实现,如果存有疑问的话可以参照vm-loader

loader API

在loader中,咱们可以通过关键词this访问当前执行环境的所有变量
1、同步回调时,可以执行this.callback(),默认第一个参数是err错误信息(不报错时返回null),第二个参数是解析完模块后的返回结果,第三个参数是sourceMap(可选),在链式调用时可将sourceMap传给下一个loader;
2、异步回调时,可以执行this.async(),参数同上;
3、this.addDependency(filePath)可以把对应filePath的文件添加到webpack的依赖树,webpack可以监测它的文件变动并刷新(filePath要是绝对路径);
4、this.resolve()可以解析处理文件路径;
5、this.query:获取loader的配置选项。

loader-utils方法

1、getOptions:检索被调用loader的配置选项;
2、parseQuery:解析被调用loader的配置选项;
3、stringifyRequest:将一个请求转化为非绝对路径的可被require或import的字符串;
4、urlToRequest:将url转换成适合webpack环境的模块请求;
5、interpolateName:自定义资源名称、hash等;
6、getHashDigest:通过限制字符长度获取文件部分哈希值。

开始

先在github上创建一个test-loader仓库,
《写出自己的loader》 - 图1
在跳转的页面复制仓库链接,clone到本地(可能会clone不成功,需要事先配置好ssh key,也可以直接下载压缩包到本地)
《写出自己的loader》 - 图2

  1. git clone git@github.com:kingvid-chan/test-loader.git
  2. cd test-loader
  3. npm init -y
  4. npm install loader-utils --save-dev
  5. touch index.js

copy以下代码到index.js

  1. 'use strict';
  2. // loader-utils可以解析webpack配置文件中loader传入的参数
  3. const loaderUtils = require('loader-utils');
  4. module.exports = function(source) { // source是字符串,包含静态资源的文件内容
  5. // webpack2 默认使用缓存,启动webpack-dev-server时,只热更新被修改的模块
  6. // 如果你想要禁止缓存功能,只要传入fasle参数即可
  7. // this.cacheable(false);
  8. const params = loaderUtils.parseQuery(this.query);
  9. if (typeof params === "object" && params.signStr && typeof params.signStr === "string") {
  10. source = '<!-- ' + params.signStr + ' -->\n' + source;
  11. }
  12. return source;
  13. };

做完这一步之后,咱们已经能够实现给html模块添加签名了,接着添加敏感词汇替换的功能。
修改index.js如下:

  1. 'use strict';
  2. // loader-utils可以解析webpack配置文件中loader传入的参数
  3. const loaderUtils = require('loader-utils'),
  4. path = require('path'),
  5. fs = require('fs');
  6. module.exports = function(source) { // source是字符串,包含静态资源的文件内容
  7. // webpack2 默认使用缓存,启动webpack-dev-server时,只热更新被修改的模块
  8. // 如果你想要禁止缓存功能,只要传入fasle参数即可
  9. // this.cacheable(false);
  10. const params = loaderUtils.parseQuery(this.query),
  11. callback = this.async(); // 异步解析模块
  12. if (typeof params === "object") {
  13. // 添加个人签名
  14. if (params.signStr && typeof params.signStr === "string") {
  15. source = '<!-- ' + params.signStr + ' -->\n' + source;
  16. }
  17. // 自动替换掉敏感词汇
  18. if (params.dataPath && typeof params.dataPath === "string") {
  19. let dataPath = path.resolve(params.dataPath); // 转换为绝对路径
  20. this.addDependency(dataPath); // 添加依赖关系,当文件修改时会被webpack检测到
  21. // 异步读取敏感词汇的json文件
  22. fs.readFile(dataPath, 'utf-8', function(err, text) {
  23. if (err) {
  24. console.error('数据文件路径出错', params.dataPath, '找不到该文件');
  25. return callback(err, source);
  26. }
  27. let data = JSON.parse(text),
  28. regexRule = '(';
  29. for (let value in data) {
  30. regexRule += data[value] + '|';
  31. }
  32. regexRule = regexRule.slice(0, -1) + ')';
  33. let regex = new RegExp(regexRule, 'g'); // 正则替换
  34. source = source.replace(regex, '');
  35. callback(null, source); // 异步回调处理结果
  36. });
  37. } else {
  38. callback({
  39. error: 'dataPath is not legal'
  40. }, source);
  41. }
  42. // console.log(source);
  43. }
  44. };

至此,自动替换敏感词汇的功能也已经实现了,咱们试着将它运行到项目中去,在运行到具体项目之前,咱们需要先把它发布到npm的包管理服务器上,可供所有人在线下载使用。

  1. touch .npmignore

写入以下代码:

  1. node_modules/

发布之前需要先登陆你的npm账号,运行npm adduser,输入你在npm官网上的账户—username、password、email,如果没有的话就新注册一个,完成之后运行npm whoami就可以看到你的名字了。点击发布

  1. npm publish

提示发布失败,失败原因是没有权限发布名为test-loader的包,也就是这个包名字已经被别人占用了,上npm官网搜索显示确实已经存在这个包,咱们换个名字,把package.json的name属性修改一下:

  1. "name": "test-webpack-loader"

再尝试发布一下

  1. npm publish

发布成功,到npm主页也能被检索到。
《写出自己的loader》 - 图3

运行到实际项目中

咱们依然在lesson6的基础上做修改,把lesson6的src目录、app.jswebpack.config.jswebpack.entry.jswebpackDevServer.jscopy进来,初始化再安装依赖包

  1. npm init -y
  2. npm install babel-core babel-loader babel-preset-env css-loader extract-text-webpack-plugin file-loader html-loader html-webpack-plugin node-sass open-browser-webpack-plugin sass-loader style-loader url-loader webpack webpack-dev-server --save-dev
  3. npm install bootstrap express font-awesome gulp-util jquery mockjs --save

再安装下咱们刚刚发布到npm的test-webpack-loader

  1. npm install test-webpack-loader --save-dev

webpack.config.js需要修改下html文件的loader配置

  1. {
  2. test: /\.html$/,
  3. use: ['html-loader?interpolate=require', 'test-webpack-loader?signStr=CreatedByKingvid&dataPath=./src/words.json']
  4. }

另外在body.html中加入一些敏感词汇

  1. <h1 class="body-title">this is body</h1>
  2. <i class="fa fa-cog fa-spin fa-3x fa-fw"></i>
  3. <ul class="body-list">
  4. <li class="body-list-item" id="body-input">你可以使用BannerPlugin给你的每个打包文件加上你的签名<br>webpack教程<br>by kingvid</li>
  5. </ul>
  6. <button id="body-btn" class="btn">点我</button>
  7. <button id="pack-btn" class="btn">打包</button>
  8. <button id="getData-btn" class="btn">获取本地测试数据</button>
  9. <div id="mockData-con"></div>
  10. <div class="mask"><span><i class="fa fa-spinner fa-pulse fa-3x fa-fw"></i></span></div>
  11. <h3>这里包含敏感词汇:比如说最便宜、销量第一等等等等</h3>

运行node app.js,可以看到敏感词汇已经自动过滤了
《写出自己的loader》 - 图4
点击打包按钮,完成后查看index.html代码,签名以及敏感词汇过滤的功能都已经实现。
《写出自己的loader》 - 图5

补充知识

注意不要给被处理模块中添加绝对路径,当项目被移动到别的位置时,会破坏webpack的hash解析,可以使用loaderUtilsstringifyRequest函数,它能将绝对路径转化为相对路径;

npm发布的时候注意是否已有重名npm包导致发布失败,最好能一开始就查询好能用的命名,不用像我一样,为了规范回头还要去把github仓库重命名一次,关于test-webpack-loader的详细源码可以查看 https://github.com/kingvid-chan/test-webpack-loader
测试时不一定需要把loader发布到npm上,也可以直接把test-webpack-loader文件夹copy到lesson7的node_modules下,就能够调用它了。