Vite 能满足你吗?
上篇文章讲解了如何使用 Vite 配置自己的项目,除 Router 一节演示了如何使用 vue-router 配合 Vue3 外,其余配置都是脱离框架的。上文文末说过,Vite 目前的各个配套设施还不是特别全面,所以有时候难免需要 “自己动” “自己动手,丰衣足食”。本文就以 React 搭配 antd 为例,教给你如何编写一个属于自己的 Vite 插件📝
瞅一哈🥷
我们先使用 Vite 创建一个 React + Typescript 的项目并安装 antd,完毕之后我们先在 App.tsx
引入 Button
组件并展示在页面上:
function App() {
// ...
return (
<div className="App">
{/* ... */}
<Button type="primary">自己动手,丰衣足食</Button>
</div>
);
}
🗣 wdnmd,爷的 primary
样式呢?
👉 你不引样式文件有个 P 的样式?
我们在 main.tsx
中引入样式后就可以了:
import 'antd/dist/antd.min.css';
但为什么我们使用 Webpack 时不用手动引入样式呢?其实并不是 Webpack 而是因为 babel 插件 babel-plugin-import
:
// babel.config.js
module.exports = {
plugins: [
// ...
[
'import',
{
libraryName: 'antd',
libraryDirectory: 'es',
style: true,
},
],
],
};
因为 antd 默认支持基于 ES Module 的 tree shaking,而 babel-plugin-import
的功能我们可以简单的理解为给引入的组件添加对应的样式。那么接下来我们就要写一个类似的小插件☸️
搞一哈🚀
首先之前文章有说到 Vite 是使用 rollup 打包,同时 Vite 的插件 API 设计规范是参考的 rollup,所以 Vite 插件其实就是编写 rollup 插件,如官方文档所述:
Vite plugins extends Rollup's well-designed plugin interface with a few extra vite-specific options. As a result, you can write a Vite plugin once and have it work for both dev and build.
插件也有自己的生命周期,rollup 的生命周期过多就不赘述,感兴趣的大家可以自己学习:rollupjs.org/guide/en/#p… Vite 插件的生命周期来讲解🏃♂️
启动服务时调用的生命周期
options
通过这个在这个方法中我们可以获取到在 vite.config.ts
中的配置内容。我们先创建一个插件的脚本:
// plugin.ts
import { Plugin } from 'vite';
export default function myPlugin(): Plugin {
return {
name: 'my-plugin',
options: (options) => {
console.info(options);
return null;
},
};
};
并在 vite.config.ts
中使用我们编写的插件,顺便加点别的别的配置,比如 alias
:
import { defineConfig } from 'vite';
import reactRefresh from '@vitejs/plugin-react-refresh';
import myPlugin from './my-plugin';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [reactRefresh(), plugin()],
resolve: {
alias: {
'@': './src',
},
},
})
然后我们执行一下 yarn build
,瞅一眼 terminal
中的输出结果:
// vite v2.1.3 building for production...
{
input: '...',
preserveEntrySignatures: false,
plugins: [
{
name: 'alias',
buildStart: [Function: buildStart],
resolveId: [Function: resolveId]
},
{
name: 'react-refresh',
enforce: 'pre',
configResolved: [Function: configResolved],
resolveId: [Function: resolveId],
load: [Function: load],
transform: [Function: transform],
transformIndexHtml: [Function: transformIndexHtml]
},
{
name: 'vite:resolve',
configureServer: [Function: configureServer],
resolveId: [Function: resolveId],
load: [Function: load]
},
{
name: 'my-plugin',
options: [Function: options],
buildStart: [Function: buildStart]
},
// ...
],
}
可以看到上面的输出中有我们使用的 alias
、react-refresh
等配置,并且可以了解到 alias
这些配置功能也是通过插件来支持的。在生命周期中返回 null
是表示对传入的值没有进行任何处理。
在 options
这个生命周期中,我们可以替换或操作传递给打包工具的 options
,但如果你只是想读取 options
的内容,那么更建议你在下面的 buildStart
生命周期中读取。因为,buildStart
周期中的 options
都是转换后的内容,并且 options
钩子无法访问大多数的插件的上下文,因为该周期在配置好之前就执行了。
buildStart
同理我们打印一下 buildStart
钩子中输出的 options
参数:
{
acorn: {...},
acornInjectPlugins: [...],
context: 'undefined',
experimentalCacheExpiry: 10,
external: [Function],
inlineDynamicImports: undefined,
input: [ '...' ],
manualChunks: undefined,
moduleContext: [Function],
onwarn: [Function],
perf: false,
plugins: [...],
// ...
}
除了 options
中打印出的几个属性外,会发现多了很多属性。该生命周期在每次 build
时都会执行,在这个周期中 options
钩子中的配置项都已被转换,并且也为一些为设置的 options
配置了默认值。
传入模块请求时调用的生命周期
resolveId
该生命周期接收三个参数:source
、importer
以及 options
。options
已在前两个钩子中讲解过,故不再赘述,下面我们来看看 source
和 importer
两个参数。首先明确一点,该钩子是用来自定义 resolver
的,比如用来定位第三方依赖位置等。source
中内容是在 import
声明语句中写的路径,比如:
import { throttle } from '../lodash';
则 source
就是 ../lodash
。而第二个参数 importer
则是 resolve
之后的导入模块,当在 resolve
入口时,importer
的值会是 undefined
,所以我们可以利用这一点来自定义入口的代理模块。比如下面这个官方🌰就是暴露入口文件中的默认导出内容但保留具名导出供内部使用:
async resolveId(source,importer) {
if (!importer) {
// 跳过本插件避免无限循环
const resolution = await this.resolve(source, undefined, { skipSelf: true });
// 如果不能被 resolve 则返回 null 使 Rollup 报错
if (!resolution) return null;
return `${resolution.id}?entry-proxy`;
}
return null;
},
load(id) {
if (id.endsWith('?entry-proxy')) {
const importee = id.slice(0, -'?entry-proxy'.length);
// 如果没有默认导出内容则会抛异常
return `export {default} from '${importee}';`;
}
return null;
}
需要注意一点的是,我们在生命周期中除了可以返回 null
、对应的 resolved
路径和对应对象之外,还可以返回布尔类型。当我们返回 false
时就表示 source
会被处理成外部依赖,不被打包进来。
resolvedId
生命周期可以有很多操作,详细请见官方文档:rollupjs.org/guide/en/#r…
load
在 load
钩子中,我们可以自定义 loader
,为了防止多余的解析,该生命周期已使用 this.parse
生成了 AST
,并且我们可以选择性的返回 { code, ast, map }
对象 (需要注意的是,返回的须是标准的 ESTree AST,每个节点都包涵 start
和 end
属性)。
load
生命周期只接受一个 id
参数,具体使用方式可以参照上一个生命周期 resolveId
。
transform
在 transform
周期中,方法接受两个参数:code
和 id
。第二个参数 id
同 load
周期,而第一个 code
则为转换后的代码,我们在 src
目录下随便创建一个文件 utils.ts
,并向外导出一个方法,比如:
// src/utils.ts
export const whatever = (a: number, b: number) => a === 1 ? a + b : a - b;
然后我们在 transform
方法中打印一下获取的 code
:
// my-plugin.ts
export default function myPlugin(): Plugin {
return {
// ...
transform: (code, id) => {
if (id.includes('utils')) {
console.log(code);
}
return null;
},
};
}
得到 terminal
中的打印结果:
export const whatever = (a, b) => a === 1 ? a + b : a - b;
然后,我们在 main.tsx
中引入上面的 whatever
方法,并打印一下 main.tsx
的 code
:
// main.tsx
import React from 'react';
import ReactDOM from 'react-dom';
import { Button } from 'antd';
import { whatever } from './utils';
ReactDOM.render(
<>
<Button type="primary">自己动手,丰衣足食</Button>
<h1>Utils -> whatever function result: 1 + 1 = {whatever(1, 1)}</h1>
</>,
document.getElementById('root'),
);
// my-plugin.ts
export default function myPlugin(): Plugin {
return {
// ...
transform: (code, id) => {
if (id.includes('main')) {
console.log(code);
}
return null;
},
};
}
// terminal 中的打印结果
import React from "react";
import ReactDOM from "react-dom";
import {Button} from "antd";
import {whatever} from "./utils";
ReactDOM.render(/* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(Button, {
type: "primary"
}, "\u81EA\u5DF1\u52A8\u624B\uFF0C\u4E30\u8863\u8DB3\u98DF"), /* @__PURE__ */ React.createElement("h1", null, "Utils -> whatever function result: 1 + 1 = ", whatever(1, 1))), document.getElementById("root"));
此时的你是不是已经发现一丝端倪了?我们在 transform
中可以获取到我们转换后的代码,并且可以返回我们自己操作后的代码,这不就能解决最初我们所说的引入 antd
组件的同时 Vite
顺便帮我们引入对应组件样式的问题了吗🥳
分析一波如何能实现这个功能🤔
首先我们可以知道从 antd
中引入的组件,比如:Button
、Card
、DatePicker
等,并且观察 antd
中文件夹以及 style
文件的命名方式可以发现,我们只需要引入 antd/lib/component-name/style/index.css
文件即可。
// 匹配引入组件的代码行正则
const IMPORT_LINE_REG = `/import {[\w,\s]+} from (\'|\")antd(\'|\");?/g`;
// 组件名大驼峰转 KababCase 方法
const transformToKebabCase = (name: string) => {
return name.replace(/([^-])([A-Z])/g, '$1-$2').toLocaleLowerCase();
};
export default function myPlugin(): Plugin {
return {
// ...
transform: (code, id) => {
if (/\"antd\";/.test(code)) {
const importLine = code.match(IMPORT_LINE_REG)![0];
const cssLines = importLine
.match(/\w+/g)! // 匹配结果:['import', 'Button', 'from', 'antd']
.slice(1, -2) // 结果:['Button']
.map(name => `import "antd/lib/${transformToKebabCase(name)}/style/index.css";`)
// 结果:['import "antd/lib/button/style/index.css']
.join('\n');
return code.replace(IMPORT_LINE_REG, `${importLine}\n${cssLines}`)
}
return null;
},
};
}
看看 yarn dev
启动一下本地开发环境,并在 main.tsx
中多引入两个组件进来瞅瞅:
针不戳!
歇一哈😴
除上面的 5 个生命周期之外,还有两个在 关闭服务时调用的生命周期 时调用的生命周期:buildEnd
和 closeBundle
,在不在此介绍各位可以到 Rollup
官网自行学习。有了可自定义插件的能力,我们就能有无限的可能来控制输出我们的本地开发和打包时的结果。比如我们还可以模仿 Webpack
的 raw-loader
来写一个类似于 raw-loader
的插件,在上篇文章中说道,当引入一些资源类型时在文件名末尾加上 ?raw
就可以使用 raw-loader
将其处理成字符串来引入,而我们可以写一个插件来自定义一些文件的 extensions
,然后对指定类型结尾的文件加上 ?raw
,这样我们就不用在本地开发的代码中写 ?raw
字符串了。该插件的实现比上面自动引入 css
的插件更简单,相信各位可以自己实现!
其实类似于 babel-plugin-import
的 Vite
插件,蚂蚁🐜的人员早就在 2020 年年底就开发完成并开源发布了,名为:vite-plugin-import
(www.npmjs.com/package/vit… ),大家去看其源码,其实也很简单,就是使用 babel
将其转换了一下(github.com/meowtec/vit…
async transform(src) {
if ((onlyBuild && !isBuild) || !codeIncludesLibraryName(src)) {
return undefined;
}
const result = await transformAsync(src, {
plugins: babelImportPluginOptions.map((mod) => ['import', mod, `import-${mod.libraryDirectory}`]),
});
return result?.code;
}
如果各位想要自己编写一些插件,请遵循 Vite
插件的命名规范:
Vue
插件前缀:vite-plugin-vue-
;React
插件前缀:vite-plugin-react-
;Svelte
插件前缀:vite-plugin-svelte-
;- 不限框架,适用于
Vite
的插件前缀:vite-plugin-
;
当然,现在也有许多开发者为 Vite
开发了一些很 nice 的插件,如果各位需要自定义一些插件之前,可以先到 awesome-vite 上自行查找,如果没有再自己造轮子也不晚:github.com/vitejs/awes…
同时有一点需要注意,在打包插件时需要打包成 commonjs
!commonjs
!commonjs
!
--- 今天不闲扯,结束!🕊 Peace & Love ❤️---
欢迎关注公众号:Refactor,重构只为更好的自己!