自定义一个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 对象也提供了很多事件回调供插件做扩展。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Webpack允许开发者创建自定义插件来扩展其功能。创建自定义插件的步骤如下: 1. 创建一个 JavaScript 文件作为插件的入口文件。 2. 在该文件中,定义一个 JavaScript 类作为插件的主体。 3. 在该类中,实现一个 `apply` 方法,该方法会在 Webpack 编译过程中被调用。 4. 在 `apply` 方法中,可以通过 Webpack 提供的钩子(Hook)来注册各种编译过程的事件回调函数,以实现自定义的功能。 下面是一个简单的示例,展示了如何创建一个打印构建信息的自定义插件: ```javascript class MyCustomPlugin { apply(compiler) { compiler.hooks.done.tap('MyCustomPlugin', (stats) => { console.log('Build completed!'); console.log('----------------'); console.log(stats.toString()); }); } } module.exports = MyCustomPlugin; ``` 在上述示例中,我们创建了一个名为 `MyCustomPlugin` 的自定义插件类,并在其中实现了一个 `apply` 方法。在该方法中,我们使用了 `compiler.hooks.done.tap` 方法来注册一个回调函数,在每次构建完成后打印构建信息。 要在 Webpack 配置文件中使用此自定义插件,可以像使用其他插件一样进行配置: ```javascript const MyCustomPlugin = require('./path/to/MyCustomPlugin'); module.exports = { // ...其他配置项 plugins: [ new MyCustomPlugin(), ], }; ``` 通过将自定义插件实例添加到 `plugins` 配置项中,Webpack 在构建过程中会调用该插件的 `apply` 方法,并触发注册的事件回调函数。 请注意,这只是一个简单的示例。根据你的需求,你可以在自定义插件中实现更复杂的逻辑,以满足你的特定需求。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值