手把手教你写一个简易的微前端框架(一)

简介: 手把手教你写一个简易的微前端框架

最近看了几个微前端框架的源码(single-spaqiankunmicro-app),感觉收获良多。所以打算造一个迷你版的轮子,来加深自己对所学知识的了解。

这个轮子将分为五个版本,逐步的实现一个最小可用的微前端框架:

  1. 支持不同框架的子应用(v1 分支)
  2. 支持子应用 HTML 入口(v2 分支)
  3. 支持沙箱功能,子应用 window 作用域隔离、元素隔离(v3 分支)
  4. 支持子应用样式隔离(v4 分支)
  5. 支持各应用之间的数据通信(main 分支)

每一个版本的代码都是在上一个版本的基础上修改的,所以 V5 版本的代码是最终代码。

Github 项目地址:https://github.com/woai3c/mini-single-spa

V1 版本

V1 版本打算实现一个最简单的微前端框架,只要它能够正常加载、卸载子应用就行。如果将 V1 版本细分一下的话,它主要由以下两个功能组成:

  1. 监听页面 URL 变化,切换子应用
  2. 根据当前 URL、子应用的触发规则来判断是否要加载、卸载子应用

监听页面 URL 变化,切换子应用

一个 SPA 应用必不可少的功能就是监听页面 URL 的变化,然后根据不同的路由规则来渲染不同的路由组件。因此,微前端框架也可以根据页面 URL 的变化,来切换到不同的子应用:

// 当 location.pathname 以 /vue 为前缀时切换到 vue 子应用
https://www.example.com/vue/xxx
// 当 location.pathname 以 /react 为前缀时切换到 react 子应用
https://www.example.com/react/xxx

这可以通过重写两个 API 和监听两个事件来完成:

  1. 重写 window.history.pushState()
  2. 重写 window.history.replaceState()
  3. 监听 popstate 事件
  4. 监听 hashchange 事件

其中 pushState()replaceState() 方法可以修改浏览器的历史记录栈,所以我们可以重写这两个 API。当这两个 API 被 SPA 应用调用时,说明 URL 发生了变化,这时就可以根据当前已改变的 URL 判断是否要加载、卸载子应用。

// 执行下面代码后,浏览器的 URL 将从 https://www.xxx.com 变为 https://www.xxx.com/vue
window.history.pushState(null, '', '/vue')

当用户手动点击浏览器上的前进后退按钮时,会触发 popstate 事件,所以需要对这个事件进行监听。同理,也需要监听 hashchange 事件。

这一段逻辑的代码如下所示:

import { loadApps } from '../application/apps'
const originalPushState = window.history.pushState
const originalReplaceState = window.history.replaceState
export default function overwriteEventsAndHistory() {
    window.history.pushState = function (state: any, title: string, url: string) {
        const result = originalPushState.call(this, state, title, url)
        // 根据当前 url 加载或卸载 app
        loadApps()
        return result
    }
    window.history.replaceState = function (state: any, title: string, url: string) {
        const result = originalReplaceState.call(this, state, title, url)
        loadApps()
        return result
    }
    window.addEventListener('popstate', () => {
        loadApps()
    }, true)
    window.addEventListener('hashchange', () => {
        loadApps()
    }, true)
}

从上面的代码可以看出来,每次 URL 改变时,都会调用 loadApps() 方法,这个方法的作用就是根据当前的 URL、子应用的触发规则去切换子应用的状态:

export async function loadApps() {
    // 先卸载所有失活的子应用
    const toUnMountApp = getAppsWithStatus(AppStatus.MOUNTED)
    await Promise.all(toUnMountApp.map(unMountApp))
    // 初始化所有刚注册的子应用
    const toLoadApp = getAppsWithStatus(AppStatus.BEFORE_BOOTSTRAP)
    await Promise.all(toLoadApp.map(bootstrapApp))
    const toMountApp = [
        ...getAppsWithStatus(AppStatus.BOOTSTRAPPED),
        ...getAppsWithStatus(AppStatus.UNMOUNTED),
    ]
    // 加载所有符合条件的子应用
    await toMountApp.map(mountApp)
}

这段代码的逻辑也比较简单:

  1. 卸载所有已失活的子应用
  2. 初始化所有刚注册的子应用
  3. 加载所有符合条件的子应用
    根据当前 URL、子应用的触发规则来判断是否要加载、卸载子应用
    为了支持不同框架的子应用,所以规定了子应用必须向外暴露 bootstrap()mount()unmount() 这三个方法。bootstrap() 方法在第一次加载子应用时触发,并且只会触发一次,另外两个方法在每次加载、卸载子应用时都会触发。

不管注册的是什么子应用,在 URL 符合加载条件时就调用子应用的 mount() 方法,能不能正常渲染交给子应用负责。在符合卸载条件时则调用子应用的 unmount() 方法。

registerApplication({
    name: 'vue',
    // 初始化子应用时执行该方法
    loadApp() { 
        return {
            mount() {                
                // 这里进行挂载子应用的操作
                app.mount('#app')
            },
            unmount() {
                // 这里进行卸载子应用的操作 
                app.unmount()
            },
        }
    },
    // 如果传入一个字符串会被转为一个参数为 location 的函数
    // activeRule: '/vue' 会被转为 (location) => location.pathname === '/vue'
    activeRule: (location) => location.hash === '#/vue'
})

上面是一个简单的子应用注册示例,其中 activeRule() 方法用来判断该子应用是否激活(返回 true 表示激活)。每当页面 URL 发生变化,微前端框架就会调用 loadApps() 判断每个子应用是否激活,然后触发加载、卸载子应用的操作。

何时加载、卸载子应用

首先我们将子应用的状态分为三种:

  • bootstrap,调用 registerApplication() 注册一个子应用后,它的状态默认为 bootstrap,下一个转换状态为 mount
  • mount,子应用挂载成功后的状态,它的下一个转换状态为 unmount
  • unmount,子应用卸载成功后的状态,它的下一个转换状态为 mount,即卸载后的应用可再次加载。

现在我们来看看什么时候会加载一个子应用,当页面 URL 改变后,如果子应用满足以下两个条件,则需要加载该子应用:

  1. activeRule() 的返回值为 true,例如 URL 从 / 变为 /vue,这时子应用 vue 为激活状态(假设它的激活规则为 /vue)。
  2. 子应用状态必须为 bootstrapunmount,这样才能向 mount 状态转换。如果已经处于 mount 状态并且 activeRule() 返回值为 true,则不作任何处理。

如果页面的 URL 改变后,子应用满足以下两个条件,则需要卸载该子应用:

  1. activeRule() 的返回值为 false,例如 URL 从 /vue 变为 /,这时子应用 vue 为失活状态(假设它的激活规则为 /vue)。
  2. 子应用状态必须为 mount,也就是当前子应用必须处于加载状态(如果是其他状态,则不作任何处理)。然后 URL 改变导致失活了,所以需要卸载它,状态也从 mount 变为 unmount

API 介绍

V1 版本主要向外暴露了两个 API:

  1. registerApplication(),注册子应用。
  2. start(),注册完所有的子应用后调用,在它的内部会执行 loadApps() 去加载子应用。

registerApplication(Application) 接收的参数如下:

interface Application {
    // 子应用名称
    name: string
    /**
     * 激活规则,例如传入 /vue,当 url 的路径变为 /vue 时,激活当前子应用。
     * 如果 activeRule 为函数,则会传入 location 作为参数,activeRule(location) 返回 true 时,激活当前子应用。
     */
    activeRule: Function | string
    // 传给子应用的自定义参数
    props: AnyObject
    /**
     * loadApp() 必须返回一个 Promise,resolve() 后得到一个对象:
     * {
     *   bootstrap: () => Promise<any>
     *   mount: (props: AnyObject) => Promise<any>
     *   unmount: (props: AnyObject) => Promise<any>
     * }
     */
    loadApp: () => Promise<any>
}

一个完整的示例

现在我们来看一个比较完整的示例(代码在 V1 分支的 examples 目录):

let vueApp
registerApplication({
    name: 'vue',
    loadApp() {
        return Promise.resolve({
            bootstrap() {
                console.log('vue bootstrap')
            },
            mount() {
                console.log('vue mount')
                vueApp = Vue.createApp({
                    data() {
                        return {
                            text: 'Vue App'
                        }
                    },
                    render() {
                        return Vue.h(
                            'div',     // 标签名称
                            this.text  // 标签内容
                        )
                    },
                })
                vueApp.mount('#app')
            },
            unmount() {
                console.log('vue unmount')
                vueApp.unmount()
            },
        })
    },
    activeRule:(location) => location.hash === '#/vue',
})
registerApplication({
    name: 'react',
    loadApp() { 
        return Promise.resolve({
            bootstrap() {
                console.log('react bootstrap')
            },
            mount() {
                console.log('react mount')
                ReactDOM.render(
                    React.createElement(LikeButton),
                    $('#app')
                );
            },
            unmount() {
                console.log('react unmount')
                ReactDOM.unmountComponentAtNode($('#app'));
            },
        })
    },
    activeRule: (location) => location.hash === '#/react'
})
start()

演示效果如下:

小结

V1 版本的代码打包后才 100 多行,如果只是想了解微前端的最核心原理,只看 V1 版本的源码就可以了。

V2 版本

V1 版本的实现还是非常简陋的,能够适用的业务场景有限。从 V1 版本的示例可以看出,它要求子应用提前把资源都加载好(或者把整个子应用打包成一个 NPM 包,直接引入),这样才能在执行子应用的 mount() 方法时,能够正常渲染。

举个例子,假设我们在开发环境启动了一个 vue 应用。那么如何在主应用引入这个 vue 子应用的资源呢?首先排除掉 NPM 包的形式,因为每次修改代码都得打包,不现实。第二种方式就是手动在主应用引入子应用的资源。例如 vue 子应用的入口资源为:

那么我们可以在注册子应用时这样引入:

registerApplication({
    name: 'vue',
    loadApp() { 
        return Promise.resolve({
            bootstrap() {
                import('http://localhost:8001/js/chunk-vendors.js')
                import('http://localhost:8001/js/app.js')
            },
            mount() {
                // ...            
            },
            unmount() {
                // ...            
            },
        })
    },
    activeRule: (location) => location.hash === '#/vue'
})

这种方式也不靠谱,每次子应用的入口资源文件变了,主应用的代码也得跟着变。还好,我们有第三种方式,那就是在注册子应用的时候,把子应用的入口 URL 写上,由微前端来负责加载资源文件。

registerApplication({
    // 子应用入口 URL
    pageEntry: 'http://localhost:8081'
    // ...
})


目录
相关文章
|
1月前
|
前端开发 JavaScript 开发者
颠覆传统:React框架如何引领前端开发的革命性变革
【10月更文挑战第32天】本文以问答形式探讨了React框架的特性和应用。React是一款由Facebook推出的JavaScript库,以其虚拟DOM机制和组件化设计,成为构建高性能单页面应用的理想选择。文章介绍了如何开始一个React项目、组件化思想的体现、性能优化方法、表单处理及路由实现等内容,帮助开发者更好地理解和使用React。
77 9
|
2月前
|
人工智能 前端开发 JavaScript
前端架构思考 :专注于多框架的并存可能并不是唯一的方向 — 探讨大模型时代前端的分层式微前端架构
随着前端技术的发展,微前端架构成为应对复杂大型应用的流行方案,允许多个团队使用不同技术栈并将其模块化集成。然而,这种设计在高交互性需求的应用中存在局限,如音视频处理、AI集成等。本文探讨了传统微前端架构的不足,并提出了一种新的分层式微前端架构,通过展示层与业务层的分离及基于功能的横向拆分,以更好地适应现代前端需求。
|
2月前
|
JavaScript 前端开发 API
Vue.js:现代前端开发的强大框架
【10月更文挑战第11天】Vue.js:现代前端开发的强大框架
81 41
|
1月前
|
前端开发 JavaScript API
前端界的秘密武器:掌握这些框架,让你轻松秒杀99%的同行!
前端开发日新月异,掌握几个明星框架如React、Vue.js和Angular,不仅能让工作更得心应手,还能轻松超越同行。React以高效的虚拟DOM和组件化著称;Vue.js简洁易懂,灵活性高;Angular提供全面的解决方案,适合大型应用。此外,轻量级的Svelte也值得关注,其编译时处理设计提升了应用性能。掌握这些框架,结合深刻理解和灵活运用,助你在前端领域脱颖而出。
31 9
|
2月前
|
JavaScript 前端开发 测试技术
前端全栈之路Deno篇(五):如何快速创建 WebSocket 服务端应用 + 客户端应用 - 可能是2025最佳的Websocket全栈实时应用框架
本文介绍了如何使用Deno 2.0快速构建WebSocket全栈应用,包括服务端和客户端的创建。通过一个简单的代码示例,展示了Deno在WebSocket实现中的便捷与强大,无需额外依赖,即可轻松搭建具备基本功能的WebSocket应用。Deno 2.0被认为是最佳的WebSocket全栈应用JS运行时,适合全栈开发者学习和使用。
135 7
|
2月前
|
缓存 前端开发 JavaScript
前端serverless探索之组件单独部署时,利用rxjs实现业务状态与vue-react-angular等框架的响应式状态映射
本文深入探讨了如何将RxJS与Vue、React、Angular三大前端框架进行集成,通过抽象出辅助方法`useRx`和`pushPipe`,实现跨框架的状态管理。具体介绍了各框架的响应式机制,展示了如何将RxJS的Observable对象转化为框架的响应式数据,并通过示例代码演示了使用方法。此外,还讨论了全局状态源与WebComponent的部署优化,以及一些实践中的改进点。这些方法不仅简化了异步编程,还提升了代码的可读性和可维护性。
|
2月前
|
前端开发 JavaScript 中间件
前端全栈之路Deno篇(四):Deno2.0如何快速创建http一个 restfulapi/静态文件托管应用及oak框架介绍
Deno 是由 Node.js 创始人 Ryan Dahl 开发的新一代 JavaScript 和 TypeScript 运行时,旨在解决 Node.js 的设计缺陷,具备更强的安全性和内置的 TypeScript 支持。本文介绍了如何使用 Deno 内置的 `Deno.serve` 快速创建 HTTP 服务,并详细讲解了 Oak 框架的安装和使用方法,包括中间件、路由和静态文件服务等功能。Deno 和 Oak 的结合使得创建 RESTful API 变得高效且简便,非常适合快速开发和部署现代 Web 应用程序。
115 2
|
2月前
|
前端开发 JavaScript 开发者
qiankun(乾坤)微前端框架简介
qiankun(乾坤)微前端框架简介
221 1
|
2月前
|
前端开发 JavaScript 开发者
探索现代Web前端技术:React框架入门
【10月更文挑战第9天】 探索现代Web前端技术:React框架入门
|
2月前
|
存储 前端开发 JavaScript
前端技术深度探索:从基础到现代框架的实践之旅
前端技术深度探索:从基础到现代框架的实践之旅
47 3