简述:作为一名“现代切图仔“,我们都用过webpack,知道它有一个叫插件的玩意儿,大家也经常用过插件,但是有多少人自己去研究和写过插件呢?(感觉应该不多,毕竟我也是伸手党),所以就有了今天这个分享主题“实现一个简单的webpack插件“,让大家了解自定义插件的一些基本步骤。
webpack 插件是一个具有 apply 方法的 JavaScript对象。apply 方法会被 webpack compiler 调用,并且在整个编译生命周期都可以访问 compiler 对象。
插件是webpack 的 支柱 功能。Webpack自身也是构建于你在webpack配置中用到的相同的插件系统之上!
插件目的在于解决 loader 无法实现的其他事。Webpack 提供很多开箱即用的 插件。
来实现一个打包的时候,去除掉console的插件吧(这个插件目前很多,我只是举个例子)。
开发webpack插件,我们先需要一个demo项目来调试我们的插件,先创建一个简陋的项目吧!
先创建一个demo的项目文件夹
新建一个index.js
// index.js
console.warn('我是一个大怨种!');
const a = 1, b = 2;
let c = a + b;
console.log('c:', c);
function jisuanFn(){
console.log('d:', 1 + c);
return 1 + c;
};
document.title = '插件demo';
const d = jisuanFn();
function returnMsg(){
return '你是老六'
};
const msg = returnMsg();
console.log(msg, d);
接着初始化项目的webpack吧
npm init
npx webpack-cli init
npm install webpack --save-dev
npm install html-webpack-plugin --save-dev // 安装一下这个插件,自动生成一个index.html
创建一个基础的webpack配置文件webpack.config.js
// webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
mode: "production",
entry: "./index.js",
output: {
path: path.resolve(__dirname, "dist"),
filename: "[name].js",
publicPath: "./",
},
plugins: [
new HtmlWebpackPlugin()
],
};
然后在package.json中配置一下启动build命令
// package.json
"scripts": {
"build": "webpack --config webpack.config.js"
},
好了,这个时候你可以打个包试一下
npm run build‘
打开dist文件夹应该是如下:
打开index.js,我们会看到这样的内容:
// main.js
(()=>{console.warn("我是一个大怨种!");console.log("c:",3),document.title="插件demo";const o=(console.log("d:",4),4);console.log("你是老六",o)})();
现在我们想要在发布生产的时候去掉这些console语句,该怎么办呢?
新建一个叫no-console-plugin.js文件
扩展工具
Esprima 是一个用于对 JS 代码做词法或者语法分析的工具(只支持js,这里可以使用babel插件(@babel/parser,@babel/generator,@babel/traverse,支持JSX, Flow, Typescript))
主要提供两个API:
1.parseScript 解析不包含 import 和 export 之类的js 代码
2.parseModule 解析含 import 和 export 之类的js 代码
esprima.parseScript(input, config, delegate)
esprima.parseModule(input, config, delegate)
// node 包含节点类型等信息,metadata 包含该节点位置等信息
function (node, metadata) {
console.log(node.type, metadata);
}
AST:将javascript转化为json
链接: 了解AST
把我们想要识别的代码 console.log(‘xx’) 复制到AST网站中,选择我们要转化的类型esprima
可以看到转化的结构如下,range就是匹配上的位置,含有开始和结束的字符索引,现在需要做的就是遍历这个json,找到对应的节点的range,找到后有两种方式删除。
方式1:直接在整个文本中通过索引范围删除对应的字符串。
方式2:修改AST树,然后再把AST树转化为文本。
简述一下方式2: 利用@babel/parser把js代码转化成AST树,再利用@babel/traverse对AST树进行增删改查,最后用@babel/generator把修改后的AST转化为js代码。
// no-console-plugin.js 方式1:完整代码
// 把源代码转成AST语法树
const esprima = require('esprima');
function isJs(filename){
const arr = filename.split('.');
return ['js'].includes(arr[arr.length - 1]);
};
// 判断节点是不是console
function isConsoleCall(node) {
return (node.type === 'CallExpression') &&
(node.callee.type === 'MemberExpression') &&
(node.callee.object.type === 'Identifier') &&
(node.callee.object.name === 'console');
}
// 删除文本中的console语句
function removeCalls(source) {
const entries = [];
esprima.parseScript(source, {}, function (node, meta) {
if (isConsoleCall(node)) {
entries.push({
start: meta.start.offset,
end: meta.end.offset
});
}
});
entries.sort((a, b) => { return b.end - a.end }).forEach(n => {
let end = n.end;
if([',',';'].includes(source.slice(end,end+1))) end = n.end+1;
source = source.slice(0, n.start) + source.slice(end);
});
return source;
}
class NoConsolePlugin {
apply(compiler) {
// emit钩子函数:输出 asset 到 output 目录之前执行
compiler.hooks.emit.tap('NoConsolePlugin',(compilation) => {
// compilation.assets:即将输出的资源文件
for (const name in compilation.assets) {
// 只需要处理js文件
if(isJs(name)){
const content = compilation.assets[name].source();
// 去除文本中的console语句
const newContent = removeCalls(content);
// 修改资源
compilation.assets[name] = {
source: () => newContent,
size: () => newContent.length
}
}
}
});
}
};
module.exports = NoConsolePlugin;
安装babel插件:npm install @babel/generator @babel/parser @babel/traverse @babel/types --save-dev
// no-console-plugin.js 方式2:完整代码
const generator = require('@babel/generator');
const parser = require('@babel/parser');
const traverse = require('@babel/traverse');
const types = require('@babel/types');
// 是否包含需要处理的文件类型
String.prototype.includesFileType = function(suffixs){
const arr = this.toString().split('.');
return suffixs.includes(arr[arr.length - 1]);
};
function isJs(filename){
const arr = filename.split('.');
return ['js'].includes(arr[arr.length - 1]);
};
class NoConsolePlugin {
apply(compiler) {
// emit钩子函数:输出 asset 到 output 目录之前执行
compiler.hooks.emit.tap('NoConsolePlugin',(compilation) => {
for (const name in compilation.assets) {
// 只需要处理js文件
if(isJs(name)){
const content = compilation.assets[name].source();
let ast = parser.parse(content);
traverse.default(ast, {
// 遍历调用表达式
CallExpression(path) {
const { callee } = path.node;
if (types.isCallExpression(path.node) && types.isMemberExpression(callee)) {
const { object, property } = callee;
if (object.name === 'console' && property.name === 'log') {
path.remove();
}
}
},
});
const transformCode = generator.default(ast, {
minified: true
}).code;
compilation.assets[name] = {
source: () => transformCode,
size: () => transformCode.length
}
}
}
});
}
};
module.exports = NoConsolePlugin;