Vite 插件开发实践:微前端的资源处理

开发 前端
最近实现的简单、透明、组件化微前端方案总体感觉不错,也收到了很多人的反馈,很具有学习参考价值。

最近实现的简单、透明、组件化微前端方案总体感觉不错,也收到了很多人的反馈,很具有学习参考价值。

但有不少朋友使用该方案打包配置出现了一些问题,做事应有始有终,挖的坑总得完善一下。今天分享一下 Vite 针对微应用方案插件开发历程。

通过文章你可以学到:

问题点

总结下来,在 Vite 中使用该微前端方案会遇到如下问题:

Vite 打包后的资源默认是以 HTML 为入口,我们的微前端方案需要以 JS 为入口

JS 为入口方案打包导出代码被移除掉了

import.meta 语句打包被转译成 {} 空对象了

chunk 分离后的 CSS 文件,Vite 默认以document.head.appendChild 处理

打包后的 CSS 文件默认在 main.js 中没有引用

资源路径手动写 new URL(image, import.meta.url) 太繁琐

通过配置解决问题

首先前三个问题可以通过 Vite 解决。Vite 兼容了 rollup 的配置

问题一,修改 JS 入口则需要修改 Vite 配置,设置 build.rollupOptions.input 为 src/main.tsx,这样 Vite 会默认以自定义配置的main.tsx 为入口文件做打包处理,不再生成 index.html。

问题二,rollup 的一个特性默认会清理掉入口文件的导出模块,可以配置 preserveEntrySignatures: 'allow-extension' 来保证打包之后 export 的模块不被移除掉。

问题三,看了 Vite 的 Issue,很多人遇到了这个问题,最初以为是 Vite 默认对它做了处理,后面看了 Vite 源码也没有发现处理的逻辑所在,应该是被 esbuild 做了转译。因此将 build.target 设置为 esnext 即可解决问题,即import.meta 属于 es2020,设置为具体的 es2020 也行。

配置:

export default defineConfig({
build: {
// es2020 支持 import.meta 语法
target: 'es2020',
rollupOptions: {
// 用于控制 Rollup 尝试确保入口块与基础入口模块具有相同的导出
preserveEntrySignatures: 'allow-extension',
// 入口文件
input: 'src/main.tsx',
},
},
});


写 Vite 插件

我们可以写一个插件将上面的配置封装。

一个普通的 Vite 插件很简单

defineConfig({
plugins: [
{
// 可以使用 Vite 和 rollup 提供的钩子
},
],
});


插件可以做很多事情,通过 Vite 和 rollup提供的钩子对代码解析、编译、打包、输出的整体流程进行自定义处理。

插件一般不直接写在 vite.config.ts 中,可以定义一个方法导出这个插件,这里可以用config 这个钩子来提供默认的 Vite 配置,将自定义的配置进行封装:

export function microWebPlugin(): Plugin {
// 插件钩子
return {
name: 'vite-plugin-micro-web',
config() {
return {
build: {
target: 'es2020',
rollupOptions: {
preserveEntrySignatures: 'allow-extension',
input: 'src/main.tsx',
},
},
};
},
};
}


这样一个简单的插件就完成了。

Vite 独有钩子

config - 在解析 Vite 配置前调用,它可以返回一个将被深度合并到现有配置中的部分配置对象,或者直接改变配置

configResolved - 在解析 Vite 配置后调用,使用这个钩子读取和存储最终解析的配置

configureServer - 是用于配置开发服务器的钩子

transformIndexHtml - 转换 index.html 的专用钩子。钩子接收当前的 HTML 字符串和转换上下文

handleHotUpdate - 执行自定义 HMR 更新处理。

rollup 钩子

rollup 钩子非常多,一共分两个阶段

编译阶段:

输出阶段:

这里我们会用到的钩子有:

  • transform - 用于转换已加载的模块内容
  • generateBundle - 已经编译过的代码块生成阶段

样式插入节点处理

问题四,document.head.appendChild 处理

使用 transform 钩子,替换 Vite 默认的document.head.appendChild 为自定义节点

cssCodeSplit 打包为一个 CSS 文件

我们默认采用 cssCodeSplit 打包为一个 CSS 文件,免去了用插件 transform 修改 Vite 的逻辑。

问题五,即打包后的 CSS 没有引用的问题,获取这个带 hash 的 CSS 我们可以有多种解决方案

使用 HTML 打包模式,抽取 index.html 中的 JS 、CSS 文件再单独处理

不添加样式文件名 hash ,通过约定固定该样式名称

通过钩子提取文件名处理

权衡之下,最终采用 generateBundle 阶段提取 Vite 编译生成的 CSS 文件名,通过修改入口代码将其插入。但 generateBundle 已经在输出阶段,不会再走 transform 钩子。

发现一个两全其美的办法:创建极小的入口文件main.js,还可以配合 hash 和主应用时间戳缓存处理。

async generateBundle(options, bundle) {
// 主入口文件
let entry: string | undefined;
// 所有的 CSS 模块
const cssChunks: string[] = [];
// 找出入口文件和 CSS 文件
for (const chunkName of Object.keys(bundle)) {
if (chunkName.includes('main') && chunkName.endsWith('.js')) {
entry = chunkName;
}
if (chunkName.endsWith('.css')) {
// 使用相对路径,避免后续 ESM 无法解析模块
cssChunks.push(`./${chunkName}`);
}
}
// 接下面代码
},


生成新的入口文件

通过 bundle 提取可以获取到带 hash 的JS、CSS 入口文件了。现在需要写入一个新的文件 main.js。rollup 中有个 API emitFile可以触发创建一个资源文件。

接下来对它进行处理:

// 接上面代码
if (entry) {
const cssChunksStr = JSON.stringify(cssChunks);

// 创建极小的入口文件,配合 hash 和主应用时间戳缓存处理
this.emitFile({
fileName: 'main.js',
type: 'asset',
source: `
// 带上 microAppEnv 参数,使用相对路径避免报错
import defineApp from './${entry}?microAppEnv';

// 创建 link 标签
function createLink(href) {
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = href;
return link;
}

// 入口文件导出一个方法,将打包的 CSS 文件通过 link 的方式插入到对应的节点中
defineApp.styleInject = (parentNode) => {
${cssChunksStr}.forEach((css) => {
// import.meta.url 让路径保持正确,中括号取值避免被 rollup 转换掉
const link = createLink(new URL(css, import.meta['url']));
parentNode.prepend(link);
});
};

export default defineApp;
`,
});
}


插件需要应用入口配合导出一个 styleInject方法提供样式插入,我们通过封装入口方法得以解决。

封装一个方法给应用入口调用:

export function defineMicroApp(callback) {
const defineApp = (container) => {
const appConfig = callback(container);
// 处理样式局部插入
const mountFn = appConfig.mount;
// 获取到插件中的方法
const inject = defineApp.styleInject;
if (mountFn && inject) {
appConfig.mount = (props) => {
mountFn(props);
// 装载完毕后,插入样式
inject(container);
};
}
return appConfig;
};

return defineApp;
}


现在 build 之后会生成一个不带 hash 的 main.js 文件,主应用可以正常加载打包后的资源了。

进一步优化,main.js 的压缩混淆,可以用 Vite 导出 transformWithEsbuild 进行编译:

const result = await transformWithEsbuild(customCode, 'main.js', {
minify: true,
});

this.emitFile({
fileName: 'main.js',
type: 'asset',
source: result.code,
});


子应用路径问题

之前我们需要手动添加 new URL(image, import.meta.url) 来修复子应用路径问题。通过 transform 钩子自动处理该逻辑。

在这个插件之前,Vite 会将所有的资源文件转换为路径

import logo from './logo.svg';

// 转换为:

export default '/src/logo.svg';


因此,我们只需要将 export default "资源路径" 替换为 export default new URL("资源路径", import.meta['url']).href 就可以了。

const imagesRE = new RegExp(`\\.(png|webp|jpg|gif|jpeg|tiff|svg|bmp)($|\\?)`);

transform(code, id) {
// 修正图片资源使用绝对地址
if (imagesRE.test(id)) {
return {
code: code.replace(
/(export\s+default)\s+(".+")/,
`$1 new URL($2, import.meta['url']).href`
),
map: null,
};
}
return undefined;
},


完成,一个比较完善的 Vite 微应用方案由此而生。

看看效果:

更多

有了插件,可以发挥出意想不到的事情。本微前端方案没有实现以下的隔离方式,不保证后续会实现,大家可以发挥更多的想象力。

CSS 样式隔离

通过插件将主应用节点中的 id 添加并修改CSS

.name {
color: red;
}

/* 转换为 */

#id .name {
color: red;
}


但前提是需要为每个 设置一个唯一的 id。并且样式性能会受到影响,CSSModules 方案会更好。

JS 沙箱

虽然在 ESM 中做运行时沙箱目前没有现成的方案,但运行时沙箱性能非常差。换个思路,可以从编译时沙箱入手。用 transform 钩子将应用所有的 window 转译为沙箱fakeWindow,从而达到隔离效果。

代码示例

大家可以 clone 下来学习

插件仓库:https://github.com/MinJieLiu/micro-app/tree/main/packages/micro-vite-plugin

微前端示例:https://github.com/MinJieLiu/micro-app-demo


责任编辑:武晓燕 来源: 前端星辰
相关推荐

2022-01-17 11:41:50

前端Vite组件

2022-05-09 09:28:04

Vite前端开发

2024-03-06 11:14:13

ViteReact微前端

2020-05-06 09:25:10

微前端qiankun架构

2024-09-23 00:00:10

2020-09-22 12:20:23

前端架构插件

2018-07-10 15:30:42

Go前端更新

2021-06-22 06:52:46

Vite 插件机制Rollup

2022-05-12 11:41:16

开发框架程序

2022-02-14 14:34:10

Next.js路由系统

2024-11-29 08:35:38

2022-08-10 10:32:47

编程实践

2022-07-13 09:01:17

前端开发文件处理库

2022-07-27 22:56:45

前端应用缓存qiankun

2019-03-25 13:12:59

前端开发编程

2017-06-02 10:25:26

Java异常处理

2017-09-12 10:50:55

前端SDK开发

2023-12-29 09:04:01

前端文件处理库PDF.js

2020-10-18 12:00:27

前端开发架构

2022-10-17 15:21:18

点赞
收藏

51CTO技术栈公众号