--- url: /guide/API/loadApp.md --- 与 `Garfish.run` 函数不同,run 方法是在执行后,当路由发生变化时会自动的匹配符合条件的应用执行渲染和销毁逻辑,`Garfish.loadApp` 提供了更加灵活的加载微前端应用模式,通过 `Garfish.loadApp` API 可以手动控制子应用的渲染预销毁 ### 示例 import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; ```jsx import React from 'react'; import Garfish from 'garfish'; import { BrowserRouter, Route, Link, Switch } from 'react-router-dom'; function VueApp(basename) { useEffect(async () => { const app = await Garfish.loadApp('vue-app', { cache: true, basename, domGetter: '#container', entry: 'http://localhost:8092', }); // 若已经渲染触发 show,只有首次渲染触发 mount,后面渲染都可以触发 show 提供性能 app.mounted ? app.show() : await app.mount(); return () => app.hide(); }); return
; } function App() { return ( VueApp // 分配一个路由给 vue 应用 VueApp('/vue-app')}> ); } ```
> 提供 ReactApp 的 Vue 组件 ```html ``` > 将 ReactApp 组件添加到路由中 ```js // index.js import Vue from 'vue'; import VueRouter from 'vue-router'; import ReactApp from './component/ReactApp.vue'; const router = new VueRouter({ mode: 'history', base: '/', routers: [{ path: '/react-app', component: ReactApp }], }); new Vue({ router, store, render: (h) => h(App), }).$mount('#app'); ```
### 参数
name: string
子应用的名称,也是子应用的的唯一 id,若 name 的应用已经通过 run、setOptions 注册过,提供 name 时将直接获取应用的信息
Options: AppInfo
basename?: string(默认值: /)
子应用的基础路径,子应用所有的路由都在此基础上
entry: string
子应用的入口资源地址,可以为 HTML 子应用入口地址,也可以为JS 子应用入口地址
domGetter?: string | () => Element
子应用的挂载点,提供 string 类型时需要其值是 `cssSelector`,Garfish 内部会使用 `document.querySelector(domGetter)` 去选中子应用的挂载点。当提供函数时,子应用在路由驱动挂载和手动挂载时将会执行该函数并且期望返回一个 dom 元素
cache?: boolean(默认值: true)
在调用 loadApp 时若已经加载过应用实例将返回相同的应用实例
props?: Object
传递给子应用的参数,子应用的生命周期将接受到该参数
### 返回值 `AppInstance`
mounted: boolean
是否已经触发 mount 渲染函数
mount: Function
触发子应用的渲染流程:创建子应用的渲染容器、创建一个子应用的执行环境、执行子应用的所有代码、执行 provider 提供的子应用 render 函数
show: Function
触发子应用的显示流程:显示子应用的渲染容器、执行子应用的 render 函数(不会创建新的执行上下文)
unmount: Function
触发子应用的销毁流程:移除子应用的渲染容器、销毁子应用的执行上下文、子应用在渲染过程中产生的副作用都会被清除、执行 provider 提供的子应用 destroy 函数
hide: Function
触发子应用的隐藏流程:隐藏子应用的渲染容器、执行 provider 提供的子应用 destroy 函数
### 不需要缓存的手动加载方案: ```js // options 是可选的,如果不传,默认会从 Garfish.options 上深拷贝一份过来 const app = await Garfish.loadApp('appName', { domGetter: () => document.getElementById('id'), }); // 渲染:编译子应用的代码 -> 创建应用容器 -> 调用 provider.render 渲染 // 注意:由于沙箱的实现,相同应用重复渲染,可能会导致内存泄漏问题 const success = await app.mount(); // 卸载:清除子应用的副作用 -> 调用 provider.destroy -> 销毁应用容器 const success = await app.unmount(); ``` ### 需要缓存的手动加载方案(推荐使用缓存) ```js const cache = true; const app = await Garfish.loadApp('appName', { domGetter: () => document.getElementById('id'), }); // 渲染 if (cache && app.mounted) { const success = app.show(); } else { const success = await app.mount(); } // 卸载 const success = app.hide(); ``` ### app.mount 做了哪些事情 1. 创建 `app` 容器并添加到文档流上 2. 编译子应用的代码 3. 拿到子应用的 `provider` 4. 调用 `app.options.beforeMount` 钩子 5. 调用 `provider.render` 6. 将 `app.display` 和 `app.mounted` 设置为 `true` 7. 将 `app` set 到 `Garfish.activeApps` 中 8. 调用 `app.options.afterMount` 钩子 > 如果渲染失败,`app.mount` 会返回 `false`,否则渲染成功会返回 `true`,你可以根据返回值做对应的处理。 ### app.unmount 做了哪些事件 1. 调用 `app.options.beforeUnmount` 钩子 2. 调用 `provider.destroy` 3. 清除编译的副作用 4. 将 `app` 的容器从文档流上移除 5. 将 `app.display` 和 `app.mounted` 设置为 `false` 6. 在 `Garfish.activeApps` 中移除当前的 `app` 实例 7. 调用 `app.options.afterUnmount` 钩子 > 同上,可以根据返回值来判断是否卸载成功。 ### app.show 做了哪些事件 1. 将 `app` 的容器添加到文档流上 2. 调用 `provider.render` 3. 将 `app.display` 设置为 `true` > 同上,可以根据返回值来判断是否渲染成功。 ### app.hide 做了哪些事件 1. 调用 `provider.destroy` 2. 将 `app` 的容器从文档流上移除 3. 将 `app.display` 设置为 `false` > 同上,可以根据返回值来判断是否隐藏成功。 ### 缓存 手动加载提供的了 `cache` 功能,以便复用 `app`,避免重复的编译代码造成的性能浪费,在 `Garfish.loadApp` 时,传入 `cache` 参数就可以。  例如下面的代码: ```js const app1 = await Garfish.loadApp('appName', { cache: true, }); const app2 = await Garfish.loadApp('appName', { cache: true, }); console.log(app1 === app2); // true ``` 实际上,对于加载的 `promise` 也会是同一份,例如下面的 demo ```js const promise1 = Garfish.loadApp('appName', { cache: true, }); const promise2 = Garfish.loadApp('appName', { cache: true, }); console.log(promise1 === promise2); // true const app1 = await promise1; const app2 = await promise2; console.log(app1 === app2); // true ``` --- url: /guide/advance/__meta__.md --- --- url: /guide/advance/nested.md --- `Garfish` 目前的源码设计并没有针对嵌套场景进行非常好的兼容,因此不要在使用 Garfish 的嵌套场景,否则出现问题会导致问题难以排查。 ## Garfish 为什么不容易支持嵌套场景 在正式介绍如何在嵌套场景下使用前,先简单介绍一下为什么 `Garfish` 现在并不推荐用户使用嵌套模式的原因: - `Garfish` 导出的是实例不是构造函数,多个项目通过 `npm` 包的方式引用始终都是同一个实例(Garfish 最终并没有考虑多实例的情况),因此嵌套场景下使用的都是同一个 `Garfish` 实例,同一个实例会带来很明显的弊端,全局配置会影响其他应用的使用 - `Garfish` 路由驱动会劫持 `history` 等方法,沙箱会劫持 `dom` 的原型方法,这些劫持方法再应用销毁后进行恢复成本较高 ## 嵌套场景如何使用 ### 名称统一约束 由于在支持嵌套场景前,在 Garfish 中仅有两种项目类型:一种是主应用,另外一种是子应用,但是在支持嵌套场景后,他们的关系会有更多层次,因此约束一下他们之间的名称: - 「子应用」:本身就是一个单纯的应用,本身不包含其他应用 - 「主应用」:包含其他的子应用,本身不作为子应用使用 - 「微主应用」:包含其他子应用,并且本身也作为其他主应用的子应用 ### 使用约束 在 「Garfish 为什么不容易支持嵌套场景」一节中提到 `Garfish` 目前的整体设计并不太适合嵌套场景的使用,因此希望通过一些约束来减少上述的使用问题,并且将约束后可能产生的影响也同步给用户,让用户根据实际情况进行判断。 由于 `Garfish` 并未直接导出构造函数,而是通过直接导出 `Garfish` 实例,因此在实际使用中要尽可能的避免实例上全局配置带来的影响: - 「主应用」、「微主应用」都不要使用 `Garfish.run`、`Garfish.setOptions`, 这两个 `API` 都会直接更改 `Garfish` 实例上的全局配置,可以通过 [`Garfish.loadApp`](/api/loadapp) 的方式加载子应用,这样可以保证不会对 `Garfish` 实例的全局配置产生影响 - 如果「主应用」使用了 `Garfish.run` 的注意事项 - 将配置放到 `AppInfo` 维度,这样不会对全局所有的 `App` 实例生效,只会对实际使用的 App 生效,这样会尽可能减少全局配置 - 在 `Garfish.run` 中配置的生命周期函数不仅会触发主应用配置的加载、渲染销毁的 `hook`,在嵌套场景中「微主应用」的加载渲染子应用也会触发,因为嵌套场景使用的是同一个 `Garfish` 实例 - 作为「微主应用」要清楚主应用上的全局配置,例如比较关键的沙箱配置,这些配置都会直接影响「微主应用」加载子应用的行为 - 「微主应用」如何判断是由「主应用」加载的,需要主应用增加环境变量例如:`window.__GARFISH_PARENT__ = true` 等标识,这个时候「微主应用」可以根据标识决定是否独立渲染 - 「微主应用」中需要保证应用的 `name` 不会与「主应用」列表的 `name` 不会发生冲突,因此为微主应用的子应用 `name` 增加特殊前缀避免与主应用发生冲突 - 在使用 `Garfish.router`、`Garfish.channel` 等实例上的方法时,可能会受到「主应用」、「微主应用」的影响,例如: - [`Garfish.router.push`](/api/router#garfishrouterpush) 的 `basename` 如果在「主应用」或者「微主应用」中设置了全局的 `basename` - [`Garfish.channel`](/api/channel) 的实例在「主应用」或者「微主应用」都是同一个,如果使用了相同的事件名或者回调函数会互相应用 - 在使用基础类型时都需要注意这些问题 - `Garfish` 包的升级,会同时影响、主应用和微主应用 :::info 约束总结 - 不要使用 `Garfish.run`、`Garfish.setOptions` API、避免修改 `Garfish` 全局配置 - 主应用增加环境标识,通过环境判断让微主应用能够独立运行 - 将配置信息以子应用维度存放,放置 `AppInfo` 中 - [`Garfish.loadApp`](/api/loadapp) 的方式加载、渲染微前端子应用 - 使用 `Garfish` 实例的方法时,需要注意默认配置是否收到了全局配置或其他应用的影响 - `Garfish` 包的升级,会同时影响、主应用和微主应用 ::: --- url: /guide/quick-start/index.md --- # 介绍 微前端是一种类似于微服务的架构,是一种由独立交付的多个前端应用组成整体的架构风格,将前端应用分解成一些更小、更简单的能够独立开发、测试、部署的应用,而在用户看来仍然是内聚的单个产品。 它主要解决了两个问题: - 随着项目迭代应用越来越庞大,难以维护; - 跨团队或跨部门协作开发项目导致效率低下的问题; ## Garfish 起源 Garfish 起源于 [头条号](http://mp.toutiao.com) 的实际场景,随着业务发展变成一个 Monolithic-Applications ([巨石应用](https://en.wikipedia.org/wiki/Monolithic_application))。同时由于维护的团队人员都比较分散,工程大,导致开发调试效率低、上线困难(代码合并相互依赖),成为阻塞业务发展的一个重要因素。 于是在 2018 年衍生了 Garfish 这个微前端框架,经过大量业务方实际场景的验证和打磨,Garfish 逐渐趋于成熟。并且随着更多的业务对微前端的需求,Garfish 也在不断迭代之中,已经积累了丰富的微前端问题解决经验。 ## Garfish 是什么 Garfish 是一套 [微前端](https://micro-frontends.org/) 解决方案,主要用于解决现代 web 应用在前端生态繁荣和 web 应用日益复杂化两大背景下带来的跨团队协作、技术体系多样化、web 应用日益复杂化等问题:从架构层面出发将多个独立交付的前端应用组成整体,这些前端应用能够「**独立开发**」、「**独立测试**」、「**独立部署**」,但是最终在用户看来仍然是**内聚的单个产品**。 ## 框架特性 - 🌈 **丰富高效的产品特征** - Garfish 微前端子应用支持任意多种框架、技术体系接入 - Garfish 微前端子应用支持「**独立开发**」、「**独立测试**」、「**独立部署**」 - 强大的预加载能力,自动记录用户应用加载习惯增加加载权重,应用切换时间极大缩短 - 支持依赖共享,极大程度的降低整体的包体积,减少依赖的重复加载 - 内置数据收集,有效的感知到应用在运行期间的状态 - 支持多实例能力,可在页面中同时运行多个子应用提升了业务的拆分力度 - 📦 **高扩展性的核心模块** - 通过 Loader 核心模块支持 HTML entry、JS entry 的支持,接入微前端应用简单易用 - Router 模块提供了路由驱动、主子路由隔离,用户仅需要配置路由表应用即可完成自主的渲染和销毁,无需关心内部逻辑 - Sandbox 模块为应用的 Runtime 提供运行时隔离能力,能有效隔离 JS、Style 对应用的副作用影响 - Store 提供了一套简单的通信数据交换机制 - 🎯 **高度可扩展的插件机制** - 提供业务插件满足各种定制需求 ## 设计理念 ![image.png](https://p-vcloud.byteimg.com/tos-cn-i-em5hxbkur4/d456c7d2235c41daa298aba69ade435f~tplv-em5hxbkur4-noop.image?width=1126&height=454) 具体可参考 [微前端架构设计](/blog/architecture) 这篇文章中的详细介绍 ## 什么时候用 如果你的团队成员多、项目类型多,并且想将其打造成「内聚的单个产品」: - 项目的团队成员来自多个团队 - 项目内多条迭代出现需求挤兑,影响测试、发布效率 - 跨空间、跨时间维度导致团队内技术体系无法统一 - 多个前端应用需要达到「内聚的单个产品」特征 - 「内聚的单个产品」中部分内容希望达到独立开发、独立发布、独立测试、独立灰度等能力 --- url: /guide/quick-start/start.md --- # 快速开始 本节分别从主、子 应用视角出发,介绍如何通过 [Garfish API](/api/index.md) 来将应用接入 Garfish 框架 :::tip 在线预览 ::: ## 主应用 通过 Garfish API 接入主应用整体流程分为 2 步: 1. 添加 `garfish` 依赖包 2. 通过 `Garfish.run`,提供挂载点、basename、子应用列表 ### 1.安装依赖 ```bash npm2yarn npm install garfish --save ``` ### 2.注册子应用并启动 Garfish ```js // index.js(主应用入口处) import Garfish from 'garfish'; Garfish.run({ basename: '/', domGetter: '#subApp', apps: [ { name: 'react', activeWhen: '/react', entry: 'http://localhost:3000', // html入口 }, { name: 'vue', activeWhen: '/vue', entry: 'http://localhost:8080/index.js', // js入口 }, ], }); ``` 当引入 Garfish 实例,执行实例方法 `Garfish.run` 后,`Garfish` 将会立刻启动路由劫持能力。 这时 `Garfish` 将会监听浏览器路由地址变化,当浏览器的地址发生变化时,`Garfish` 框架内部便会执行匹配逻辑,当解析到当前路径符合子应用匹配逻辑时,便会自动将应用挂载至指定的 `dom` 节点上,并在此过程中会依次触发子应用加载、渲染过程中的 [生命周期钩子函数](/guide/concept/lifecycle.md). :::tip 注意 请确保指定的节点存在于页面中,否则可能会导致出现 `Invalid domGetter "xxx"​` 错误。在 `Garfish` 开始渲染时,无法查询到该挂载节点则会提示该错误 > 解决方案 1. 将挂载点设置为常驻挂载点,不要跟随路由变化使子应用挂载点销毁和出现 2. 保证 Garfish 在渲染时挂载点存在 ::: 如果你的业务需要手动控制应用加载,可以使用 [Garfish.loadApp](/api/loadApp.md) 手动加载 APP: ```typescript // 使用 loadApp 动态挂载应用 import Garfish from 'garfish'; const app = await Garfish.loadApp('vue-app', { domGetter: '#container', entry: 'http://localhost:3000', cache: true, }); // 若已经渲染触发 show,只有首次渲染触发 mount,后面渲染都可以触发 show 提升性能 app.mounted ? app.show() : await app.mount(); ``` ## 子应用 通过 Garfish API 接入子应用整体流程分为 3 步: 1. 调整子应用的构建配置(目前 Garfish 仅支持 umd 格式的产物) 2. 导出子应用生命周期 3. 设置应用路由 `basename` ### 1.调整子应用的构建配置 ```js // webpack.config.js const webpack = require('webpack'); const isDevelopment = process.env.NODE_ENV !== 'production'; module.exports = { output: { // 开发环境设置 true 将会导致热更新失效 clean: isDevelopment ? false : true, filename: '[name].[contenthash].js', chunkFilename: '[name].[contenthash].js', // 需要配置成 umd 规范 libraryTarget: 'umd', // 修改不规范的代码格式,避免逃逸沙箱 globalObject: 'window', // webpack5 使用 chunkLoadingGlobal 代替,或不填保证 package.json name 唯一即可 jsonpFunction: 'garfish-demo-react', // 保证子应用的资源路径变为绝对路径 publicPath: 'http://localhost:8080', }, plugin: [ // 保证错误堆栈信息及 sourcemap 行列信息正确 new webpack.BannerPlugin({ banner: 'Micro front-end', }), ], devServer: { // 保证在开发模式下应用端口不一样 port: '8000', headers: { // 保证子应用的资源支持跨域,在上线后需要保证子应用的资源在主应用的环境中加载不会存在跨域问题(**也需要限制范围注意安全问题**) 'Access-Control-Allow-Origin': '*', }, }, }; ``` 【重要】注意: 1. libraryTarget 需要配置成 umd 规范; 2. globalObject 需要设置为 'window',以避免由于不规范的代码格式导致的逃逸沙箱; 3. 如果你的 webpack 为 v4 版本,需要设置 jsonpFunction 并保证该值唯一(否则可能出现 webpack chunk 互相影响的可能)。若为 webpack5 将会直接使用 package.json name 作为唯一值,请确保应用间的 name 各不相同; 4. publicPath 设置为子应用资源的绝对地址,避免由于子应用的相对资源导致资源变为了主应用上的相对资源。这是因为主、子应用处于同一个文档流中,相对路径是相对于主应用而言的 5. 'Access-Control-Allow-Origin': '\*' 允许开发环境跨域,保证子应用的资源支持跨域。另外也需要保证在上线后子应用的资源在主应用的环境中加载不会存在跨域问题(**也需要限制范围注意安全问题**); ```js // vite.config.js export default defineConfig({ base: 'http://localhost:3000/', server: { port: 3000, cors: true, origin: 'http://localhost:3000', }, }); ``` 【重要】注意: 1. base 提供资源绝对路径,避免相对路径带来的资源访问问题; 2. origin 提供资源绝对路径,避免相对路径带来的资源访问问题; 3. 需要将子应用沙箱关闭 `Garfish.run({ apps: [{ ..., sandbox: false }] })` 4. 子应用的副作用将会发生逃逸,在子应用卸载后需要将对应全局的副作用清除 ### 2.导出 provider 函数 > 针对子应用需要导出生命周期函数,我们提供了桥接函数 [`@garfish/bridge-react`](/guide/bridge.md) 自动包装应用的生命周期,使用`@garfish/bridge-react` 可以降低接入成本与用户出错概率,也是 garfish 推荐的子应用接入方式。 ```bash npm2yarn // 安装 @garfish/bridge-react: npm install @garfish/bridge-react --save ``` ```jsx import { reactBridge } from '@garfish/bridge-react'; export const provider = reactBridge({ el: '#root', rootComponent: RootComponent, errorBoundary: () => , }); ``` ```tsx import React from 'react'; import ReactDOM from 'react-dom'; import { BrowserRouter, Switch, Route, Link } from 'react-router-dom'; export const provider = () => ({ // render 渲染函数,必须提供 render: ({ dom, basename }) => { // 和子应用独立运行时一样,将子应用渲染至对应的容器节点,根据不同的框架使用不同的渲染方式 ReactDOM.render( , // 需要注意的一点是,子应用的入口是否为 HTML 类型(即在主应用的中配置子应用的 entry 地址为子应用的 html 地址), // 如果为 HTML 类型,需要在 dom 的基础上选中子应用的渲染节点 // 如果为 JS 类型,则直接将 dom 作为渲染节点即可 dom.querySelector('#root'), ); }, // destroy 应用销毁函数,必须提供 destroy: ({ dom, basename }) => { // 使用框架提供的销毁函数销毁整个应用,已达到销毁框架中可能存在得副作用,并触发应用中的一些组件销毁函数 // 需要注意的时一定要保证对应框架得销毁函数使用正确,否则可能导致子应用未正常卸载影响其他子应用 ReactDOM.unmountComponentAtNode( dom ? dom.querySelector('#root') : document.querySelector('#root'), ); }, }); ``` ### 3. 设置应用路由 `basename` ```ts // src/component/rootComponent import React from "react"; import { BrowserRouter } from "react-router-dom"; const RootComponent = ({ basename }) => { return ( }> } /> } /> ) } ``` 我们在 [接入指南](/guide/demo/demo.md) 章节详细中介绍了各框架的子应用接入 Garfish 的 demo 案例及接入过程注意事项,目前提供了: * react (version 16, 17, 18) * vue (version 2, 3) * vite (version 2) * angular (version 13) 可移步 [接入指南](/guide/demo.md) 查看详细接入步骤。 ## 总结 使用 Garfish API 搭建一套微前端主子应用的主要成本来自两方面 * 主应用的搭建 * 注册子应用的基本信息 * 使用 Garfish 在主应用上调度管理子应用 * 子应用的改造 * 增加对应的构建配置 * 使用 `@garfish/bridge-react` 包提供的函数包装子应用后返回 `provider` 函数并导出 * 子应用针对不同的框架类型,添加不同 `basename` 的设置方式 * React 在根组件中获取 `basename` 将其传递至 `BrowserRouter` 的 `basename` 属性中 * Vue 将 `basename` 传递至 `VueRouter` 的 `basename` 属性中 --- url: /guide/quick-start/env.md --- # 环境变量 有时候需要使用环境变量(Environment Variables)以按需控制 `Garfish` 的行为,或者通过环境变量来区分微前端的子应用是否在微前端环境下运行,进行一些兼容性逻辑的处理,下面来看看如何使用环境变量来控制 `Garfish` 的行为。 ## 环境变量列表 | 名称 | 描述 | 使用场景 | | -------------------- | ---------------------------------------------------------- | ----------------------------------------------------------------------------- | | `window.__GARFISH__` | 在引入 `garfish` 包后, `window.__GARFISH__` 为 `true` | 主要让子应用在校验是否处于微前端环境,因此建议子应用不要单独引入 `garfish` 包 | | `window.Garfish` | 在引入 `garfish` 包后, `window.Garfish` 为 `Garfish` 实例 | 可以使用 `Garfish` 实例上的方法,子应用也可使用该变量 | ## 使用场景 ### `window.__GARFISH__` 用于子应用判断当前是否处于微前端环境中。 如:在子应用入口处。增加子应用独立运行时逻辑: ```ts if (!window.__GARFISH__) { ReactDOM.render( , document.querySelector('#root'), ); } ``` ### `window.Garfish` 使用 `Garfish` 的路由进行路由跳转 ```ts window.Garfish.router.push({ path: '/test' }); ``` --- url: /guide/demo/demo.md --- # 概述 本节我们会详细讲述不同框架下的子应用如何接入 Garfish, 提供抄得走的接入案例,以下所有 demo 均可在 [garfish demo](https://github.com/modern-js-dev/garfish/tree/main/dev) 中找到实际使用案例,目前提供的 demo 案例包含: - react (version 16, 17, 18) - vue (version 2, 3) - vite (version 2) - angular (version 13) ## demo 案例 子应用的导出提供通过 `@garfish/bridge-*` 的方式和自定义导出函数两种方式,我们将在下列 demo 案例中分别讲述。 - [react 子应用](/guide/demo/react) - [vue 子应用](/guide/demo/vue) - [vite 子应用](/guide/demo/vite) - [angular 子应用](/guide/demo/angular) --- url: /guide/demo/react.md --- # react 子应用 本节我们将详细介绍 react 框架的应用作为子应用的接入步骤。[v16/17 demo](https://github.com/modern-js-dev/garfish/tree/main/dev/app-react-17)、[v18 demo](https://github.com/modern-js-dev/garfish/blob/main/dev/app-react-18) ## react 子应用接入步骤 ### 1. bridge 依赖安装 :::tip 1. 请注意,桥接函数的安装不是必须的,你可以自定义导出函数。 2. 我们提供桥接函数是为了进一步降低用户接入成本并降低用户出错概率,桥接函数中将会内置一些默认行为,可以避免由于接入不规范导致的错误,所以这也是我们推荐的接入方式。 ::: ```bash npm2yarn npm install @garfish/bridge-react --save ``` ### 2. 入口文件处导出 provider 函数 更多 bridge 函数参数介绍请参考 [这里](/guide/concept/bridge.md) ### react v16、v17 导出 ```tsx // src/index.tsx import { reactBridge } from '@garfish/bridge-react'; import RootComponent from './components/root'; import Error from './components/ErrorBoundary'; export const provider = reactBridge({ // 子应用挂载点,若子应用构建成 js ,则不需要传递该值 el: '#root', // 根组件, bridge 会默认传递 basename、dom、props 等信息到根组件 rootComponent: RootComponent, // 设置应用的 errorBoundary errorBoundary: () => , }); ``` ```tsx // src/index.tsx import React from "react"; import ReactDOM from "react-dom"; import RootComponent from "./components/root"; export const provider = () => { return { // 和子应用独立运行时一样,将子应用渲染至对应的容器节点,根据不同的框架使用不同的渲染方式 render({ dom, basename, props}) { ReactDOM.render(, root); }, destroy({ dom, basename}) { // 使用框架提供的销毁函数销毁整个应用,已达到销毁框架中可能存在得副作用,并触发应用中的一些组件销毁函数 // 需要注意的时一定要保证对应框架得销毁函数使用正确,否则可能导致子应用未正常卸载影响其他子应用 ReactDOM.unmountComponentAtNode( dom ? dom.querySelector('#root') : document.querySelector('#root'), ); }, }; }; ``` ### react v18 导出 ```tsx // src/index.tsx import { reactBridge } from '@garfish/bridge-react-v18'; import RootComponent from './root'; import ErrorBoundary from './ErrorBoundary'; export const provider = reactBridge({ el: '#root', rootComponent: RootComponent, errorBoundary: (e: any) => , }); ``` ```tsx // src/index.tsx import { createRoot } from 'react-dom/client'; import RootComponent from './root'; // 在首次加载和执行时会触发该函数 export const provider = () => { let root = null; return { render({ basename, dom, store, props }) { const container = dom.querySelector('#root'); root = createRoot(container!); (root as any).render(); }, destroy({ dom }) { (root as any).unmount(); }, }; }; ``` ### 3. 根组件设置路由的 basename :::info 1. 为什么要设置 basename?请参考 [issue](/issues/index.md#子应用拿到-basename-的作用) 2. 我们强烈建议使用从主应用传递过来的 basename 作为子应用的 basename,而非主、子应用约定式,避免 basename 后期变更未同步带来的问题。 3. 目前主应用仅支持 history 模式的子应用路由,[why](/issues/index.md#为什么主应用仅支持-history-模式) ::: ```tsx // src/component/rootComponent import React from "react"; import { BrowserRouter } from "react-router-dom"; const RootComponent = ({ basename }) => { return ( }> } /> } /> ) } ``` ### 4. 更改 webpack 配置 ```js // webpack.config.js const webpack = require('webpack'); const isDevelopment = process.env.NODE_ENV !== 'production'; module.exports = { output: { // 开发环境设置 true 将会导致热更新失效 clean: isDevelopment ? false : true, filename: '[name].[contenthash].js', chunkFilename: '[name].[contenthash].js', // 需要配置成 umd 规范 libraryTarget: 'umd', // 修改不规范的代码格式,避免逃逸沙箱 globalObject: 'window', // webpack5 使用 chunkLoadingGlobal 代替,或不填保证 package.json name 唯一即可 jsonpFunction: 'garfish-demo-react', // 保证子应用的资源路径变为绝对路径 publicPath: 'http://localhost:8080', }, plugin: [ // 保证错误堆栈信息及 sourcemap 行列信息正确 new webpack.BannerPlugin({ banner: 'Micro front-end', }), ], devServer: { // 保证在开发模式下应用端口不一样 port: '8000', headers: { // 保证子应用的资源支持跨域,在上线后需要保证子应用的资源在主应用的环境中加载不会存在跨域问题(**也需要限制范围注意安全问题**) 'Access-Control-Allow-Origin': '*', }, }, }; ``` 【重要】注意: 1. libraryTarget 需要配置成 umd 规范; 2. globalObject 需要设置为 'window',以避免由于不规范的代码格式导致的逃逸沙箱; 3. 如果你的 webpack 为 v4 版本,需要设置 jsonpFunction 并保证该值唯一(否则可能出现 webpack chunk 互相影响的可能)。若为 webpack5 将会直接使用 package.json name 作为唯一值,请确保应用间的 name 各不相同; 4. publicPath 设置为子应用资源的绝对地址,避免由于子应用的相对资源导致资源变为了主应用上的相对资源。这是因为主、子应用处于同一个文档流中,相对路径是相对于主应用而言的 5. 'Access-Control-Allow-Origin': '\*' 允许开发环境跨域,保证子应用的资源支持跨域。另外也需要保证在上线后子应用的资源在主应用的环境中加载不会存在跨域问题(**也需要限制范围注意安全问题**); ### 5. 增加子应用独立运行兼容逻辑 :::tip last but not least, 别忘了添加子应用独立运行逻辑,这能够让你的子应用脱离主应用独立运行,便于后续开发和部署。 ::: ```tsx // src/index.tsx if (!window.__GARFISH__) { ReactDOM.render( , document.getElementById("root")); } ``` ```tsx // src/index.tsx if (!window.__GARFISH__) { const container = document.getElementById('root'); const root = createRoot(container!); root.render( ); } ``` --- url: /guide/demo/vue.md --- # vue 子应用 本节我们将详细介绍 vue 框架的应用作为子应用的接入步骤。[v2 demo](https://github.com/modern-js-dev/garfish/tree/main/dev/app-vue-2)、[v3 demo](https://github.com/modern-js-dev/garfish/tree/main/dev/app-vue-3) ## vue 子应用接入步骤 ### 1. bridge 依赖安装 :::tip 1. 请注意,桥接函数的安装不是必须的,你可以自定义导出函数。 2. 我们提供桥接函数是为了进一步降低用户接入成本并降低用户出错概率,桥接函数中将会内置一些默认行为,可以避免由于接入不规范导致的错误,所以这也是我们推荐的接入方式。 3. 我们分别为 vue 2、3 应用提供不同的 bridge 包,目的是为了更好的类型提示及精简参数。 ::: ```bash npm2yarn npm install @garfish/bridge-vue-v2 --save ``` ```bash npm2yarn npm install @garfish/bridge-vue-v3 --save ``` ### 2. 入口文件处导出 provider 函数 更多 bridge 函数参数介绍请参考 [这里](/guide/concept/bridge.md) ### vue2 导出 ```js import Vue from 'vue'; import VueRouter from 'vue-router'; import store from './store'; import App from './App.vue'; import Home from './components/Home.vue'; import { vueBridge } from '@garfish/bridge-vue-v2'; Vue.use(VueRouter); Vue.config.productionTip = false; function newRouter(basename) { const router = new VueRouter({ mode: 'history', base: basename, routes: [ { path: '/home', component: Home }, ], }); return router; } export const provider = vueBridge({ // 根组件 rootComponent: App, // 可选,注册 vue-router或状态管理对象 appOptions: ({ basename, dom, appName, props, appInfo }) => { // pass the options to Vue Constructor. check https://vuejs.bootcss.com/api/#%E9%80%89%E9%A1%B9-%E6%95%B0%E6%8D%AE return { el: '#app', router: newRouter(basename), store, }; }, }); ``` ```js import Vue from 'vue'; import App from './App.vue'; import store from './store'; import VueRouter from 'vue-router'; import HelloWorld from './components/HelloWorld.vue'; Vue.use(VueRouter); Vue.config.productionTip = false; const render = ({ dom, basename = '/' }) => { const router = new VueRouter({ mode: 'history', base: basename, router, routes: [ { path: '/', component: HelloWorld }, ], }); const vm = new Vue({ store, render: (h) => h(App, { props: { basename } }), }).$mount(); (dom || document).querySelector('#app').appendChild(vm.$el); }; ``` ### vue3 导出 ```js import { h, createApp } from 'vue'; import { createRouter, createWebHistory } from 'vue-router'; import { stateSymbol, createState } from './store.js'; import App from './App.vue'; import Home from './components/Home.vue'; import { vueBridge } from '@garfish/bridge-vue-v3'; const routes = [ { path: '/home', component: Home }, ]; function newRouter(basename) { const router = createRouter({ history: createWebHistory(basename), routes, }); return router; } export const provider = vueBridge({ rootComponent: App, // 可选,注册 vue-router或状态管理对象 handleInstance: (vueInstance, { basename, dom, appName, props, appInfo}) => { vueInstance.use(newRouter(basename)); vueInstance.provide(stateSymbol, createState()); }, }); ``` ```js import { h, createApp } from 'vue'; import { createRouter, createWebHistory } from 'vue-router'; import { stateSymbol, createState } from './store.js'; import App from './App.vue'; import HelloGarfish from './components/HelloGarfish.vue'; export function provider({ dom, basename }) { let app = null; return { render() { app = createApp(App); app.provide(stateSymbol, createState()); const router = createRouter({ history: createWebHistory(basename), base: basename, routes: [{ path: '/home', component: HelloGarfish }] }); app.use(router); app.mount( dom ? dom.querySelector('#app') : document.querySelector('#app'), ); }, destroy() { if (app) { app.unmount( dom ? dom.querySelector('#app') : document.querySelector('#app'), ); } }, }; } ``` ### 3. 根组件设置路由的 basename :::tip 1. 为什么要设置 basename?请参考 [issue](/issues/index.md#子应用拿到-basename-的作用) 2. 我们强烈建议使用从主应用传递过来的 basename 作为子应用的 basename,而非主、子应用约定式,避免 basename 后期变更未同步带来的问题。 3. 目前主应用仅支持 history 模式的子应用路由,[why](/issues/index.md#为什么主应用仅支持-history-模式) ::: ```js import Vue from 'vue'; import VueRouter from 'vue-router'; import store from './store'; import App from './App.vue'; import Home from './components/Home.vue'; import { vueBridge } from '@garfish/bridge-vue-v2'; Vue.use(VueRouter); Vue.config.productionTip = false; function newRouter(basename) { const router = new VueRouter({ mode: 'history', base: basename, routes: [ { path: '/home', component: Home }, ], }); return router; } ``` ```js import { h, createApp } from 'vue'; import { createRouter, createWebHistory } from 'vue-router'; import { stateSymbol, createState } from './store.js'; import App from './App.vue'; import Home from './components/Home.vue'; import { vueBridge } from '@garfish/bridge-vue-v3'; const routes = [ { path: '/home', component: Home }, ]; function newRouter(basename) { const router = createRouter({ history: createWebHistory(basename), base: basename, routes, }); return router; } ``` ### 4. 更改 webpack 配置 ```js // webpack.config.js const webpack = require('webpack'); const isDevelopment = process.env.NODE_ENV !== 'production'; module.exports = { output: { // 开发环境设置 true 将会导致热更新失效 clean: isDevelopment ? false : true, filename: '[name].[contenthash].js', chunkFilename: '[name].[contenthash].js', // 需要配置成 umd 规范 libraryTarget: 'umd', // 修改不规范的代码格式,避免逃逸沙箱 globalObject: 'window', // webpack5 使用 chunkLoadingGlobal 代替,或不填保证 package.json name 唯一即可 jsonpFunction: 'garfish-demo-react', // 保证子应用的资源路径变为绝对路径 publicPath: 'http://localhost:8080', }, plugin: [ // 保证错误堆栈信息及 sourcemap 行列信息正确 new webpack.BannerPlugin({ banner: 'Micro front-end', }), ], devServer: { // 保证在开发模式下应用端口不一样 port: '8000', headers: { // 保证子应用的资源支持跨域,在上线后需要保证子应用的资源在主应用的环境中加载不会存在跨域问题(**也需要限制范围注意安全问题**) 'Access-Control-Allow-Origin': '*', }, }, }; ``` 【重要】注意: 1. libraryTarget 需要配置成 umd 规范; 2. globalObject 需要设置为 'window',以避免由于不规范的代码格式导致的逃逸沙箱; 3. 如果你的 webpack 为 v4 版本,需要设置 jsonpFunction 并保证该值唯一(否则可能出现 webpack chunk 互相影响的可能)。若为 webpack5 将会直接使用 package.json name 作为唯一值,请确保应用间的 name 各不相同; 4. publicPath 设置为子应用资源的绝对地址,避免由于子应用的相对资源导致资源变为了主应用上的相对资源。这是因为主、子应用处于同一个文档流中,相对路径是相对于主应用而言的 5. 'Access-Control-Allow-Origin': '\*' 允许开发环境跨域,保证子应用的资源支持跨域。另外也需要保证在上线后子应用的资源在主应用的环境中加载不会存在跨域问题(**也需要限制范围注意安全问题**); ### 5. 增加子应用独立运行兼容逻辑 :::tip last but not least, 别忘了添加子应用独立运行逻辑,这能够让你的子应用脱离主应用独立运行,便于后续开发和部署。 ::: ```js // src/main.js import Vue from 'vue'; import VueRouter from 'vue-router'; // 这能够让子应用独立运行起来,以保证后续子应用能脱离主应用独立运行,方便调试、开发 if (!window.__GARFISH__) { const router = new VueRouter({ mode: 'history', base: '/', routes: [ { path: '/home', component: Home }, ], }); new Vue({ store, router, render: (h) => h(App), }).$mount('#app'); } ``` ```js // src/main.js import { h, createApp } from 'vue'; import VueRouter from 'vue-router'; // 这能够让子应用独立运行起来,以保证后续子应用能脱离主应用独立运行,方便调试、开发 if (!window.__GARFISH__) { const router = new VueRouter({ mode: 'history', base: '/', routes: [ { path: '/home', component: Home }, ], }); const app = createApp(App); app.provide(stateSymbol, createState()); app.use(router); app.mount('#app'); } ``` --- url: /guide/demo/vite.md --- # vite 子应用 本节我们将详细介绍 vite 框架的应用作为子应用的接入步骤。[demo](https://github.com/modern-js-dev/garfish/tree/main/dev/app-vue-vite) ### 子应用沙箱状态 当 vite 应用作为子应用接入 garfish 时,我们要求子应用沙箱需关闭,否则应用将不能正常运行。 :::info 请注意: 1. 子应用沙箱默认为开启状态,请[设置子应用沙箱关闭](/guide/demo/vite.md#设置子应用沙箱关闭); 2. 在关闭沙箱的场景下,子应用的副作用将会发生逃逸,请确保子应用卸载后对应全局的副作用被清除; ::: ### 设置子应用沙箱关闭 ```js // 主应用工程中,Garfish.run 处设置: Garfish.run({ ..., apps: [ { name: 'sub-app', activeWhen: '/vite', sandbox: false } ] }) ``` :::danger 注意,不要设置 Garfish.run() 顶层的 sandbox 属性,这会导致所有子应用的沙箱关闭。 ::: ## vite 子应用接入步骤 ### 1. bridge 依赖安装 :::tip 1. 请注意,桥接函数的安装不是必须的,你可以自定义导出函数。 2. 我们提供桥接函数是为了进一步降低用户接入成本并降低用户出错概率,桥接函数中将会内置一些默认行为,可以可以避免由于接入不规范导致的错误,所以这也是我们推荐的接入方式。 ::: ```bash npm2yarn npm install @garfish/bridge-vue-v3 --save ``` ### 2. 入口文件处导出 provider 函数 更多 bridge 函数参数介绍请参考 [这里](/guide/concept/bridge.md) ```js // src/main.js import { h } from 'vue'; import { createRouter, createWebHistory } from 'vue-router'; import { vueBridge } from '@garfish/bridge-vue-v3'; import App from './App.vue'; function newRouter(basename) { const router = createRouter({ history: createWebHistory(basename), base: basename, routes, }); return router; } export const provider = vueBridge({ rootComponent: App, // 可选,注册 vue-router或状态管理对象 appOptions: ({ basename, dom, appName, props }) => ({ el: '#app', render: () => h(App), router: newRouter(basename), }), }); ``` ### 3. 根组件设置路由的 basename :::info 1. 为什么要设置 basename?请参考 [issue](/issues/index.md#子应用拿到-basename-的作用) 2. 我们强烈建议使用从主应用传递过来的 basename 作为子应用的 basename,而非主、子应用约定式,避免 basename 后期变更未同步带来的问题。 3. 目前主应用仅支持 history 模式的子应用路由,[why](/issues/index.md#为什么主应用仅支持-history-模式) ::: ```js // src/main.js import { h } from 'vue'; import { createRouter, createWebHistory } from 'vue-router'; import { vueBridge } from '@garfish/bridge-vue-v3'; import App from './App.vue'; function newRouter(basename) { const router = createRouter({ history: createWebHistory(basename), base: basename, routes, }); return router; } ``` ### 4. 更改 vite 配置 ```js // vite.config.js export default defineConfig({ base: 'http://localhost:3000/', server: { port: 3000, cors: true, origin: 'http://localhost:3000', }, }); ``` 【重要】注意: 1. base 提供资源绝对路径,避免相对路径带来的资源访问问题; 2. origin 提供资源绝对路径,避免相对路径带来的资源访问问题; 3. 需要将子应用沙箱关闭 `Garfish.run({ apps: [{ ..., sandbox: false }] })` 4. 子应用的副作用将会发生逃逸,在子应用卸载后需要将对应全局的副作用清除 ### 5. 增加子应用独立运行兼容逻辑 :::tip last but not least, 别忘了添加子应用独立运行逻辑,这能够让你的子应用脱离主应用独立运行,便于后续开发和部署。 ::: ```js // src/main.js if (!window.__GARFISH__) { // 非微前端环境直接运行 const vueInstance = createApp(App); vueInstance.mount(document.querySelector('#app')); } ``` --- url: /guide/demo/angular.md --- # angular 子应用 本节我们将详细介绍 angular 框架的应用作为子应用的接入步骤。[demo](https://github.com/modern-js-dev/garfish/tree/main/dev/app-angular) ## angular 子应用接入步骤 ### 1. 插件安装 ```bash npm2yarn # 1. 安装 @angular-builders/custom-webpack:browser npm install @angular-builders/custom-webpack:browser -D # 2. 安装 @angular-builders/custom-webpack:dev-server npm install @angular-builders/custom-webpack:dev-server -D ``` ### 2. 修改 angular.json 1. 修改 \[packageName] > architect > build > builder ```json // angular.json "builder": "@angular-builders/custom-webpack:browser", ``` 2. 修改 \[packageName] > architect > build > options ```json // angular.json "options": { "customWebpackConfig": { // 新增 webpack 配置 "path": "./custom-webpack.config.js" }, "index": "", } ``` 3. 修改 \[packageName] > architect > serve > builder ```json // angular.json "builder": "@angular-builders/custom-webpack:dev-server", ``` :::danger 1. 请注意,在 \[packageName] > architect > build > options 的配置中,index 属性我们设置为空,这是因为在 angular 13 中编译产物默认会带上 esm 标识,即 type=module, 即使打包产物是 umd 格式,这会导致 garfish 加载子应用失败; 2. index 置空后,编译产物会去除 es module 标识,子应用加载正常; ::: ### 3. 添加 webpack 配置文件 :::tip danger 【重要】注意: 1. libraryTarget 需要配置成 umd 规范; 2. globalObject 需要设置为 'window',以避免由于不规范的代码格式导致的逃逸沙箱; 3. 如果你的 webpack 为 v4 版本,需要设置 jsonpFunction 并保证该值唯一(否则可能出现 webpack chunk 互相影响的可能)。若为 webpack5 将会直接使用 package.json name 作为唯一值,请确保应用间的 name 各不相同; 4. publicPath 设置为子应用资源的绝对地址,避免由于子应用的相对资源导致资源变为了主应用上的相对资源。这是因为主、子应用处于同一个文档流中,相对路径是相对于主应用而言的 5. 'Access-Control-Allow-Origin': '\*' 允许开发环境跨域,保证子应用的资源支持跨域。另外也需要保证在上线后子应用的资源在主应用的环境中加载不会存在跨域问题(**也需要限制范围注意安全问题**); ::: ```js // custom-webpack.config.js const HtmlWebpackPlugin = require('html-webpack-plugin'); module.exports = { output: { filename: '[name].[contenthash].js', chunkFilename: '[name].[contenthash].js', libraryTarget: 'umd', globalObject: 'window', chunkLoadingGlobal: 'Garfish-demo-angular', publicPath: 'http://localhost:8080' }, plugins: [ new HtmlWebpackPlugin({ filename: 'index.html', template: path.join(__dirname, 'src/index.html'), chunksSortMode: 'manual', chunks: ['styles', 'runtime', 'polyfills', 'scripts', 'vendors', 'main'], scriptLoading: 'defer', }), ], devServer: { headers: { 'Access-Control-Allow-Origin': '*', }, }, }; ``` ### 4. 更改 package.json 启动脚本 ```json "scripts": { "builder": "@angular-builders/custom-webpack:dev-server" } ``` ### 5. 入口文件处导出 provider 函数 ```ts // src/main.ts import { enableProdMode, NgModuleRef } from '@angular/core'; import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; import { AppModule } from './app/app.module'; import { environment } from './environments/environment'; if (environment.production) { enableProdMode(); } let app: void | NgModuleRef; async function render() { await platformBrowserDynamic() .bootstrapModule(AppModule) .catch((err) => console.error(err)); } export const provider = ({ dom, basename, props}) => { return { render, destroy({ dom }) { const root = dom ? dom.querySelector('#root') : document.querySelector('#root'); }, }; }; ``` ### 6. 根组件设置路由的 basename :::info 1. 为什么要设置 basename?请参考 [issue](/issues/index.md#子应用拿到-basename-的作用) 2. 我们强烈建议使用从主应用传递过来的 basename 作为子应用的 basename,而非主、子应用约定式,避免 basename 后期变更未同步带来的问题。 3. 目前主应用仅支持 history 模式的子应用路由,[why](/issues/index.md#为什么主应用仅支持-history-模式) ::: ```ts // app.module.ts import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { RouterModule } from '@angular/router'; import { ReactiveFormsModule } from '@angular/forms'; import { AppComponent } from './app.component'; import { TopBarComponent } from './topBar/topBar.component'; import { HomeComponent } from './home/home.component'; import { APP_BASE_HREF } from '@angular/common'; @NgModule({ imports: [ BrowserModule, ReactiveFormsModule, RouterModule.forRoot([ { path: '/home', component: HomeComponent } ]) ], providers: [{ provide: APP_BASE_HREF, useValue: '/examples/angular' }], declarations: [ AppComponent, TopBarComponent, ], bootstrap: [AppComponent], }) export class AppModule {} ``` ### 7. 增加子应用独立运行兼容逻辑 :::tip last but not least, 别忘了添加子应用独立运行逻辑,这能够让你的子应用脱离主应用独立运行,便于后续开发和部署。 ::: ```js // src/main.ts import { enableProdMode, NgModuleRef } from '@angular/core'; import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; import { AppModule } from './app/app.module'; async function render() { await platformBrowserDynamic() .bootstrapModule(AppModule) .catch((err) => console.error(err)); } if (!(window as any).__GARFISH__) { render(); } ``` --- url: /guide/concept/bridge.md --- # bridge ## 介绍 Garfish bridge 是 `garfish` 提供的帮助用户降低接入成本的工具函数,它能自动提供 `provider` 函数所需的应用生命周期函数 `render` 和 `destroy` ,并实现框架不同版本的兼容。封装底层实现,降低接入成本和出错概率。 :::info 1. garfish bridge 应用在子应用接入场景; 2. 使用 garfish bridge 后不再需要显示提供 `render` 和 `destroy` 函数; 3. 目前 garfish 仅针对 react 和 vue 框架提供 bridge 函数支持,支持的版本分别为 react v16、v17、v18,vue v2、v3; 4. garfish bridge 暂未针对构建工具如 webpack、vite 提供相应的构建工具插件,我们后期会针对这块能力进行补全,请持续关注; ::: ## 工具包 ### @garfish/bridge-react [@garfish/bridge-react](https://www.npmjs.com/package/@garfish/bridge-react) 工具包是 garfish 为 react v16/v17 应用 提供的 bridge 工具函数包,其导出的 [reactBridge](/guide/concept/bridge.md#reactbridgefor-react-v16v17) 可用于 react v16/v17 子应用的接入,`@garfish/bridge-react` 的使用见 [demo](/guide/demo/react.md#react-v16v17-导出)。 ### @garfish/bridge-react-v18 [@garfish/bridge-react-v18](https://www.npmjs.com/package/@garfish/bridge-react-v18) 工具包是 garfish 为 react v18 应用 提供的 bridge 工具函数包,其导出的 [reactBridge](/guide/concept/bridge.md#reactBridge) 可用于 react v18 子应用的接入,`@garfish/bridge-react-v18` 的使用见 [demo](/guide/demo/react.md#react-v18-导出)。 ### @garfish/bridge-vue-v2 [@garfish/bridge-vue-v2](https://www.npmjs.com/package/@garfish/bridge-vue-v2) 工具包是 garfish 为 vue v2 应用 提供的 bridge 工具函数包,其导出的 [vueBridge](/guide/concept/bridge.md#vuebridge) 可用于 vue v2 子应用的接入,`@garfish/bridge-vue-v2` 的使用见 [demo](/guide/demo/vue.md#vue2-导出)。 ### @garfish/bridge-vue-v3 [@garfish/bridge-vue-v3](https://www.npmjs.com/package/@garfish/bridge-vue-v3) 工具包是 garfish 为 vue v3 应用 提供的 bridge 工具函数包,其导出的 [vueBridge](/guide/concept/bridge.md#vuebridge) 可用于 vue v3 子应用的接入,`@garfish/bridge-vue-v3` 的使用见 [demo](/guide/demo/vue.md#vue3-导出)。 ## 安装 ```bash npm2yarn npm install @garfish/bridge-react --save ``` ```bash npm2yarn npm install @garfish/bridge-vue-v2 --save ``` ```bash npm2yarn npm install @garfish/bridge-vue-v3 --save ``` ## reactBridge(for react v16/v17/v18) reactBridge 是 `@garfish/bridge-react` 工具包为 react 子应用提供的 bridge 工具函数。 :::info * 针对 react v16/v17 子应用,请使用 `@garfish/bridge-react` 工具包 * 针对 react v18 子应用,请使用 `@garfish/bridge-react-v18` 工具包 ::: reactBridge 是 `@garfish/bridge-react` 或 `@garfish/bridge-react-v18` 工具包为 react 子应用提供的 bridge 工具函数。 ### Type ```ts function reactBridge(userOpts: Options): ( appInfo: any, props: any, ) => Promise<{ render: (props: any) => any; destroy: (props: any) => any; update: (props: any) => any; }>; ``` ### 示例 > 可访问 [react16 子应用](https://github.com/modern-js-dev/garfish/tree/main/dev/app-react-16)、 [react17 子应用](https://github.com/modern-js-dev/garfish/tree/main/dev/app-react-17)、[react18 子应用](https://github.com/modern-js-dev/garfish/tree/main/dev/app-react-18) 查看完整 demo ```ts import { reactBridge } from '@garfish/bridge-react'; import RootComponent from './components/root'; import ErrorComponent from './components/ErrorBoundary'; export const provider = reactBridge({ el: '#root', rootComponent: RootComponent, errorBoundary: () => , }); ``` ```ts import { reactBridge } from '@garfish/bridge-react-v18'; import RootComponent from './components/root'; import ErrorComponent from './components/ErrorBoundary'; export const provider = reactBridge({ el: '#root', rootComponent: RootComponent, errorBoundary: () => , }); ``` ### 参数 `Options` * el * Type: `string` * 非必传 * 子应用挂载点 * 若子应用构建为 `JS` 入口时,不需要传挂载点,Bridge 将会以子应用的渲染节点作为挂载点; * 若子应用构建成 `HTML` 入口时,则直接传入选择器,bridge 内部通过 `dom.querySelector` 来基于子应用的 `dom` 来找到挂载点; * rootComponent * Type:`React.ComponentType` * 此参数和 `loadRootComponent` 至少传一个 * 当前应用的顶层 React 组件,该组件中将接受到 garfish 传递的 appInfo 应用相关参数: ```ts // components/root.tsx const RootComponent = ({ appName, basename, dom, props }) => { ... } ``` * 当同时传入了 `loadRootComponent` 参数时,`rootComponent` 参数将失效,且 `rootComponent` 组件不会默认接收到 garfish 传递的子应用相关参数; * loadRootComponent * Type:`loadRootComponentType = (opts: Record) => Promise;` * 此参数和 `rootComponent` 至少传一个 * 当前应用的顶层 React 组件,该组件中将接收到 garfish 传递的子应用相关参数: ```ts // components/root.tsx const RootComponent = ({ appName, basename, dom, props }) => { ... } ``` * `loadRootComponent` 是一个函数,返回一个 Promise 对象,resolve 后需要返回当前 React 应用的顶层组件,该顶层组件含义与 `rootComponent` 含义相同。当需要在 render 前进行异步操作时,可使用 `loadRootComponent` 加入副作用逻辑。 * `loadRootComponent` 将默认接收到 garfish 传递的子应用相关参数: ```ts import { reactBridge } from "@garfish/bridge-react"; export const provider = reactBridge({ ..., loadRootComponent: ({ basename, dom, appName, props }) => { // do something async return Promise.resolve(() => ); } }); ``` 此时,`RootComponent` 接收到的参数取决于此处 `loadRootComponent` 传递的参数。 * 当同时传入了 `rootComponent` 参数时,`loadRootComponent` 的优先级更高, `rootComponent` 将失效; * errorBoundary * Type:`errorBoundary: (caughtError: boolean, info: string, props: any) => ReactNode | null;` * 非必传 * 设置应用的 errorBoundary 组件,`errorBoundary` 是一个函数,并在子应用发生错误时触发,该函数将传递 error 报错信息及报错相关应用堆栈信息: ```ts import { reactBridge } from "@garfish/bridge-react"; export const provider = reactBridge({ ..., errorBoundary: ( error, info ) => , }); ``` ## vueBridge(for vue v2) :::info 针对 vue v2 子应用,请使用 `@garfish/bridge-vue-v2` 工具包。 ::: vueBridge 是 `@garfish/bridge-vue-v2` 工具包为 vue v2 子应用提供的 bridge 工具函数。 ### 类型 ```ts function vueBridge(userOpts: Options): ( appInfo: any, props: any, ) => Promise<{ render: (props: any) => any; destroy: (props: any) => any; update: (props: any) => any; }>; ``` ### 示例 > 可访问 [vue2 子应用](https://github.com/modern-js-dev/garfish/tree/main/dev/app-vue-2) 查看完整 demo ```ts import { vueBridge } from '@garfish/bridge-vue-v2'; import store from './store'; import App from './App.vue'; import Home from './components/Home.vue'; Vue.use(VueRouter); Vue.config.productionTip = false; function newRouter(basename) { const router = new VueRouter({ mode: 'history', base: basename, routes: [{ path: '/home', component: Home }], }); return router; } export const provider = vueBridge({ rootComponent: App, // 可选,注册 vue-router或状态管理对象 appOptions: ({ basename, dom, appName, props }) => { // pass the options to Vue Constructor. check https://vuejs.bootcss.com/api/#%E9%80%89%E9%A1%B9-%E6%95%B0%E6%8D%AE return { el: '#app', router: newRouter(basename), store, }; } }); ``` ### 参数 `Options` * rootComponent * Type:`vue.Component` * 非必传。此参数和 `loadRootComponent` 至少传一个 * 当前应用的顶层 Vue 组件,该组件中将接受到 garfish 传递的子应用相关参数: ```ts // components/root.tsx const RootComponent = ({ appName, basename, dom, props, appInfo}) => { ... } ``` * 当同时传入了 `loadRootComponent` 参数时,`rootComponent` 将失效,且 `rootComponent` 组件不会默认接受到 garfish 传递的子应用相关参数; * loadRootComponent * Type:`loadRootComponentType = (opts: Record) => Promise;` * 非必传。此参数和 `rootComponent` 至少传一个 * 当前应用的顶层 Vue 组件,该组件中实例的 data 对象中将接收到 garfish 传递的子应用相关参数 * `loadRootComponent` 是一个函数,返回一个 Promise 对象,resolve 后需要返回当前 Vue 应用的顶层组件,该顶层组件含义与 `rootComponent` 含义相同。当需要在 render 前进行异步操作时,可使用 `loadRootComponent` 加入副作用逻辑。 * `loadRootComponent` 将默认接收到 garfish 传递的子应用相关参数: ```ts import { vueBridge } from "@garfish/bridge-vue-v2"; export const provider = vueBridge({ ..., loadRootComponent: ({ appName, basename, dom, props, appInfo }) => { // do something async return Promise.resolve(App); } }); ``` * 当同时传入了 `rootComponent` 参数时,`loadRootComponent` 的优先级更高, `rootComponent` 将失效; * appOptions * Type: `appOptions: (opts: Record) => Record | Record` * 非必传 * 作为函数时,接收 garfish 传递的子应用相关参数并返回用来实例化 Vue 应用的对象参数,也可作为对象类型直接返回用来实例化 Vue 应用的对象参数。实例化完成后,garfish 子应用相关参数将会自动注入到组件实例的 `data` 对象中。 * `appOptions` 参数将直接透传为 Vue 构造函数实例化时的初始化参数 new Vue(appOptions),此时参数类型与 [vue](https://vuejs.bootcss.com/api/#%E9%80%89%E9%A1%B9-%E6%95%B0%E6%8D%AE) 保持一致。若未传递 `appOptions` 参数,则将自动提供 vue2 应用 `render` 函数用于渲染:`render: (h) => h(opts.rootComponent)`。 * 若需要指定子应用挂载点,可在此参数中指定:`appOptions: { el: '#app', ...}`,若未指定 `el` 参数,将默认使用全局挂载点。 :::tip 需要注意的是,`appOpitons` 中并不会默认包含路由或状态逻辑的处理,可显示在 `appOpitons` 中传递路由参数信息。 ::: ```js import { vueBridge } from '@garfish/bridge-vue-v2'; export const provider = vueBridge({ rootComponent: App, appOptions: ({ basename, dom, appName, props, appInfo }) => { // pass the options to Vue Constructor. check https://vuejs.bootcss.com/api/#%E9%80%89%E9%A1%B9-%E6%95%B0%E6%8D%AE return { el: '#app', router: newRouter(basename), store, }; } }); ``` * handleInstance * Type: ` handleInstance: (vueInstance: InstanceType, opts: optionsType) => void;` * 非必传 * 处理 app 实例对象的函数,接受创建的 app 实例对象及 garfish 子应用相关参数,可自定义处理逻辑如路由注册或状态管理等相关能力。 ## vueBridge(for vue v3) :::info 针对 vue v3 子应用,请使用 `@garfish/bridge-vue-v3` 工具包。 ::: vueBridge 是 `@garfish/bridge-vue-v3` 工具包为 vue v3 子应用提供的 bridge 工具函数。 ### 类型 ```ts function vueBridge(userOpts: Options): ( appInfo: any, props: any, ) => Promise<{ render: (props: any) => any; destroy: (props: any) => any; update: (props: any) => any; }>; ``` ### 示例 > 可访问 [vue v3 子应用](https://github.com/modern-js-dev/garfish/tree/main/dev/app-vue-3) 查看完整 demo ```ts import { createRouter, createWebHistory } from 'vue-router'; import { stateSymbol, createState } from './store.js'; import App from './App.vue'; import Home from './components/Home.vue'; import { vueBridge } from '@garfish/bridge-vue-v3'; function newRouter(basename) { const router = createRouter({ history: createWebHistory(basename), routes: [{ path: '/home', component: Home }], }); return router; } export const provider = vueBridge({ rootComponent: App, // 可选,注册 vue-router或状态管理对象 handleInstance: (vueInstance, { basename, dom, appName, props, appIndfo }) => { vueInstance.use(newRouter(basename)); vueInstance.provide(stateSymbol, createState()); }, }); ``` ### 参数 `Options` * rootComponent * Type:`vue.Component` * 非必传。此参数和 `loadRootComponent` 至少传一个 * 当前应用的顶层 Vue 组件,该组件中将接受到 garfish 传递的子应用相关参数: ```ts // components/root.tsx const RootComponent = ({ appName, basename, dom, props, appInfo}) => { ... } ``` * 当同时传入了 `loadRootComponent` 参数时,`rootComponent` 将失效,且 `rootComponent` 组件不会默认接受到 garfish 传递的子应用相关参数; * loadRootComponent * Type:`loadRootComponentType = (opts: Record) => Promise;` * 非必传。此参数和 `rootComponent` 至少传一个 * 当前应用的顶层 Vue 组件,该组件中实例的 data 对象中将接收到 garfish 传递的子应用相关参数 * `loadRootComponent` 是一个函数,返回一个 Promise 对象,resolve 后需要返回当前 Vue 应用的顶层组件,该顶层组件含义与 `rootComponent` 含义相同。当需要在 render 前进行异步操作时,可使用 `loadRootComponent` 加入副作用逻辑。 * `loadRootComponent` 将默认接收到 garfish 传递的子应用相关参数: ```ts import { vueBridge } from "@garfish/bridge-vue-v3"; export const provider = vueBridge({ ..., loadRootComponent: ({ appName, basename, dom, props, appInfo }) => { // do something async return Promise.resolve(App); } }); ``` * 当同时传入了 `rootComponent` 参数时,`loadRootComponent` 的优先级更高, `rootComponent` 将失效; * appOptions * Type: `appOptions: (opts: Record) => Record | Record` * 非必传 * 作为函数时,接收 garfish 传递的子应用相关参数并返回用来实例化 Vue 应用的对象参数,也可作为对象类型直接返回用来实例化 Vue 应用的对象参数。实例化完成后,garfish 子应用相关参数将会自动注入到组件实例的 `data` 对象中。 * 在 Vue3 中,`appOptions` 参数将直接透传给 `createApp` 函数调用: `createApp(appOptions)`,此时参数类型与 [createApp](https://vuejs.org/api/application.html#createapp) 保持一致。若未传递 `appOptions` 参数,则将直接调用 `createApp(rootComponent)` 创建根组件。 * 若需要指定子应用挂载点,可在此参数中指定:`appOptions: { el: '#app', ...}`,若未指定 `el` 参数,将默认使用全局挂载点。 :::tip 需要注意的是,`appOptions` 中并不会默认包含路由或状态逻辑的处理,可通过 `handleInstance` 函数拿到创建的 vue 实例对象后进行路由注册。 ::: * handleInstance * Type: `handleInstance: (vueInstance: vue.App, opts: optionsType) => void;` * 非必传 * 处理 app 实例对象的函数,接受创建的 app 实例对象及 garfish 子应用相关参数,可自定义处理逻辑如路由注册或状态管理等相关能力。 ```js import { vueBridge } from '@garfish/bridge-vue-v3'; export const provider = vueBridge({ rootComponent: App, // 获取 vue 实例并进行路由注册和状态注册 handleInstance: (vueInstance, { basename, dom, appName, props, appInfo }) => { vueInstance.use(newRouter(basename)); vueInstance.provide(stateSymbol, createState()); }, }); ``` --- url: /guide/concept/cache.md --- # 缓存机制 `Garfish` 的设计的初衷并不是为了取代 `iframe`,而是为了将一个单体应用拆分成多个子应用后也能保证应用一体化的使用体验,`Garfish` 为了提升应用的渲染性能,提供了缓存渲染模式。 缓存的形式分为两种,一种是缓存 `App` 的实例,缓存 `App` 的实例比较容易理解,`Garfish` 在通过 `loadApp` 加载子应用后可以保留 `App` 的实例,另外一种则是缓存子应用的执行上下文,第二遍执行时不执行所有代码来提升整体的渲染速度。 ## 提升渲染速度 为什么 `Garfish` 的子应用需要提供 `provider` 函数呢?原因是通过提供 `provider` 生命周期,我们可以尽可能的优化渲染速度。 - 在应用销毁时触发对应框架应用的销毁函数,以达到对框架类型的销毁操作,应用中的一些销毁 `hook` 也可以正常触发 - 在第二次应用加载时可以启动缓存模式 - 在应用第一次渲染时的路径为,`html` 下载=> `html` 拆分=> 渲染 `dom` => 渲染 `style` => 执行 `JS` => 执行 `provider` 中的函数 - 在第二次渲染时可以将整个渲染流程简化为,还原子应用的 `html` 内容=> 执行 `provider` 中的渲染函数。因为子应用的真实执行环境并未被销毁,而是通过 `render` 和 `destroy` 控制对应应用的渲染和销毁 - 避免内存泄漏 - 由于目前 `Garfish` 框架的沙箱依赖于浏览器的 `API`,无法做到物理级别的隔离。由于 `JavaScript` 语法的灵活性和闭包的特性,第二次重复执行子应用代码可能会导致逃逸内容重复执行 - 采用缓存模式时,将不会执行所有代码,仅执行 `render` ,将会避免逃逸代码造成的内存问题 > 缓存模式下的弊端 - 启动缓存模式后也存在一定弊端,第二遍执行时 `render` 中的逻辑获取的还是上一次的执行环境并不是一个全新的执行环境,下面代码中在缓存模式时和非缓存模式不同的表现 - 在缓存模式中,多次渲染子应用会导致 `list` 数组的值持续增长,并导致影响业务逻辑 - 在非缓存模式中,多次渲染子应用 `list` 数组的长度始终为 `1` - `Garfish` 框架无法有效区分哪些副作用需要销毁 - 在缓存模式中并不会执行子应用的所有代码,只会还原子应用的上下文并执行子应用的 `render` 函数,因此无法区分哪些副作用是实际应用 `render` 过程中创建的还是其他基础库造成的 - 目前 `Garfish` 框架在缓存模式下仅会收集和清除:样式副作用、环境变量 ```js const list = []; export const provider = () => { return { render: ({ dom, basename }) => { list.push(1); ReactDOM.render( , dom.querySelector('#root'), ); }, destroy: ({ dom, basename }) => { ReactDOM.unmountComponentAtNode(dom.querySelector('#root')); }, }; }; ``` 具体使用如何使用缓存模式请参考:[Garfish.loadApp](/api/loadApp) ## 缓存 App 实例 手动加载提供了 `cache` 功能,以便复用 `app`,避免重复的编译代码造成的性能浪费,在 `Garfish.loadApp` 时,传入 `cache` 参数就可以。  例如下面的代码: ```js const app1 = await Garfish.loadApp('appName', { cache: true, }); const app2 = await Garfish.loadApp('appName', { cache: true, }); console.log(app1 === app2); // true ``` 实际上,对于加载的 `promise` 也会是同一份,例如下面的 demo ```js const promise1 = Garfish.loadApp('appName', { cache: true, }); const promise2 = Garfish.loadApp('appName', { cache: true, }); console.log(promise1 === promise2); // true const app1 = await promise1; const app2 = await promise2; console.log(app1 === app2); // true ``` --- url: /guide/concept/lifecycle.md --- # 生命周期 `Garfish` 应用的生命周期可以归结为:加载、渲染、销毁 三个阶段,因此 `Garfish` 应用的生命周期也是围绕着这三个阶段而展开的。应用的加载主要是通过 [Garfish.loadApp](../../api/loadApp.md),通过 `loadApp` API 会自动创建应用的实例,可以通过应用实例上的 `mount` 和 `show` 方法对应用进行渲染,通过 `unmount` 和 `hide` 方法对应用进行销毁,用户在实际使用的过程中通过 [Garfish.run](../../api/run.md)会发现当路由发生变化时符合加载条件的应用会自动加载渲染,实际上是 [`Garfish Router Plugin`](./router.md) 通过监听路由变化来触发 `loadApp` 和 `mount` 自动完成应用的加载、渲染、销毁。 ### mount app.mount 做了哪些事情 1. 创建 `app` 容器并添加到文档流上 2. 编译子应用的代码 3. 拿到子应用的 `provider` 4. 调用 `app.options.beforeMount` 钩子 5. 调用 `provider.render` 6. 将 `app.display` 和 `app.mounted` 设置为 `true` 7. 将 `app` set 到 `Garfish.activeApps` 中 8. 调用 `app.options.afterMount` 钩子 9. 如果渲染失败,`app.mount` 会返回 `false`,否则渲染成功会返回 `true`,你可以根据返回值做对应的处理。 ### unmount app.unmount 做了哪些事件 1. 调用 `app.options.beforeUnmount` 钩子 2. 调用 `provider.destroy` 3. 清除编译的副作用 4. 将 `app` 的容器从文档流上移除 5. 将 `app.display` 和 `app.mounted` 设置为 `false` 6. 在 `Garfish.activeApps` 中移除当前的 `app` 实例 7. 调用 `app.options.afterUnmount` 钩子 8. 同上,可以根据返回值来判断是否卸载成功。 ### show app.show 做了哪些事件 1. 将 `app` 的容器添加到文档流上 2. 调用 `provider.render` 3. 将 `app.display` 设置为 `true` 4. 同上,可以根据返回值来判断是否渲染成功。 ### hide app.hide 做了哪些事件 1. 调用 `provider.destroy` 2. 将 `app` 的容器从文档流上移除 3. 将 `app.display` 设置为 `false` 4. 同上,可以根据返回值来判断是否隐藏成功。 --- url: /guide/concept/router.md --- # 路由机制 为什么需要 `Router`,其实从现在主流的前端框架可以发现他们都是可以通过路由驱动的,开发者只需要配置路由的 `map` 规则,即可在进入指定路由后载入子应用。这无疑大大降低了单页应用的复杂度,微前端上我们也可以借用单页应用的路由驱动模式,将每个子应用作为组件,并且只托管子应用的根路由,二级及以下路由交由子应用自己负责。其他比较复杂的情况,可以通过与手动载入配合。 ![image](https://user-images.githubusercontent.com/27547179/165110838-2f907b10-40b4-4e00-afab-808ef08d1570.png) `Garfish Router` 如何处理路由,通过上面理想的路由模式案例发现,微前端应用拆分成子应用后,子应用路由应具备自治能力,可以充分的利用应用解耦后的开发优势,但与之对应的是应用间的路由可能会发生冲突、两种路由模式下可能产生用户难以理解的路由状态、无法激活不同前端框架的下带来的视图无法更新等问题。 > 目前 `Garfish` 主要提供了以下三条策略 - 提供 `Router Map`,减少典型中台应用下的开发者理解成本 - 为不同子应用提供不同的 `basename` 用于隔离应用间的路由抢占问题 - 路由发生变化时能准确激活并触发应用视图更新 ## `Router Map` 降低开发者理解成本 在典型的中台应用中,通常可以将应用的结构分为两块,一块是菜单另一块则是内容区域,依托于现代前端 Web 应用的设计理念的启发,通过提供路由表来自动化完成子应用的调度,将公共部分作为拆离后的子应用渲染区域。 ## 提供 basename 在自动挂载模式下 `Garfish` 会根据用户提供的 `activeWhen` 自动计算出子应用的 basename,子应用使用该 `basename` ,子应用设置 basename 后可以保证应用间的路由互不影响且能达到多个微前端应用组合成单个 `SPA` 应用的体验,并且这些微前端应用能具备自己的路由。 ## 如何有效的触发不同应用间的视图更新 目前主流框架实现路由的方式并不是监听路由变化触发组件更新,让开发者通过框架包装后的 API 进行跳转,并内部维护路由状态,在使用框架提供 API 方法发生路由更新时,内部状态发生变更触发组件更新。 由于框架的路由状态分别维护在各自的内部,那么如何保证在路由发生变化时能及时有效的触发应用的视图更新呢,答案是可以的,目前主要有两种实现策略: 1. 收集框架监听的 `popstate` 事件 2. 主动触发 `popstate` 事件 因为目前支持 SPA 应用的前端框架都会监听浏览器后退事件,在浏览器后退时根据路由状态触发应用视图的更新,那么其实也可以利用这种能力主动触发应用视图的更新,可以通过收集框架的监听事件,也可以触发 `popstate` 来响应应用的 `popstate` 事件 --- url: /guide/concept/buildConfig.md --- # 构建配置 在 **「快速开始」** 章节可以发现,`Garfish` 针对不同的构建工具:`webpack`、`vite` 都要求其配置一些构建配置,那么这些构建配置分别在 `Garfish` 微前端应用中起到什么作用呢,「构建配置」这一章节主要是希望能够给帮助你了解背后这些构建配置带来的作用,下面的内容主要以 `webpack` 构建配置为例: ```js // webpack.config.js { output: { // 需要配置成 umd 规范 libraryTarget: 'umd', // 修改不规范的代码格式,避免逃逸沙箱 globalObject: 'window', // 请求确保每个子应用该值都不相同,否则可能出现 webpack chunk 互相影响的可能 // webpack 5 使用 chunkLoadingGlobal 代替,若不填 webpack 5 将会直接使用 package.json name 作为唯一值,请确保应用间的 name 各不相同 jsonpFunction: 'vue-app-jsonpFunction', // 保证子应用的资源路径变为绝对路径,避免子应用的相对资源在变为主应用上的相对资源,因为子应用和主应用在同一个文档流,相对路径是相对于主应用而言的 publicPath: 'http://localhost:8000', }, } ``` ## libraryTarget 在构建配置中通过 `libraryTarget` 可以将项目的产物分别构建成不同规范的产物:`umd`、`commonjs` 等规范的产物,实际 `Garfish` 希望子应用导出的是 `commonjs` 的产物,但是为了避免后续 `Garfish` 采用其他规范的产物,因此通常让微前端子应用构建成 `umd` 的产物,那么 `Garfish` 为什么希望子应用导出的是 `commonjs` 的产物呢?主要是解决两个问题: 1. 获取 `provider` 渲染协议 2. 进行依赖注入和共享 为什么设置为 `commonjs` 协议后,`Garfish` 框架能够获取 `provider` 协议和进行依赖注册和共享呢,这一切都要从 `commonjs` 的规范讲起,如果了解过 `commonjs` 实现原理的一定对下面这段代码非常熟悉: ```js let code = `(function(exports,require,module,__dirname,__filename){ })`; vm.runInThisContext(fn).call(module.exports, module.exports, req, module); ``` 在 `node` 环境中拥有 `exports`、`require` ... 这几个全局环境变量,通过这些环境能够完成模块的加载,完成内容的导出和载入,那么同时也能够利用 `commonjs` 的机制在代码运行时注入 `exports`、`require` 环境变量从而实现控制子应用获取依赖和拿到子应用导出内容的目的 ```js // 实际代码 let code = ``; new Function('require', 'exports', code)(fakeRequire, fakerExports); ``` ## globalObject `globalObject` 的配置主要与 `Garfish` 的 `sandbox` 执行代码的机制有关,可以参考 [沙箱机制的异常 case 逃逸](./sandbox.md#特殊-case) ## jsonpFunction > webpackjsonp 是什么? `webpack` 使用 `webpackjsonp` 来解决分 chunk 之后的加载问题,在 webpack 5 中采用 chunkLoadingGlobal 代替。 `webpackjsonp` 是一个全局变量,用于存储 chunk 的信息。 如下图: ![image](https://user-images.githubusercontent.com/27547179/167364472-8e3ce4b9-94b3-490f-a4bf-b904579ea3a1.png) > `jsonpFunction` 配置的作用 `jsonpFunction` 的配置主要也与沙箱的机制有关, `Garfish` 的沙箱子应用的执行上下文 `window` 主要来自于主应用,当主应用与子应用都是用相同的 `key` 作为子应用 `jsonp` 存储 `chunk` 的方式时,子应用的 `chunk` 可能会受到主应用和其他应用的影响,因此通过 `jsonpFunction` 配置能够避免应用间的 `chunk` 互相应用 ## publicPath 通过 `publicPath` 配置将微前端子应用的资源路径转换成绝对路径,为什么需要将子应用的资源路径转换成绝对路径呢? - 子应用在独立运行时,使用相对路径的接口时,接口请求的路径是,当前页面域名+相对路径 - 但是在主应用时,子应用使用相对路径的接口,请求的路径按道理来说还是,当前域名+相对路径 当在微前端的场景下如果 `Garfish` 让子应用走「当前域名+相对路径」会发生更多的异常请求(`hmr` 热更新、`websocket`、`server worker` ...),因为子应用的域名并不一定是与主应用一致,因此 `Garfish` 框架会对相对路径的资源和请求去进行修正,修正的参照物为基础域名为子应用的路径,在本地开发时可能是正常的,但是发到线上出现问题,原因在于发布到线上之后,子应用的入口有可能会走 `CDN`。因此参照的基础路径就变为了 CDN 前缀。那么此时子应用的相对路径请求就变为了 `CDN` 前缀。这一块做了很多权衡,因为 `hmr`、`websocket`、`server worker` 这些内容可能难以被用户控制,所以默认走的还是修正模式。 --- url: /guide/concept/sandbox.md --- # 沙箱机制 ## Changelog | 版本 | 日期 | 修订人 | ChangeLog | | ---- | --------- | ----------- | ---------------- | | v0.1 | 2022-4-20 | zengkunpeng | 沙箱机制文章开源 | | v0.2 | 2022-4-29 | zhouxiao | 调整 DOM 副作用 | ## 背景 在微前端的场景,由于多个独立的应用被组织到了一起,在没有类似 `iframe` 的原生隔离下,势必会出现冲突,如全局变量冲突、样式冲突,这些冲突可能会导致应用样式异常,甚至功能不可用。所以想让微前端达到生产可用的程度,让每个子应用之间达到一定程度隔离的沙箱机制是必不可少的。 此外沙箱功能还需要满足多实例的场景,先来了解一下什么是微前端里的多实例。 ![image](https://user-images.githubusercontent.com/27547179/164134100-2c4d311e-4849-4ca4-a965-de1ff1a980d0.png) ## 手动执行代码 常规的脚本加载,是通过 `script` 标签去执行的,如 ```html // 内连 ``` > 将 ReactApp 组件添加到路由中 ```js // index.js import Vue from 'vue'; import VueRouter from 'vue-router'; import ReactApp from './component/ReactApp.vue'; const router = new VueRouter({ mode: 'history', base: '/', routers: [{ path: '/react-app', component: ReactApp }], }); new Vue({ router, store, render: (h) => h(App), }).$mount('#app'); ``` ## 子应用 ### 安装依赖 ```bash npm2yarn npm install @garfish/bridge-react --save ``` ### 通过 Bridge 函数包装子应用 ```jsx import { BrowserRouter, Switch, Route, Link } from 'react-router-dom'; import { reactBridge } from '@garfish/bridge-react'; function App({ basename }) { return ( // 根组件使用传递过来的 basename,作为应用的基础路径 Home ); } export const provider = reactBridge({ rootComponent: App, domElementGetter: '#root', // 应用的挂载点,如果子应用打包为 JS 入口,可不填写 }); ``` ```js import App from './App.vue'; import { vueBridge } from '@garfish/bridge-vue-v2'; function newRouter(basename) { const router = new VueRouter({ router, mode: 'history', base: basename, routes: [{ path: '/', component: HelloGarfish }], }); return router; } export const provider = vueBridge({ rootComponent: App, appOptions: ({ basename }) => { const router = newRouter(basename); return { store, router, el: '#app', }; }, }); ``` ### 调整子应用的构建配置 ```js // webpack.config.js const webpack = require('webpack'); module.exports = { output: { // 需要配置成 umd 规范 libraryTarget: 'umd', // 修改不规范的代码格式,避免逃逸沙箱 globalObject: 'window', // 请求确保每个子应用该值都不相同,否则可能出现 webpack chunk 互相影响的可能 jsonpFunction: 'vue-app-jsonpFunction', // 保证子应用的资源路径变为绝对路径,避免子应用的相对资源在变为主应用上的相对资源,因为子应用和主应用在同一个文档流,相对路径是相对于主应用而言的 publicPath: 'http://localhost:8000', }, plugin: [ // 保证错误堆栈信息及 sourcemap 行列信息正确 new webpack.BannerPlugin({ banner: 'Micro front-end', }) ], devServer: { // 保证在开发模式下应用端口不一样 port: '8000', headers: { // 保证子应用的资源支持跨域,在线上后需要保证子应用的资源在主应用的环境中加载不会存在跨域问题(**也需要限制范围注意安全问题**) 'Access-Control-Allow-Origin': '*', }, }, }; ``` --- url: /issues/index.md --- # 常见问题 ## "provider" is "null". 出现这个问题是因为 garfish 无法从子应用中正确获取到 `provider` 导出函数,可以先按照以下步骤自查: 1. 检查子应用是否正确 export 了 provider 函数。[参考](/guide/quick-start/start.md#2导出-provider-函数) 2. 检查子应用是否正确配置了 webpack 的 output 配置: ```js // webpack.config.js const webpack = require('webpack'); const isDevelopment = process.env.NODE_ENV !== 'production'; module.exports = { output: { // 开发环境设置 true 将会导致热更新失效 clean: isDevelopment ? false : true, filename: '[name].[contenthash].js', chunkFilename: '[name].[contenthash].js', // 需要配置成 umd 规范 libraryTarget: 'umd', // 修改不规范的代码格式,避免逃逸沙箱 globalObject: 'window', // webpack5 使用 chunkLoadingGlobal 代替,或不填保证 package.json name 唯一即可 jsonpFunction: 'garfish-demo-react', // 保证子应用的资源路径变为绝对路径 publicPath: 'http://localhost:8080', }, plugin: [ // 保证错误堆栈信息及 sourcemap 行列信息正确 new webpack.BannerPlugin({ banner: 'Micro front-end', }), ], devServer: { // 保证在开发模式下应用端口不一样 port: '8000', headers: { // 保证子应用的资源支持跨域,在上线后需要保证子应用的资源在主应用的环境中加载不会存在跨域问题(**也需要限制范围注意安全问题**) 'Access-Control-Allow-Origin': '*', }, }, }; ``` 【重要】注意: 1. libraryTarget 需要配置成 umd 规范; 2. globalObject 需要设置为 'window',以避免由于不规范的代码格式导致的逃逸沙箱; 3. 如果你的 webpack 为 v4 版本,需要设置 jsonpFunction 并保证该值唯一(否则可能出现 webpack chunk 互相影响的可能)。若为 webpack5 将会直接使用 package.json name 作为唯一值,请确保应用间的 name 各不相同; 4. publicPath 设置为子应用资源的绝对地址,避免由于子应用的相对资源导致资源变为了主应用上的相对资源。这是因为主、子应用处于同一个文档流中,相对路径是相对于主应用而言的 5. 'Access-Control-Allow-Origin': '\*' 允许开发环境跨域,保证子应用的资源支持跨域。另外也需要保证在上线后子应用的资源在主应用的环境中加载不会存在跨域问题(**也需要限制范围注意安全问题**); 3) 确认子应用 `entry` 地址设置正确:若为 html 的入口类型 `entry` 配置为 html 入口地址,若为 js 类型,子应用 `entry` 配置为 js 入口地址; 4) 若子应用为 js 入口,需要保证子应用的资源被打包成了单 bundle,若有部分依赖未被打包成 bundle 会导致子应用无法正常加载。例如子应用使用了 webpack splitChunk 进行拆包且为 js 入口时,会导致上述报错; 5) 如以上途径都无法解决,请试图通过环境变量导出,这将会让 Garfish 框架更准确的获取到导出内容: ```js if (window.__GARFISH__ && typeof __GARFISH_EXPORTS__ !== 'undefined') { // eslint-disable-next-line no-undef __GARFISH_EXPORTS__.provider = provider; } ``` ## 微应用 JSONP 跨域错误怎么处理? 在使用 Garfish 时,微应用的动态脚本(如 JSONP)会被转化为 fetch 请求,这要求后端服务支持跨域请求,否则会产生错误。 可以使用 [excludeAssetFilter](/api/registerApp.md#sandbox) 参数来放行这些资源请求,但请注意,被该参数放行的资源会逃逸出沙箱,可能导致副作用,需自行处理。 ## Uncaught (in promise) TypeError: \[Garfish warning]: Cannot read properties of undefined (reading 'call') * 错误原因 * 这个问题出现在子应用构建为 umd 格式后存在脚本出现了 `type="module"` 的标识,这将导致该 script 逃逸出沙箱执行,而其余脚本在沙箱内执行,找不到 chunk 导致报错。 * 解决方案 * 请确保子应用构建为 umd 格式后 script 不会带上 `type="module"` 标识,保证子应用的正常解析和渲染。 ## Invalid domGetter "xxx" 错误原因:在 Garfish 开始渲染时,无法查询到该挂载节点则会提示该错误 > 解决方案 1. 将挂载点设置为常驻挂载点,不要跟随路由变化使子应用挂载点销毁和出现 2. 保证 Garfish 在渲染时挂载点存在 ## 如何获取主应用的 localStorage 可按照如下配置获取主应用的 localStorage: ```ts import Garfish from 'garfish'; Garfish.run({ ..., sandbox: { modules: [ () => ({ override: { localStorage: window.localStorage, }, }), ], } }); ``` 类似 localStorage,子应用若需要获取被沙箱隔离机制隔离的全局变量上的变量,均可通过上述方式获取。 ## 如何判断子应用是否微前端应用中 可通过环境变量 `window.__GARFISH__` 判断。 ## 如何手动挂载子应用 可通过 [Garfish.loadApp](/api/loadApp.md) 动态加载子应用。 ## HTML entry 和 JS entry 差异 * HTML entry * 指的是子应用配置的资源地址是 HTML 的地址 * 指定子应用的 entry 地址为 HTML 地址,支持像 iframe 一样的能力,将对应的子应用渲染至当前应用中 * HTML entry 模式的作用设计的初衷,解决子应用:**独立开发**、**独立测试** 的能力 * JS entry * 指的是子应用配置的资源地址就是一个 JS 地址 * 二者在使用层面上的差异 * 在作为 `html entry` 时,子应用的挂载点需要基于传入的 `dom` 节点进行选中挂载点 * 因为在 `html entry` 时,其实类似于 `iframe` 的模式,子应用在独立运行时的所有 `dom` 结构都会被挂到主应用的文档流上(整个文档流会挂载在当前 html 上) * 所以子应用在渲染时需要根据子应用的 `dom` 结构去找他的挂载点。 - HTML entry 正确渲染销毁写法 ```js {6} export const provider = () => { return { render({ dom }) { ReactDOM.render( React.createElement(HotApp), dom.querySelector('#root'), // 基于 dom 去选中文档流中的 #root,就和在独立运行时使用 document.querySelector('#root') 一样 ); }, destroy({ dom }) { // 此外,destroy 应该正确的执行 const root = dom && dom.querySelector('#root'); if (root) { ReactDOM.unmountComponentAtNode(root); } }, }; }; ``` * JS entry 正确渲染销毁写法 ```js export const provider = ({ dom, basename }) => ({ render() { ReactDOM.render(, dom); // 作为 js entry 时,没有自己的文档流,只有提供的渲染节点 }, destroy({ dom }) { ReactDOM.unmountComponentAtNode(dom); // 没有自己的文档流,直接销毁 }, }); ``` ## garfish 支持多实例吗 支持。 目前 garfish 支持多实例场景,业务使用场景可分为 「非嵌套场景」 和 「嵌套场景」: * 非嵌套场景下 - 非嵌套场景下,子应用请勿在安装引入 Garfish 包,并导入使用。 - 子应用如果想要在微前端场景下使用 Garfish 包的相关能力,可判断在微前端环境内时,通过 `window.Garfish` 使用相关接口。 ```js if (window.__GARFISH__) { window.Garfish.xx; } ``` * 嵌套场景 - Garfish 目前内部的设计都支持嵌套场景,如果业务对这一块有诉求可以使用,协助我们一起推进在嵌套场景下的能力。 ## 子应用销毁后重定向逻辑影响其他子应用 可能原因,出现该问题的原因是子应用未正常销毁,当子应用未正常销毁时,其路由监听事件也未跟随子应用的销毁而销毁 > React 应用解决方案 * 需要保证渲染的节点和销毁的节点为同一个节点,否则导致 React 组件销毁不正常,[ReactDOM.unmountComponentAtNode API 使用说明](https://reactjs.org/docs/react-dom.html#unmountcomponentatnode) * 这里需要注意的是子应用的入口类型,如果子应用是构建为 js 入口时,则不存在 html 模板,可以直接将 dom 作为挂载点。但也需要保证渲染和销毁的为同一个节点 ```js export const provider = () => { return { render: ({ dom, basename }) => { const root = dom ? dom.querySelector('#root') : document.querySelector('#root'); ReactDOM.render( , root, ); }, destroy: ({ dom, basename }) =>{ const root = dom ? dom.querySelector('#root') : document.querySelector('#root'); ReactDOM.unmountComponentAtNode(root), }, }; }; ``` ## You are attempting to use a basename on a page whose URL path does not begin with the basename. > 问题原因 * 出现这个错误的原因一般是因为子应用没有正确的设置子应用的 basename 所导致的。 * 子应用的 `basename` = 主应用的 `basename` + 子应用设置的激活路径 `activeWhen`,这个值会在生命周期函数中由 garfish 默认通过通过参数传递过来,直接使用即可。 > 解决方案 * 将生命周期函数中主应用传递过来的 `basename` 设置为子应用的 `basename`。[参考](/guide/demo/react.md#3-根组件设置路由的-basename) ## 刷新直接返回子应用内容 > 问题原因 * 微前端是一个 SPA 应用,加载子应用是通过 SPA 模式来动态的加载其他子应用内容 * 当访问到主应用的某个路径下激活子应用时是不存在这个路径下的静态资源的,从而 failback 到主应用的内容 * Garfish 在初始化时根据当前路径来确定加载的子应用 * 如果在访问主应用的某个路径时来加载子应用,而这个地址已经存在一个静态资源,浏览器将会直接返回该资源 > 解决方案 * 子应用的资源地址不要和主应用上面激活路径的资源地址一致 ## 子应用的接口和资源路径不正确 尽可能将子应用的接口请求和资源路径调整为绝对路径 1. 子应用在独立运行时,使用相对路径的接口,接口请求的路径是,当前页面域名+相对路径 2. 但是在主应用时,子应用使用相对路径的接口,请求的路径按道理来说还是,当前域名+相对路径 当在微前端的场景下如果 Garfish 让子应用走「当前域名+相对路径」会发生更多的异常请求(hmr 热更新、websock、server worker ...),因为子应用的域名并不一定是与主应用一致,因此 Garfish 框架会对相对路径的资源和请求去进行修正,修正的参照物为基础域名为子应用的路径,在本地开发时可能是正常的,但是发到线上出现问题,原因在于发布到线上之后,Goofy web 为了提升子应用资源加载的性能,子应用的入口会走 CDN。因此参照的基础路径就变为了 CDN 前缀。那么此时子应用的相对路径请求就变为了 CDN 前缀。这一块做了很多权衡,因为 hmr、websock、server worker 这些内容可能难以被用户控制,所以默认走的还是修正模式。 ## 为什么主应用仅支持 history 模式? * 目前 Garfish 是通过命名空间去避免应用间的路由发生冲突的。 * 主应用仅支持 `history` 模式的原因在于,`hash` 路由无法作为子应用的基础路由,从而可能导致主应用和子应用发生路由冲突。 ## 根路由作为子应用的激活条件? * 有部分业务想将根路径作为子应用的激活条件,例如 `garfish.bytedance.com` 就触发子应用的渲染,由于目前子应用 **字符串的激活条件为最短匹配原则**,若子应用 `activeWhen: '/'` 表明 `'/xxx'` 都会激活。 * 之所以为最短匹配原则的原因在于,我们需要判断是否某个子应用的子路由被激活,如果可能是某个子应用的子路由,我们则可能激活该应用。 * 之所以有该限制是由于若某个子应用的激活条件为 `/`,则该应用的 `/xx` 都可能为改子应用的子路由,则可能与其他应用产生冲突,造成混乱。 ## 子应用拿到 basename 的作用? 为什么推荐子应用拿通过 `provider` 传递过来的 `basename` 作为子应用的 `basename`,有些业务方在实际过程中直接通过约定形式直接在子应用增加 `basename` 已到达隔离的效果,但该使用方式可能导致主应用如果变更 `basename` 可能导致子应用无法一起变更生效。 例如: 1. 当前主应用访问到 `garfish.bytedance.com` 即可访问到该站点的主页,当前 `basename` 为 `/`,子应用 vue,访问路径为 `garfish.bytedance.com/vue` 2. 如果主应用想更改 `basename` 为 `/site`,则主应用的访问路径变为`garfish.bytedance.com/site`,子应用 vue 的访问路径变为 `garfish.bytedance.com/site/vue` 3. 所以推荐子应用直接将 `provider` 中传递的 `basename` 作为自身应用的基础路由,以保证主应用在变更路由之后,子应用的相对路径还是符合整体变化 > 微前端场景下,每个子应用可能都有自己的路由场景,为保证子应用间路由不冲突,Garfish 框架将配置的 `basename` + `子应用的 activeWhen` 匹配的路径作为子应用的基路径。 * 若在 Garfish 上配置 `basename: /demo`,子应用的激活路径为:`/vue2`,则子应用得到的激活路径为:`/demo/vue2` * 若子应用的激活条件为函数,在每次发生路由变化时会通过校验子应用的激活函数若函数返回 `true` 表明符合当前激活条件将触发路由激活, * Garfish 会将当前的路径传入激活函数分割以得到子应用的最长激活路径,并将 `basename` + `子应用最长激活路径传` 给子应用参数 * **子应用如果本身具备路由,在微前端的场景下,必须把 basename 作为子应用的基础路径,没有基础路由,子应用的路由可能与主应用和其他应用发生冲突** ## 子应用使用 style-component 切换子应用后样式丢失 * 开启 Style-component 后在生产模式下 style 将会插入到 sheet 中([React Styled Components stripped out from production build](https://stackoverflow.com/questions/53486470/react-styled-components-stripped-out-from-production-build)) * 应用重渲染后 style 重新插入后依然,但是 sheet 未恢复 解决方案在使用 `style-component` 的子应用添加环境变量:`REACT_APP_SC_DISABLE_SPEEDY=true` ### arco-design 多版本样式冲突 1. [Arco-design 全局配置 ConfigProvider](https://arco.design/react/components/config-provider) 2. 给子应用分别设置不同的 `prefixCls` 前缀 ### ant-design 样式冲突 1. 配置 `webpack` 配置 ```js module.exports = { module: { rules: [ { test: /\.less$/i, use: [ { loader: 'style-loader' }, { loader: 'css-loader' }, { loader: 'less-loader', options: { modifyVars: { '@ant-prefix': 'define-prefix', // 定制自己的前缀 }, javascriptEnabled: true, }, }, ], }, ], }, }; ``` 2. 配置公共前缀:[antdesign-config](https://ant.design/components/config-provider/#API) ```js import { ConfigProvider } from 'antd'; export default () => ( ); ``` ## 子应用热更新问题 garfish 子应用热更新问题请参考 [博客](/blog/hmr.md) ## 如何独立运行子应用 通过 `window.__GARFISH__` 可判断当前子应用是否处于微前端下,通过此变量判断何时独立运行子应用: ```js // src/index.tsx import React from 'react'; import ReactDOM from 'react-dom'; import App from './components/App'; // 这能够让子应用独立运行起来,以保证后续子应用能脱离主应用独立运行,方便调试、开发 if (!window.__GARFISH__) { ReactDOM.render(, document.querySelector('#root')); } ``` ## 已有 `SPA` 应用如何改造为 garfish 子应用 ### 场景描述 * 很多需要改造成微前端的 `SPA` 应用,都是已经存在的旧应用。 * 可能需要逐步拆解应用内的部分路由,变为子应用。 * 主应用现有路由如何与微前端路由驱动共存,是迁移过程中常遇到的。 ### 如何逐步改造(以 `react` 为例) 1. 增加 `id` 为 `micro-app` 的挂载点,预留给子应用挂载,`Router` 部分的内容为主应用其他路由。 2. 主应用增加匹配到子应用路由前缀时,`Router` 内容为空。 3. 配置子应用列表时以 `Router` 内容为空时的前缀作为子应用激活条件前缀。 主应用的根组件: ```jsx
``` routes: ```js export default [ { path: '/platform/search', component: Search, }, { // 以 /platform/micro-app 开头的应用Router都不展示内容 path: '/platform/micro-app', component: function () { return null; }, }, { component: Home, }, ]; ``` 主入口处: ```js Garfish.run({ domGetter: '#micro-app', basename: '/platform/micro-app', apps: [ ... ], }); ``` ## 子应用动态插入到 body 上的节点逃逸? * 首先 garfish 会对每一个子应用创建一个 app container 用于包裹子应用,会创建 `__garfishmockhtml__`、 `__garfishmockbody__` 等 mock 节点。 * 对于在子应用运行过程中动态添加到 body 上的节点(如 drawer 组件),garfish 并未 将此类节点移动到 mock 的 `__garfishmockbody__` 中,原因是有些组件库会计算在 dom 层级中的位置,所以目前 garfish 会主动让其逃逸到上层。 * 在子应用运行过程中动态添加到 body 上的节点在子应用卸载时,garfish 并不会默认回收其 DOM 副作用,需要用户主动在组件的销毁回调里触发 dom 的回收,防止 DOM 副作用未销毁带来的影响。 ## 子应用 addEventListener 注册的事件监听在子应用卸载后并未销毁 * 若子应用默认开启了缓存模式,在子应用卸载时会保留应用的上下文,不会默认清除 addEventListener 注册的事件监听,这是因为再次渲染该子应用时 garfish 只会执行 render 函数,因此子应用的副作用不会随意被清除。 * 这种情况建议用户在组件的销毁函数里面手动释放组件的副作用,若有些逻辑确实需要清除,并且需要保证应用可用性可以将 cache 设置成 false。 ## garfish 缓存模式 * garfish 目前默认启用了缓存模式,在缓存模式下 garfish 会保留应用的上下文,且不会重新执行所有代码,只会执行 render 和 destory 函数,因此应用的性能将得到很大的提升。 * 在缓存模式下 garfish 只会隔离环境变量和样式,子应用卸载时会保留应用的上下文,不会默认清除子应用的副作用。若业务存在需要销毁的副作用,一般来说建议用户在组件的销毁函数里面手动释放组件的副作用,如果有些逻辑确实需要清除,并且需要保证应用可用性可以把 cache 设置成 false。 ## JS 错误上报 Script error 0 * 一般错误收集的工具都是通过: * `window.addEventListener('error', (...args) => { console.log(args) })` * `window.addEventListener('unhandledrejection', (...args) => { console.log(args) })` * 如果能打印出 error 对象,但是只能拿到类似 Script error 0. 这类信息。说明当前 js error 跨域了【由于浏览器跨域的限制,非同域下的脚本执行抛错,捕获异常的时候,不能拿到详细的异常信息,只能拿到类似 Script error 0. 这类信息】。通常跨域的异常信息会被忽略,不会上报。可以通过一下方法验证是否跨域(如果输出 Script error 0. 则为跨域) > 解决方案 由于浏览器跨域的限制,非同域下的脚本执行抛错,捕获异常的时候,不能拿到详细的异常信息,只能拿到类似 Script error 0. 这类信息。通常跨域的异常信息会被忽略,不会上报。解决方案: 所有 ``,设置该属性后对应的 script 内容将不会运行在 commonjs 环境,对应的环境变量也会正常的插入到子应用的 window 上 ## SyntaxError: Identifier 'exports' has already been declared > 问题概述 这个问题其实和上面那个 cdn 的问题,原因是一样的,由于 garfish 会注入一个 exports 变量,而子应用某个脚本(比如 vite 自己的热更引入的`react-refresh-runtime.development.js`)的代码也写了类似`const exports = {}`的代码,导致出现重复声明而报错。 > 解决方案 解决办法还是和上面加`no-entry`一样,不会注入 commonjs 相关的环境变量,但是,考虑到某些脚本可能是构建工具默认注入的,无法修改 script 标签,所以可以在 html 入口处加入以下配置代码来达到同样的效果(以 vite 的`react-refresh`为例): ```html vue sub app
``` ## ESModule Garfish 核心库默认支持 esModule,但是需要关掉 vm 沙箱或者为快照沙箱时,才能够使用。 ```js Garfish.run({ ... apps: [ { name: 'vue', activeWhen: '/vue', entry: 'http://localhost:8080', sandbox: { open: false, // snapshot: true, 或者只开启快照沙箱 }, }, ], }) ``` 如果需要在 vm 沙箱下开启 esModule 的能力,可以使用 `@garfish/es-module` 插件。 > `@garfish/es-module` 会在运行时分析子应用的源码做一层 esModule polyfill,但他会带来严重的首屏性能问题,如果你的项目不是很需要在 vm 沙箱下使用 esModule 就不应该使用此插件。 > 在短期的规划中,为了能在生产环境中使用,我们会尝试使用 wasm 来优化整个编译性能。在未来如果 [module-fragments](https://github.com/tc39/proposal-module-fragments) 提案成功进入标准并成熟后,我们也会尝试使用此方案,但这需要时间。 ```js import { GarfishEsModule } from '@garfish/es-module'; Garfish.run({ ... plugins: [GarfishEsModule()], }) ``` > 提示:当子项目使用 `vite` 开发时,你可以在开发模式下使用 esModule 模式,生产环境可以打包为原始的无 esModule 的模式。 ## 子应用堆栈信息丢失、sourcemap 行列信息错误 > 问题背景 微前端场景下,存在沙盒机制,基于 eval 和 new Function 的形式去实现沙箱机制,在手动执行代码的情况下,会产生堆栈丢失、sourcemap 还原错行等问题。 > 解决方案 可通过增加如下 webpack 配置解决: ```js // webpack.config.js const webpack = require('webpack'); config.plugins = [ new webpack.BannerPlugin({ banner: 'Micro front-end', }); ] ``` 具体原因可参考 [博客](/blog/sourcemap.md) --- url: /hello.md --- # Hello World! ## Start Write something to build your own docs! 🎁 --- url: /index.md --- --- url: /plugins/__meta__.md --- --- url: /plugins/es-module.md --- ## garfish es-module plugin --- url: /plugins/plugins.md --- ## Garfish 插件