自定义一个webpack一个插件

自定义实现一个webpack插件

简述:作为一名“现代切图仔“,我们都用过webpack,知道它有一个叫插件的玩意儿,大家也经常用过插件,但是有多少人自己去研究和写过插件呢?(感觉应该不多,毕竟我也是伸手党),所以就有了今天这个分享主题“实现一个简单的webpack插件“,让大家了解自定义插件的一些基本步骤。

1.何为插件?

webpack 插件是一个具有 apply 方法的 JavaScript对象。apply 方法会被 webpack compiler 调用,并且在整个编译生命周期都可以访问 compiler 对象。

插件是webpack 的 支柱 功能。Webpack自身也是构建于你在webpack配置中用到的相同的插件系统之上!
插件目的在于解决 loader 无法实现的其他事。Webpack 提供很多开箱即用的 插件。

2.话不多说,开始吧!

来实现一个打包的时候,去除掉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文件夹应该是如下:

  • dist
    • index.html
    • main.js

打开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代码。

方式1,利用Esprima

// 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;

方式2,利用babel

安装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;
webpack其他知识:
  1. compiler 和 compilation 对象
    在开发 Plugin 过程中最常用的两个对象就是 Compiler 和 Compilation:
    1.1: Compiler 对象在 Webpack 启动时被实例化,该对象包含了 Webpack 环境所有的配置信息,包括 options、loaders、plugins 等。在整个 Webpack 构建过程中,Compiler 对象是全局唯一的, 它提供了很多事件钩子回调供插件使用。
    1.2: Compilation 对象包含了当前的模块资源、编译生成资源、变化的文件等。Compilation 对象在 Webpack 构建过程中并不是唯一的,如果在开发模式下 Webpack 开启了文件检测功能,每当文件变化时,Webpack 会重新构建,此时会生成一个新的 Compilation 对象。Compilation 对象也提供了很多事件回调供插件做扩展。

你可能感兴趣的:(webpack,javascript,前端)