【实操】vite和webpack plugin编写指南

100 阅读5分钟

前言

大家好,我是electrolux。这篇文章来讲一下 vite和webpack写插件的编写方法论,会结合我平时做项目时遇到的场景进行分析。在编写plugin之前我们先梳理一下我们要了解的目录吧。

  • 代码转化: 一切的基础,咱们编写plugin最主要就是要将数据从 a 代码变成b嘛。因此我们这里可以分化出两个小点
    • css 的 代码转化,用postcss之类的做转化
    • jsx,vue,ts的代码转化,用babel进行转化。这里我们用jsx做实例
  • webpack: 咱们会讲到他的常用钩子,怎么进行传参。然后是怎么根据取得文件数据并且进行转化。
  • vite: 同上,区别在于两个框架用的api有一些区别

plugin 的原理简单来说就是三个 , parse,transform-generate

然后吐槽一下目前社区里的plugin编写教程,其实大多数感觉落入了一个误区,就是强依赖于webpackvite的环境,但是其实很多plugin可以与这些框架进行解耦。

在咱们的教程,只要你有node环境就可以跑起来咱们的代码转化代码,这是跟其他教程区别较大的地方。废话不多说直接开始

代码转化

css的转化(拿父子样式侵占做例子)

咱们在这里举一个我实际项目中发生的问题,我们的场景是一个微前端的场景,基座应用->主应用->子应用->子包。在项目的层层传递中发现 主应用的antd样式会把子应用的antd样式侵占了。子包的style标签打上了hash也没用。因为他不是 antd_hash 而是 antd , .hash的格式,导致了每一次主应用的样式都能够覆盖子包。这导致了样式的混乱。场景复现如下

image.png

因此我们这里可以写一个插件,针对于 .module.less的文件的 :global中的属性动态添加 !important 标签。因为只有 :global的会被命中,而其他类名都是 classname_hash 的格式。不会命中,所以咱们针对 :global 进行操作即可。

在这个小章节,我会给 :global中的属性动态添加 !important 标签。然后针对 .module.less的文件 则可以到 vitewebpack 层去做操作

安装依赖

npm install postcss

这是index.less文件

.container{
    :global{
        .ant-btn{
            color: red;
        }
    }
    display: flex;
}
import postcss from "postcss";
import fs from "fs";
// 读取index.less文件
const lessContent = fs.readFileSync("./index.less", "utf-8");

function ruleHandler(rule) {
    rule.nodes.forEach((node) => {
      if (node.type === 'decl') {
        node.important = true;
      }
      if (node.type === 'rule') {
        ruleHandler(node);
      }
    })
  }
const plugin = () => {
	return {
		// @ts-ignore
		Root: (root) => {
			// walkRules 模块维护了整棵树的状态,并且负责替换、移除和添加节点。
			root.walkRules(/:global/, ruleHandler);
		},
		postcssPlugin: "postcss-global-important-plugin",
	};
};
plugin.postcss = true;
async function transform(src) {
	return new Promise((resolve) => {
		// step2: transform 
		postcss([plugin])
			.process(src)
			.then(({ css }) => {
				resolve({
					code: css,
					map: null,
				});
			});
	});
}
transform(lessContent).then((result) => {
	console.log(result);
});

简单讲一下代码。

  • step1: process(src) 将代码传进来,对应parse
  • step2: postcss([plugin]) , root.walkRules(/:global/, ruleHandler) 就是将代码进行 transform,其中walkRules 维护了整棵树的状态,并且负责替换、移除和添加节点。类似于 traverse。对应 transform
  • step: resolve({ code: css, map: null, }) :对应generate

输出的结果如下

.container{
    :global{
        .ant-btn{
            color: red !important;
        }
    }
    display: flex;
}

完美

然后完整的工程化插件可以见 github.com/electroluxc…

里面带了一个react的demo,大家可以运行看看。更多css的转化api可以参考postcss的官网

jsx(== 转化成 ===)

jsx 咱们讲一下怎么将代码中的 == 变成 ===,

首先安装一下依赖

npm install babel-traverse babel-generator babylon

这是index.jsx

import React from 'react'

export default function index() {
    const name1 = 'index1'
    if(name1 == "index1"){
        console.log("输出了index")
    }
    return (
        <div>index</div>
    )
}


这是转化代码

import * as babylon from "babylon";
import generate from "babel-generator";
import * as fs from "fs";
import traverse from "babel-traverse";

const code = fs.readFileSync("./index.jsx", "utf-8");
// https://github.com/jamiebuilds/babel-handbook/blob/master/translations/zh-Hans/plugin-handbook.md#toc-babel-traverse

const ast = babylon.parse(code,{
    plugins: ["jsx"],
    sourceType: "module"
});

traverse.default(ast, {
    enter(path) {
        console.log(path.node.operator)
        if (path.node.operator === "==") {
            path.node.operator = "===";
        }
    }
});

fs.writeFileSync('traverse.txt',JSON.stringify(traverseArr,null,2));
const result = generate.default(ast, {}, code);
console.log(result.code);
// {
//   code: "...",
//   map: "..."
// }

简单讲一下代码。

  • step1: babylon.parse 将代码传进来,对应parse
  • step2: traverse 就是将代码进行 transform,traverse 维护了整棵树的状态,并且负责替换、移除和添加节点。对应 transform
  • step: generate.default(ast, {}, code) :对应generate,跟 postcss一样,产物是 {code: any, map: any} 的结构,对应 generate

最后输出的代码如下

import React from 'react'

export default function index() {
    const name1 = 'index1'
    if(name1 == "index1"){
        console.log("输出了index")
    }
    return (
        <div>index</div>
    )
}

也是成功完成了任务。如果有特定逻辑,在traverse 自定义节点操作即可

webpack

在webpack中插件的编写,在代码转化的基础上我们需要编写一个class类,在这个类中,一般来说添加两个方法

  • contructor: 构造器,用来接受插件的传参,如果你的插件不需要传参也可以不写
  • apply:用来执行代码转化操作,一般我们在里面可以定义
    • 代码转化的时机
    • 在什么文件中执行代码转化
    • 怎么进行代码转化(对应我们上一小章节的内容)

基本模板如下

class CopyrightWebpackPlugin {
    constructor(params) {
        this.params = params
    }
    apply(compiler) {
        console.log(`-----------${this.params.name}-----------------`);
        // 在资源输出之前触发
        compiler.hooks.emit.tapAsync("emit", (compilation,callback) => {
            for (const name in compilation.assets) {
                let source = compilation.assets[name].source()
                if (name.endsWith('.js') | name.endsWith('.ts')) {
                    source = source
                    compilation.assets[name] = {
                        source: function () {
                            return source;
                        },
                        size: function () {
                            return source.length;
                        }
                    }
                }
            }
            callback()
        });
        
    }
}
module.exports = CopyrightWebpackPlugin;

这段代码虽然什么都没做,但是把一个插件的所有要素都展示了出来

  • 代码转化的时机:参考 compiler.hooks.emit.tapAsync("emit" xxxxx ,在资源输出之前触发。并且是异步的方式,更多时机参考webpack官方api
  • 在什么文件中执行代码转化: 参考if (name.endsWith('.js') | name.endsWith('.ts')) 。在js或者ts文件中代码转化
  • 怎么进行代码转化(对应我们上一小章节的内容):参考 source: function () {return source;}, source就是你的文件内容。里面可以添加你自己的逻辑进行代码转化

然后webpack调用实例如下

const CopyrightWebpackPlugin = require('./plugin/CopyrightWebpackPlugin')

module.exports = {
        configureWebpack: {
            plugins: [
                new CopyrightWebpackPlugin({
                    name:"electrolux"
                })
            ],
    }
}

更多的webpack的plugin实例可以参考我这篇文章或者webpack的api,juejin.cn/post/729685…

vite

在vite中插件的编写,在代码转化的基础上我们需要编写一个function,在这个funtion中,需要返回

interface returnType = {
    name: string
    enforce: string;
    transform:(src, id)=>{code: any, map: any}
    
}

最简单实例如下

import type { Plugin } from 'vite'

type PluginConfig = {
  fileMatch?: RegExp;
}

export default (config: PluginConfig = {}): Plugin => {
  const fileMatch = config.fileMatch ?? /\.(module.less)$/
  return {
    name: 'css-modules-important',
    enforce: 'pre',
    transform(src, id) {
      if (!fileMatch.test(id)) {return void 0;}
      let data = src
      return new Promise((resolve) => {
        resolve({
          code: data,
          map: null
        })
      })
    },
  }
}


这段代码同样虽然什么都没做,但是也把一个插件的所有要素都展示了出来

  • 代码转化的时机: enforce: 'pre', 插件加载顺序,有prepost 可以选择,
  • 在什么文件中执行代码转化: if (!fileMatch.test(id)) {return void 0;} 。自定义正则校验就可以了
  • 怎么进行代码转化(对应我们上一小章节的内容): let data = src 里面可以添加你自己的逻辑进行代码转化

然后调用实例如下

import { defineConfig } from 'vite'
import inlineCSSModules from 'vite-plugin-css-modules-important'

export default defineConfig({
  plugins: [ inlineCSSModules({
    "fileMatch": /\.(module.less)$/,
  })],
})

行文至此,所有的plugin的方法论和核心的编写思路已经展示完了,最后谢谢大家观看