咱们先来看两个实际的场景:css
一般,咱们的后台项目都长这样:html
若是咱们的项目须要开发某个新的功能,而这个功能另外一个项目已经开发好,咱们想直接复用时。PS:咱们须要的只是别人项目的这个功能页面的「内容部分」,不须要别人项目的顶部导航和菜单。前端
一个比较笨的办法就是直接把别人项目这个页面的代码拷贝过来,可是万一别人不是 vue
开发的,或者说 vue
版本、UI
库等不一样,以及别人的页面加载以前操做(路由拦截,鉴权等)咱们都须要拷贝过来,更重要的问题是,别人代码有更新,咱们如何作到同步更新。vue
长远来看,代码拷贝不太可行,问题的根本就是,咱们须要作到让他们的代码运行在他们本身的环境之上,而咱们对他们的页面仅仅是“引用”。这个环境包括各类插件( vue
、 vuex
、 vue-router
等),也包括加载前的逻辑(读 cookie
,鉴权,路由拦截等)。私有 npm
能够共享组件,可是依然存在技术栈不一样/UI库不一样等问题。node
举个栗子,大家的产品有几百个页面,功能齐全且强大,客户只须要其中的部分页面,并且须要大家提供源码,这时候把全部代码都给出去确定是不可能的,只能挑出来客户须要,这部分代码须要另外制定版本维护,就很浪费。react
微前端的诞生也是为了解决以上两个问题:webpack
使用微前端的好处:ios
目前微前端主要有两种解决方案:iframe
方案和 single-spa
方案web
iframe
方案iframe
你们都很熟悉,使用简单方便,提供自然的 js/css
隔离,也带来了数据传输的不便,一些数据没法共享(主要是本地存储、全局变量和公共插件),两个项目不一样源(跨域)状况下数据传输须要依赖 postMessage
。ajax
iframe
有不少坑,可是大多都有解决的办法:
iframe
和主页面共享链接池,而浏览器对相同域的链接有限制,因此会影响页面的并行加载,阻塞 onload
事件。每次点击都须要从新加载,虽然能够采用 display:none
来作缓存,可是页面缓存过多会致使电脑卡顿。「(没法解决)」
iframe
必须给一个指定的高度,不然会塌陷。
解决办法:子项目实时计算高度并经过 postMessage
发送给主页面,主页面动态设置 iframe
高度。有些状况会出现多个滚动条,用户体验不佳。
弹窗只能在 iframe
范围内垂直水平居中,无法在整个页面垂直水平居中。
iframe
内的
div
没法全屏
弹窗的全屏,指的是在浏览器可视区全屏。这个全屏指的是占满用户屏幕。
全屏方案,原生方法使用的是 Element.requestFullscreen()
,插件:vue-fullscreen。当页面在 iframe
里面时,全屏会报错,且 dom
结构错乱。
iframe
和主页面共用一个浏览历史,iframe
会影响页面的前进后退。大部分时候正常,iframe
屡次重定向则会致使浏览器的前进后退功能没法正常使用。而且 iframe
页面刷新会重置(好比说从列表页跳转到详情页,而后刷新,会返回到列表页),由于浏览器的地址栏没有变化,iframe
的 src
也没有变化。
iframe
加载失败的状况很差处理
非同源的 iframe
在火狐及 chorme
都不支持 onerror
事件。
onload
事件里面判断页面的标题,是否
404
或者
500
try catch
解决此问题,尝试获取
contentDocument
时将抛出异常。
解决办法参考:stackoverflow上的问题:Catch error if iframe src fails to load
single-spa
微前端方案spa
单页应用时代,咱们的页面只有 index.html
这一个 html
文件,而且这个文件里面只有一个内容标签 <div id="app"></div>
,用来充当其余内容的容器,而其余的内容都是经过 js
生成的。也就是说,咱们只要拿到了子项目的容器 <div id="app"></div>
和生成内容的 js
,插入到主项目,就能够呈现出子项目的内容。
<link href=/css/app.c8c4d97c.css rel=stylesheet>
<div id=app></div> <script src=/js/chunk-vendors.164d8230.js> </script> <script src=/js/app.6a6f1dda.js> </script> 复制代码
咱们只须要拿到子项目的上面四个标签,插入到主项目的 HTML
中,就能够在父项目中展示出子项目。
这里有个问题,因为子项目的内容标签是动态生成的,其中的 img/video/audio
等资源文件和按需加载的路由页面 js/css
都是相对路径,在子项目的 index.html
里面,能够正确请求,而在主项目的 index.html
里面,则不能。
举个例子,假设咱们主项目的网址是 www.baidu.com
,子项目的网址是 www.taobao.com
,在子项目的 index.html
里面有一张图片 <img src="./logo.jpg">
,那么这张图片的完整地址是 www.taobao.com/logo.jpg
,如今将这个图片的 img
标签生成到了父项目的 index.html
,那么图片请求的地址是 www.baidu.com/logo.jpg
,很显然,父项目服务器上并无这张图。
解决思路:
js/css/img/video
等都是相对路径,可否经过
webpack
打包,将这些路径所有打包成绝对路径?这样就能够解决文件请求失败的问题。
node
)将子项目的文件所有拷贝到主项目服务器上,
node
监听子项目文件有更新,就自动拷贝过来,而且按
js/css/img
文件夹合并
CDN
同样,一个服务器挂了,会去其余服务器上请求对应文件。或者说服务器之间的文件共享,主项目上的文件请求失败会自动去子服务器上找到并返回。
一般作法是动态修改 webpack
打包的 publicPath
,而后就能够自动注入前缀给这些资源。
single-spa
是一个微前端框架,基本原理如上,在上述呈现子项目的基础上,还新增了 bootstrap
、 mount
、 unmount
等生命周期。
相对于 iframe
,single-spa
让父子项目属于同一个 document
,这样作既有好处,也有坏处。好处就是数据/文件均可以共享,公共插件共享,子项目加载就更快了,缺点是带来了 js/css
污染。
single-spa
上手并不简单,也不能开箱即用,开发部署更是须要修改大量的 webpack
配置,对子项目的改造也很是多。
qiankun
方案qiankun
是蚂蚁金服开源的一款框架,它是基于 single-spa
的。他在 single-spa
的基础上,实现了开箱即用,除一些必要的修改外,子项目只须要作不多的改动,就能很容易的接入。若是说 single-spa
是自行车的话,qiankun
就是个汽车。
微前端中子项目的入口文件常见的有两种方式:JS entry
和 HTML entry
纯 single-spa
采用的是 JS entry
,而 qiankun
既支持 JS entry
,又支持 HTML entry
。
JS entry
的要求比较苛刻:
(1)将 css
打包到 js
里面
(2)去掉 chunk-vendors.js
,
(3)去掉文件名的 hash
值
(4)将 single-spa
模式的入口文件( app.js
)放置到 index.html
目录,其余文件不变,缘由是要截取 app.js
的路径做为 publicPath
APP entry | 优势 | 缺点 |
---|---|---|
JS entry |
能够配合 systemJs ,按需加载公共依赖( vue , vuex , vue-router 等) |
须要各类打包配置配合,没法实现预加载 |
HTML entry |
打包配置无需作太多的修改,能够预加载 | 多一层请求,须要先请求到 HTML 文件,再用正则匹配到其中的 js 和 css |
其实 qiankun
还支持 config entry
:
{
entry: { scripts: [ "app.3249afbe.js" "chunk-vendors.75fba470.js", ], styles: [ "app.3249afbe.css" "chunk.75fba470.css", ], html: 'http://localhost:5000' } } 复制代码
建议使用 HTML entry
,使用起来和 iframe
同样简单,可是用户体验比 iframe
强不少。qiankun
请求到子项目的 index.html
以后,会先用正则匹配到其中的 js/css
相关标签,而后替换掉,它须要本身加载 js
并运行,而后去掉 html/head/body
等标签,剩下的内容原样插入到子项目的容器中 :
使用 qiankun
的好处:
qiankun
自带
js/css
沙箱功能,
singles-spa
能够解决
css
污染,可是须要子项目配合
single-spa
方案只支持
JS entry
的特色,限制了它只能支持
vue
、
react
、
angular
等技术开发的项目,对一些
jQuery
老项目则无能为力。
qiankun
则没有限制
qiankun
支持子项目预请求功能。
js
沙箱js/css
污染是没法避免的,而且是一个可大可小的问题。就像一颗定时炸弹,不知道何时会出问题,排查也麻烦。做为一个基础框架,解决这两个污染很是重要,不能仅凭“规范”开发。
js
沙箱的原理是子项目加载以前,对 window
对象作一个快照,子项目卸载时恢复这个快照,如图:
那么如何监测 window
对象的变化呢,直接将 window
对象进行一下深拷贝,而后深度对比各个属性显然可行性不高,qiankun
框架采用的是ES6
新特性,proxy
代理方法。具体如何操做的,以前的文章有写(连接在文末),就再也不赘述。
可是 proxy
是不兼容 IE11
的,为了兼容,低版本 IE
采用了 diff
方法:浅拷贝 window
对象,而后对比每个属性。
qiankun
的 css
沙箱的原理是重写 HTMLHeadElement.prototype.appendChild
事件,记录子项目运行时新增的 style/link
标签,卸载子项目时移除这些标签。
single-spa
方案中我用了换肤的思路来解决 css
污染:首先 css-scoped
解决大部分的污染,对于一些全局样式,在子项目给 body/html
加一个惟一的 id/class
(正常开发部署用),而后这个全局的样式前面加上这个 id/class
,而 single-spa
模式则在 mount
周期给 body/html
加上这个惟一的 id/class
,在 unmount
周期去掉,这样就能够保证这个全局 css
只对这个项目生效了。
这两个方案的致命点都在于没法解决多个子项目同时运行时的 css
污染,以及子项目对主项目的 css
污染。
虽说两个项目同时运行常见并不常见,可是若是想实现 keep-alive
,就须要使用 display: none
将子项目隐藏起来,子项目不须要卸载,这时候就会存在两个子项目同时运行,只不过其中一个对用户不可见。
css
沙箱还有个思路就是将子项目的样式局限到子项目的容器范围内生效,这样只须要给不一样的子项目不一样的容器就能够了。可是这样也会有新的问题,子项目中 append
到 body
的弹窗,样式就没法生效。因此说样式污染还须要制定规范才行,约定 class
命名前缀。
在个人前几篇文章(连接在文末)中,single-spa
和 qiankun
的 demo
已经实现了,开发部署流程也都有,接下来就是实践出真知,用在实际项目中,才知道有那些坑。
qiankun
子项目因为咱们是 vue
技术栈,因此我就以改造一个 vue
项目为例说明,其余的技术栈原理是同样的。
src
目录新增文件
public-path.js
:
if (window.__POWERED_BY_QIANKUN__) {
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__; } 复制代码
修改 index.html
中项目初始化的容器,不要使用 #app
,避免与其余的项目冲突,建议换成项目 name
的驼峰写法
修改入口文件 main.js
:
import './public-path';
import Vue from 'vue' import App from './App.vue' import VueRouter from 'vue-router' import store from './store'; Vue.use(VueRouter) Vue.config.productionTip = false let router = null; let instance = null; function render(parent = {}) { const router = new VueRouter({ // histroy模式的路由须要设置base,app-history-vue根据项目名称来定 base: window.__POWERED_BY_QIANKUN__ ? '/app-history-vue' : '/', mode: 'history', // hash模式不须要上面两行 routes: [] }) instance = new Vue({ router, store, render: h => h(App), data(){ return { parentRouter: parent.router, parentVuex: parent.store, } }, }).$mount('#appVueHistory'); } //全局变量来判断环境,独立运行时 if (!window.__POWERED_BY_QIANKUN__) { render(); } export async function bootstrap() { console.log('vue app bootstraped'); } export async function mount(props) { console.log('props from main framework', props); render(props.data); } export async function unmount() { instance.$destroy(); instance = null; router = null; } 复制代码
主要改动是引入修改 publicPath
的文件和 export
三个生命周期。
注意:
webpack
的
publicPath
值只能在入口文件修改,之因此单独写到一个文件并在入口文件最开始引入,是由于这样作可让下面全部的代码都能使用这个。
export
路由数据,而不是实例化的路由对象,路由的钩子函数也须要移到入口文件。
mount
生命周期,能够拿到父项目传递过来的数据,
router
用于跳转到主项目/其余子项目的路由,
store
是父项目的实例化的
Vuex
。
vue.config.js
:
const { name } = require('./package');
module.exports = { devServer: { headers: { 'Access-Control-Allow-Origin': '*', }, }, // 自定义webpack配置 configureWebpack: { output: { library: `${name}-[name]`, libraryTarget: 'umd',// 把子应用打包成 umd 库格式 jsonpFunction: `webpackJsonp_${name}`, }, }, }; 复制代码
注: 这个 name
默认从 package.json
获取,能够自定义,只要和父项目注册时的 name
保持一致便可。
这个配置主要就两个,一个是容许跨域,另外一个是打包成 umd
格式。为何要打包成 umd
格式呢?是为了让 qiankun
拿到其 export
的生命周期函数。咱们能够看下其打包后的 app.js
就知道了:
root
在浏览器环境就是 window
, qiankun
拿这三个生命周期,是根据注册应用时,你给的 name
值,name
不一致则会致使拿不到生命周期函数
src
目录,不要放在
public
或 者
static
资源放 src
目录,会通过 webpack
处理,能统一注入 publicPath
。不然在主项目中会404。
参考:vue-cli3的官方文档介绍:什么时候使用-public-文件夹
暴露给运维人员的配置文件 config.js
,能够放在 public
目录,由于在 index.html
中 url
为相对连接的 js/css
资源,qiankun
会给其注入前缀。
axios
实例添加拦截器,而不是
axios
对象
后续会考虑子项目共享公共插件,这时就须要避免公共插件的污染
// 正确作法:给 axios 实例添加拦截器
const instance = axios.create(); instance.interceptors.request.use(function () {/*...*/}); // 错误用法:直接给 axios 对象添加拦截器 axios.interceptors.request.use(function () {/*...*/}); 复制代码
css
污染
组件内样式的 css-scoped
是必须的。
对于一些插入到 body
的弹窗,没法使用 scoped
,请不要直接使用原 class
修改样式,请添加本身的 class
,来修改样式。
.el-dialog{
/* 不推荐使用组件原有的class */ } .my-el-dialog{ /* 推荐使用自定义组件的class */ } 复制代码
position:fixed
在父项目中,这个定位未必准确,应尽可能避免使用,确有相对于浏览器窗口定位需求,能够用 position: sticky
,可是会有兼容性问题(IE不支持)。若是定位使用的是 bottom
和 right
,则问题不大。
还有个办法,位置能够写成动态绑定 style
的形式:
<div :style="{ top: isisQiankun ? '10px' : '0'}">
复制代码
body
、
document
等绑定的事件,请在
unmount
周期清除
js
沙箱只劫持了 window.addEventListener
,使用 document.body.addEventListener
或者 document.body.onClick
添加的事件并不会被沙箱移除,会对其余的页面产生影响,请在 unmount
周期清除
qiankun
常见问题及解决方案qiankun
常见报错export
须要的生命周期函数
先检查下子项目的入口文件有没有 export
生命周期函数,再检查下子项目的打包,最后看看请求到的子项目的文件对不对。
检查容器 div
是不是写在了某个路由里面,路由没匹配到全部未加载。若是只在某个路由页面加载子项目,能够在页面的 mounted
周期里面注册子项目并启动。
history
模式吗?因为 qiankun
是经过 location.pathname
值来判断当前应该加载哪一个子项目的,因此须要给每一个子项目注入不一样的路由 path
,而 hash
模式子项目路由跳转不改变 path
,因此无影响,history
模式子项目路由设置 base
属性便可。
若是主项目使用 hash
模式,那么得用 location.hash
值来判断当前应该加载哪一个子项目,而且子项目都得是 hash
模式,还须要给子项目全部的路由都添加一个前缀,子项目的路由跳转若是以前使用的是 path
也须要修改,用 name
跳转则不用。
若是主项目是 hash
模式子项目为 history
模式,那么跳转到子项目以后,没法跳转到另外一个 history
模式的子项目,也没法回到主项目的页面。
vue
项目 hash
模式改 history
模式也很简单:
new Router
时设置
mode
为
history
:
webpack
打包的配置(
vue.config.js
) :
<img src="./img/logo.jpg">
改成
<img src="/img/logo.jpg">
便可
css
污染问题及加载 bug
qiankun
只能解决子项目之间的样式相互污染,不能解决子项目的样式污染主项目的样式
主项目要想不被子项目的样式污染,子项目是 vue
技术,样式能够写 css-scoped
,若是子项目是 jQuery
技术呢?因此主项目自己的 id/class
须要特殊一点,不能太简单,被子项目匹配到。
css
未加载的
bug
产生这个问题的缘由是:在子项目跳转到父项目时,子项目的卸载须要一点点的时间,在这段时间内,父项目加载了,插入了 css
,可是被子项目的 css
沙箱记录了,而后被移除了。父项目的事件监听也是同样的,因此须要在子项目卸载完成以后再跳转。我本来想在路由钩子函数里面判断下,子项目是否卸载完成,卸载完成再跳转路由,然而路由不跳转,子项目根本不会卸载。
临时解决办法:先复制一下 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(); }); 复制代码
在子项目里面如何跳转到另外一个子项目/主项目页面呢,直接写 <router-link>
或者用 router.push/router.replace
是不行的,缘由是这个 router
是子项目的路由,全部的跳转都会基于子项目的 base
。写 <a>
连接能够跳转过去,可是会刷新页面,用户体验很差。
解决办法也比较简单,在子项目注册时将主项目的路由实例对象传过去,子项目挂载到全局,用父项目的这个 router
跳转就能够了。
可是有一丢丢不完美,这样只能经过 js
来跳转,跳转的连接没法使用浏览器自带的右键菜单(如图:Chrome
自带的连接右键菜单)
项目之间的不要有太多的数据依赖,毕竟项目仍是要独立运行的。通讯操做须要判断是否 qiankun
模式,作兼容处理。
经过 props
传递父项目的 Vuex
,若是子项目是 vue
技术栈,则会很好用。假如子项目是 jQuery/react/angular
,就不能很好的监听到数据的变化。
qiakun
提供了一个全局的 GlobalState
来共享数据。主项目初始化以后,子项目能够监听到这个数据的变化,也能提交这个数据。
// 主项目初始化
import { initGlobalState } from 'qiankun'; const actions = initGlobalState(state); // 主项目项目监听和修改 actions.onGlobalStateChange((state, prev) => { // state: 变动后的状态; prev 变动前的状态 console.log(state, prev); }); actions.setGlobalState(state); // 子项目监听和修改 export function mount(props) { props.onGlobalStateChange((state, prev) => { // state: 变动后的状态; prev 变动前的状态 console.log(state, prev); }); props.setGlobalState(state); } 复制代码
vue
项目之间数据传递仍是使用共享父组件的 Vuex
比较方便,与其余技术栈的项目之间的通讯使用 qiankun
提供的 GlobalState
。
若是主项目和子项目都用到了同一个版本的 Vue/Vuex/Vue-Router
等,主项目加载一遍以后,子项目又加载一遍,就很浪费。
要想复用公共依赖,前提条件是子项目必须配置 externals
,这样依赖就不会打包进 chunk-vendors.js
,才能复用已有的公共依赖。
按需引入公共依赖,有两个层面:
UI
组件库的按需加载、
echarts/lodash
的按需加载。
webpack
的 externals
是支持大插件的按需引入的:
subtract : {
root: ['math', 'subtract'] } 复制代码
subtract
能够经过全局 math
对象下的属性 subtract
访问(例如 window['math']['subtract']
)。
single-spa
能够按需引入子项目的公共依赖single-spa
是使用 systemJs
加载子项目和公共依赖的,将公共依赖和子项目一块儿配置到 systemJs
的配置文件 importmap.json
,就能够实现公共依赖的按需加载:
{
"imports": { "appVueHash": "http://localhost:7778/app.js", "appVueHistory": "http://localhost:7779/app.js", "single-spa": "https://cdnjs.cloudflare.com/ajax/libs/single-spa/4.3.7/system/single-spa.min.js", "vue": "https://cdn.jsdelivr.net/npm/vue@2.6.10/dist/vue.js", "vue-router": "https://cdn.jsdelivr.net/npm/vue-router@3.0.7/dist/vue-router.min.js", "echarts": "https://cdn.bootcss.com/echarts/4.2.1-rc1/echarts.min.js" } } 复制代码
qiankun
如何按需引入公共依赖巨无霸应用的公共依赖和公共函数被太多的页面使用,致使升级和改动困难,使用微前端可让各个子项目独立拥有本身的依赖,互不干扰。而咱们想要复用公共依赖,这与微前端的理念是相悖的。
因此个人想法是:父项目提供公共依赖,子项目能够自由选择用或者不用。
这个也很好实现,父项目先加载好依赖,而后在注册子项目时,将 Vue/Vuex/Vue-Router
等经过 props
传过去,子项目能够选择用或者不用。
主项目:
import Vue from 'vue'
import App from './App.vue' import router from './router' import store from './store' import { registerMicroApps, start } from 'qiankun'; import Vuex from 'vuex'; import VueRouter from 'vue-router'; new Vue({ router, store, render: h => h(App) }).$mount("#app"); registerMicroApps([ { name: 'app-vue-hash', entry: 'http://localhost:1111', container: '#appContainer', activeRule: '/app-vue-hash', props: { data : { store, router, Vue, Vuex, VueRouter } } }, ]); start(); 复制代码
子项目:
import Vue from 'vue'
export async function bootstrap() { console.log('vue app bootstraped'); } export async function mount(props) { console.log('props from main framework', props); const { VueRouter, Vuex } = props.data; Vue.use(VueRouter); Vue.use(Vuex); render(props.data); } export async function unmount() { instance.$destroy(); instance = null; router = null; } 复制代码
这样作不太可行,缘由有两个:
Vue-Router/Vuex
这些依赖从哪里来?子项目是只部署一份的,既能够独立运行,也能够被
qiankun
集成。
配置 webpack
的 externals
以后,子项目独立运行时,这些依赖的来源「有且仅有」 index.html
中的外链 script
标签。
在这个前提下,子项目和主项目的 vue
版本一致的状况下,使用同一份服务器文件。即便没法共享,也是能够作 http
缓存的。
那么 qiankun
可否作到,某个依赖加载了以后,再也不加载,直接复用呢?好比说子项目 A 请求了服务器上的 2.6 版本 vue
,切换到子项目 B,B 项目也用了这个 vue
文件,可否再也不次加载,直接复用呢?
实际上是能够的,能够看到 qiankun
将子项目的外链 script
标签,内容请求到以后,会记录到一个全局变量中,下次再次使用,他会先从这个全局变量中取。这样就会实现内容的复用,只要保证两个连接的url
一致便可。
const fetchScript = scriptUrl => scriptCache[scriptUrl] ||
(scriptCache[scriptUrl] = fetch(scriptUrl).then(response => response.text())); 复制代码
因此只要子项目配置了 webpack
的 externals
,并在 index.html
中使用外链 script
引入这些公共依赖,只要这些公共依赖在同一台服务器上,即可以实现子项目的公共依赖的按需引入,一个项目使用了以后,另外一个项目使用再也不重复加载,能够直接复用这个文件。
qiankun
更完美的按需引入虽然 qiankun
不会重复请求相同 url
的公共依赖,可是这也仅比 http
缓存强了一丢丢。
有缺陷的地方在于:
js
沙箱在子项目卸载时,会移除
window
上新增的变量,而
webpack
的
externals
偏偏是将这些公共依赖挂载在
window
上,可否看状况移除这些公共依赖?
url
也就不一样,就不会复用)可是这里可能会有一些疑问,既然使用无差异,为何不升级插件?
这些问题可能须要去改动 qiankun
的源码。
jQuery
老项目的资源加载问题子项目的内容标签插到父项目的 index.html
后,其中的资源( img/video/audio
等)路径都是相对的,致使资源没法正确显示。上面我列举了三种解决方案。
通常来讲,jQuery
项目是不通过 webpack
打包的,因此无法经过修改 publicPath
来注入路径前缀。后面两种方法操做起来比较麻烦,或者说咱们应该「优先从框架自己」解决这个问题,而不是其余方法。因此我想了以下三种方案:
<base>
标签html
有一个原生标签 <base>
,这个标签只能放在 <head>
里面,它的 href
属性是一个 url
值。 mdn
地址: base 文档根 URL 元素
设置了 <base>
标签以后,页面上全部的连接和 url
都基于它的 href
。例如页面访问地址是 https://www.taobao.com
,设置 <base href="https://www.baidu.com">
以后,页面中本来的图 <img src="./img/jQuery1.png" alt="">
的实际请求地址会变成 https://www.baidu.com/img/jQuery1.png
,页面上的 <a>
连接:<a href="/about"></a>
,点击以后,页面会跳转到:https://www.baidu.com/about
能够看到,<base>
标签和 webpack
的 publicPath
有同样的效果,那么可否在 jQuery
项目加载以前,把 jQuery
项目的地址赋给 <base>
标签,而后插入到 <head>
?这样就能够解决 jQuery
项目的资源加载问题。
作法也很简单,在 qiankun
提供的 beforeLoad
生命周期,判断当前是不是 jQuery
项目:
beforeLoad: app => {
if(app.name === 'purehtml'){ const baseTag = document.createElement('base'); baseTag.setAttribute('href',app.entry); console.log(baseTag); document.head.appendChild(baseTag); } }, beforeUnmount: app => { if(app.name === 'purehtml'){ const baseTag = document.head.querySelector('base'); document.head.removeChild(baseTag); } } 复制代码
这样作子项目资源能够正确加载,可是 <base>
标签的威力太强大了,会致使全部的路由没法正常跳转,跳转到其余的子项目时,<a>
连接是基于 <base>
的,会跳转到 jQuery
子项目的不存在的路由。解决了一个 bug
,又出现了新的 bug
,这样是不行的。因此这个方案可行性特别小。
这个方案分两步:
HTML
中已有的
img/audio/video
等标签,
qiankun
支持重写
getTemplate
函数,能够将入口文件
index.html
中的静态资源路径替换掉
img/audio/video
等标签,劫持
appendChild
、
innerHTML
、
insertBefore
等事件,将资源的相对路径替换成绝对路径
前面咱们说到,对于子项目是 HTML entry
的,qiankun
拿到入口文件 index.html
以后,会用正则匹配到 <body>
标签及其内容,<head>
中的 link/style/script/meta
等标签,而后插入到父项目的容器中。
咱们能够传递一个 getTemplate
函数,将图片的相对路径转为绝对路径,它会在处理模板时使用:
start({
getTemplate(tpl,...rest) { // 为了直接看到效果,因此写死了,实际中须要用正则匹配 return tpl.replace('<img src="./img/jQuery1.png">', '<img src="http://localhost:3333/img/jQuery1.png">'); } }); 复制代码
对于动态插入的标签,劫持其插入 DOM
的函数,注入前缀。
假如子项目动态插入一张图:
const render = $ => {
$('#purehtml-container').html('<p>Hello, render with jQuery</p><img src="./img/jQuery2.png">'); return Promise.resolve(); }; 复制代码
主项目劫持 jQuery
的 html
方法:
beforeMount: app => {
if(app.name === 'purehtml'){ // jQuery 的 html 方法是一个挺复杂的函数,这里只是为了看效果,简写了 $.prototype.html = function(value){ const str = value.replace('<img src="/img/jQuery2.png">', '<img src="http://localhost:3333/img/jQuery2.png">') this[0].innerHTML = str; } } } 复制代码
固然了,还有个简单粗暴的写法,给 jQuery
项目的图片路径写成绝对路径,可是不建议这么作,换个服务器部署就不能用了。
jQuery
项目加上 webpack
打包这个方案的可行性不高,都是陈年老项目了,不必这样折腾。
qiankun
自己就对接入 jQuery
多页应用比较乏力,通常使用场景就是,一个大项目只接入某个/某几个页面,这样的话使用方案二比较合理。
qiankun
使用总结只有一个子项目时,要想启用预加载,必须使用start({ prefetch: 'all' })
js
沙箱并不能解决全部的 js
污染,例如我用 onclick
或 addEventListener
给 <body>
添加了一个点击事件,js
沙箱并不能消除它的影响,因此说,还得靠代码规范和本身自觉
qiankun
框架不太好实现 keep-alive
需求,由于解决 css/js
污染的办法就是删除子项目插入的 css
标签和劫持 window
对象,卸载时还原成子项目加载前的样子,这与 keep-alive
相悖: keep-alive
要求保留这些,仅仅是样式上的隐藏。
qiankun
没法很好嵌入一些老项目
虽然 qiankun
支持 jQuery
老项目,可是彷佛对「多页应用」没有很好的解决办法。每一个页面都去修改,成本很大也很麻烦,可是使用 iframe
嵌入这些老项目就比较方便。
qiankun
将每一个子项目的 js/css
文件内容都记录在一个全局变量中,若是子项目过多,或者文件体积很大,可能会致使内存占用过多,致使页面卡顿。
另外,qiankun
运行子项目的 js
,并非经过 script
标签插入的,而是经过 eval
函数实现的,eval
函数的安全和性能是有一些争议的:MDN的eval介绍
npm-run-all
插件来实现:一个命令,运行全部项目。
{
"scripts": { "install:hash": "cd app-vue-hash && npm install", "install:history": "cd app-vue-history && npm install", "install:main": "cd main && npm install", "install:purehtml": "cd purehtml && npm install", "install-all": "npm-run-all install:*", "start:hash": "cd app-vue-hash && npm run serve ", "start:history": "cd app-vue-history && npm run serve", "start:main": "cd main && npm run serve", "start:purehtml": "cd purehtml && npm run serve", "start-all": "npm-run-all --parallel start:*", "serve-all": "npm-run-all --parallel start:*", "build:hash": "cd app-vue-hash && npm run build", "build:history": "cd app-vue-history && npm run build", "build:main": "cd main && npm run build", "build-all": "npm-run-all --parallel build:*" } } 复制代码
其中 --parallel
参数表示并行,没有这个参数则是等上一个命令执行完才会执行下一个命令。
不要对 iframe
抱有偏见,它也是微前端的一种实现方式,若是页面上无弹窗、无全屏等操做,iframe
也是很好用的。配置缓存和 cdn
加速,若是是内网访问,也不会很慢。
iframe
和 qiankun
能够并存,jQuery
多页应用使用 iframe
接入就挺好,何时什么场景该用哪一种方案,具体状况具体分析。
最后,文章有什么问题或错误欢迎指出,谢谢!
single-spa
和 qiankun
的 demo
如何实现以及部分原理浅析,能够看个人这三篇文章:
PS: 第三篇文章是今年3月份写的,里面涉及的 qiankun
源码是 1.0 版本,qiankun
在4月份发布了2.0版本,可是基本原理大体没变。
行业内其余前端团队对微前端的见解和实践:
qiankun
的在线案例
本文使用 mdnice 排版