鉴于前一段时间公司内部项目用到了微前端框架qiankun,总理了一些常见的坑:
指定 yarn 模块下载源
yarn config set registry https://registry.npm.taobao.org
指定 node-sass 下载源
yarn config set sass-binary-site http://npm.taobao.org/mirrors/node-sass
指定 electron 下载源
yarn config set electron_mirror https://npm.taobao.org/mirrors/electron/
指定 chromedriver 下载源
yarn config set chromedriver_cdnurl https://npm.taobao.org/mirrors/chromedriver
qiankun 常见报错
子项目未 export 需要的生命周期函数
先检查下子项目的入口文件有没有 export 生命周期函数,再检查下子项目的打包,最后看看请求到的子项目的文件对不对。
子项目加载时,容器未渲染好
检查容器 div 是否是写在了某个路由里面,路由没匹配到所有未加载。如果只在某个路由页面加载子项目,可以在页面的 mounted 周期里面注册子项目并启动。 ⚠️:在mounted注册也会有问题 主项目路由的** hash 与 history **之争
主项目 history : 需要使用 location.pathname 来区分不同的子项目 hash 模式子项目路由跳转不改变 path,所以无影响, history 模式子项目路由设置 base 属性即可。 缺点:
- history 模式路由需要设置 base
- 子项目之间的跳转需要使用父项目的 router 对象(不用 <a> 链接直接跳转的原因是 <a> 链接会刷新页面)。
其实不传递 router 对象,用原生的 history 对象跳转也行: history.pushState(null, 'name', '/app-vue-hash/#/about'),同样不会刷新页面。
不管是父项目的 router 对象,还是原生的 history 对象,跳转都是 js 的方式。这里有一个小小的用户体验问题:标签(<router-link> 和 <a>)形式的跳转是支持浏览器默认的右键菜单的,js 方式则没有:
主项目路由用 hash 模式且子项目没有history 模式路由
也就是说主项目和所有子项目都是 hash 模式,这种情况下也有两种做法:
- 用 path 来区分子项目
做法就不赘述了
优点:无需修改子项目内部代码
缺点:项目之间的跳转,都得靠原生的 history 对象
- 用 hash 来区分子项目
这样做主项目和子项目会共同接管路由,举个栗子:
/#/vue/home: 会加载 vue 子项目的 home 页面,但是其实,单独访问这个子项目的 home 页面的完整路由就是/#/vue/home
/#/react/about: 会加载 react 子项目的 about 页面,同样,单独访问这个子项目的 about 页面的完整路由就是/#/react/about
/#/about: 会加载主项目的about页面
做法就是自定义 activeRule :
const getActiveRule = hash => location => location.hash.startsWith(hash);
registerMicroApps([
{
name: 'app-vue-hash',
entry: 'http://localhost:1111',
container: '#appContainer',
activeRule: getActiveRule('#/app-vue-hash'),
},
])
复制代码
然后需要在子项目的所有路由前加上这个前缀,或者将子项目的根路由设置为这个前缀。
const routes = [
{
path: '/app-vue-hash',
name: 'Home',
component: Home,
children: [
// 其他的路由都写到这里
]
}
]
复制代码
如果子项目是新项目还好,如果是旧项目,则影响还是比较大,子项目里面的路由跳转(<router-link>、router.push()、router.repace())如果使用的是 path ,则需要修改,得加上这个前缀,如果使用的是 name跳转,则无需改动:router.push({ name: 'user'})。
优点: 所有项目之间的跳转都可以直接使用自己的 router 对象或者 <router-link>,不需要借助父项目的路由对象或者原生的 history 对象
缺点: 对子项目是入侵式修改,如果是全新项目,则无影响。
主项目路由用 hash 模式且子项目有history 模式路由
主项目是hash 模式,子项目间的跳转就只能借助原生的 history 对象了,我们既可以用 path 也可以用 hash 来区分子项目:
- 用 path 来区分子项目
与主项目是 history 没有太大的差异,优缺点也一样。
- /vue-hash/#/home: 会加载 vue 子项目的 home 页面
- /vue-history/about: 会加载 vue-history 子项目的 about 页面
- /#/about: 会加载主项目的about页面
- 用 hash 来区分子项目
这样做其实不太好,有点反常规,但是也可以用:
- /home/#/vue: 会加载 vue 子项目的 home 页面
- /#/vue-hash/about: 会加载 vue-hash 子项目的 about 页面
- /#/about: 会加载主项目的about页面
优点:无
缺点: 对 hash 子项目是入侵式修改,如果是全新项目,则无影响。
总结
主项目路由的 hash 与 history 模式都可以使用,各有优劣,看情况取舍。
vue 项目 hash 模式改 history:
new Router 时设置 mode 为 history
webpack 打包的配置( vue.config.js )
如果一些资源报 404,相对路径改为绝对路径
<img src="./img/logo.jpg"> 改为 <img src="/img/logo.jpg"> 即可
css 污染问题及加载 bug
1,qiankun 只能解决子项目之间的样式相互污染,不能解决子项目的样式污染主项目的样式
主项目要想不被子项目的样式污染,子项目是 vue 技术,样式可以写 css-scoped ,如果子项目是 jQuery 技术呢?所以主项目本身的 id/class 需要特殊一点,不能太简单,被子项目匹配到。 2,从子项目页面跳转到主项目自身的页面时,主项目页面的 css 未加载的bug 临时解决办法:先复制一下 HTMLHeadElement.prototype.appendChild 和 window.addEventListener ,路由钩子函数 beforeEach 中判断一下,如果当前路由是子项目,并且去的路由是父项目的,则还原这两个对象.
const childRoute = ['/app-vue-hash','/app-vue-history'];
const isChildRoute = path => childRoute.some(item => path.startsWith(item))
const rawAppendChild = HTMLHeadElement.prototype.appendChild;
const rawAddEventListener = window.addEventListener;
router.beforeEach((to, from, next) => {
// 从子项目跳转到主项目
if(isChildRoute(from.path) && !isChildRoute(to.path)){
HTMLHeadElement.prototype.appendChild = rawAppendChild;
window.addEventListener = rawAddEventListener;
}
next();
});
主子通信
三种方式:
1,动态通信 通过rx.js
部署二级目录
必须配置 publicPath,vue-cli3 官网描述:
entry 最后面加一个 /,正确是 http://location:5000/good/ 而不是 http://location:5000/good
要配置 publicPath :https://cli.vuejs.org/zh/config/#publicpath ,
打包之后,你的js的路径应该是/good/static/css.js,而不是/static/css.js
div[id^='_qiankun_microapp_wrapper'] {
height: 100%;
}
主子应用css隔离方案
子应用:
配置:
// postcss.config.js
module.exports = {
plugins: {
autoprefixer: {},
'postcss-selector-namespace': {
namespace: function(css) {
// element-ui的样式不需要添加命名空间
if (css.includes('element-variables.scss')) return '';
return ‘#micro-view’;
},
},
},
};
micro-view #app[data-v-7ba5bd90] {}
主应用:
业务线使用element-ui
dialog,drawer 的v-modal 层级问题:设置属性append-to-body=true
静态资源 404(字体,img等)
webpack打包注入完整路径配置
file-loader|url-loader 增加publicPath
const publicPath = http://localhost:${port}
{
test: /.(woff2?|eot|ttf|otf)$/i,
use: [
{
loader: 'file-loader',
options: {
name: 'fonts/[name].[hash:8].[ext]',
publicPath
},
},
],
},
Uncaught Error: application 'cmsClient' died in status LOADING_SOURCE_CODE: only one instance of babel-polyfill is allowed
子应用通过<script src=“babel-polyfill” ignore /> 引入, script 标签加上 ignore 属性
Throw Error "NavigationDuplicated"
使用 $router.push 更改“ page”。如果您使用的是同一个页面,vue-router 抛出一个 Error。
这个是 vue-router 的报错,你应该是 push 相同路由了
// 避免跳转相同路由报错
const originalPush = VueRouter.prototype.push
VueRouter.prototype.push = function push(location) {
return originalPush.call(this, location).catch(err => err)
}
至于 Uncaught SyntaxError: Unexpected token '<',这是 vue-cli-service 的问题
默认情况下,它打包出来的 js 资源地址是相对路径,比如 <script type="text/javascript" src="js/app.js"></script>
当你在子应用路径下的时候(比如 http://localhost:7770/basic/about) ,上面的 js 资源就指向了 /basic/js/app.js,
1. publicPath
部署到二级目录也是要配置 publicPath 的:https://cli.vuejs.org/zh/config/#publicpath
- webpack 的 webpack_public_path 配置是用来给动态加载的资源做路径补全的,不适用于那些直接在 html 中静态引入的 js/css/img 等,这些资源要想变更路径前缀需要配置 webpack publicPath,这个可以翻下 webpack 文档或者自己做下测试验证
- 你的 html 里静态引入的 /static/css.js 是绝对路径的资源,这种写法无论是 http://localhost:5000 还是 http://localhost:5000/good/ 上下文中访问,计算出来的地址都会是 http://localhost:5000/static/css.js,原因上面 @gongshun 的截图里说明了,你可以自己随便搞个 html 测试一下
- assetPublicPath 或许应该叫 assetContext 或者 assetRuntimePublicPath,现在这个命名确实容易让人误解其等价于 webpack 的 publicPath 配置
webpack配置的publicpath设置成当前运行时的路径
父应用使用了babel-pollfill,子应用不要在在bable-pollfill
子应用的代理将失效,代理需要配置在父应用中
项目中使用了百度地图等组件,会出现在子应用中无法使用的情况 ,在看源码时发现子应用的document.body中添加script标签失败没报错,但无法正确添加到body中,类似性质的问题还有美洽客服的引用,pdfjs的引用两种解决方法:
更新qiankun版本至2.0.17,在start中添加excludeAssetFilter,在主应用中引入script标签
- 使用iframe单独调用百度地图的页面(没有太多页面交互推荐用这种)
<div id="output"></div>
<script type="text/javascript">
//document.write('<' + 'script src="' + src + '"' +' type="text/javascript"><' + '/script>')
document.querySelector("#output").innerHTML = '<' + 'script src="' + src + '"' + ' type="text/javascript"><' + '/script>'
</script>