--- nav: title: 常见问题 toc: menu --- # 常见问题 ## `Application died in status LOADING_SOURCE_CODE: You need to export the functional lifecycles in xxx entry` qiankun 抛出这个错误是因为无法从微应用的 entry js 中识别出其导出的生命周期钩子。 可以通过以下几个步骤解决这个问题: 1. 检查微应用是否已经导出相应的生命周期钩子,参考[文档](/zh/guide/getting-started#1-导出相应的生命周期钩子)。 2. 检查微应用的 webpack 是否增加了指定的配置,参考[文档](/zh/guide/getting-started#2-配置微应用的打包工具)。 3. 检查微应用的 webpack 是否配置了 `output.globalObject` 的值,如果有请确保其值为 `window`,或者移除该配置从而使用默认值。 4. 检查微应用的 `package.json` 中的 `name` 字段是否是微应用中唯一的。 5. 检查微应用的 entry html 中入口的 js 是不是最后一个加载的脚本。如果不是,需要移动顺序将其变成最后一个加载的 js,或者在 html 中将入口 js 手动标记为 `entry`,如: ```html {2} ``` 6. 如果开发环境可以,生产环境不行,检查微应用的 `index.html` 和 `entry js` 是否正常返回,比如说返回了 `404.html`。 7. 如果你正在使用 webpack5,请看[这个 issues](https://github.com/umijs/qiankun/issues/1092)。 8. 检查主应用和微应用是否使用了 AMD 或 CommonJS 模块化。检查方法:单独运行微应用和主应用,在控制台输入如下代码:`(typeof exports === 'object' && typeof module === 'object') || (typeof define === 'function' && define.amd) || typeof exports === 'object'`,如果返回 `true`,则说明是这种情况,主要有以下两个解决办法: - 解决办法1:修改微应用 `webpack` 的 `libraryTarget` 为 `'window'` 。 ```diff const packageName = require('./package.json').name; module.exports = { output: { library: `${packageName}-[name]`, - libraryTarget: 'umd', + libraryTarget: 'window', jsonpFunction: `webpackJsonp_${packageName}`, }, }; ``` - 解决办法2:微应用不打包成 umd ,直接在入口文件把生命周期函数挂载到 window 上,参考[非 webpack 构建的微应用](/zh/guide/tutorial#非-webpack-构建的微应用)。 9. 如果在上述步骤完成后仍有问题,通常说明是浏览器兼容性问题导致的。可以尝试 **将有问题的微应用的 webpack `output.library` 配置成跟主应用中注册的 `name` 字段一致**,如: 假如主应用配置是这样的: ```ts {4} // 主应用 registerMicroApps([ { name: 'brokenSubApp', entry: '//localhost:7100', container: '#yourContainer', activeRule: '/react', }, ]); ``` 将微应用的 `output.library` 改为跟主应用中注册的一致: ```js {4} module.exports = { output: { // 这里改成跟主应用中注册的一致 library: 'brokenSubApp', libraryTarget: 'umd', jsonpFunction: `webpackJsonp_${packageName}`, }, }; ``` ## `Application died in status NOT_MOUNTED: Target container with #container not existed after xxx mounted!` qiankun 抛出这个错误是因为微应用加载后容器 DOM 节点不存在了。可能的原因有: 1. 微应用的根 `id` 与其他 DOM 冲突。解决办法是:修改根 `id` 的查找范围。 `vue` 微应用: ```js function render(props = {}) { const { container } = props; instance = new Vue({ router, store, render: (h) => h(App), }).$mount(container ? container.querySelector('#app') : '#app'); } export async function mount(props) { render(props); } ``` `react` 微应用: ```js function render(props) { const { container } = props; ReactDOM.render(, container ? container.querySelector('#root') : document.querySelector('#root')); } export async function mount(props) { render(props); } export async function unmount(props) { const { container } = props; ReactDOM.unmountComponentAtNode(container ? container.querySelector('#root') : document.querySelector('#root')); } ``` 2. 微应用的某些 js 里面使用了 `document.write`,比如高德地图 1.x 版本,腾讯地图 2.x 版本。 如果是地图 js 导致的,先看看升级能否解决,比如说高德地图升级到 2.x 版本即可。 如果升级无法解决,建议将地图放到主应用加载,微应用也引入这个地图 js(独立运行时使用),但是给 ` ``` 如果是其他的情况,请不要使用 `document.write` 。 ## `Application died in status NOT_MOUNTED: Target container with #container not existed while xxx mounting!` 这个报错通常出现在主应用为 vue 时,容器写在了路由页面并且使用了路由过渡效果,一些特殊的过渡效果会导致微应用在 mounting 的过程中容器不存在,解决办法就是换成其他的过渡效果,或者去掉路由过渡。 ## `Application died in status NOT_MOUNTED: Target container with #container not existed while xxx loading!` 与上面的报错类似,这个报错是因为微应用加载时容器 DOM 不存在。一般是因为 `start` 函数调用时机不正确导致的,调整 `start` 函数调用时机即可。 如何判断容器 DOM 加载完成?vue 应用可以在 `mounted` 生命周期调用,react 应用可以在 `componentDidMount` 生命周期调用。 如果仍然报错,检查容器 DOM 是否放在了主应用的某个路由页面,请参考[如何在主应用的某个路由页面加载微应用](#如何在主应用的某个路由页面加载微应用)。 ## `[import-html-entry]: error occurs while excuting xxx script http://xxx.xxx.xxx/x.js` ![](https://user-images.githubusercontent.com/22413530/109919189-41563d00-7cf3-11eb-8328-711228389d63.png) 其中第一行只是 qiankun 通过 `console.error` 打印出来的一个辅助信息,目的是帮助用户更快的知道是哪个 js 报错了,并不是真的异常。真正的异常信息在第二行。 比如上图这样一个报错,指的是 qiankun 在执行子应用的 `http://localhost:9100/index.bundle.js` 时,这个 js 本身抛异常了。而具体的异常信息就是 `Uncaught TypeError: Cannot read property 'call' of undefined`. 子应用本身的异常,可以尝试通过以下步骤排查解决: 1. 根据具体的异常信息,检查报错的 js 是否有语法错误,比如少了分号、依赖了未初始化的变量等。 2. 是否依赖了主应用提供的全局变量,但实际主应用并未初始化。 3. 兼容性问题。子应用这个 js 本身在当前运行环境存在语法兼容性问题。 ## 如何在主应用的某个路由页面加载微应用 必须保证微应用加载时主应用这个路由页面也加载了。 `vue` + `vue-router` 技术栈的主应用: 1. 主应用注册这个路由时给 `path` 加一个 `*`,**注意:如果这个路由有其他子路由,需要另外注册一个路由,仍然使用这个组件即可**。 ```js const routes = [ { path: '/portal/*', name: 'portal', component: () => import('../views/Portal.vue'), }, ]; ``` 2. 微应用的 `activeRule` 需要包含主应用的这个路由 `path`。 ```js registerMicroApps([ { name: 'app1', entry: 'http://localhost:8080', container: '#container', activeRule: '/portal/app1', }, ]); ``` 3. 在 `Portal.vue` 这个组件的 `mounted` 周期调用 `start` 函数,**注意不要重复调用**。 ```js import { start } from 'qiankun'; export default { mounted() { if (!window.qiankunStarted) { window.qiankunStarted = true; start(); } }, }; ``` `react` + `react-router` 技术栈的主应用:只需要让微应用的 `activeRule` 包含主应用的这个路由即可。 `angular` + `angular-router` 技术栈的主应用,与 vue 项目类似: 1. 主应用给这个路由注册一个通配符的子路由,内容为空。 ```ts const routes: Routes = [ { path: 'portal', component: PortalComponent, children: [{ path: '**', component: EmptyComponent }], }, ]; ``` 2. 微应用的 `activeRule` 需要包含主应用的这个路由 `path`。 ```js registerMicroApps([ { name: 'app1', entry: 'http://localhost:8080', container: '#container', activeRule: '/portal/app1', }, ]); ``` 3. 在这个路由组件的 `ngAfterViewInit` 周期调用 `start` 函数,**注意不要重复调用**。 ```ts import { start } from 'qiankun'; export class PortalComponent implements AfterViewInit { ngAfterViewInit(): void { if (!window.qiankunStarted) { window.qiankunStarted = true; start(); } } } ``` ## Vue Router 报错 `Uncaught TypeError: Cannot redefine property: $router` qiankun 中的代码使用 Proxy 去代理父页面的 window,来实现的沙箱,在微应用中访问 `window.Vue` 时,会先在自己的 window 里查找有没有 `Vue` 属性,如果没有就去父应用里查找。 在 VueRouter 的代码里有这样三行代码,会在模块加载的时候就访问 `window.Vue` 这个变量,微应用中报这个错,一般是由于父应用中的 Vue 挂载到了父应用的 `window` 对象上了。 ```javascript if (inBrowser && window.Vue) { window.Vue.use(VueRouter); } ``` 可以从以下方式中选择一种来解决问题: 1. 在主应用中不使用 CDN 等 external 的方式来加载 `Vue` 框架,使用前端打包软件来加载模块 2. 在主应用中,将 `window.Vue` 变量改个名称,例如 `window.Vue2 = window.Vue; delete window.Vue` ## 为什么微应用加载的资源会 404? 原因是 webpack 加载资源时未使用正确的 `publicPath`。 可以通过以下两个方式解决这个问题: ### a. 使用 webpack 运行时 publicPath 配置 qiankun 将会在微应用 bootstrap 之前注入一个运行时的 publicPath 变量,你需要做的是在微应用的 entry js 的顶部添加如下代码: ```js __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__; ``` 关于运行时 publicPath 的技术细节,可以参考 [webpack 文档](https://webpack.js.org/guides/public-path/#on-the-fly)。 runtime publicPath 主要解决的是微应用动态载入的 脚本、样式、图片 等地址不正确的问题。 ### b. 使用 webpack 静态 publicPath 配置 你需要将你的 webpack `publicPath` 配置设置成一个绝对地址的 url,比如在开发环境可能是: ```js { output: { publicPath: `//localhost:${port}`, } } ``` ### 微应用打包之后 css 中的字体文件和图片加载 404 原因是 `qiankun` 将外链样式改成了内联样式,但是字体文件和背景图片的加载路径是相对路径。 而 `css` 文件一旦打包完成,就无法通过动态修改 `publicPath` 来修正其中的字体文件和背景图片的路径。 主要有以下几个解决方案: 1. 所有图片等静态资源上传至 `cdn`,`css` 中直接引用 `cdn` 地址(**推荐**) 2. 借助 `webpack` 的 `url-loader` 将字体文件和图片打包成 `base64`(适用于字体文件和图片体积小的项目)(**推荐**) ```js module.exports = { module: { rules: [ { test: /\.(png|jpe?g|gif|webp|woff2?|eot|ttf|otf)$/i, use: [ { loader: 'url-loader', options: {}, }, ], }, ], }, }; ``` `vue-cli3` 项目写法: ```js module.exports = { chainWebpack: (config) => { config.module.rule('fonts').use('url-loader').loader('url-loader').options({}).end(); config.module.rule('images').use('url-loader').loader('url-loader').options({}).end(); }, }; ``` `vue-cli5` 项目,使用 `asset/inline` 替代 `url-loader`,写法: ```js module.exports = { chainWebpack: (config) => { config.module.rule('fonts').type('asset/inline').set('generator', {}); config.module.rule('images').type('asset/inline').set('generator', {}); }, }; ``` 3. 借助 `webpack` 的 `file-loader` ,在打包时给其注入完整路径(适用于字体文件和图片体积比较大的项目) ```js const publicPath = process.env.NODE_ENV === 'production' ? 'https://qiankun.umijs.org/' : `http://localhost:${port}`; module.exports = { module: { rules: [ { test: /\.(png|jpe?g|gif|webp)$/i, use: [ { loader: 'file-loader', options: { name: 'img/[name].[hash:8].[ext]', publicPath, }, }, ], }, { test: /\.(woff2?|eot|ttf|otf)$/i, use: [ { loader: 'file-loader', options: { name: 'fonts/[name].[hash:8].[ext]', publicPath, }, }, ], }, ], }, }; ``` `vue-cli3` 项目写法: ```js const publicPath = process.env.NODE_ENV === 'production' ? 'https://qiankun.umijs.org/' : `http://localhost:${port}`; module.exports = { chainWebpack: (config) => { const fontRule = config.module.rule('fonts'); fontRule.uses.clear(); fontRule .use('file-loader') .loader('file-loader') .options({ name: 'fonts/[name].[hash:8].[ext]', publicPath, }) .end(); const imgRule = config.module.rule('images'); imgRule.uses.clear(); imgRule .use('file-loader') .loader('file-loader') .options({ name: 'img/[name].[hash:8].[ext]', publicPath, }) .end(); }, }; ``` 4. 将两种方案结合起来,小文件转 `base64` ,大文件注入路径前缀 ```js const publicPath = process.env.NODE_ENV === 'production' ? 'https://qiankun.umijs.org/' : `http://localhost:${port}`; module.exports = { module: { rules: [ { test: /\.(png|jpe?g|gif|webp)$/i, use: [ { loader: 'url-loader', options: {}, fallback: { loader: 'file-loader', options: { name: 'img/[name].[hash:8].[ext]', publicPath, }, }, }, ], }, { test: /\.(woff2?|eot|ttf|otf)$/i, use: [ { loader: 'url-loader', options: {}, fallback: { loader: 'file-loader', options: { name: 'fonts/[name].[hash:8].[ext]', publicPath, }, }, }, ], }, ], }, }; ``` `vue-cli3` 项目写法: ```js const publicPath = process.env.NODE_ENV === 'production' ? 'https://qiankun.umijs.org/' : `http://localhost:${port}`; module.exports = { chainWebpack: (config) => { config.module .rule('fonts') .use('url-loader') .loader('url-loader') .options({ limit: 4096, // 小于4kb将会被打包成 base64 fallback: { loader: 'file-loader', options: { name: 'fonts/[name].[hash:8].[ext]', publicPath, }, }, }) .end(); config.module .rule('images') .use('url-loader') .loader('url-loader') .options({ limit: 4096, // 小于4kb将会被打包成 base64 fallback: { loader: 'file-loader', options: { name: 'img/[name].[hash:8].[ext]', publicPath, }, }, }); }, }; ``` 5. `vue-cli3` 项目可以将 `css` 打包到 `js`里面,不单独生成文件(不推荐,仅适用于 `css` 较少的项目) 配置参考 [vue-cli3 官网](https://cli.vuejs.org/zh/config/#css-extract): ```js module.exports = { css: { extract: false, }, }; ``` ## 微应用静态资源一定要支持跨域吗? 是的。 由于 qiankun 是通过 fetch 去获取微应用的引入的静态资源的,所以必须要求这些静态资源支持[跨域](https://developer.mozilla.org/zh/docs/Web/HTTP/Access_control_CORS)。 如果是自己的脚本,可以通过开发服务端跨域来支持。如果是三方脚本且无法为其添加跨域头,可以将脚本拖到本地,由自己的服务器 serve 来支持跨域。 参考:[Nginx 跨域配置](https://segmentfault.com/a/1190000012550346) ## 如何解决由于运营商动态插入的脚本加载异常导致微应用加载失败的问题 运营商插入的脚本通常会用 async 标记从而避免 block 微应用的加载,这种通常没问题,如: ```html ``` 但如果有些插入的脚本不是被标记成 async 的,这类脚本一旦运行失败,将会导致整个应用被 block 且后续的脚本也不再执行。我们可以通过以下几个方式来解决这个问题: ### 使用自定义的 getTemplate 方法 通过自己实现的 getTemplate 方法过滤微应用 HTML 模板中的异常脚本 ```js import { start } from 'qiankun'; start({ getTemplate(tpl) { return tpl.replace('