什么是微前端
微前端用通俗的话来讲就是将一个大型的应用拆分开来,实现多团队协作开发,并且无关技术栈,比如一个项目可以包含Vue、React、Angular等等,用三个关键词来概括就是:多合一、跨技术栈、团队协作,近几年来,越来越多的企业级公司不断普及微前端技术,因此掌握一门微前端框架是非常有必要的
qiankun乾坤
随着微前端概念的火热,自然也衍生出了许多的微前端框架,其中比较有名的有singleSpa、qiankun等,后者在基于前者的基础上进行了改进,因此大多数人所知道的第一个微前端框架就是 qiankun,目前qiankun是阿里团队在维护,并且阿里内部也有不少项目是基于qiankun开发的,可见qiankun是得到了一定程度上的认可,所以今天我们就来实现一个简易的qiankun,让我们能在一定程度上对微前端有一个更加深入的了解,lets go!
微前端项目初始化
这里需要大家先去看一下qiankun的官方文档,直接点击上一个标题进去即可,花五分钟阅读一下,能够更好地了解微前端机制!ok,首先,微前端一定得有一个父容器,父容器就是用于将不同技术栈的子应用包裹起来,随后监听url地址的变化去切换不同的子应用,而子应用必须导出三个生命周期函数,分别为:bootstrap、mount、unmount,分别对应子组件的加载、挂载、卸载。知道这些微前端基本规则后,就可以开始手写实现简易版的qiankun了。
- 首先通过vue-cli以及create-react-app分别创建vue2、vue3以及react项目充当这次微前端的子应用
- 再使用任意一个框架去创建一个父容器。
创建完之后的项目整体结构如下所示:
父容器基本骨架
- 父容器主要用于注册子应用并启动,因此我们需要一个注册和启动的方法,根据qiankun的官方文档,我们也按照这个标准去编写父容器的基本骨架:
这里简单解释一下四个字段的含义,name就是当前子应用的名称,entry是当前子应用所运行的路径,container是当前子应用要挂载到父容器的哪个容器中去,activeRule是当前子应用在url地址为该参数的时候激活子应用,我这边在生成html文件中添加了几个id容器,用于挂载不同子应用。
- src/qiankun的文件中一共包含5个文件,分别对应路由的监听、注册以及启动方法的导出、静态资源的处理、路由方法的重写、以及工具类库
子应用基本骨架
- 这里我们先只用关心子应用中的 index.js文件,参照qiankun本身的官方文档,我们可以很快速的完成react、vue2、vue3子应用的基本搭建。(注意,子应用中的生命周期函数都是异步的)
核心流程
- index.js
这部分主要有三个函数,分别对应获取所有的app子应用,注册app子应用的函数(就一个步骤,就是保存当前的子应用内容),启动app,可以看到,我们在父容器最外层中引用的注册app和启动app函数就是在这导出的
import { listenRouter} from "./handleRouter"
import { rewriteRouter } from "./rewriteRouter"
let apps = []
// 获取所有app
export const getApps = () => apps
export const registerMicroApps = (_apps) => {
// 保存所有app
apps = _apps
}
export const start = async () => {
rewriteRouter()
await listenRouter()
}
- rewriteRouter.js
在index.js中,我们启动app的第一步就是进入路由里面,重写路由函数,这里需要大家对浏览器路由的两个api有了解,首先是hash路由的hashchange,这个api在我们进行hash路由跳转的时候,能够监听到路由的hash变化并且能够获取到路由的url地址;第二个api则是history,它和hashchange不同,它这边只支持调用,不支持监听,什么意思呢,我们本身只能使用 window.history.pushState(url),直接跳转,本身并不能监听到我这个跳转的行为,这就有点麻烦了,毕竟微前端就是监听路由的url变化去加载不同的子应用,无法监听到的话,那不是废了嘛?别急,qiankun这边的处理很巧妙,它先是分别创建了两个变量去保存了当前的history中的pushState和replaceState方法,随后将浏览器本身的history方法进行了重写,再在重写的函数内部进行监听,能够监听到之后就可以使用刚才保存的跳转函数去实现页面跳转了,是不是很巧妙。所以我们这里就只实现较为繁琐的history监听。
import { listenRouter } from "./handleRouter"
// 最开始的值 一定要给, 这里用于获取之前的url以及之后的url;
// 在切换url时,通过上一个url去卸载上一个应用,再通过下一个url去加载新应用
// 避免不同的url上面存在多个不该出现的子应用
let nextRoute = window.location.pathname
let prevRoute = ""
export const getPrevRoute = () => prevRoute
export const getNextRoute = () => nextRoute
export const rewriteRouter = () => {
// 必须使用 addEventListener 不会覆盖
window.addEventListener('popstate', () => {
prevRoute = nextRoute
nextRoute = window.location.pathname
listenRouter()
})
// 保存原生的 pushState方法
const originPushState = window.history.pushState
// 监听 pushState方法方法
window.history.pushState = (...args) => {
prevRoute = window.location.pathname
originPushState.apply(window.history, args)
nextRoute = window.location.pathname
listenRouter()
}
const originReplaceState = window.history.replaceState
window.history.replaceState = (...args) => {
prevRoute = window.location.pathname
originReplaceState.apply(window.history, args)
nextRoute = window.location.pathname
listenRouter()
}
}
-
handleRouter.js
在监听到 url 地址的变化后,就要开始加载当前应用了,这边主要分为三个步骤:卸载上一个子应用 => 加载当前应用的html资源等 => 挂载当前应用,在目前的步骤中,挂载子应用会有静态资源不显示的问题(比如图片),那是因为默认静态资源的路径是以 “ / ” 开头的,也就是当前的父容器的绝对路径,而每个子应用是一个单独的个体,他们是根据自身的路径去加载静态资源的,所以我们需要添加一个webpack打包配置:publicPath,这个api是为所有的静态资源文件添加一个路径前缀,那么如何添加呢?让我们看一下官方文档
可以看到,qiankun要求我们在src目录下创建一个 public-path.js,然后通过判断是否为qiankun环境,如果是qiankun环境的话就会给webpack添加一个全局的静态资源前缀,那么我们就照做吧,给所有的子应用都添加一下
做到这里还不够,细心的人会发现官方文档下面还会要求我们配置一下webpack和跨域请求,跨域是为了方便我们后期加载js文件以及其他静态资源,webpack的打包格式为umd格式是为了让我们获取到当前子应用导出的三个生命周期函数,我这里就偷懒,只配了vue子应用(注意:devserver中的port要和父容器中注册的端口号一致)
这部分是 umd 格式下 打包的子应用代码,可以看到如果判断到exports对象存在的话,我们就能够获取到导出的三个生命周期函数,这就是为什么要统一使用umd格式打包
综上所述,我们在加载应用前还要做两件事:添加一个 POWERED_BY_QIANKUN 的全局变量,添加一个 INJECTED_PUBLIC_PATH_BY_QIANKUN 的静态资源前缀
import { getApps } from "./index"
import { importHtml } from "./resolveResource"
import { getPrevRoute, getNextRoute } from "./rewriteRouter"
export const listenRouter = async () => {
const apps = getApps()
// 卸载前面的 app
const prevApp = apps.find(app => getPrevRoute().startsWith(app.activeRule))
unmount(prevApp)
// 获取当前 激活的app
const activeApp = apps.find(app => getNextRoute().startsWith(app.activeRule))
if (!activeApp) return
const { template, getScripts, execScripts } = await importHtml(activeApp.entry)
// 获取挂载的容器
const container = document.querySelector(activeApp.container)
// 添加app到容器中
container.appendChild(template)
// 注入全局变量
window.__POWERED_BY_QIANKUN__ = true
// 给 静态资源url添加前缀
window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__ = activeApp.entry + '/'
const app = await execScripts()
activeApp.bootstrap = app.bootstrap
activeApp.mount = app.mount
activeApp.unmount = app.unmount
// 加载app
await bootstrap(activeApp)
// 挂载app
await mount(activeApp)
}
async function bootstrap(app) {
app?.bootstrap && (await app.bootstrap())
}
// react应用挂载的时候需要一个container
async function mount(app) {
app?.mount && (await app.mount({
container: document.querySelector(app.container),
}))
}
// react应用卸载的时候需要一个container
async function unmount(app) {
app?.unmount && (await app.unmount({
container: document.querySelector(app.container),
}))
}
- resolveResource.js
最后一步便是加载js文件了,这边之所以说是实现一个简易的qiankun,是因为没有考虑到 css 污染 以及 js沙箱 的实现步骤,主要是为了让大家能够对微前端有一定的认识。
import { fetchSource } from "./utils"
export const importHtml = async (url) => {
const template = document.createElement("div");
const html = await fetchSource(url);
template.innerHTML = html
// 获取 所有的 js 脚本
function getScripts() {
const scripts = template.querySelectorAll("script");
return Promise.all(Array.from(scripts).map(script => {
const src = script.getAttribute("src");
if (!src) return Promise.resolve(script.innerHTML)
// 判断是否为本地的 js
return fetchSource(src.startsWith("http") ? src : url + src)
}))
}
async function execScripts() {
const scripts = await getScripts()
// umd 打包格式 会判断 是否为 es5 如果为 es5的话 导出的就是 app 这边手动模拟 es5 环境
const module = { exports: {} }
const exports = module.exports
// 简易版 直接使用 eval 去执行js代码,开发中不推荐使用 eval
scripts.map(script => eval(script))
// 此处 是 app
return module.exports
}
return {
template,
getScripts,
execScripts
}
}
utils.js代码如下,就一个简单的资源请求方法
export const fetchSource = url => fetch(url).then(res => res.text())
预览以及总结总结
完成以上步骤后,我们就启动所有的子应用以及父应用来看看效果吧
这是初次加载父应用的页面
我们点击 react 去跳转到react应用(由于react前面没有配置跨域,因此图片没有加载出来)
我们再来跳转一下 vue2应用,可以看到 vue2 应用成功地被挂载到了我们指定样式的右边容器中
再看一下 vue3应用,也是成功的加载了
总结
本篇文章主要实现了一个基本的qiankun框架,整体上难度不高,仔细阅读文章内容并理解代码含义,能够较为快速地对微前端有相对深入的认知,另外这边建议各位初学者们多动手写,毕竟纸上得来终觉浅。