Application died in status LOADING_SOURCE_CODE: You need to export the functional lifecycles in xxx entry
qiankun 抛出这个错误是因为无法从微应用的 entry js 中识别出其导出的生命周期钩子。
可以通过以下几个步骤解决这个问题:
检查微应用是否已经导出相应的生命周期钩子,参考文档。
检查微应用的 webpack 是否增加了指定的配置,参考文档。
检查微应用的 webpack 是否配置了 output.globalObject
的值,如果有请确保其值为 window
,或者移除该配置从而使用默认值。
检查微应用的 package.json
中的 name
字段是否是微应用中唯一的。
检查微应用的 entry html 中入口的 js 是不是最后一个加载的脚本。如果不是,需要移动顺序将其变成最后一个加载的 js,或者在 html 中将入口 js 手动标记为 entry
,如:
<script src="/antd.js"></script><script src="/appEntry.js" entry></script><script src="https://www.google.com/analytics.js"></script>
如果开发环境可以,生产环境不行,检查微应用的 index.html
和 entry js
是否正常返回,比如说返回了 404.html
。
如果你正在使用 webpack5,但没用使用模块联邦,请看这个 issues。
如果你正在使用 webpack5,并且使用了使用模块联邦。需要在 index 文件中暴露生命周期函数,然后在 bootstrap 文件向外暴露生命周期函数。
const promise = import("index");export const bootstrap = () => promise.then(m => m.boostrap());export const mount = () => promise.then(m => m.mount());export const unmount = () => promise.then(m => m.unmount());
检查主应用和微应用是否使用了 AMD 或 CommonJS 模块化。检查方法:单独运行微应用和主应用,在控制台输入如下代码:(typeof exports === 'object' && typeof module === 'object') || (typeof define === 'function' && define.amd) || typeof exports === 'object'
,如果返回 true
,则说明是这种情况,主要有以下两个解决办法:
webpack
的 libraryTarget
为 'window'
。const packageName = require('./package.json').name;module.exports = {output: {library: `${packageName}-[name]`,- libraryTarget: 'umd',+ libraryTarget: 'window',jsonpFunction: `webpackJsonp_${packageName}`,},};
如果在上述步骤完成后仍有问题,通常说明是浏览器兼容性问题导致的。可以尝试 将有问题的微应用的 webpack output.library
配置成跟主应用中注册的 name
字段一致,如:
假如主应用配置是这样的:
// 主应用registerMicroApps([{name: 'brokenSubApp',entry: '//localhost:7100',container: '#yourContainer',activeRule: '/react',},]);
将微应用的 output.library
改为跟主应用中注册的一致:
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 节点不存在了。可能的原因有:
微应用的根 id
与其他 DOM 冲突。解决办法是:修改根 id
的查找范围。
vue
微应用:
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
微应用:
function render(props) {const { container } = props;ReactDOM.render(<App />, 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'));}
微应用的某些 js 里面使用了 document.write
,比如高德地图 1.x 版本,腾讯地图 2.x 版本。
如果是地图 js 导致的,先看看升级能否解决,比如说高德地图升级到 2.x 版本即可。
如果升级无法解决,建议将地图放到主应用加载,微应用也引入这个地图 js(独立运行时使用),但是给 <script>
标签加上 ignore
属性:
<script src="https://map.qq.com/api/gljs?v=1.exp" ignore></script>
如果是其他的情况,请不要使用 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
其中第一行只是 qiankun 通过 console.error
打印出来的一个辅助信息,目的是帮助用户更快的知道是哪个 js 报错了,并不是真的异常。真正的异常信息在第二行。
比如上图这样一个报错,指的是 qiankun 在执行子应用的 http://localhost:9100/index.bundle.js
时,这个 js 本身抛异常了。而具体的异常信息就是 Uncaught TypeError: Cannot read property 'call' of undefined
.
子应用本身的异常,可以尝试通过以下步骤排查解决:
必须保证微应用加载时主应用这个路由页面也加载了。
vue
+ vue-router
技术栈的主应用:
path
加一个 *
,注意:如果这个路由有其他子路由,需要另外注册一个路由,仍然使用这个组件即可。const routes = [{path: '/portal/*',name: 'portal',component: () => import('../views/Portal.vue'),},];
activeRule
需要包含主应用的这个路由 path
。registerMicroApps([{name: 'app1',entry: 'http://localhost:8080',container: '#container',activeRule: '/portal/app1',},]);
Portal.vue
这个组件的 mounted
周期调用 start
函数,注意不要重复调用。import { start } from 'qiankun';export default {mounted() {if (!window.qiankunStarted) {window.qiankunStarted = true;start();}},};
react
+ react-router
技术栈的主应用:只需要让微应用的 activeRule
包含主应用的这个路由即可。
angular
+ angular-router
技术栈的主应用,与 vue 项目类似:
主应用给这个路由注册一个通配符的子路由,内容为空。
const routes: Routes = [{path: 'portal',component: PortalComponent,children: [{ path: '**', component: EmptyComponent }],},];
微应用的 activeRule
需要包含主应用的这个路由 path
。
registerMicroApps([{name: 'app1',entry: 'http://localhost:8080',container: '#container',activeRule: '/portal/app1',},]);
在这个路由组件的 ngAfterViewInit
周期调用 start
函数,注意不要重复调用。
import { start } from 'qiankun';export class PortalComponent implements AfterViewInit {ngAfterViewInit(): void {if (!window.qiankunStarted) {window.qiankunStarted = true;start();}}}
Uncaught TypeError: Cannot redefine property: $router
qiankun 中的代码使用 Proxy 去代理父页面的 window,来实现的沙箱,在微应用中访问 window.Vue
时,会先在自己的 window 里查找有没有 Vue
属性,如果没有就去父应用里查找。
在 VueRouter 的代码里有这样三行代码,会在模块加载的时候就访问 window.Vue
这个变量,微应用中报这个错,一般是由于父应用中的 Vue 挂载到了父应用的 window
对象上了。
if (inBrowser && window.Vue) {window.Vue.use(VueRouter);}
可以从以下方式中选择一种来解决问题:
Vue
框架,使用前端打包软件来加载模块window.Vue
变量改个名称,例如 window.Vue2 = window.Vue; delete window.Vue
原因是 webpack 加载资源时未使用正确的 publicPath
。
可以通过以下两个方式解决这个问题:
qiankun 将会在微应用 bootstrap 之前注入一个运行时的 publicPath 变量,你需要做的是在微应用的 entry js 的顶部添加如下代码:
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
关于运行时 publicPath 的技术细节,可以参考 webpack 文档。
你需要将你的 webpack publicPath
配置设置成一个绝对地址的 url,比如在开发环境可能是:
{output: {publicPath: `//localhost:${port}`,}}
原因是 qiankun
将外链样式改成了内联样式,但是字体文件和背景图片的加载路径是相对路径。
而 css
文件一旦打包完成,就无法通过动态修改 publicPath
来修正其中的字体文件和背景图片的路径。
主要有以下几个解决方案:
所有图片等静态资源上传至 cdn
,css
中直接引用 cdn
地址(推荐)
借助 webpack
的 url-loader
将字体文件和图片打包成 base64
(适用于字体文件和图片体积小的项目)(推荐)
module.exports = {module: {rules: [{test: /\.(png|jpe?g|gif|webp|woff2?|eot|ttf|otf)$/i,use: [{loader: 'url-loader',options: {},},],},],},};
vue-cli3
项目写法:
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
,写法:
module.exports = {chainWebpack: (config) => {config.module.rule('fonts').type('asset/inline').set('generator', {});config.module.rule('images').type('asset/inline').set('generator', {});},};
webpack
的 file-loader
,在打包时给其注入完整路径(适用于字体文件和图片体积比较大的项目)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
项目写法:
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();},};
base64
,大文件注入路径前缀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
项目写法:
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将会被打包成 base64fallback: {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将会被打包成 base64fallback: {loader: 'file-loader',options: {name: 'img/[name].[hash:8].[ext]',publicPath,},},});},};
vue-cli3
项目可以将 css
打包到 js
里面,不单独生成文件(不推荐,仅适用于 css
较少的项目)配置参考 vue-cli3 官网:
module.exports = {css: {extract: false,},};
是的。
由于 qiankun 是通过 fetch 去获取微应用的引入的静态资源的,所以必须要求这些静态资源支持跨域。
如果是自己的脚本,可以通过开发服务端跨域来支持。如果是三方脚本且无法为其添加跨域头,可以将脚本拖到本地,由自己的服务器 serve 来支持跨域。
参考:Nginx 跨域配置
运营商插入的脚本通常会用 async 标记从而避免 block 微应用的加载,这种通常没问题,如:
<script async src="//www.rogue.com/rogue.js"></script>
但如果有些插入的脚本不是被标记成 async 的,这类脚本一旦运行失败,将会导致整个应用被 block 且后续的脚本也不再执行。我们可以通过以下几个方式来解决这个问题:
通过自己实现的 getTemplate 方法过滤微应用 HTML 模板中的异常脚本
import { start } from 'qiankun';start({getTemplate(tpl) {return tpl.replace('<script src="/to-be-replaced.js"><script>', '');},});
通过自己实现的 fetch 方法拦截有问题的脚本
import { start } from 'qiankun';start({async fetch(url, ...args) {if (url === 'http://to-be-replaced.js') {return {async text() {return '';},};}return window.fetch(url, ...args);},});
原理是运营商只能识别 response content-type 为 text/html 的请求并插入脚本,text/plain 类型的响应则不会被劫持。
修改微应用 HTML 的 content-type 方法可以自行 google,也有一个更简单高效的方案:
微应用发布时从 index.html 复制出一个 index.txt 文件出来
将主应用中的 entry 改为 txt 地址,如:
registerMicroApps([- { name: 'app1', entry: '//localhost:8080/index.html', container, activeRule },+ { name: 'app1', entry: '//localhost:8080/index.txt', container, activeRule },],);
qiankun 将会自动隔离微应用之间的样式(开启沙箱的情况下),你可以通过手动的方式确保主应用与微应用之间的样式隔离。比如给主应用的所有样式添加一个前缀,或者假如你使用了 ant-design 这样的组件库,你可以通过这篇文档中的配置方式给主应用样式自动添加指定的前缀。
以 antd 为例:
配置 webpack 修改 less 变量
{loader: 'less-loader',+ options: {+ modifyVars: {+ '@ant-prefix': 'yourPrefix',+ },+ javascriptEnabled: true,+ },}
配置 antd ConfigProvider
import { ConfigProvider } from 'antd';export const MyApp = () => (<ConfigProvider prefixCls="yourPrefix"><App /></ConfigProvider>);
详细文档参考 antd 官方指南。
有些时候我们希望直接启动微应用从而更方便的开发调试,你可以使用这个全局变量来区分当前是否运行在 qiankun 的主应用的上下文中:
if (!window.__POWERED_BY_QIANKUN__) {render();}export const mount = async () => render();
微应用何时被激活完全取决于你的 activeRule
配置,比如下面的例子里,我们将 reactApp
和 react15App
的 activeRule
逻辑设置成一致的:
registerMicroApps([{ name: 'reactApp', entry: '//localhost:7100', container, activeRule: () => isReactApp() },{ name: 'react15App', entry: '//localhost:7102', container, activeRule: () => isReactApp() },{ name: 'vueApp', entry: '//localhost:7101', container, activeRule: () => isVueApp() },]);start({ singular: false });
当在 start
方法中配置好 singular: false
后,只要 isReactApp()
返回 true
时,reactApp
和 react15App
将会同时被 mount。
不要共享运行时,即便所有的团队都是用同一个框架。- 微前端
虽然共享依赖并不建议,但如果你真的有这个需求,你可以在微应用中将公共依赖配置成 external
,然后在主应用中导入这些公共依赖。
qiankun 2.0 版本将提供一种更智能的方式使其自动化。
兼容.
但是 IE 环境下(不支持 Proxy 的浏览器)只能使用单实例模式,qiankun 会自动将 singular
配置为 true
。
你可以在这里找到 singular 相关说明。
如果希望 qiankun (或其依赖库、或者您的应用本身)在 IE 下正常运行,你至少需要在应用入口引入以下这些 polyfills:
import 'whatwg-fetch';import 'custom-event-polyfill';import 'core-js/stable/promise';import 'core-js/stable/symbol';import 'core-js/stable/string/starts-with';import 'core-js/web/url';
通常我们建议您直接使用 @babel/preset-env 插件完成自动引入 IE 需要的 polyfill 的能力,所有的操作文档您都可以在 babel 官方文档 找到。
Here is no "fetch" on the window env, you need to polyfill it
qiankun 依赖的 import-html-entry 通过 window.fetch
来获取微应用的资源, 部分不支持 fetch 的浏览器需要在入口处打上相应的 polyfill
qiankun 会将微应用的动态 script 加载(例如 JSONP)转化为 fetch 请求,因此需要相应的后端服务支持跨域,否则会导致错误。
在单实例模式下,你可以使用 excludeAssetFilter
参数来放行这部分资源请求,但是注意,被该选项放行的资源会逃逸出沙箱,由此带来的副作用需要你自行处理。
若在多实例模式下使用 JSONP,单纯使用 excludeAssetFilter
并不能取得好的效果,因为各应用被沙箱所隔离;你可以在主应用提供统一的 JSONP 工具,微应用调用主应用提供的该工具来曲线救国。
通常是因为你使用的是 browser 模式的路由,这种路由模式的开启需要服务端配合才行。具体配置方式参考:
首先不应该写通配符 *
,可以将 404 页面注册为一个普通路由页面,比如说 /404
,然后在主应用的路由钩子函数里面判断一下,如果既不是主应用路由,也不是微应用,就跳转到 404 页面。
以vue-router
为例,伪代码如下:
const childrenPath = ['/app1', '/app2'];router.beforeEach((to, from, next) => {if (to.name) {// 有 name 属性,说明是主应用的路由return next();}if (childrenPath.some((item) => to.path.includes(item))) {return next();}next({ name: '404' });});
微应用之间的跳转,或者微应用跳主应用页面,直接使用微应用的路由实例是不行的,如 react-router 的 Link 组件或 vue 的 router-link,原因是微应用的路由实例跳转都基于路由的 base
。有这几种办法可以跳转:
history.pushState()
:mdn 用法介绍<a href="http://localhost:8080/app1">app1</a>
window.location.href = 'http://localhost:8080/app1'
服务器需要给微应用的 index.html
配置一个响应头:Cache-Control no-cache
,意思就是每次请求都检查是否更新。
以 Nginx
为例:
location = /index.html {add_header Cache-Control no-cache;}
有些场景下我们会使用 config entry 的方式加载微应用(不推荐):
loadMicroApp({name: 'configEntry',entry: {scripts: ['//t.com/t.js'],styles: ['//t.com/t.css'],},});
微应用的 entry js 由于没有附属的 html,mount 钩子直接这么写的:
export async function mount(props) {ReactDOM.render(<App />, props.container);}
因为 props.container
并不是一个空的容器,里面会包含微应用通过 styles 配置注册进来的样式表等信息,所以当我们直接以props.container
为 react 应用的容器渲染时,会把容器里原来的所有 dom 结构全部覆盖掉,从而导致样式表丢失。
我们需要给使用 config entry 的微应用构造一个空的渲染容器,专门用来挂载 react 应用:
loadMicroApp({name: 'configEntry',entry: {+ html: '<div id="root"></div>',scripts: ['//t.com/t.js'],styles: ['//t.com/t.css']}});
mount 钩子里不是直接渲染到 props.container
,而是渲染到其 root
节点里:
export async function mount(props) {- ReactDOM.render(<App/>, props.container);+ ReactDOM.render(<App/>, props.container.querySelector('#root'));}
因为拉取微应用 entry 的请求都是跨域的,所以当你的微应用是依赖 cookie (如登陆鉴权)的情况下,你需要通过自定义 fetch 的方式,开启 fetch 的 cors 模式:
如果你是通过 registerMicroApps 加载微应用的,你需要在 start 方法里配置自定义 fetch,如:
import { start } from 'qiankun';start({fetch(url, ...args) {// 给指定的微应用 entry 开启跨域请求if (url === 'http://app.alipay.com/entry.html') {return window.fetch(url, {...args,mode: 'cors',credentials: 'include',});}return window.fetch(url, ...args);},});
如果你是通过 loadMicroApp 加载微应用的,你需要在调用时配置自定义 fetch,如:
import { loadMicroApp } from 'qiankun';loadMicroApp(app, {fetch(url, ...args) {// 给指定的微应用 entry 开启跨域请求if (url === 'http://app.alipay.com/entry.html') {return window.fetch(url, {...args,mode: 'cors',credentials: 'include',});}return window.fetch(url, ...args);},});
如果你是通过 umi plugin 来使用 qiankun 的,那么你只需要给对应的微应用开启 credentials 配置即可:
export default {qiankun: {master: {apps: [{name: 'app',entry: '//app.alipay.com/entry.html',+ credentials: true,}]}}}