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

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

通过文章你可以学到:

1. 写一个 Vite 插件
2. 通过 rollup 编译生成一个单独的资源文件
3. import 资源路径处理
4. rollup 一些配置含义
5. 一些解决问题的思路

问题点

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

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

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

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

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

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

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

通过配置解决问题

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

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

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

问题三,看了 ViteIssue,很多人遇到了这个问题,最初以为是 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 提供的钩子},],
});

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

插件一般不直接写在 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 处理

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

  2. cssCodeSplit 打包为一个 CSS 文件

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

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

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

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

  3. 通过钩子提取文件名处理

权衡之下,最终采用 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 提取可以获取到带 hashJSCSS 入口文件了。现在需要写入一个新的文件 main.jsrollup 中有个 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 之后会生成一个不带 hashmain.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;
}

但前提是需要为每个 <MicroApp /> 设置一个唯一的 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

【总结】1259- Vite 插件开发实践:微前端的资源处理相关推荐

  1. 当 Vite 遇上微前端

    什么是微前端 微前端是一种多个团队通过独立发布功能的方式来共同构建现代化 web 应用的技术手段及方法策略. 微前端借鉴了微服务的架构理念,将一个庞大的前端应用拆分为多个独立灵活的小型应用,每个应用都 ...

  2. iframe 接班人-微前端框架 qiankun 在中后台系统实践

    「福利」 ✿✿ ヽ(°▽°)ノ ✿:文章最后有抽奖,转转纪念 T 恤,走过路过不要错过哦 背景 在转转的中台业务中,交易流转.业务运营和商户赋能等功能,主要集中在两个系统中(暂且命名为 inner/o ...

  3. 基于 qiankun 的微前端实践

    前言 微前端(Micro-Frontends)是一种类似于微服务的架构,它将微服务的理念应用于浏览器端,即将 Web 应用由单一的单体应用转变为多个小型前端应用聚合为一的应用. 微前端并不是前端领域的 ...

  4. qiankun微前端实践

    qiankun微前端方案实践 qiankun 微前端方案实践 创建qiankun主项目 引入react-app-rewired 配置多环境 配置proxy 创建react子项目 创建vue子项目 个人 ...

  5. 微前端乾坤js react实践

    微前端乾坤js实践 微前端是一种多个团队通过独立发布功能的方式来共同构建现代化 web 应用的技术手段及方法策略.解耦团队之间的龚和渡,解耦技术栈的龚和渡,节省团队间沟通成本,解决技术栈之间无法业务打 ...

  6. 乾坤 微前端_微前端架构初探以及我的前端技术盘点

    前言 最近几年微前端一直是前端界的热门议题, 它类似于微服务架构, 主要面向于浏览器端,能将一个复杂而庞大的单体应用拆分为多个功能模块清晰且独立的子应用,且共同服于务同一个主应用.各个子应用可以独立运 ...

  7. 字节跳动是如何落地微前端的

    大厂技术  高级前端  Node进阶 点击上方 程序员成长指北,关注公众号 回复1,加入高级Node交流群 本文内提及的 Garfish 微前端解决方案已开源:https://github.com/m ...

  8. qiankun 微前端_看滴普大前端是如何玩转基于“qiankun”(乾坤)的微前端架构的...

    前言 「微前端」可以算是 2019年前端技术领域中最热门的话题.各个大厂也纷纷贡献出了自己的解决方案和实践分享.滴普科技作为一家致力于为企业提供数字化转型服务的技术公司,前端也需要灵活聚合,快速复用, ...

  9. 你怎么会懂?字节跳动是如何落地微前端的?

    微前端的出现的背景和意义 微前端是什么:微前端是一种类似于微服务的架构,是一种由独立交付的多个前端应用组成整体的架构风格,将前端应用分解成一些更小.更简单的能够独立开发.测试.部署的应用,而在用户看来 ...

最新文章

  1. cocos2d_x_03_经常使用类的使用_事件_画图
  2. 结构型模式之Bridge模式
  3. 总结ASP.NET中的各种弹窗
  4. 从JDK 6升级到JDK 7过程中遇到的一个问题(卸载rpm)
  5. docker nacos mysql nginx 集群一台
  6. 统计学习方法-李航(6)
  7. cocos2dx遇到的坑1
  8. 宽依赖和窄依赖_Kardemir开始生产窄钢板,进入板材市场
  9. 好的文案,极大的降低沟通成本
  10. java电器类代码_阅读下列说明和Java代码,将应填入(n)处的字句写在对应栏内。...
  11. pe系统如何读取手机_五分钟教会你pe系统制作
  12. 2022程序员都需要知道的 6 个挺火的开源项目
  13. emoji 表情图片解决方法
  14. 用Python写了一个微信聊天机器人
  15. linux iscsi软件,Redhat Linux 配置 iSCSI 连接存储
  16. 破解利器C32Asm和IDApro
  17. 团队项目用户验收评审
  18. 面试整理:关于代价函数,正则化
  19. oracle日志在哪里看,Oracle日志文件管理与查看
  20. 次时代建模的大致流程是什么呢?

热门文章

  1. Elasticsearch搭建
  2. 【机器视觉案例】(12) 自制AI视觉小游戏--贪吃蛇,附python完整代码
  3. [sips]搭建opensip:ubuntu+ARM 64位
  4. 计算机语言中脚本的意思,文字脚本是什么意思
  5. 数据库、实体-数据库设计原则-by小雨
  6. 电脑蓝屏进不了win7系统怎么解决?
  7. ip地址在同一网段什么意思
  8. 郑州大学“战役杯”第三次比赛题解
  9. 自主研发-谭八爷代理下单系统开发
  10. 导航居中分布css,导航居中背景色通栏显示 DIV + CSS 代码