为了验证某些事,我实现了一个toy微前端框架【万字长文,请君一览】

news2024/10/2 12:21:44

众所周知微前端是企业中前端发展的必经之路。

我司搭建了自己的微前端脚手架工具,它将项目变成了“多页应用”,跳转方式由 location.href 这类api进行。所以笔者之前在想:这种方式跳转能不能有动画效果呢?
在“上层建筑”中进行“反直觉”的操作,结果当然是失败的。但是笔者又有了一个新想法:自己实现一个微前端框架,由通过劫持路由和 history 实现一些小操作!

本文将我实现第一版微前端架子的步骤呈现给各位,希望对大家能有一些帮助。


结构

toy微前端结构

这是toy微前端的项目基本结构,简单介绍下主要目录文件:

  • 控制启动的build目录
  • 项目所需后端service目录(由node实现)
  • 主应用main目录
  • 自应用vue2目录(存放vue2项目代码)
  • 自应用vue3目录(存放vue3项目代码)
  • 框架启动和版本控制文件package.json

在vue2、vue3同级,你当然还可以创建react相关目录结构或者使用其他技术栈实现相关项目。这是不限制的,也是微前端的优势。

拿vue来说,你必然知道的是:项目启动是和 package.json 息息相关的,我们来看本框架的此文件:

{
  "name": "toy-micro-web",
  "version": "1.0.0",
  "description": "create micro project for myself",
  "main": "index.js",
  "scripts": {
    "start": "node ./build/run.js"
  },
  "author": "yancy",
  "license": "ISC",
  "devDependencies": {},
  "dependencies": {}
}

没有多余代码,也不必有。其中关键在于:scripts 字段。这是我们命令行的执行命令。
它的意思是:当我们按下 npm start(或 npm run start)时,会通过node去执行 ./build/run.js 文件。

启动你的微前端

这就是上面我们说的启动目录下的唯一一个文件。它要做的也很简单 —— 进入项目对应目录,执行他们各自的启动命令。

const childProcess = require('child_process')
const path = require('path')

const filePath = {
    vue2: path.join(__dirname, '../vue2'),
    vue3: path.join(__dirname, '../vue3'),
    service: path.join(__dirname, '../service'), //启动后端应用node
    // react15: path.join(__dirname, '../react15'),
    // react16: path.join(__dirname, '../react16'),
    main: path.join(__dirname, '../main')
}

// cd 子应用的目录 npm start 启动项目
function runChild () {
    Object.values(filePath).forEach(item => {
      childProcess.spawn(`cd ${item} && npm start`, { stdio: "inherit", shell: true })
    })
}

runChild()

我们借助了 child_process 模块的帮助。

这里是qiankun的思想,一次将所有子应用全部启动。本来想按照qiankun的思路去实现,但是写的时候突然想到这就是一个为了我研究其他东西的小玩具,那么精细干啥。后面第二版第三版也有修改但是代码量就多了不放出来了先。
各位在用新技术尝试的时候也可以把这里按照自己想法改进下。

子应用:vue2

执行完这个文件后,命令行中你会发现依次进入到主应用以及各个子应用中了。我们先来看子应用:以vue2为例

vue2子应用结构

这就是一个普通的vue2项目目录,将其置身于“微前端”场景下时,我们需要着重关注 vue.config.js 以及 src 下的 main.js 文件。

在 vue.config.js 中笔者进行了端口号的重置、指定打包路径、热更新的开启、本地服务的跨域内容等等。最重要的还得是“自定义webpack配置项”了:

// 自定义webpack配置
configureWebpack: {
  resolve: {
    alias: {
      '@': resolve('src'),
    },
  },
  output: {
    // 把子应用打包成 umd 库格式 commonjs 浏览器,node环境
    libraryTarget: 'umd',
    library: `${packageName}`, //在全局环境下获取到打包的内容 ——umd,把这一行去掉与显示在浏览器控制台打印 window.此vue项目文件夹名
    // filename: 'vue2.js', //打包的名字
    // jsonpFunction: `webpackJsonp_${packageName}`
  },
},

我们通过 libraryTarget 字段指定打包格式为 umd!并且通过 library 字段指定了打包出来的名字。

这里是关键,也是微前端中很多操作的根本!其原因随后陈述。

先来看 main.js 文件。都知道这里面主要干了一件事:new Vue

new Vue({
  router,
  render: h => h(App)
}).$mount('#app-vue');

这是一般的写法。但是现在是微前端!在子应用加载前会不会要处理必要的参数?在加载时是不是可能要进行一些额外的处理?切换其他应用后要不要取消监听?…

子应用微前端场景改造

所以我们应当区分情况,并暴露出三个函数:

let instance = null;

const render = () => {
  // 这个函数就可以在微前端框架中应用,我们也可以通过window.vue2获取到内容
  instance = new Vue({
    router,
    render: h => h(App)
  }).$mount('#app-vue');
}

// 通过render触发,但是有一个前提:如果当前是微前端环境则不自己触发,而是需要根据我们的微前端生命周期去对应的触发当前的render函数
if (!window.__MICRO_WEB__) {
  render()
}

// 微前端下
export const bootstrap = () => {
  // 开始加载结构 - 比如一些在加载之前必要的参数处理
  console.log('开始加载')
}
export const mount = () => {
  render();
  // 然后得到vue实例
  console.log('渲染成功')
}
export const unmount = () => {
  // 比如撤销监听事件,或者处理当前容器的显示内容,,,
  console.log('卸载', instance)
}

现在,让我们执行 npm start,看一看 umd 的“魅力”:
window.vue2

而若是将 libraryTarget: 'umd', 这一行去掉,则会打印undefined。说白了就是把子应用打包好作为节点挂载道window上供全局调用。这也就是其原因所在了。

很明显就是依赖webpack的打包特性和 umd 模块的特殊性。
其实微前端并不是一个新的技术,只是新的概念罢了。早期的“服务端组合SSI技术”、以及后来重新被大家认识的“iframe”、还有新兴的“web components技术”,不同于微前端发展初期概念里的“页面级别组合”,他们甚至可以进行组件级别的组合。

这里和另一个技术也“异曲同工” —— webpack5 联邦模块!

const { ModuleFederationPlugin } = require("webpack").container;

//...
  plugins: [
    new ModuleFederationPlugin({
      // MF 应用名称
      name: "app1",
      library: { type: "var", name: "app_1" },
      // MF 模块入口,可以理解为该应用的资源清单
      filename: `remoteEntry.js`,
      // 定义应用导出哪些模块
      exposes: {
        "./utils": "./src/utils",
        "./foo": "./src/foo",
      },
    }),
  ],

这段代码就是联邦模块导出的配置。其中libraryname字段即是 umd 导出的name!

主应用:main

OK,让我们说回toy微前端。
子应用部分已经结束,接下来该“重中之重”,主应用的介绍。主应用担负起了调配子应用、路由拦截、全局通信、应用启动等诸多功能。
我们来看下主应用的结构:
主应用结构

主应用是以vue3实现的,其中micro目录是重点 —— 微前端控制器。
让我来一一介绍。

首先依然是 main.js文件,这里稍有不同:主应用是微前端主体,它当然也要有生命周期,用来和子应用生命周期“遥相呼应”,但正因为是主体所以自己调用自己不太合适,故而主应用的生命周期不能以export的方式写在这里!
上面提到主应用应该肩负起调配子应用的职责,故而我们应当在主应用初始化前注册子应用:

import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import { subNavList } from "./store/sub"
import { registerApp } from "./util"

// 在整个实例初始化之前先将子应用注册好
registerApp(subNavList)

createApp(App).use(router()).mount('#micro_web_main_app')

subNavList很简单,就是子应用的一些信息:路径、名字、需要挂载的节点等,后面我们要根据这些信息处理子应用

export const subNavList = [
    {
      name: 'vue2',
      activeRule: '/vue2',
      container: '#micro-container',
      entry: '//localhost:9004/',
    },
    {
      name: 'vue3',
      activeRule: '/vue3',
      container: '#micro-container',
      entry: '//localhost:9005/',
    },
]

然后registerApp用来将这些子应用注册到微前端中,并且在这个时候注册主应用的生命周期!

import { registerMicroApps, start } from "../../micro"

import { loading } from "../store"

export const registerApp = (list) => {
    // 将子应用注册到主应用里是没有任何效果的,所以我们应该注册到微前端框架里
    // 主应用的生命周期也需要在此时注册好!
    registerMicroApps(list, {
        beforeLoad: [
            () => {
                // 在生命周期中控制loading
                loading.changeLoading(true)
                console.log('开始加载')
            }
        ],
        mounted: [
            () => {
                loading.changeLoading(false)
                console.log('渲染完成')
            }
        ],
        destoryed: [
            () => {
                console.log('卸载完成')
            }
        ]
    })

    start()
}

我们通过参数的方式将生命周期传到处理函数中,这里需要注意的是,主应用的生命周期函数可能不止一个(要干的事情和时机不一样),所以我们用数组形式处理。

先来看registerMicroApps函数:

export const registerMicroApps = (appList, lifeCycle) => {
    setList(appList)

    setMainLifeCycle(lifeCycle)
}

很简答,首先接收子应用列表,然后处理子应用和对应生命周期。setList函数其实就是一个赋值函数,对应的还有一个getList取值函数,这是为了避免随意往window上挂载东西。
setMainLifeCycle也是如此,先将生命周期“存起来”。

什么时候用呢?上一段代码最后还有一个start函数,我们来看下:

export const start = () => {
    // 首先验证当前子应用列表是否为空
    let apps = getList();
    if(!apps.length) {
        console.error('当前没有子应用注册')
        return;
    }
    // 查找到符合当前url的子应用
    let app = currentApp()

    if(app) {
        const { pathname, hash } = window.location
        const url = pathname + hash
        window.history.pushState('', '', url)
    }

    // 这时候我们会发现路由被触发了不止一次,我们可以加一个限制
    window.__CURRENT_SUB_APP__ = app.activeRule
}

首先判断当前有没有子应用注册,若有,取到子应用并将路由赋值,而后将当前行为“告知”全局,我们用“公共变量”__CURRENT_SUB_APP__去接收,这一点是为了方便后面区分“上一个子应用”和“在一个子应用”。

currentApp中即是先通过window.location.pathname获取当前路由,然后在getList中查找:

const filterApp = (key, value) => {
    const currentApp = getList().filter(item => item[key] === value)

    return currentApp && currentApp.length ? currentApp[0] : {}
}

路由监听

我们会发现上面的操作大多和“router”相关,这里就引出了微前端中另一个重点概念:路由监听。以及后期的“路由劫持”!

其实就是“重写路由跳转函数”,重写的原因也很简单:我们需要做一些符合自己需求的额外处理。

export const rewriteRouter = () => {
    window.history.pushState = patchRouter(window.history.pushState, 'micro_push')
    window.history.replaceState = patchRouter(window.history.replaceState, 'micro_repalce')

    // 给新事件加一个事件绑定
    window.addEventListener('micro_push', turnApp);
    window.addEventListener('micro_replace', turnApp);

    // 给返回事件也添加绑定
    window.onpopstate = function () {
        turnApp()
    }
}

我们通过重写pushStatereplaceState绑定了两个新事件,在路由切换后去执行生命周期~

export const turnApp = async () => {
    if(!isTurnChild()) {
        return
    }
    console.log('路由切换')
    // 执行微前端生命周期
    await lifeCycle()
}

其中判断子应用是否切换的函数isTurnChild就是利用了我们刚刚说到的挂载到window上的“公共变量”:

export const isTurnChild = () => {
    // 需要在判断之前先拿到全局变量,用两个全局变量获取上一个子应用和下一个子应用
    window.__ORIGIN_APP__ = window.__CURRENT_SUB_APP__
    if(window.__CURRENT_SUB_APP__ === window.location.pathname) {
        return false
    }
    let currentApp = window.location.pathname.match(/(\/\w+)/)
    if(!currentApp) {
        return
    }
    window.__CURRENT_SUB_APP__ = currentApp[0]
    return true
}

其中currentApp的判断是为了处理路由字符串,方便后面加载资源。

然后我们来看如何执行微前端生命周期lifeCycle

export const lifeCycle = async () => {
    // 获取到上一个子应用:执行他的unload。我们需要先将上一个子应用卸载掉
    const prevApp = findAppByRoute(window.__ORIGIN_APP__)

    // 获取到要跳转的子应用:执行他的生命周期
    const nextApp = findAppByRoute(window.__CURRENT_SUB_APP__)

    console.log(prevApp, nextApp)

    if(!nextApp) {
        return
    }

    if(prevApp && prevApp.destoryed) {
        await destoryed(prevApp)
    }

    const app = await beforeLoad(nextApp)

    await mounted(app)
}

// 作为微前端控制器,也需要有自己的生命周期函数
export const beforeLoad = async (app) => {
    await runMainLifeCycle('beforeLoad')
    app && app.beforeLoad && app.beforeLoad()

    // 获取到子应用所有的显示内容 —— 获取真正的子应用
    const appContext = await loadHtml(app)
    appContext && appContext.beforeLoad && appContext.beforeLoad()

    return appContext
}

export const mounted = async (app) => {
    app && app.mount && app.mount()
    await runMainLifeCycle('mounted')
}

export const destoryed = async (app) => {
    app && app.unmount && app.unmount()
    // 对应的执行一下主应用的生命周期
    await runMainLifeCycle('destoryed')
}

export const runMainLifeCycle = async (type)=> {
    const mainlife = getMainLifeCycle(); //get函数,获取到主应用生命周期数组

    // 等到所有的生命周期执行完毕才能下一步
    await Promise.all(mainlife[type].map(async item => await item()))
}

简单来讲就是一句话:先获取到上一个(也就是要跳出的)子应用,去执行它的卸载生命周期,然后获取到下一个(也就是即将跳转的)子应用,去执行它的“预挂载”生命周期,获取到子应用的内容!然后执行子应用的“挂载”生命周期。因为子应用的节点实际由主应用控制,所以在每次操作时都要去执行主应用对应的生命周期!

findAppByRoute和上面的currentApp作用一样,就是根据 router 获取到子应用信息。

这段代码里为什么充斥着async-await?除了在runMainLifeCycle中要等待主应用生命周期执行外还有一点:在mounted中加载子应用资源!
这也就到了第三个重点:资源的获取和处理。我们来看下。

dom获取、js代码获取&执行

export const loadHtml = async (app) => {
    // 子应用需要显示在哪里
    let container = app.container
    // 子应用的入口是啥
    let entry = app.entry

    const [dom, scripts] = await parseHtml(entry)

    console.log(scripts)

    const ct = document.querySelector(container)

    if(!ct) {
        console.error('容器不存在')
        return
    }
    console.log(ct)
    ct.innerHTML = dom

    return app
}

在此函数中,我们的主要内容就是“下载”html、和链接的css、js资源。app参数就是获取到的子应用信息,前面提到的结构这里就派上用场了。
下载资源并不只是单纯的下载html就行了,headbody中的linkscripts标签也是我们需要关注的地方:我们需要将 js 内容也下载下来方便后面运行:

export const parseHtml = async (entry) => {
    // 资源加载其实是一个get请求,我们去模拟这个过程
    const html = await fetchResource(entry)

    let allScripts = []

    const div = document.createElement('div')
    div.innerHTML = html

    // 标签、link、script(src、代码)
    const [dom, scriptUrl, script] = await getResources(div, entry)

    // 获取所有的js资源
    const fetchedScripts = await Promise.all(scriptUrl.map(async item => await fetchResource(item)))
    allScripts = script.concat(fetchedScripts)
    return [dom, allScripts]
}

fetchResource就是fetch请求函数。不必细说。

笔者在getResources函数中进行了上述的处理。可以看到我将结构分为三类:domscript代码和script链接(然后根据连接请求资源得到代码合并进script中),为了归类我利用了递归扫描html代码。这里只放核心代码 —— 判断标签中有没有srchref属性,如果有,则说明是链接文件(被scriptUrl接收),这时还需要判断链接有没有前缀,因为有的链接是本地连接,如果直接请求资源则会“找不到资源路径”,需要处理下:

// 处理script
if(element.nodeName.toLowerCase() === 'script') {
    const src = element.getAttribute('src')
    if(!src) {
        // 说明是直接在script标签中写代码
        script.push(element.outerHTML)
    } else {
        if(src.startsWith('http')) {
            scriptUrl.push(src)
        } else {
            scriptUrl.push(`http:${entry}/${src}`)
        }
    }

    if(parent) {
        parent.replaceChild(document.createComment('此js文件已经被微前端替换'), element)
    }
}

// link中也会有js内容
if(element.nodeName.toLowerCase() === 'link') {
    const href = element.getAttribute('href')

    if(href.endsWith('.js')) {
        if(href.startsWith('http')) {
            scriptUrl.push(href)
        } else {
            scriptUrl.push(`http:${entry}/${href}`)
        }
    }
}

然后让我们回到loadHtml函数中。此时我们已经拿到 dom 和 script代码了,除了将 dom 挂载到容器中,我们还需要执行 js :

scripts.forEach(item => {
     // 不仅执行js代码,还要挂载生命周期
     sandBox(app, item)
     // performScriptForFunction(item)
})

微前端场景改造

这里当然不止要进行js代码的执行操作 —— 拿 vue2 子项目来说,在上面的描述中我们可以发现除了在实现的时候加入了“微前端场景的判断”和“生命周期函数”以外,我们似乎还没有真正将这些函数用到微前端场景中。
所以,我们在这里 —— 在子应用真正挂载的时候,必须将子应用的生命周期变为“可控态”。

const isCheckLiftCycle = lifecycle => lifecycle && lifecycle.bootstrap && lifecycle.mount && lifecycle.unmount

// 对子应用生命周期的处理和环境变量的设置
export const sandBox = (app, script) => {
    // 设置环境变量
    window.__MICRO_WEB__ = true

    // 运行js代码
    const lifecycle = performScriptForFunction(script, app.name)

    // 将生命周期挂载到app上
    if(isCheckLiftCycle(lifecycle)) {
        app.bootstrap = lifecycle.bootstrap
        app.mount = lifecycle.mount
        app.unmount = lifecycle.unmount
    }
}

话说,这里的参数 app 是什么?“下一个子应用”啊!那为什么我们要这么麻烦,把子应用打包的节点下暴露的函数挂载到子应用上?

其实这么做是为了和前面提到的“生命周期”控制器lifeCycle函数结合起来。在那里我们获取到子应用并执行它的生命周期,这些函数如果不挂载到子应用下,我们其实是没有办法找到他们的。
也就是说,如果我们不用sandBox函数而是直接走performScriptForFunction函数,我们的参数 app 中则只会有一个内容 —— 注册的第一个子应用。
子应用一览

而对于我们之前写的对于生命周期的切换和调用都是没有执行到的。
换句话说,我们其实一直走的是这里的逻辑:

if (!window.__MICRO_WEB__) {
  render()
}

(:不知道各位还是否记得子应用 main.js 文件中的这个判断

performScriptForFunction函数是用来“执行 js 代码”的,这里有两种方案:

  • new Function
  • eval

笔者采用的是第一种,为了展现这两种方式的不同,这里将两种写法都列出:

export const performScriptForFunction = (script, appName) => {
    const scriptText = `
        ${script}
        return window['${appName}']
    `
    return new Function(scriptText).call(window, window)
}

export const performScriptForEval = (script, appName) => {
    const scriptText = `
        () => {
            ${script}
            return window['${appName}']
        }`
    return eval(scriptText).call(window, window)
}

ok。在进入到下一环节之前,还有一个问题“让人疑惑”:为什么要执行 js 代码?在 loadHtml函数中不是已经ct.innerHTML = dom了么?

这里需要注意:你往容器中插入的 dom 是什么 dom?这里又涉及到vue的编译。但这里我们不需关心,如下图便是上文中的“dom”:
控制台打印-请求资源

运行环境沙箱隔离

接下来是微前端中第四个重点:沙箱隔离(也叫“沙箱快照”)。
运行环境沙箱隔离是必要的,它能为“非公共全局变量”提供保护,它和微前端应该算是相辅相成吧。

想比于Map/object,笔者采用了性能更高的Proxy


let defaultValue = {} //子应用沙箱容器

// 更流批的快照沙箱
export class ProxySandbox {
    constructor() {
        this.proxy = null

        this.active()
    }

    active() {
        this.proxy = new Proxy(window, {
            get(target, key) {
                // 解决路由劫持后window指向问题:TypeError Illegal invocation报错
                if(typeof target[key] === 'function') {
                    return target[key].bind(target)
                }
                return defaultValue[key] || target[key]
            },
            set(target, key, value) {
                defaultValue[key] = value
                return true
            }
        })
    }

    inactive() {
        defaultValue = {}
    }
}

简单来说就是把“顶层对象”和window区分开,window并不能拿到当前子应用顶层对象中新加入的值。

沙箱建成后,所有的操作都不再经过window。所以我们应该修改下 js 执行函数performScriptForFunction —— 将快照作为参数传入:

export const performScriptForFunction = (script, appName, global) => {
    window.proxy = global
    const scriptText = `
        return ((window) => {
            ${script}
            return window['${appName}']
        })(window.proxy)
    ` //模仿jquery写法
    return new Function(scriptText)()
}

怎么用沙箱?
在子应用生命周期处理函数sandBox中,我们在“环境变量设置”前实例化沙箱并挂载到子应用上:

const proxy = new ProxySandbox()
if(!app.proxy) {
    app.proxy = proxy
}

并新增传参:

// 运行js代码
const lifecycle = performScriptForFunction(script, app.name, app.proxy.proxy)

然后在生命周期控制器lifeCycle中,卸载子应用前,进行沙箱的销毁:

if(prevApp && prevApp.unmount) {
    if(prevApp.proxy) {
        prevApp.proxy.inactive() //将沙箱销毁
    }
    await destoryed(prevApp)
}

js 已经达到了“隔离”的效果。那如何让一个应用的样式不会影响其他应用呢?
这便是“css样式隔离”。
样式隔离有几种方案:

  • css modules:webpack打包时提供的一种类似命名空间的方案
  • shadow dom:新的js api,将html、样式写到一个虚拟 dom 中,和其他内容没有冲突
  • minicss:将css打包成单独的css文件

第一种方案使用时只需要在 webpack.config.js 中新增 rules 配置即可:

{
  test: /\.(cs|scs)s$/,
  use: [MiniCssExtractPlugin.loader, {
    loader: 'css-loader',
    options: {
      module: true
    }
  }, 'sass-loader']
},

这个就…看各位自己怎么选择吧。但是在一个项目甚至框架中是不可能只有一种处理方式的。

应用间通信

和同一个应用中“组件间通信”类似,微前端中应用间通信也有两种方案:

  • props
  • customEvent

哪种都是我们熟悉的。

对于props来说,因为微前端的特殊性,比如主应用向子应用传参,我们必须通过“子应用处理lifeCycle”函数中的mounted生命周期调用去实现传参。方法如下:
假如某个元素的控制变量是headerStatus

//store.js文件
export const headerStatus = ref(true)

export const changeHeader = type => headerStatus.value = type;

在子应用信息列表sub中引入:

import * as appInfo from "../store"

export const subNavList = [
    {
      name: 'vue2',
      activeRule: '/vue2',
      container: '#micro-container',
      entry: '//localhost:9004/',
      appInfo,
    },
    //...
]

然后在 mount 生命周期被执行时传入:

// 修改上面写到过的mounted函数
export const mounted = async (app) => {
    // mount中的参数是props方式主子应用传参时的参数,与store中的index和sub文件有关
    app && app.mount && app.mount({
        appInfo: app.appInfo,
        entry: app.entry
    })
    await runMainLifeCycle('mounted')
}

然后在对应子应用(我们想让谁接收参数)的 mount 生命周期中就可以获取到:

export const mount = (app) => {
  app.appInfo.header.changeHeader(false) //通过函数修改主应用变量
  render();
  // 然后得到vue实例
  console.log('渲染成功')
}

可以发现,这种方式的弊端和效率之低如此明显,以至于笔者不能忍受。
所以我们采取另一种方案:custom

首先创建一个类 —— 他用来在 window 上创建一个事件:

export class Custom {
    // 监听一定要在触发之前操作
    // 事件监听
    on(name, cb) {
        window.addEventListener(name, (e)=> {
            cb(e.detail)
        })
    }
    // 事件触发
    emit(name, data) {
        const event = new CustomEvent(name, {
            detail: data
        })
        window.dispatchEvent(event)
    }
}

还是拿主应用向子应用传参来说,我们可以在“路由监听”操作前实例化,并挂载到 window 对象上,供全局调用:

const custom = new Custom()
custom.on('test', (data) => {
    console.log(data)
})

window.custom = custom

然后在子应用到 mount 生命周期中执行:

window.custom.emit('test', {
  a: 1
})

切记:监听一定要在触发之前!
此言何解?
我们再来看“子应用之间如何传参”:我们先在 vue3 子应用的 mount 生命周期中监听一个事件:

window.custom.on('test1', (data) => {
  console.log(data)
})

然后在 vue2 子应用的 mount 生命周期中触发这个事件:

window.custom.emit('test1', {
  a: '1-子vue2'
})

现在是没有问题的。
然后我们再在 vue2 子应用的 mount 生命周期中监听一个事件:

window.custom.on('test2', (data) => {
  console.log(data)
})
// 这是刚刚触发的事件
window.custom.emit('test1', {
  a: '1-子vue2'
})

然后到 vue3 子应用的 mount 生命周期中触发这个事件:

// 这是刚刚监听的事件
window.custom.on('test1', (data) => {
  console.log(data)
})
window.custom.emit('test2', {
  a: "1-子vue3"
})

这时我们会发现浏览器中切换相应应用时并没有打印{a: "1-子vue3"}。这是因为“触发和监听时机错误”导致的。我们可以这么改写 vue3 的 mount 生命周期:

window.custom.on('test1', () => {
  window.custom.emit('test2', {
    a: "1-子vue3"
  })
})

应该说在只要有不止一个 emiton 的子应用中都应该采用这种写法保证顺序!这也是 custom 这种方式的瑕疵之处。

全局状态管理

从某种角度来说,全局状态管理 store 和通信是一个作用。我们的写法也类似。
创建 store,笔者选择了“自执行函数”方式:

export const createStore = (initData={}) => (() => {
    let store = initData
    const observers = [] //管理所有的订阅者

    // 获取store
    const getStore = () => store

    // 更新store
    const update = (value) => {
        if(value !== store) {
            // 执行store操作
            const oldValue = store
            store = value
            observers.forEach(async item => await item(store, oldValue))
        }
    }

    // 添加订阅者
    const subscribe = (fn) => {
        observers.push(fn)
    }

    return {
        getStore,
        update,
        subscribe
    }
})()

然后在应用执行的一开始将其初始化并挂载到 window 上。就可以在全局使用了~

子应用预加载

最后,我们可以给“非当前展示的子应用”的子应用一个“预加载”,提高加载性能:
在“子应用启动 start 函数”中:

// 获取到所有子应用的列表,但不包括当前正在显示的
const list = getList().filter(item => !window.location.pathname.startsWith(item.activeRule))

console.log(list, '==-')

// 预加载剩下的所有子应用
await Promise.all(list.map(async item => await parseHtml(item.entry, item.name)))

parseHtml 就是上面说过的“资源加载”函数。再配合上 cache 缓存的话,就可以保证切换到加载过的子应用时迅速展现而不用去再次加载资源!


虎头蛇尾的结尾~

因为当前是第一版,还不确定有没有什么隐藏问题,故而本文没有放出所有代码(太多了!)。当前正在多场景测试 & 邀请了几位好友加入仓库一起以新技术构建第二版,到时可能会将文章拆分细说并放出更多代码,搞成一个专栏,方便各位交流。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/7501.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【文件同步和备份软件】上海道宁为您带来GoodSync软件,让您轻松备份和同步您的文件

GoodSync发布于2006年 是一种简单可靠的文件同步和 备份解决方案 使用GoodSync可以轻松备份和 同步您的文件 确保您的文件绝对不会丢失 GoodSync企业版是 为商用设计的 数据备份、同步和恢复软件 适用于所有平台、项目或业务环境 保护我们的商业数据 开发商介绍 Siber…

golang开发相关面试题

目录 go有哪些数据类型? 方法与函数有什么区别? 方法中值接收者与指针接收者的区别是什么? 函数返回局部变量的指针是否安全? 函数参数传递值是值传递还是引用传递? defer关键字的实现原理? 内置函数make和new的区别? slice底层实现原理? array与slice的区别是…

GICv3和GICv4虚拟化

本文档翻译自文档Arm Generic Interrupt Controller v3 and v4 - Virtualization 1 虚拟化 Armv8-A选择性的支持虚拟化。为了完成该功能,GICv3也支持虚拟化。GICv3中对虚拟化的支持包括如下功能: CPU Interface寄存器的硬件虚拟化产生和发送虚拟中断的…

pytorch深度学习实战lesson14

第十四课 丢弃法(Dropout) 目录 理论部分 实践部分 从零开始实现: 简洁实现: 理论部分 这节课很重要,因为沐神说这个丢弃法比上节课的权重衰退效果更好! 为什么期望没变? 如上图所示&#…

java main方法控制日志级别

背景: 今天想用main方法去调用http请求,结果已经没什么问题了,但是打印了一大堆Http业务内部的日志信息,特别挡路,导致想看到的业务输出看不到,所以经过多方求证,进行了日志等级处理。 默认情…

【Pytorch with fastai】第 5 章 :图像分类

🔎大家好,我是Sonhhxg_柒,希望你看完之后,能对你有所帮助,不足请指正!共同学习交流🔎 📝个人主页-Sonhhxg_柒的博客_CSDN博客 📃 🎁欢迎各位→点赞…

电商项目缓存问题的解决方案(初步)

内容分类 容量规化 架构设计 数据库设计 缓存设计 框架选型 数据迁移方案 性能压测 监控报警 领域模型 回滚方案 高并发 分库分表 优化策略 负载均衡 软件负载 nginx:它自身的高可用是用lvs去保证。 下单需要登录 > 需要Session > 分布式Ses…

好书赠送丨海伦·尼森鲍姆著:《场景中的隐私——技术、政治和社会生活中的和谐》,王苑等译

开放隐私计算 收录于合集#书籍分享1个 开放隐私计算 开放隐私计算OpenMPC是国内第一个且影响力最大的隐私计算开放社区。社区秉承开放共享的精神,专注于隐私计算行业的研究与布道。社区致力于隐私计算技术的传播,愿成为中国 “隐私计算最后一公里的服…

[附源码]java毕业设计基于javaweb电影购票系统

项目运行 环境配置: Jdk1.8 Tomcat7.0 Mysql HBuilderX(Webstorm也行) Eclispe(IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持)。 项目技术: SSM mybatis Maven Vue 等等组成,B/S模式 M…

公众号运营建议与反思分享,建议收藏

正所谓有总结才会有成长,公众号运营也是如此。 公众号运营不是一朝一夕的事情,经过岁月的洗礼和千锤百炼,也总归是有了自己的一套经验和技巧。 对于公众号运营有什么建议?值得大家反思什么?今天伯乐网络传媒就来给大…

Boost升压电路调试

背景: 项目用到了一款升压电路,将12V升压到32V,电流要求有12A,最大18A。 设计的方案是使用Boost Controller 外置MOS来实现。 选定的Controller芯片为Maxim的MAX25203。 问题: 回板后进行调试,在不使能…

活动预告|“构建新安全格局”专家研讨会即将开幕

应急管理承担着防范化解重大风险、及时应对处置各类突发事件的重要职责,担负保护人民群众生命财产安全和维护社会稳定的重要使命。过去一年是我国应急管理体系和能力建设经受严峻考验的一年,也是实现大发展的一年。 11月17日,由中央党校科研部…

Python简单实现人脸识别检测, 对某平台美女主播照片进行评分排名

前言 嗨喽~大家好呀,这里是魔王呐 ❤ ~! 开发环境: Python 3.8 Pycharm 2021.2 模块使用: 第三方模块 requests >>> pip install requests tqdm >>> pip install tqdm 简单实现进度条效果 自带模块 os base64 采集代码 导入模块 # 数…

vue封装的echarts组件被同一个页面多次引用无法正常显示问题(已解决)

问题:第二张图显示空白,折线图并没有展示出来 当我们在封装了echarts组件之后,需要在同一个页面中引入多次时,会出现数据覆盖等一系列问题 当时我是修改了id也无济于事,达不到我需要的效果 解决方案 将我们封装的组件…

HTML5简明教程系列之HTML5 表格与表单(二)

HTML的第二弹也来了,最近高产似母猪,状态也不错,代码来源为实验课。本期主要内容为:HTML表格与DIV应用、HTML表单。上期基础部分的传送门: HTML5简明教程系列之HTML5基础(一)_Thomas_Lbw的博客-…

【进程复制】

目录地址偏移量fork函数fork练习地址偏移量 PCB结构体: struct task_struct { PID ststus ; … } 页面的内存大小是固定的,不足一页会给一页,大于一页会给一个整页数 比如一页大小为4K,地址除4K商是页号,余数是在该页…

Vue(六)——使用脚手架(3)

目录 webStorage localStorage sessionStorage todolist案例中使用 组件自定义事件 绑定 解绑 总结 全局事件总线 消息发布与订阅 nextTick 过渡与动画 webStorage 这不是vue团队开发的,不需要写在xx.vue当中,只需写在xx.html当中即可。 什…

Linux下C++开发笔记--g++命令

目录 1--前言 2--开发环境搭建 3--g重要编译参数 4--实例 1--前言 最近学习在linux环境下进行C开发的基础知识,参考的教程是基于VSCode和CMake实现C/C开发 | Linux篇,非常适合小白入门学习。 2--开发环境搭建 ①安装gcc、g和gdb: sud…

深度学习入门(三十七)计算性能——硬件(TBC)

深度学习入门(三十七)计算性能——硬件(CPU、GPU)前言计算性能——硬件(CPU、GPU)课件电脑提升CPU利用率①提升CPU利用率②CPU VS GPU提升GPU利用率CPU/GPU带宽更多的CPU和GPUCPU/GPU高性能计算编程总结教材…

SpringBoot整合dubbo(一)

第一次整合,使用无注册中心方式 一、首先,项目分为三个模块,如下图,dubbo-interface(要发布的接口)、dubbo-provider(接口的具体实现,服务提供者)、dubbo-consumer&#…