一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第3天,点击查看活动详情。 前言: Iframe?Single-Spa?QianKun? 微前端很多,技术栈百花齐放,我们自己实现一波?
继上篇文章讲了,微前端的rollup搭建环境、导出导入相关的register和start和rerouter方法、生命
周期等,这节主要讲如何实现注册预加载应用。
一、 新建src/lifecycles/load.js
import { LOADING_SOURCE_CODE, NOT_BOOTSTRAPPED } from "../applications/app.helpers";
// 传入的启动方法有可能是个数组,需要组装成一个大的Promise对象, 就是redux中源码的compose写法
function flattenFnArray (fns) {
fns = Array.isArray(fns) ? fns : [fns]
return (props) => fns.reduce((p,fn) => p.then(() => fn(props)), Promise.resolve())
}
export async function toLoadPromise (app) {
app.status = LOADING_SOURCE_CODE
let { bootstrap, mount, unmount } = await app.loadApp(app.customProps)
app.status = NOT_BOOTSTRAPPED
app.bootstrap = flattenFnArray(bootstrap)
app.mount = flattenFnArray(mount)
app.unmount = flattenFnArray(unmount)
return app
}
1.1 reroute.js中的loadApps方法去加载应用
// 预加载应用
async function loadApps () {
let apps = await Promise.all(appsToLoad.map(toLoadPromise))// bootstrap, mount, unmount 方法放在App上
console.log(apps,'apps');
}
1.2 尝试解析index.html中的注册应用的实例
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<script src="/lib/umd/single-spa.js"></script>
<script>
// 参数1 注册应用的名字 参数2 加载app的方法必须返回一个pormise方法
singleSpa.registerApplication(
'app1',
async props => {
// 这个函数需要返回结果
return {
bootstrap: [async props => {}, async props => {}],
mount: async props => {},
unmount: async props => {},
}
},
location => location.hash.startsWith('#/app1'),
{ store: { name: 'zf', age: 10 } }
)
// 启动这个应用
singleSpa.start()
</script>
</body>
</html>
注册应用成功
二、load和mount应用
2.1 reroute.js加载应用方法
// 根据路径来装载应用
async function performaAppChanges () {
// 先卸载不需要的应用
let unmountPoromises = appsTouUmount.map(toUnmountPromise)
// 去加载需要的应用
appsToLoad.map(async app => {
app = await toLoadPromise(app)
app = await toBootstrapPromise(app)
return await toMountPromise(app)
})
// 去挂载应用
appsToMount.map(async app => {
app = await toBootstrapPromise(app)
return await toMountPromise(app)
})
}
2.2 src/lifecycles/bootstrap.js
import { BOOTSTRAPPING, NOT_BOOTSTRAPPED, NOT_MOUNTED } from "../applications/app.helpers";
export async function toBootstrapPromise (app) {
if (app.status !== NOT_BOOTSTRAPPED) {
return app
}
app.status = BOOTSTRAPPING
await app.bootstrap(app.customProps)
app.status = NOT_MOUNTED
return app
}
2.3 src/lifecycles/mounted.js
import { MOUNTED, MOUNTING, NOT_MOUNTED } from "../applications/app.helpers";
export async function toMountPromise (app) {
if (app.status !== NOT_MOUNTED) {
return app
}
app.status = MOUNTING
await app.mount(app.customProps)
app.status = MOUNTED
return app
}
2.4 src/lifecycles/unmount.js
import { MOUNTED, NOT_MOUNTED, UNMOUNTING } from "../applications/app.helpers";
export async function toUnmountPromise (app) {
// 当前应用没有被挂载直接什么都不做
if (app.status != MOUNTED) {
return app
}
app.status = UNMOUNTING
await app.unmount(app.customProps)
app.status = NOT_MOUNTED
return app
}
尝试启动, bootstrap和mount的执行了
2.5 问题与思考
我们在index.html中打印load
<script>
// 参数1 注册应用的名字 参数2 加载app的方法必须返回一个pormise方法
singleSpa.registerApplication(
'app1',
async props => {
console.log('load');
// 这个函数需要返回结果
return {
bootstrap: async props => { console.log('bootstrap1')},
mount: async props => {console.log('mount');},
unmount: async props => {console.log('unmount');},
}
},
location => location.hash.startsWith('#/app1'),
{ store: { name: 'zf', age: 10 } }
)
// 启动这个应用
singleSpa.start()
</script>
如图,是执行了两次
2.6 优化和解决执行两次的问题
改造load.js
export async function toLoadPromise (app) {
if (app.loadPromise) {
return app.loadPromise // 缓存机制
}
return (app.loadPromise = Promise.resolve().then(async () => {
app.status = LOADING_SOURCE_CODE
let { bootstrap, mount, unmount } = await app.loadApp(app.customProps)
// console.log(bootstrap,'bootstrap999');
app.status = NOT_BOOTSTRAPPED
app.bootstrap = flattenFnArray(bootstrap)
app.mount = flattenFnArray(mount)
app.unmount = flattenFnArray(unmount)
return app
}))
}
load现在就只有一次了
2.7 尝试注册两个应用
<script>
// 参数1 注册应用的名字 参数2 加载app的方法必须返回一个pormise方法
singleSpa.registerApplication(
'app1',
async props => {
console.log('load');
// 这个函数需要返回结果
return {
bootstrap: async props => { console.log('bootstrap1')},
mount: async props => {console.log('mount');},
unmount: async props => {console.log('unmount');},
}
},
location => location.hash.startsWith('#/app1'),
{ store: { name: 'zf', age: 10 } }
)
// 参数1 注册应用的名字 参数2 加载app的方法必须返回一个pormise方法
singleSpa.registerApplication(
'app2',
async props => {
console.log('load2');
// 这个函数需要返回结果
return {
bootstrap: async props => { console.log('bootstrap2')},
mount: async props => {console.log('mount2');},
unmount: async props => {console.log('unmount2');},
}
},
location => location.hash.startsWith('#/app2'),
{ store: { name: 'zf2', age: 20 } }
)
// 启动这个应用
singleSpa.start()
</script>
问题和思考: 应用二确实注册成功了,但是只有重新刷新页面才会成功,路由切换并不会成功
三、重写路由
路由改变主要是hashchange和popstate,我们基于这个来重写 新建/src/navigations/navigator-evetns
// hashchange popstate
import { reroute } from './reroute'
export const routingEventsListeningTo = ['hashchange', 'popstate']
function urlReroute () {
reroute([], arguments)
}
const capturedEventListeners = {
hashchange: [],
popstate: []
}
window.addEventListener('hashchange', urlReroute)
window.addEventListener('popstate', urlReroute)
const originalAddEventListener = window.addEventListener
const originalRemoveEventListener = window.removeEventListener
window.addEventListener = function (eventName, fun) {
if (routingEventsListeningTo.indexOf(eventName) >= 0 && !capturedEventListeners[eventName].some(listener => listener == fn)) {
capturedEventListeners[eventName].push(fn)
}
return originalAddEventListener.apply(this,arguments)
}
window.removeEventListener = function (eventName, fn) {
if (routingEventsListeningTo.indexOf(eventName) >= 0) {
capturedEventListeners[eventName] = capturedEventListeners[eventName].filter(l => l !== fn)
return originalRemoveEventListener.apply(this,arguments)
}
}
// 用户可能还会绑定自己的路由事件 vue
// 当我们应用切换后, 还需要处理原来的方法, 需要在应用切换后在执行
- index.html
<div>
<a href="#/app1">应用1</a>
<a href="#/app2">应用2</a>
</div>
- reroute.js
import './navigator-events'
现在点击切换路由也可重写初始化微应用
3.2 兼容h5路由
// 如果是hash路由 hash变化时可以切换
// 浏览器路由,浏览器路由是h5api的 如果切换时不会触发popstate
function patchedUpdateState (updateState, methodName) {
return function () {
const urlBefore = window.location.href
updateState.apply(this, arguments)
const urlAfter = window.location.href
if (urlBefore !== urlAfter) {
// 重新加载应用 传入事件源
urlReroute(new PopStateEvent('popstate'))
}
}
}
window.history.pushState = patchedUpdateState(window.history.pushState, 'pushState')
window.history.replaceState = patchedUpdateState(window.history.replaceState, 'replaceState')
index.html
<div>
<a onclick="app1()">应用1</a>
<a onclick="app2()">应用2</a>
</div>
function app1() {
history.pushState({},'','/app1')
}
function app2() {
history.pushState({},'','/app2')
}