diff --git a/website-new/.gitignore b/website-new/.gitignore new file mode 100644 index 000000000..044373fb2 --- /dev/null +++ b/website-new/.gitignore @@ -0,0 +1,25 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? +doc_build diff --git a/website-new/docs/_meta.json b/website-new/docs/_meta.json new file mode 100644 index 000000000..5dce1191c --- /dev/null +++ b/website-new/docs/_meta.json @@ -0,0 +1,22 @@ +[ + { + "text": "指南", + "link": "/guide/quick-start/", + "activeMatch": "/guide/" + }, + { + "text": "API", + "link": "/api/index", + "activeMatch": "/api/" + }, + { + "text": "博客", + "link": "/blog/architecture", + "activeMatch": "/blog/" + }, + { + "text": "常见问题", + "link": "/issues/index", + "activeMatch": "/issues/" + } +] diff --git a/website-new/docs/api/__meta__.md b/website-new/docs/api/__meta__.md new file mode 100644 index 000000000..6d7eff77b --- /dev/null +++ b/website-new/docs/api/__meta__.md @@ -0,0 +1,5 @@ +--- +title: API +collapsed: false +order: 4 +--- diff --git a/website-new/docs/api/_meta.json b/website-new/docs/api/_meta.json new file mode 100644 index 000000000..d1735c939 --- /dev/null +++ b/website-new/docs/api/_meta.json @@ -0,0 +1 @@ +["index", "run", "registerApp", "loadApp", "loader", "preloadApp", "channel", "router", "setOptioins", "setExternal", "setGlobal", "clearEscapeEffect"] diff --git a/website-new/docs/api/channel.mdx b/website-new/docs/api/channel.mdx new file mode 100644 index 000000000..124178ff0 --- /dev/null +++ b/website-new/docs/api/channel.mdx @@ -0,0 +1,40 @@ +# Garfish.channel + +import Highlight from '@components/Highlight'; + +用于应用间的通信,`Garfish.channel` 为 Garfish 的实例属性,该属性是 [EventEmitter2](https://github.com/EventEmitter2/EventEmitter2) 的实例。 + +### Type + +```ts +channel: EventEmitter2; +``` + +### 默认值 + +- 无 + +### 示例 + +```js +// 子应用监听登录事件 +const App = () => { + const handleLogin = (userInfo) => { + console.log(`${userInfo.name} has login`); + }; + + useEffect(() => { + window?.Garfish.channel.on('login', handleLogin); + return () => { + window?.Garfish.channel.removeListener('login', handleLogin); + }; + }); +}; + +// 主应用触发监听事件 +api.getLoginInfo().then((res) => { + if (res.code === 0) { + window.Garfish.channel.emit('login', res.data); + } +}); +``` diff --git a/website-new/docs/api/clearEscapeEffect.md b/website-new/docs/api/clearEscapeEffect.md new file mode 100644 index 000000000..539815972 --- /dev/null +++ b/website-new/docs/api/clearEscapeEffect.md @@ -0,0 +1,16 @@ +# Garfish.clearEscapeEffect + +用来清除逃逸沙箱的变量。 + +> 在微前端应用下,子应用将默认开启沙箱模式。在沙箱模式下,若发现有一些特殊的行为会逃逸沙箱系统,可以使用此方法来清除逃逸的变量; + +## Type +```ts +clearEscapeEffect: (key: string, value?: any) => void; +``` + +## 示例 + +```js +Garfish.clearEscapeEffect('webpackJsonp'); +``` diff --git a/website-new/docs/api/getGlobal.mdx b/website-new/docs/api/getGlobal.mdx new file mode 100644 index 000000000..fd135d1a7 --- /dev/null +++ b/website-new/docs/api/getGlobal.mdx @@ -0,0 +1,31 @@ +# Garfish.getGlobalObject + + +import Highlight from '@components/Highlight'; + +用于子应用获取真实 window 的值。 + +> 在微前端应用下,子应用将默认开启沙箱模式。在沙箱模式下,子应用中全局变量为被 proxy 的 'fakeWindow',而全局变量(真实 window)默认会被隔离。若子应用需求获取真实 window 的值,可以通过此方法获取。 + + + +:::tip +1. 一般情况下我们不建议直接通过此 API 获取真实 window,这样建议的原因是: + - 使用此 API 后子应用产生了一个无法独立运行的逻辑,导致子应用失去独立运行的能力; + - 由于环境变量的修改并不是单向数据流,造成主应用无法感知哪些子应用会去修改 window 上的哪些变量,可能造成数据管理的混乱; + +2. 若需要获取真实 window 上的环境变量,可通过 [`protectVariable`](/api/run#protectvariable) 属性,将需要共享的属性放入列表中即可通过子应用的全局变量获取,这样主应用能感知到哪些值是会被修改的,哪些值是不会被修改的,能在一定程度上控制 `window` 变量的修改; + +::: + +## Type +```ts +getGlobalObject: () => Window & typeof globalThis; +``` +## 示例 + +```js +import Garfish from 'garfish'; + +const nativeWindow = Garfish.getGlobalObject(); +``` diff --git a/website-new/docs/api/index.md b/website-new/docs/api/index.md new file mode 100644 index 000000000..b5ce0b4b8 --- /dev/null +++ b/website-new/docs/api/index.md @@ -0,0 +1,27 @@ +# 概览 + +```ts +import Garfish from "garfish"; +``` +在主应用中,我们通过 `import Garfish from "garfish";` 来引入 Garfish,并调用相关 Garfish api 去注册子应用或运行微前端应用。 + +其中,Garfish 是 `garfish` 包默认导出的实例,实例上包含微前端相关API,用户可以通过相应 API 完成对整个微前端应用的管理。 + + +:::tip +这里需要特殊说明的是,子应用不需要额外引入 Garfish 实例,子应用可通过 `window.Garfish` 获取全局 Garfish 实例信息,参考 [Garfish 环境变量](../guide/quickStart/env.md)。 + + +::: + + +## Garfish 实例方法 +- [Garfish.run](/api/run) (用于初始化应用参数、启动路由监听,当路由发生变化时自动激活应用或销毁应用) +- [Garfish.registerApp](/api/registerapp)(用于动态注册应用信息) +- [Garfish.loadApp](/api/loadapp)(可以手动控制子应用加载和销毁) +- [Garfish.router](/api/router)(提供路由跳转和路由守卫能力) +- [Garfish.channel](/api/channel)(提供应用间通信的能力) +- [Garfish.setExternal](/api/setexternal)(支持应用间的依赖共享) +- [Garfish.getGlobalObject](/api/getglobalobject)(用于获取真实 Window) +- [Garfish.setGlobalObject](/api/getglobalobject)(用于设置真实 Window 的值) +- [Garfish.clearEscapeEffect](/api/getglobalobject)(用于清除逃逸的副作用) diff --git a/website-new/docs/api/loadApp.mdx b/website-new/docs/api/loadApp.mdx new file mode 100644 index 000000000..f3904ef61 --- /dev/null +++ b/website-new/docs/api/loadApp.mdx @@ -0,0 +1,303 @@ +# Garfish.loadApp + +import Highlight from '@components/Highlight'; +import { Tab, Tabs } from 'rspress/theme'; + +用于手动挂载子应用,可动态控制子应用的渲染和销毁。 + +> Garfish 支持路由驱动式的应用挂载和销毁模式, +> 如果你的应用配置了 `activeWhen`, Garfish 则将自动监听路由变化并在路由命中时加载对应的子应用。这种模式属于路由驱动式的应用加载模式。如果你希望手动控制应用的加载和销毁,我们提供了 `Garfish.loadApp()` API 以供用户手动加载和销毁应用,这是一种更加灵活的子应用加载模式。 + + +:::info + +1. 基于路由匹配的应用加载模式会通过子应用的 `activeWhen` 参数在在路由变化后自动判断当前应加载的子应用; +2. 在手动加载模式下(Garfish.loadApp),Garfish 不会根据路径匹配而是完全由开发者控制应用加载和销毁,此时应用加载不会受到 `activeWhen` 参数的影响; + + +::: + +## 类型 + +```ts +loadApp(appName: string, options?: Omit): Promise; +``` + +## 默认值 + +- 无 + +## 示例 + + + + +```jsx +import React, { useState } from 'react'; +import Garfish from "garfish"; +import type { interfaces } from "garfish"; + +// 提供 VueApp 的 React 组件 +export default App = () => { + const [app, setApp] = useState(null); + const loadApplication = async () => { + if (!app) { + setLoadAsync(true); + const app = await Garfish.loadApp('vue-app', { + cache: true, + basename, + domGetter: '#container', + entry: 'http://localhost:8092', + }); + setApp(app); + setLoadAsync(false); + } else { + app.display ? app.hide() : app.show(); + } + } + + return ( + <> + +
+ {loadAsync && } +
+ + ) +} +``` +
+ + +```html + + + +``` + +- 优点 + - 不需要用户手动增加配置 +- 缺点 + - 子应用的节点会受到主应用的影响 + - 子应用独立运行表现和在主应用运行表现可能不一致 + - 需要配合节点的处理一起进行(组件库可能会创建弹窗到 `body` 下,需要将节点劫持添加到容器内) + +多实例下的样式隔离 +在多实例场景下,可能会存在多份不同版本的 UI 组件库,从而导致样式冲突,目前的一种解决方案是通过构建工具给所有的样式都加上 namespace,如 + +```css +#garfish_app1 { + ...css; +} +``` + +由于挂载的时候会把所有的节点都挂载在#garfish_app1 上,所以样式仍然能够生效。 + +```html +
+ +
+``` + +> Shadow DOM + +基于 `Web Components` 的 `Shadow DOM` 能力,将每个子应用包裹到一个 `Shadow DOM` 中,保证其运行时的样式的绝对隔离 `WebComponents Polyfill` + +`Shadow dom` 是实现 `Web Components` 的主要技术之一,另外两项分别为 `custom element`、`HTML templates`,在 `Shadow dom` 用简单概括为:将 `Dom` 文档树中的某个节点变为隔离节点,隔离节点内的子节点样式、行为将与外界隔离(隔离节点内的样式不会受到外部影响,也不会影响外部节点,在隔离节点内的事件最终都只会冒泡到隔离节点中) + +- Garfish 基于 ShadowDom 实现样式隔离 + + - 将容器节点变为 shadow dom + - 子应用节点操作转发到容器内,动态增加的样式和节点都会放置容器内 + - 查询节点操作转发到容器内 + - 事件向上传播,避免 `React` 依赖事件委托的库失效 + +- 优点 + - 浏览器基本的样式隔离 + - 支持主子应用样式隔离 + - 支持多实例 +- 缺点 + - 需要同时处理 DOM,将 DOM 放置容器内 + - 可能会导致部分组件库或基础库无法正常运行(不支持放置 ShadowDom 内) + +### 其他副作用 + +上面描述的其实只是对于变量的隔离,其实除了变量之外,还会有其他的副作用是需要隔离的,包括但不限于: + +- 计时器:`setInterval`、`setTimeout` +- 全局事件监听:`addEventListener` +- 全局存储:`localStorage`、`sessionStorage` + +解决方法主要分为两类,能够通过劫持收集的: + +1. 如 `setInterval`、`setTimeout`、`addEventListener`,通过重写这些方法,在调用的时候记录起来,放进一个队列里,在沙箱销毁的时候统一进行清除。 + +2. 持久化数据,无法通过劫持进行收集的,使用命名空间来区分 + 如 `localStorage`,`sessionStorage`,重写对象和方法 + +```javascript +// 代码 +localStorage.setItem('a', '1'); + +// 在SandboxA里执行实际效果 +localStorage.setItem('Garfish_A_a', '1'); + +// 逃逸的场景 +Garfish.getRawLocalStorage().setItem('a', '1'); +``` + +## 两种沙箱的对比 + +![image](https://user-images.githubusercontent.com/27547179/164144636-2d85409e-d011-43c8-929b-07eb287abf2f.png) +![image](https://user-images.githubusercontent.com/27547179/164144650-c2d26150-7779-4bb0-bfbb-3404615335b9.png) +![image](https://user-images.githubusercontent.com/27547179/164144658-3997cb63-20f2-4f8c-a4b4-199389d7f5be.png) diff --git a/website-new/docs/guide/demo/_meta.json b/website-new/docs/guide/demo/_meta.json new file mode 100644 index 000000000..461eda416 --- /dev/null +++ b/website-new/docs/guide/demo/_meta.json @@ -0,0 +1 @@ +["demo","react","vue", "vite","angular"] diff --git a/website-new/docs/guide/demo/angular.mdx b/website-new/docs/guide/demo/angular.mdx new file mode 100644 index 000000000..7874d3395 --- /dev/null +++ b/website-new/docs/guide/demo/angular.mdx @@ -0,0 +1,182 @@ +# 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", +``` +:::caution +1. 请注意,在 [packageName] > architect > build > options 的配置中,index 属性我们设置为空,这是因为在 angular 13 中编译产物默认会带上 esm 标识,即 type=module, 即使打包产物是 umd 格式,这会导致 garfish 加载子应用失败; +2. index 置空后,编译产物会去除 es module 标识,子应用加载正常; + +::: + + +### 3. 添加 webpack 配置文件 + +:::caution 【重要】注意: +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/childApp.md#子应用拿到-basename-的作用) +2. 我们强烈建议使用从主应用传递过来的 basename 作为子应用的 basename,而非主、子应用约定式,避免 basename 后期变更未同步带来的问题。 +3. 目前主应用仅支持 history 模式的子应用路由,[why](../../issues/childApp.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(); +} +``` diff --git a/website-new/docs/guide/demo/demo.md b/website-new/docs/guide/demo/demo.md new file mode 100644 index 000000000..3a096d5f3 --- /dev/null +++ b/website-new/docs/guide/demo/demo.md @@ -0,0 +1,16 @@ +# 概述 + +本节我们会详细讲述不同框架下的子应用如何接入 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) diff --git a/website-new/docs/guide/demo/react.mdx b/website-new/docs/guide/demo/react.mdx new file mode 100644 index 000000000..26a8f1aab --- /dev/null +++ b/website-new/docs/guide/demo/react.mdx @@ -0,0 +1,195 @@ +# react 子应用 + +import WebpackConfig from '@components/config/_webpackConfig.mdx'; + +本节我们将详细介绍 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/bridge) +### react v16、v17 导出 +import { Tab, Tabs } from 'rspress/theme'; + + + + +```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/childApp.md#子应用拿到-basename-的作用) +2. 我们强烈建议使用从主应用传递过来的 basename 作为子应用的 basename,而非主、子应用约定式,避免 basename 后期变更未同步带来的问题。 +3. 目前主应用仅支持 history 模式的子应用路由,[why](../../issues/childApp.md#为什么主应用仅支持-history-模式) +::: + +```tsx +// src/component/rootComponent +import React from "react"; +import { BrowserRouter } from "react-router-dom"; + +const RootComponent = ({ basename }) => { + return ( + + + }> + } /> + } /> + + + + ) +} +``` + +### 4. 更改 webpack 配置 + + + +### 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( + + ); +} +``` + + + diff --git a/website-new/docs/guide/demo/vite.mdx b/website-new/docs/guide/demo/vite.mdx new file mode 100644 index 000000000..c80ec765c --- /dev/null +++ b/website-new/docs/guide/demo/vite.mdx @@ -0,0 +1,127 @@ +# vite 子应用 + +import ViteConfig from '@components/config/_viteConfig.mdx'; + +本节我们将详细介绍 vite 框架的应用作为子应用的接入步骤。[demo](https://github.com/modern-js-dev/garfish/tree/main/dev/app-vue-vite) + +### 子应用沙箱状态 + +当 vite 应用作为子应用接入 garfish 时,我们要求子应用沙箱需关闭,否则应用将不能正常运行。 + +:::info 请注意: +1. 子应用沙箱默认为开启状态,请[设置子应用沙箱关闭](/guide/demo/vite#设置子应用沙箱关闭); +2. 在关闭沙箱的场景下,子应用的副作用将会发生逃逸,请确保子应用卸载后对应全局的副作用被清除; + +::: + +### 设置子应用沙箱关闭 + +```js +// 主应用工程中,Garfish.run 处设置: +Garfish.run({ + ..., + apps: [ + { + name: 'sub-app', + activeWhen: '/vite', + sandbox: false + } + ] +}) +``` + +:::caution +注意,不要设置 Garfish.run() 顶层的 sandbox 属性,这会导致所有子应用的沙箱关闭。 + +::: + +## vite 子应用接入步骤 + +### 1. bridge 依赖安装 + +:::tip +1. 请注意,桥接函数的安装不是必须的,你可以自定义导出函数。 +2. 我们提供桥接函数是为了进一步降低用户接入成本并降低用户出错概率,桥接函数中将会内置一些默认行为,可以可以避免由于接入不规范导致的错误,所以这也是我们推荐的接入方式。 + +::: + +```bash npm2yarn +npm install @garfish/bridge-vue-v3 --save +``` +### 2. 入口文件处导出 provider 函数 + +更多 bridge 函数参数介绍请参考 [这里](/guide/bridge) + +```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/childApp.md#子应用拿到-basename-的作用) +2. 我们强烈建议使用从主应用传递过来的 basename 作为子应用的 basename,而非主、子应用约定式,避免 basename 后期变更未同步带来的问题。 +3. 目前主应用仅支持 history 模式的子应用路由,[why](../../issues/childApp.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 配置 + + + + +### 5. 增加子应用独立运行兼容逻辑 + +:::tip +last but not least, 别忘了添加子应用独立运行逻辑,这能够让你的子应用脱离主应用独立运行,便于后续开发和部署。 + +::: + +```js +// src/main.js +if (!window.__GARFISH__) { + // 非微前端环境直接运行 + const vueInstance = createApp(App); + vueInstance.mount(document.querySelector('#app')); +} +``` diff --git a/website-new/docs/guide/demo/vue.mdx b/website-new/docs/guide/demo/vue.mdx new file mode 100644 index 000000000..12e32cce4 --- /dev/null +++ b/website-new/docs/guide/demo/vue.mdx @@ -0,0 +1,311 @@ +# vue 子应用 + +import WebpackConfig from '@components/config/_webpackConfig.mdx'; + +本节我们将详细介绍 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 包,目的是为了更好的类型提示及精简参数。 + +::: + + +import { Tab, Tabs } from 'rspress/theme'; + + + + + ```bash npm2yarn + npm install @garfish/bridge-vue-v2 --save + ``` + + + + + + ```bash npm2yarn + npm install @garfish/bridge-vue-v3 --save + ``` + + + +### 2. 入口文件处导出 provider 函数 + +更多 bridge 函数参数介绍请参考 [这里](/guide/bridge) +### 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/childApp.md#子应用拿到-basename-的作用) +2. 我们强烈建议使用从主应用传递过来的 basename 作为子应用的 basename,而非主、子应用约定式,避免 basename 后期变更未同步带来的问题。 +3. 目前主应用仅支持 history 模式的子应用路由,[why](../../issues/childApp.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 配置 + + + +### 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'); + } + ``` + + + diff --git a/website-new/docs/guide/quick-start/_meta.json b/website-new/docs/guide/quick-start/_meta.json new file mode 100644 index 000000000..73eecf9cb --- /dev/null +++ b/website-new/docs/guide/quick-start/_meta.json @@ -0,0 +1 @@ +["index","start","env"] diff --git a/website-new/docs/guide/quick-start/env.md b/website-new/docs/guide/quick-start/env.md new file mode 100644 index 000000000..3bdc67b60 --- /dev/null +++ b/website-new/docs/guide/quick-start/env.md @@ -0,0 +1,36 @@ +# 环境变量 + +有时候需要使用环境变量(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' }); +``` diff --git a/website-new/docs/guide/quick-start/index.md b/website-new/docs/guide/quick-start/index.md new file mode 100644 index 000000000..a31675f7c --- /dev/null +++ b/website-new/docs/guide/quick-start/index.md @@ -0,0 +1,56 @@ +# 介绍 + +微前端是一种类似于微服务的架构,是一种由独立交付的多个前端应用组成整体的架构风格,将前端应用分解成一些更小、更简单的能够独立开发、测试、部署的应用,而在用户看来仍然是内聚的单个产品。 + +它主要解决了两个问题: + +- 随着项目迭代应用越来越庞大,难以维护; +- 跨团队或跨部门协作开发项目导致效率低下的问题; + +## 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) 这篇文章中的详细介绍 + +## 什么时候用 + +如果你的团队成员多、项目类型多,并且想将其打造成「内聚的单个产品」: + +- 项目的团队成员来自多个团队 +- 项目内多条迭代出现需求挤兑,影响测试、发布效率 +- 跨空间、跨时间维度导致团队内技术体系无法统一 +- 多个前端应用需要达到「内聚的单个产品」特征 +- 「内聚的单个产品」中部分内容希望达到独立开发、独立发布、独立测试、独立灰度等能力 diff --git a/website-new/docs/guide/quick-start/start.md b/website-new/docs/guide/quick-start/start.md new file mode 100644 index 000000000..6df22b013 --- /dev/null +++ b/website-new/docs/guide/quick-start/start.md @@ -0,0 +1,200 @@ +# 快速开始 + +import WebpackConfig from '@components/config/_webpackConfig.mdx'; + +import ViteConfig from '@components/config/_viteConfig.mdx'; + +本节分别从主、子 应用视角出发,介绍如何通过 [Garfish API](/api) 来将应用接入 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/lifecycle). + +:::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.调整子应用的构建配置 + + + + + + + + + + + + + + +### 2.导出 provider 函数 + +> 针对子应用需要导出生命周期函数,我们提供了桥接函数 [`@garfish/bridge-react`](/guide/bridge) 自动包装应用的生命周期,使用`@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) 章节详细中介绍了各框架的子应用接入 Garfish 的 demo 案例及接入过程注意事项,目前提供了: + +- react (version 16, 17, 18) +- vue (version 2, 3) +- vite (version 2) +- angular (version 13) + +可移步 [接入指南](/guide/demo) 查看详细接入步骤。 + +## 总结 + +使用 Garfish API 搭建一套微前端主子应用的主要成本来自两方面 + +- 主应用的搭建 + - 注册子应用的基本信息 + - 使用 Garfish 在主应用上调度管理子应用 +- 子应用的改造 + - 增加对应的构建配置 + - 使用 `@garfish/bridge-react` 包提供的函数包装子应用后返回 `provider` 函数并导出 + - 子应用针对不同的框架类型,添加不同 `basename` 的设置方式 + - React 在根组件中获取 `basename` 将其传递至 `BrowserRouter` 的 `basename` 属性中 + - Vue 将 `basename` 传递至 `VueRouter` 的 `basename` 属性中 diff --git a/website-new/docs/hello.md b/website-new/docs/hello.md new file mode 100644 index 000000000..d08771599 --- /dev/null +++ b/website-new/docs/hello.md @@ -0,0 +1,5 @@ +# Hello World! + +## Start + +Write something to build your own docs! 🎁 diff --git a/website-new/docs/index.md b/website-new/docs/index.md new file mode 100644 index 000000000..df8bf41ae --- /dev/null +++ b/website-new/docs/index.md @@ -0,0 +1,28 @@ +--- +pageType: home + +hero: + name: Garfish + text: 微前端框架 + tagline: 可轻松将多个前端应用组合成内聚的单个产品 + actions: + - theme: brand + text: Quick Start + link: /guide/quick-start/index.html + - theme: alt + text: GitHub + link: https://github.com/web-infra-dev/rspress + image: + src: https://lf3-static.bytednsdoc.com/obj/eden-cn/dhozeh7vhpebvog/open-garfish/icons/icon.png + alt: Garfish Logo +features: + - title: 跨框架 + details: 支持 vue、react、angular 多种框架混合使用 + icon: 📦 + - title: API 最大简洁化 + details: 在实际应用中使用方式极大简洁化 + icon: 🎨 + - title: 路由驱动 + details: 支持配置路由激活信息可完成自动化挂载和销毁 + icon: 🌈 +--- diff --git a/website-new/docs/issues/_meta.json b/website-new/docs/issues/_meta.json new file mode 100644 index 000000000..a7d5eda49 --- /dev/null +++ b/website-new/docs/issues/_meta.json @@ -0,0 +1 @@ +["index"] diff --git a/website-new/docs/issues/index.mdx b/website-new/docs/issues/index.mdx new file mode 100644 index 000000000..b6f69dd95 --- /dev/null +++ b/website-new/docs/issues/index.mdx @@ -0,0 +1,523 @@ +# 常见问题 + +import WebpackConfig from '@components/config/\_webpackConfig.mdx'; + +## "provider" is "null". + +出现这个问题是因为 garfish 无法从子应用中正确获取到 `provider` 导出函数,可以先按照以下步骤自查: + +1. 检查子应用是否正确 export 了 provider 函数。[参考](/guide/start#2导出-provider-函数) +2. 检查子应用是否正确配置了 webpack 的 output 配置: + + + +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#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) 动态加载子应用。 + +## 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#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) + +## 如何独立运行子应用 + +通过 `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) diff --git a/website-new/docs/issues/webComponent.md b/website-new/docs/issues/webComponent.md new file mode 100644 index 000000000..4929ed2f1 --- /dev/null +++ b/website-new/docs/issues/webComponent.md @@ -0,0 +1,241 @@ +--- +title: 使用 Web component(beta) +slug: /guide/develop/webComponent +order: 1 +--- + +本节主要从主应用视角出发,通过 `Web Component` 概览性描微前端应用,Web Component 接入方式特点: + +- 便于与主应用框架结合使用,加载微前端应用像加载组件一样简单 +- 支持设置子应用 Loading 中间态、错误态占位内容 +- 可以与路由驱动模式一起使用 + +## 主应用 + +### 安装依赖 + +```bash npm2yarn +npm install garfish --save +``` + +### 入口处注册 Web Component + +```js +// index.js(主应用入口处) +import { defineCustomElements } from 'garfish'; + +// 定义 WebComponent Tag 为“micro-portal”,并指定 loading 时的内容 +defineCustomElements('micro-portal', { + loading: ({ isLoading, error, pastDelay }) => { + let loadingElement = document.createElement('div'); + // 渲染过程中发生异常,展示异常内容 + if (error) { + loadingElement.innerHTML = `load app get error: ${error.message}`; + return loadingElement; + } + // 渲染中,并且符合延迟时间(避免 loading 闪退) + if (pastDelay && isLoading) { + loadingElement.innerHTML = `loading`; + return loadingElement; + } + return null; + }, +}); +``` + +### 分配路由给微前端应用 + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + + + + +```jsx +import React from 'react'; +import { BrowserRouter, Route, Link, Switch } from 'react-router-dom'; + +function VueApp(basename) { + // name: 子应用名称 + // entry: 子应用入口资源地址,可以为 HTML、或 JS + // basename: 子应用路由的基础路径 + 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'); +``` + + + + +## 子应用 + +### 安装依赖 + +```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': '*', + }, + }, +}; +``` + + + diff --git a/website-new/docs/plugins/__meta__.md b/website-new/docs/plugins/__meta__.md new file mode 100644 index 000000000..bfb9a49c7 --- /dev/null +++ b/website-new/docs/plugins/__meta__.md @@ -0,0 +1,5 @@ +--- +title: garfish-plugins +collapsed: false +order: 1 +--- diff --git a/website-new/docs/plugins/es-module.md b/website-new/docs/plugins/es-module.md new file mode 100644 index 000000000..b29767638 --- /dev/null +++ b/website-new/docs/plugins/es-module.md @@ -0,0 +1,7 @@ +--- +title: Garfish es-module plugins +slug: /garfish-plugins/es-module.md +order: 1 +--- + +## garfish es-module plugin diff --git a/website-new/docs/plugins/plugins.md b/website-new/docs/plugins/plugins.md new file mode 100644 index 000000000..ed5ffe348 --- /dev/null +++ b/website-new/docs/plugins/plugins.md @@ -0,0 +1,7 @@ +--- +title: Garfish 插件 +slug: /garfish-plugins +order: 2 +--- + +## Garfish 插件 diff --git a/website-new/docs/public/rspress-dark-logo.png b/website-new/docs/public/rspress-dark-logo.png new file mode 100644 index 000000000..928bcc916 Binary files /dev/null and b/website-new/docs/public/rspress-dark-logo.png differ diff --git a/website-new/docs/public/rspress-icon.png b/website-new/docs/public/rspress-icon.png new file mode 100644 index 000000000..6be2af2f7 Binary files /dev/null and b/website-new/docs/public/rspress-icon.png differ diff --git a/website-new/docs/public/rspress-light-logo.png b/website-new/docs/public/rspress-light-logo.png new file mode 100644 index 000000000..1e3442eec Binary files /dev/null and b/website-new/docs/public/rspress-light-logo.png differ diff --git a/website-new/package.json b/website-new/package.json new file mode 100644 index 000000000..be99347bc --- /dev/null +++ b/website-new/package.json @@ -0,0 +1,16 @@ +{ + "name": "rspress-doc-template", + "version": "1.0.0", + "private": true, + "scripts": { + "dev": "rspress dev", + "build": "rspress build", + "preview": "rspress preview" + }, + "dependencies": { + "rspress": "^1.32.0" + }, + "devDependencies": { + "@types/node": "^16" + } +} diff --git a/website-new/rspress.config.ts b/website-new/rspress.config.ts new file mode 100644 index 000000000..13921a55f --- /dev/null +++ b/website-new/rspress.config.ts @@ -0,0 +1,31 @@ +import * as path from 'path'; +import { defineConfig } from 'rspress/config'; + +export default defineConfig({ + root: path.join(__dirname, 'docs'), + title: 'Garfish', + description: + '包含构建微前端系统时所需要的基本能力,任意前端框架均可使用。接入简单,可轻松将多个前端应用组合成内聚的单个产品', + icon: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/dhozeh7vhpebvog/open-garfish/icons/icon.png', + logo: { + light: + 'https://lf3-static.bytednsdoc.com/obj/eden-cn/dhozeh7vhpebvog/open-garfish/icons/garfish-icon.png', + dark: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/dhozeh7vhpebvog/open-garfish/icons/garfish-icon.png', + }, + themeConfig: { + socialLinks: [ + { + icon: 'github', + mode: 'link', + content: 'https://github.com/bytedance/garfish', + }, + ], + }, + builderConfig: { + source: { + alias: { + '@components': path.join(__dirname, 'src/components'), + }, + }, + }, +}); diff --git a/website-new/src/components/Highlight/index.js b/website-new/src/components/Highlight/index.js new file mode 100644 index 000000000..d3e4f8dd3 --- /dev/null +++ b/website-new/src/components/Highlight/index.js @@ -0,0 +1,16 @@ +import React from 'react'; + +export default function Highlight({ children, color }) { + return ( + + {children} + + ); +} diff --git a/website-new/src/components/TeamProfileCards/index.jsx b/website-new/src/components/TeamProfileCards/index.jsx new file mode 100644 index 000000000..d077671ec --- /dev/null +++ b/website-new/src/components/TeamProfileCards/index.jsx @@ -0,0 +1,76 @@ +import React from 'react'; +import Translate from '@docusaurus/Translate'; +import Link from '@docusaurus/Link'; +import { members } from '@site/src/data/team'; + +function WebsiteLink({to, children}) { + return ( + + {children || ( + website + )} + + ); +} + +function TeamProfileCard({ className, name, children, githubUrl, username, avatar }) { + return ( +
+
+
+
+ {username} +
+

{name}

+
+
+
+
{children}
+ {/*
+
+ {githubUrl && ( + + GitHub + + )} +
+
*/} +
+
+ ); +} + +function TeamProfileCardCol(props) { + return ( + + ); +} + +export function ActiveTeamRow() { + members.sort((pre, next)=>{ + return pre.username.charCodeAt(0) - next.username.charCodeAt(0); + }); + + return ( +
+ {members.map(member => { + const { name, username, description = '', ...rest } = member; + return ( + + + {description} + + + ); + })} +
+ ); +} diff --git a/website-new/src/components/config/_basename.mdx b/website-new/src/components/config/_basename.mdx new file mode 100644 index 000000000..294d351f5 --- /dev/null +++ b/website-new/src/components/config/_basename.mdx @@ -0,0 +1,8 @@ + + +- Type: string +- 子应用的基础路径,可选,默认值为全局 [basename](/api/run#basename); +- 通过路由驱动自动加载子应用时实际传递给子应用的 basename 为 `basename + activeWhen` 计算的值 +- 若手动载入渲染应用时 `basename` 为实际传入的值 +- 通过 [provider 函数](/guide/start#2导出-provider-函数) 的 `basename` 参数透传给子应用,子应用需要将 basename 设置为相应子应用的基础路由,这是必须的; +- [为什么子应用需要设置 basename ?](/issues#子应用拿到-basename-的作用) diff --git a/website-new/src/components/config/_domGetter.mdx b/website-new/src/components/config/_domGetter.mdx new file mode 100644 index 000000000..84d5a3687 --- /dev/null +++ b/website-new/src/components/config/_domGetter.mdx @@ -0,0 +1,16 @@ + + +- Type: interfaces.DomGetter + +```ts +export type DomGetter = + | string + | (() => Element | null) + | (() => Promise); +``` + +- 子应用的默认挂载点,可选,没有默认值,若省略需要在子应用 AppInfo 中单独指定。二者同时存在时,子应用指定优先级更高; +- 当提供 `string` 类型时需要其值是 `selector`, Garfish 内部会使用 `document.querySelector(domGetter)` 去选中子应用的挂载点 +- 当提供 `string` 类型的 `domGetter` 时,子应用在触发渲染后并不会若当前文档流上并不存在挂载点,`Garfish` 框架内部在 `3s` 内轮讯是否有挂载点 +- 当提供函数时,将在子应用挂载过程中执行此函数,并期望返回一个 dom 元素; +- 若 `domGetter` 在子应用渲染时无法查询到挂载点,则会丢出 `domGetter` 无效的异常 diff --git a/website-new/src/components/config/_insulationVariable.mdx b/website-new/src/components/config/_insulationVariable.mdx new file mode 100644 index 000000000..b1eb71c3a --- /dev/null +++ b/website-new/src/components/config/_insulationVariable.mdx @@ -0,0 +1,10 @@ + + +- Type: string[] +- 在开启沙箱的情况下,沙箱的子应用的环境变量将会从主应用中继承,举例: + - 在加载子应用前,主应用有一段:`window.xxxx = 123` + - 子应用中可以获取获取主应用的环境变量 `console.log(window.xxxx)`,输出 123 + - 因为目前 `Garfish` 的主子应用环境是隔离的,但是子应用的环境继承至主应用时可能会造成一些影响不符合预期 +- 若希望在子应用某些环境变量不继承至主应用,可以使用 `insulationVariable` 配置,例如: + - `insulationVariable: ['xxxx']` + - 'xxxx' 在子应用中将不会继承至主应用,`console.log(window.xxxx)` 输出 `undefined` diff --git a/website-new/src/components/config/_protectVariable.mdx b/website-new/src/components/config/_protectVariable.mdx new file mode 100644 index 000000000..cf2ad1796 --- /dev/null +++ b/website-new/src/components/config/_protectVariable.mdx @@ -0,0 +1,6 @@ + + +- Type: string[] +- 在开启沙箱的情况下,提供使得 window 上的某些变量处于受保护状态的能力:这些值的读写不会受到沙箱隔离机制的影响,所有应用均可读取到,可选; +- 若希望在应用间共享 window 上的某些值,可将该值放置在数组中; +- 该属性与 [setGlobalValue](/api/setGlobalObject) 功能相同,推荐使用 `protectVariable` 属性,通过 `protectVariable` 可以明确的感知哪些值可能在应用间相互影响; diff --git a/website-new/src/components/config/_sandbox.mdx b/website-new/src/components/config/_sandbox.mdx new file mode 100644 index 000000000..4dc4a99a6 --- /dev/null +++ b/website-new/src/components/config/_sandbox.mdx @@ -0,0 +1,75 @@ + + +- Type: SandboxConfig | false 可选,默认值为 [全局 sandbox 配置](/api/run#sandbox),当设置为 false 时关闭沙箱; + +- SandboxConfig: + +```ts +interface SandboxConfig { + // 是否开启快照沙箱,默认值为 false:关闭快照沙箱,开启 vm 沙箱 + snapshot?: boolean; + // 是否自动以子应用入口的域名前缀对子应用 fetch 请求的进行补齐,默认值为 false + fixBaseUrl?: boolean; + // 是否自动以子应用入口的域名前缀对相对路径资源进行前缀修正,默认值为 true,v1.15.0 版本提供 + fixStaticResourceBaseUrl?: boolean; + // 是否开启开启严格隔离,默认值为 false。开启严格隔离后,子应用的渲染节点将会开启 Shadow DOM close 模式,并且子应用的查询和添加行为仅会在 DOM 作用域内进行 + strictIsolation?: boolean; + // modules 仅在 vm 沙箱时有效,用于覆盖子应用执行上下文的环境变量,使用自定义的执行上下文,默认值为[] + modules?: Array | Record; + // disableElementtiming 1.14.4 版本提供,默认值为 false,将会给子应用元素注入 elementtiming 属性,可以通过此属性获取子应用元素的加载时间 + disableElementtiming?: boolean; + // fixOwnerDocument 1.17.2 版本提供 ,默认值 false,目前可能会存在 ownerDocument 逃逸的情况,设置为 true 之后将会避免 ownerDocument 逃逸 + fixOwnerDocument?: boolean; + // disableLinkTransformToStyle 1.18.0 版本提供 ,默认值 false,禁用掉 link 自动 transform 成 style 的行为 + disableLinkTransformToStyle?: boolean; + // excludeAssetFilter 1.18.0 版本提供,默认值为 undefined,用于过滤不需要再子应用沙箱中执行的资源例如 jsonp,url 参数为对应 script 的地址,返回 true 则会过滤掉该资源 + excludeAssetFilter?: (url: string) => boolean; +} + +type Module = (sandbox: Sandbox) => OverridesData | void; + +export interface OverridesData { + recover?: (context: Sandbox['global']) => void; + prepare?: () => void; + created?: (context: Sandbox['global']) => void; + override?: Record; +} +``` + +- 示例 + +```ts +Garfish.run({ + sandbox: { + snapshot: false, + strictIsolation: false, + // 覆盖子应用 localStorage,使用当前主应用 localStorage + modules: [ + () => ({ + override: { + localStorage: window.localStorage, + }, + }), + ], + }, +}); +``` + +:::caution +请注意: +如果你在沙箱内自定义的行为将会产生副作用,请确保在 recover 函数中清除你的副作用,garfish 将在应用卸载过程中执行 recover 函数销毁沙箱副作用,否则可能会造成内存泄漏。 +::: + +- 在什么情况下我应该关闭 sandbox ? + + > Garfish 目前已默认支持沙箱 esModule 能力,若需要在 vm 沙箱支持 esModule 应用,请使用 `@garfish/es-module` garfish 官方插件支持此能力,但这会带来严重的性能问题,[原因](/issues/#esmodule)。如果你的项目不是很需要在 vm 沙箱下运行,此时可以关闭沙箱; + +- [Garfish 沙箱机制](/guide/sandbox) + +:::info +若开启快照沙箱,请注意: + +1. 快照沙箱无法隔离主、子应用 +2. 快照沙箱无法支持多实例(同时加载多个子应用) + +::: diff --git a/website-new/src/components/config/_viteConfig.mdx b/website-new/src/components/config/_viteConfig.mdx new file mode 100644 index 000000000..b3be803a2 --- /dev/null +++ b/website-new/src/components/config/_viteConfig.mdx @@ -0,0 +1,20 @@ + +```js +// vite.config.js +export default defineConfig({ + base: 'http://localhost:3000/', + server: { + port: 3000, + cors: true, + origin: 'http://localhost:3000', + }, +}); +``` + +:::caution 【重要】注意: +1. base 提供资源绝对路径,避免相对路径带来的资源访问问题; +2. origin 提供资源绝对路径,避免相对路径带来的资源访问问题; +3. 需要将子应用沙箱关闭 `Garfish.run({ apps: [{ ..., sandbox: false }] })` +4. 子应用的副作用将会发生逃逸,在子应用卸载后需要将对应全局的副作用清除 + +::: diff --git a/website-new/src/components/config/_webpackConfig.mdx b/website-new/src/components/config/_webpackConfig.mdx new file mode 100644 index 000000000..c811320f3 --- /dev/null +++ b/website-new/src/components/config/_webpackConfig.mdx @@ -0,0 +1,46 @@ +```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': '*', + }, + }, +}; +``` + +:::caution 【重要】注意: + +1. libraryTarget 需要配置成 umd 规范; +2. globalObject 需要设置为 'window',以避免由于不规范的代码格式导致的逃逸沙箱; +3. 如果你的 webpack 为 v4 版本,需要设置 jsonpFunction 并保证该值唯一(否则可能出现 webpack chunk 互相影响的可能)。若为 webpack5 将会直接使用 package.json name 作为唯一值,请确保应用间的 name 各不相同; +4. publicPath 设置为子应用资源的绝对地址,避免由于子应用的相对资源导致资源变为了主应用上的相对资源。这是因为主、子应用处于同一个文档流中,相对路径是相对于主应用而言的 +5. 'Access-Control-Allow-Origin': '\*' 允许开发环境跨域,保证子应用的资源支持跨域。另外也需要保证在上线后子应用的资源在主应用的环境中加载不会存在跨域问题(**也需要限制范围注意安全问题**); + +::: diff --git a/website-new/src/components/lifeCycle/_afterEval.mdx b/website-new/src/components/lifeCycle/_afterEval.mdx new file mode 100644 index 000000000..03628c3e1 --- /dev/null +++ b/website-new/src/components/lifeCycle/_afterEval.mdx @@ -0,0 +1,21 @@ + + +- Type: (appInfo: AppInfo, code: string, env: `Record`, url: string, options) => void + - 该 `hook` 的参数分别为:`appInfo` 信息、`code` 执行的代码、`env` 要注入的环境变量,`url` 应用访问地址、`options` 参数选项例如 `async` 是否异步执行、`noEntry` 是否是 `noEntry` 模式; +- Kind: `sync`, `sequential` +- Previous Hook: `beforeLoad`、`afterLoad` +- Trigger: + + - 在实际执行代码后。`afterMount` 触发前触发; + - 子应用 html 内的 script 和动态创建的脚本执行时都会触发该 hook + +- 示例 + +```ts +Garfish.run({ + ..., + afterEval(appInfo) { + console.log('子应用代码执行完成', appInfo.name); + } +}) +``` diff --git a/website-new/src/components/lifeCycle/_afterLoad.mdx b/website-new/src/components/lifeCycle/_afterLoad.mdx new file mode 100644 index 000000000..5a2dc9401 --- /dev/null +++ b/website-new/src/components/lifeCycle/_afterLoad.mdx @@ -0,0 +1,19 @@ + + +- Type: async (appInfo: AppInfo, appInstance: interfaces.App) => void +- 该 `hook` 的参数分别为:应用信息、应用实例; +- Kind: `async`, `sequential` +- Trigger: + + - 在调用 `Garfish.load` 后并且子应用加载完成时触发该 `hook`; + +- 示例 + +```ts +Garfish.run({ + ..., + afterLoad(appInfo) { + console.log('子应用加载完成', appInfo.name); + } +}) +``` diff --git a/website-new/src/components/lifeCycle/_afterMount.mdx b/website-new/src/components/lifeCycle/_afterMount.mdx new file mode 100644 index 000000000..575774839 --- /dev/null +++ b/website-new/src/components/lifeCycle/_afterMount.mdx @@ -0,0 +1,22 @@ + + +- Type: (appInfo: AppInfo, appInstance: interfaces.App, cacheMode: boolean) => void + - 该 `hook` 的参数分别为:`appInfo` 信息、`appInstance` 应用实例、是否为 `缓存模式` 渲染和销毁 +- Kind: `sync`, `sequential` +- Previous Hook: `beforeLoad`、`afterLoad`、`beforeMount` +- Trigger: + + - 此时子应用 DOM 树已渲染完成,garfish 实例 `activeApps` 中已添加当前子应用 app 实例; + - 在挂载过程中,会调用应用生命周期中的 [`render` 函数](/guide/start#2导出-provider-函数),用户可在挂载前定义相关操作; + - 若挂载过程中出现异常,会触发 [errorMountApp](/api/run#errormountapp),同时会清除已创建的 app 渲染容器 appContainer + +- 示例 + +```ts +Garfish.run({ + ..., + afterMount(appInfo) { + console.log('子应用渲染结束', appInfo.name); + } +}) +``` diff --git a/website-new/src/components/lifeCycle/_afterUnmount.mdx b/website-new/src/components/lifeCycle/_afterUnmount.mdx new file mode 100644 index 000000000..01e0109a1 --- /dev/null +++ b/website-new/src/components/lifeCycle/_afterUnmount.mdx @@ -0,0 +1,8 @@ + + +- Type: ( appInfo: AppInfo, appInstance: interfaces.App) => void +- Kind: `sync`, `sequential` +- Trigger: + - 此时,应用在渲和运行过程中产生的副作用已清除,DOM 已卸载,沙箱副作用已清除,garfish 实例 `activeApps` 当前 app 已移除; + - 在应用销毁过程中会调用应用生命周期中的 [`destory` 函数](/guide/start#2导出-provider-函数),用户可在销毁前定义相关操作; + - 若应用卸载过程中出现异常,会触发 [errorUnmountApp](/api/run#errorUnmountApp) diff --git a/website-new/src/components/lifeCycle/_beforeEval.mdx b/website-new/src/components/lifeCycle/_beforeEval.mdx new file mode 100644 index 000000000..1bf1fb9aa --- /dev/null +++ b/website-new/src/components/lifeCycle/_beforeEval.mdx @@ -0,0 +1,23 @@ + + +- Type: (appInfo: AppInfo, code: string, env: `Record`, url: string, options) => void + - 该 `hook` 的参数分别为:`appInfo` 信息、`code` 执行的代码、`env` 要注入的环境变量,`url` 代码的资源地址、`options` 参数选项(例如 `async` 是否异步执行、`noEntry` 是否是 `noEntry` 模式); +- Kind: `sync`, `sequential` +- Previous Hook: `beforeMount` +- Trigger: + + - 在子应用挂载过程中、实际执行代码前触发该 hook; + - 应用 html 内的 script 和动态创建的脚本执行时都会触发该 hook + - 此时 DOM 树已添加至文档流中,子应用代码准备执行; + - 若代码执行过程中抛出异常,则将触发 [errorMountApp](/api/run#errormountapp),否则触发 [beforeEval](/api/run#afterEval) + +- 示例 + +```ts +Garfish.run({ + ..., + beforeEval(appInfo) { + console.log('子应用代码开始执行', appInfo.name); + } +}) +``` diff --git a/website-new/src/components/lifeCycle/_beforeLoad.mdx b/website-new/src/components/lifeCycle/_beforeLoad.mdx new file mode 100644 index 000000000..36dab9d08 --- /dev/null +++ b/website-new/src/components/lifeCycle/_beforeLoad.mdx @@ -0,0 +1,20 @@ + + +- Type: async (appInfo: AppInfo, appInstance: App) => false | undefined + - 该 `hook` 的参数分别为:应用信息、应用实例; + - 当返回 `false` 时将中断子应用的加载及后续流程; +- Kind: `async`, `sequential` +- Trigger: + + - 在调用 `Garfish.load` 时触发该 `hook` + - 子应用加载前触发,此时还未开始加载子应用资源; + +- 示例 + +```ts +Garfish.run({ + ..., + beforeLoad(appInfo) { + console.log('子应用开始加载', appInfo.name); + } +``` diff --git a/website-new/src/components/lifeCycle/_beforeMount.mdx b/website-new/src/components/lifeCycle/_beforeMount.mdx new file mode 100644 index 000000000..f98521f37 --- /dev/null +++ b/website-new/src/components/lifeCycle/_beforeMount.mdx @@ -0,0 +1,23 @@ + + +- Type: (appInfo: AppInfo, appInstance: interfaces.App, cacheMode: boolean) => void + - 该 `hook` 的参数分别为:`appInfo` 信息、`appInstance` 应用实例、是否为 `缓存模式` 渲染和销毁 +- Kind: `sync`, `sequential` +- Previous Hook: `beforeEval`、`afterEval` +- Trigger: + + - 此时子应用资源准备完成,运行时环境初始化完成,准备开始渲染子应用 DOM 树; + - 在调用 `app.mount` 或 `app.show` 触发该 `hook`,用户除了手动调用这两个方法外,`Garfish Router` 托管模式还会自动触发 + - 在使用 `app.mount` 渲染应用是 `cacheMode` 为 `false`; + - 在使用 `app.show` 渲染应用是 `cacheMode` 为 `true`; + +- 示例 + +```ts +Garfish.run({ + ..., + beforeMount(appInfo) { + console.log('子应用开始渲染', appInfo.name); + } +}) +``` diff --git a/website-new/src/components/lifeCycle/_beforeUnmount.mdx b/website-new/src/components/lifeCycle/_beforeUnmount.mdx new file mode 100644 index 000000000..e7c210dea --- /dev/null +++ b/website-new/src/components/lifeCycle/_beforeUnmount.mdx @@ -0,0 +1,11 @@ + + +- Type: ( appInfo: AppInfo, appInstance: interfaces.App) => void +- Kind: `sync`, `sequential` +- Previous Hook: `beforeLoad`、`afterLoad`、`beforeMount`、`afterMount` +- Trigger: + - 在调用 `app.unmount` 或 `app.hide` 触发该 `hook`,用户除了手动调用这两个方法外,`Garfish Router` 托管模式还会自动触发 + - 在使用 `app.unmount` 渲染应用是 `cacheMode` 为 `false`; + - 在使用 `app.hide` 渲染应用是 `cacheMode` 为 `true`; + - 此时子应用 DOM 元素还未卸载,副作用尚未清除; + - 此时子应用 DOM 树已渲染完成,garfish 实例 `activeApps` 中已添加当前子应用 app 实例; diff --git a/website-new/src/components/lifeCycle/_errorLoadApp.mdx b/website-new/src/components/lifeCycle/_errorLoadApp.mdx new file mode 100644 index 000000000..e836a23e7 --- /dev/null +++ b/website-new/src/components/lifeCycle/_errorLoadApp.mdx @@ -0,0 +1,21 @@ + + +- Type: (error: Error, appInfo: AppInfo, appInstance: interfaces.App) => void + - 该 `hook` 的参数分别为:`error` 实例、 `appInfo` 信息、`appInstance` 应用实例 + - 一旦设置该 hook,子应用加载错误不会 throw 到文档流中,全局错误监听将无法捕获到; +- Kind: `sync`, `sequential` +- Trigger: + + - 在调用 `Garfish.load` 过程中,并且加载失败时触发该 `hook` + +- 示例 + +```ts +Garfish.run({ + ..., + errorLoadApp(error, appInfo) { + console.log('子应用加载异常', appInfo.name); + console.error(error); + } +}) +``` diff --git a/website-new/src/components/lifeCycle/_errorMountApp.mdx b/website-new/src/components/lifeCycle/_errorMountApp.mdx new file mode 100644 index 000000000..1ca584ecc --- /dev/null +++ b/website-new/src/components/lifeCycle/_errorMountApp.mdx @@ -0,0 +1,21 @@ + + +- Type: (error: Error, appInfo: AppInfo, appInstance: interfaces.App) => void + - 一旦设置该 hook,子应用加载错误不会 throw 到文档流中,全局错误监听将无法捕获到; +- Kind: `sync`, `sequential` +- Previous Hook: `beforeLoad`、`afterLoad`、`beforeMount`、`afterMount` +- Trigger: + + - 在渲染过程中出现异常会触发该 `hook`,子应用同步执行的代码出现异常会触发该 `hook`,异步代码无法触发 + +- 示例 + +```ts +Garfish.run({ + ..., + errorMountApp(error, appInfo) { + console.log('子应用渲染异常', appInfo.name); + console.error(error); + } +}) +``` diff --git a/website-new/src/components/lifeCycle/_errorUnmountApp.mdx b/website-new/src/components/lifeCycle/_errorUnmountApp.mdx new file mode 100644 index 000000000..6872de60c --- /dev/null +++ b/website-new/src/components/lifeCycle/_errorUnmountApp.mdx @@ -0,0 +1,20 @@ + + +- Type: (error: Error, appInfo: AppInfo, appInstance: interfaces.App)=> void + - 一旦设置该 hook,子应用销毁错误不会向上 throw 到文档流中,全局错误监听将无法捕获到; +- Kind: `sync`, `sequential` +- Trigger: + + - 在 `app.unmount` 或 `app.hide` 销毁过程中出现异常则会触发该 `hook`,用户除了手动调用这两个方法外,`Garfish Router` 托管模式还会自动触发 + +- 示例 + +```ts +Garfish.run({ + ..., + errorUnmountApp(error, appInfo) { + console.log('子应用销毁异常', appInfo.name); + console.error(error); + } +}) +``` diff --git a/website-new/src/components/lifeCycle/_onNotMatchRouter.mdx b/website-new/src/components/lifeCycle/_onNotMatchRouter.mdx new file mode 100644 index 000000000..fcee307ff --- /dev/null +++ b/website-new/src/components/lifeCycle/_onNotMatchRouter.mdx @@ -0,0 +1,19 @@ + + +- Type: (path: string)=> void + - 该 `hook` 的参数分别为:应用信息、应用实例; +- Kind: `sync`, `sequential` +- Trigger: + + - 路由发生变化当前未激活子应用且未匹配到任何子应用时触发 + +- 示例 + +```ts +Garfish.run({ + ..., + onNotMatchRouter(path) { + console.log('未匹配到子应用', path); + } +}) +``` diff --git a/website-new/src/components/sumarryImg/index.jsx b/website-new/src/components/sumarryImg/index.jsx new file mode 100644 index 000000000..543eb3df0 --- /dev/null +++ b/website-new/src/components/sumarryImg/index.jsx @@ -0,0 +1,12 @@ +import React from 'react'; + + +export function summaryImg (url) { + return ( +
+ + + +
+ ) +} diff --git a/website-new/tsconfig.json b/website-new/tsconfig.json new file mode 100644 index 000000000..936218cee --- /dev/null +++ b/website-new/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2020", + "lib": ["DOM", "ES2020"], + "module": "ESNext", + "jsx": "react-jsx", + "noEmit": true, + "strict": true, + "skipLibCheck": true, + "isolatedModules": true, + "resolveJsonModule": true, + "moduleResolution": "bundler", + "useDefineForClassFields": true, + "allowImportingTsExtensions": true + }, + "include": ["docs", "theme", "rspress.config.ts"], + "mdx": { + "checkMdx": true + } +}