前言
大家好,我是electrolux。这篇文章来讲一下 vite和webpack写插件的编写方法论,会结合我平时做项目时遇到的场景进行分析。在编写plugin之前我们先梳理一下我们要了解的目录吧。
- 代码转化: 一切的基础,咱们编写plugin最主要就是要将数据从 a 代码变成b嘛。因此我们这里可以分化出两个小点
- css 的 代码转化,用postcss之类的做转化
- jsx,vue,ts的代码转化,用babel进行转化。这里我们用jsx做实例
- webpack: 咱们会讲到他的常用钩子,怎么进行传参。然后是怎么根据取得文件数据并且进行转化。
- vite: 同上,区别在于两个框架用的api有一些区别
plugin 的原理简单来说就是三个 , parse
,transform
-generate
然后吐槽一下目前社区里的plugin
编写教程,其实大多数感觉落入了一个误区,就是强依赖于webpack
和vite
的环境,但是其实很多plugin
可以与这些框架进行解耦。
在咱们的教程,只要你有node环境就可以跑起来咱们的代码转化代码,这是跟其他教程区别较大的地方。废话不多说直接开始
代码转化
css的转化(拿父子样式侵占做例子)
咱们在这里举一个我实际项目中发生的问题,我们的场景是一个微前端的场景,基座应用->主应用->子应用->子包。在项目的层层传递中发现 主应用的antd样式会把子应用的antd样式侵占了。子包的style标签打上了hash也没用。因为他不是 antd_hash
而是 antd , .hash
的格式,导致了每一次主应用的样式都能够覆盖子包。这导致了样式的混乱。场景复现如下
因此我们这里可以写一个插件,针对于 .module.less
的文件的 :global
中的属性动态添加 !important
标签。因为只有 :global
的会被命中,而其他类名都是 classname_hash
的格式。不会命中,所以咱们针对 :global
进行操作即可。
在这个小章节,我会给 :global
中的属性动态添加 !important
标签。然后针对 .module.less
的文件 则可以到 vite
和 webpack
层去做操作
安装依赖
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',
插件加载顺序,有pre
和post
可以选择, - 在什么文件中执行代码转化:
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的方法论和核心的编写思路已经展示完了,最后谢谢大家观看