从零手写微前端qiankun框架【超详细万字长文】

news2024/9/27 19:27:38

项目创建

我们创建如图几个文件夹

  • main:主应用(采用vue3作为技术栈)
  • react:子应用1
  • vue2:子应用2
  • vue3:子应用3
  • service:服务端代码

vue2子应用:

我们在App.vue中写一点点东西

<template>
  <div class="vue2">
    <h1>vue2子应用</h1> 
  </div>
</template>

<script>
export default {
  name: 'HelloWorld',
}
</script>

<style scoped>
.vue2{
  background: #F56C6C;
  height: 90vh;
  display: flex;
  justify-content: center;
  align-items: center;
  color: #ffff;
}

</style>

然后,我们需要对做一些配置,创建vue.config.js配置文件

const path = require('path')
const { name } = require('./package.json')

function resolve (dir) {
    return path.join(__dirname)
}

const port = 9004

module.exports = {
    outputDir: 'dist',                       //打包输出目录
    assetsDir: 'static',                     //静态资源目录
    filenameHashing: true,                   //打包的文件名是否带哈希信息
    publicPath: 'http://localhost:9004',     //确保当前资源不会加载出错
    devServer: {
      contentBase: path.join(__dirname, 'dist'),    //当前服务是通过dist来拿的
      hot: true,                                    //热更新
      disableHostCheck: true,                       //热更新
      port,                                          //端口
      headers: {                                     //本地服务可以被跨域调用,主应用可以拿到子应用的数据
        'Access-Control-Allow-Origin': '*', 
      },
    },
    // 自定义webpack配置
   configureWebpack: {
    resolve: {
      alias: {
        '@': resolve('src'),                        //@代表src目录
      },
    },
    output: {
      // 把子应用打包成 umd 库格式
      libraryTarget: 'umd',                          //umd格式  支持comm.js引入  浏览器、node可以识别
      filename: 'vue2.js',                           //打包出的名称
      library: 'vue2',                               //全局可以通过window.vue2拿到的应用
      jsonpFunction: `webpackJsonp_${name}`,          
    },
  },
}

main.js改造

import Vue from 'vue'
import App from './App.vue'

Vue.config.productionTip = false

let instance =null

const render = () =>{
  instance = new Vue({
    render: h => h(App),
  }).$mount('#app')
  
}


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

//开始加载
export const bootstrap = () => {
  console.log('开始加载');
}
//渲染成功
export const mount = () => {
  render()
  console.log('渲染成功');
}
//卸载
export const unmount = () => {
  console.log('卸载',instance);
}

主应用开发

主应用中央控制器的界面开发

界面效果

点击按钮时,路由有切换

main\src\App.vue

<template>
  <div>
     <MainHeader></MainHeader> 
  </div>
</template>
<script >
import MainHeader from './components/header.vue'
export default{
  components:{ MainHeader },
}
</script>
<style lang="less" scoped>

</style>

main\src\components\header.vue

<template>
    <header>
        <button @click="home()">首页</button>
        <button 
            v-for="(item,index) in navList" 
            :key="index"
             @click="change(item,index)" 
            :class="{'select':index === selectIndex}"
        >{{ item.name}}</button>
    </header>
    <main id="micro-container">
        <div class="home" v-if="selectIndex == null">主-----------------------------页</div>
    </main>
   <footer>
      京公网安备11000002000001号京ICP证030173号©2022 
   </footer>
</template> 

<script>
import { watch ,ref} from 'vue'
//引入路由方法
import { useRoute, useRouter } from 'vue-router'
export default {
    setup(props,context){
        //router是路由的实例对象,包含全局路由信息
        const router = useRouter()
        //route是路由实例内容,包含子路由内容
        const route = useRoute()
        //当前选中按钮
        let selectIndex = ref(null)
        //导航菜单
        const navList = [
            {name:"应用1",url:"vue1"},
            {name:"应用2",url:"vue2"},
            {name:"应用3",url:"react"},
        ]
        //点击导航菜单切换路由和央样式
        function change(item,index) {
             selectIndex.value = index
             router.push(item.url)
        }
        //单击主页
        function home(){
            router.push('/')
            selectIndex.value = null
        }
        //解决刷新界面时,路由和内容不匹配的异常
        watch(route,(v)=>{
            let index = navList.findIndex(res => v.fullPath.indexOf(res.url) > -1)
            selectIndex.value = index === -1 ? null : index
        },{ deep:true})
        return {
             navList,
             change,
             selectIndex,
             router,
             home
        }
    }
}
</script>

<style>
header{
    height: 40px;
    background: #409EFF;
    display: flex;
    justify-content: center;
    align-items: center;
}
button {
    margin-left: 15px;
}
main{
    height: 850px;
}
footer{
    height: 40px;
    line-height: 40px;
    text-align: center;
    color: #fff;
    background: #67C23A;
}
.select{
    background: #67C23A;
    border: none;
    color: #fff;
}
.home{
    display: flex;
    justify-content: center;
    align-items: center;
    height: 100%;
    font-size: 25px;
}
</style>

main\src\router\index.js

import { createRouter, createWebHistory } from 'vue-router';
const routes = [
  {
    path: '/react',
    component: () => import('../App.vue'),
  },
  {
    path: '/vue2',
    component: () => import('../App.vue'),
  },
  {
    path: '/vue1',
    component: () => import('../App.vue'),
  },
];

const router = (basename = '') => createRouter({
  history: createWebHistory(basename),
  routes,
});

export default router;

main\src\main.js

import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
const app = createApp(App)
//挂载
app.use(router()).mount('#app')

子应用注册

实现微前端,我们一定需要一个配置文件,包含所有子应用的信息,如子应用名称、访问路径、对应路由等等信息。

然后,将这些信息储存在我们的主应用内。当然这不是优选,我们应该创建一个微前端框架,将我们的这个配置信息传入到微前端框架里去,然后再主应用里调用微前端框架里的方法就行了。

子应用配置文件的创建

首先,我们需要创建一个用于储存子应用配置信息的文件,这个文件一定是定义在我们的主应用里面的。

在sr目录下创建store文件夹并创建sub.js文件

//main\src\store\sub.js
//创建子应用信息
export const navList = [
    {
      name: 'react',
      entry: '//localhost:9003/',
      container: '#micro-container',   //设定将来渲染的容器
      activeRule: '/react',
    },
    {
      name: 'vue1',
      entry: '//localhost:9004/',
      container: '#micro-container',
      activeRule: '/vue1',
    },
    {
      name: 'vue2',
      entry: '//localhost:9005/',
      container: '#micro-container',
      activeRule: '/vue2',
    },
  ];

微前端框架创建

我们首先在主应用main中创建个微前端micro文件夹(当然,这个文件夹和main才是最合理的),然后创建一个index.js入口文件。

index文件的逻辑一定是把我们封装的一些逻辑进行对外暴露的一个文件。因此,我们主逻辑应该写在其他文件里,index文件做一个引入。

那么,我们在main\micro\文件里创建 个start.js文件把,主要用来写我们框架的一些逻辑。我们先在start.js里面定义并暴露一个注册子应用的函数吧,这个函数要干什么,我们现在也不清楚。

//main\micro\start.js
//微前端框架核心逻辑------注册一个子应用
export const registerMicroApps = () => {
    
}

那么,index里面我们引入并对外暴露这个函数就行

//main\micro\index.js
export { registerMicroApps } from './start.js'

我们在主应用创建了一个配置文件,这个配置文件一定是要放入微前端框架里面的,因此,我们的 registerMicroApps函数可以先用来接受一个这样的配置列表,在合适的位置调用registerMicroApps(list)实现配置文件注入微前端框架即可。

为了实现模块化,我们的配置信息可以不放在main\micro\start.js进行储存,我们可以把所有的这种信息单独放在一个文件里进行统一管理。我们创建main\micro\const\subApps.js文件

//用于储存信息的声明
let list = []

//用于获取信息的函数
export const getList = () => list

//用于设置信息的函数
export const setList = appList => list = appList

这样,我们写入信息可以用 setList(list)

读取信息用 getList()

我们在start.js中,写下如下逻辑

import { setList } from "./const/subApps"

export const registerMicroApps = (appList) => {
    setList(appList)
}

这样,我们在外部调用registerMicroApps()函数时,就可以将配置文件储存在我们的框架里。

我们可以在main.js中引入这个函数,并把配置文件传递进去

main\src\main.js

//引入的不再是Vue构造函数了,引入的是一个名为createApp的工厂函数
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'

import { registerMicroApps } from '../micro/index'
import {  navList  } from './store/sub.js'

registerMicroApps(navList)

//创建应用实例对象——app(类似于之前Vue2中的vm,但app比vm更“轻”)
const app = createApp(App)
//挂载
app.use(router()).mount('#app')

ok,现在我们的子应用配置信息已经注册到我们的微前端框架里了。

微前端框架

实现微前端,我们的大致思路应该如下:

  1. 监视页面路由的变化
  2. 根据当前页面路由匹配子应用
  3. 主应用加载子应用
  4. 特定容器渲染子应用

准备工作

在qinkun里,主应用的main.js中引入两个函数registerMicroApps, start,仿照qinkun,我们创建自己的微前端框架文件夹,命名micro-fe 我们在主应用的main.js仿照qiankun做出如下配置

//引入的不再是Vue构造函数了,引入的是一个名为createApp的工厂函数
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'

import { registerMicroApps, start } from './micro-fe';

registerMicroApps([
    {
      name: 'vue1', // app name registered
      entry: 'http://localhost:9005/',
      container: '#micro-container',
      activeRule: '/vue1',
    },
    {
      name: 'vue2',
      entry: 'http://localhost:9005/',
      container: '#micro-container',
      activeRule: '/vue2',
    },
    {
        name: 'react',
        entry: 'http://localhost:9005/',
        container: '#micro-container',
        activeRule: '/react',
      },
 ]);
  
start();

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

我们的主应用src\micro-fe文件夹内应该有一个入口index.js文件,按照主应用的引入逻辑,我们应该暴露两个函数。

mainApp\src\micro-fe\index.js

 //注册微前端
export const registerMicroApps = (apps) =>{
  
}
//启动微前端
export const start = () =>{} 

主应用main.js中registerMicroApps函数在执行时,传入了各个子应用的配置文件apps,因此,在registerMicroApps函数里,我们可以将获取的apps配置列表进行储存。同时,我们可以定义一个获取配置列表的函数,方便以后拿到我们传入的apps配置列表。

//创建_apps变量储存主应用传递进来的子应用配置
 let _apps = []
//子应用配置列表获取函数
 export const getApps = () => _apps
 //微前端框架核心逻辑-----注册函数
 export const registerMicroApps = (apps) =>{
    _apps = apps
 }
 
 //微前端核心逻辑------微前端的运行原理
 export const start = () =>{} 

监视路由变化

我们的satrt函数里,应该是我们微前端框架的核心逻辑,需要实现监视页面路由的变化、根据当前页面路由匹配子应用、主应用加载子应用、特定容器渲染子应用四个核心功能。

首先,我们实现路由的监听,路由分为hash路由和history路由。

hash路由

对于hash路由的变化,我们可以使用window.onhashchange来监听,比较容易。本实例中,我们暂时不考虑。

history路由

方法说明
back()参照当前页面,返回历史记录中的上一条记录(即返回上一页),您也可以通过点击浏览器工具栏中的←按钮来实现同样的效果。
forward()参照当前页面,前往历史记录中的下一条记录(即前进到下一页),您也可以通过点击浏览器工具栏中的→按钮来实现同样的效果。
go()参照当前页面,根据给定参数,打开指定的历史记录,例如 -1 表示返回上一页,1 表示返回下一页。
pushState()向浏览器的历史记录中插入一条新的历史记录。
replaceState()使用指定的数据、名称和 URL 来替换当前历史记录。

对于history路由, 其history.go、history.back、history.forword 等方法我们可以使用popstate事件进行监听。 (浏览器的前进后退会触发history.go、history.back方法)。

因此,我们的路由变化后,核心处理函数应该是

window.addEventListener('popstate',()=>{
   console.log("1.路由history.go等方法被触发");
   //做一些事情
})

但是,浏览器历史记录的pushState、及repalceState方法不会被popstate事件监听到,因此,我们需要重写pushState、及repalceState的原生方法。

const rawPushState = window.history.pushState
window.history.pushState = (...args) =>{
  rawPushState.apply(window.history,args)
  console.log("1.触发popstate事件");
}
const rawReplaceState = window.history.replaceState
window.history.repalceState = (...args) =>{
  rawReplaceState .apply(window.history,args)
  console.log("1.触发replaceState事件");
}

OK,要实现路由的监听,我们的start函数里应该首先执行这三个方法。为了逻辑更清晰,我们将这些函数封装到一个js文件里,在satrt中进行引入执行。

在src\micro-fe\文件夹下创建rewrite-router.js

export const rewriteRouter = () =>{

    //1.监视路由的变化
    //  hash路由 window.onhashchange
    //  history路由
    //      history.go、history.back、history.forword 使用popstate事件:window.onpopstate 
    window.addEventListener('popstate',()=>{
        console.log("1.路由history.go等方法被触发");
    })

    //pushState、及repalceState  popstate事件监听不到,我们需要重写pushState、及repalceState的原生方法
    const rawPushState = window.history.pushState
    window.history.pushState = (...args) =>{
        rawPushState.apply(window.history,args)
        console.log("1.触发popstate事件");
    }
    const rawReplaceState = window.history.replaceState
    window.history.repalceState = (...args) =>{
        rawReplaceState .apply(window.history,args)
        console.log("1.触发replaceState事件");
    }

}

然后,在start函数里(mainApp\src\micro-fe\index.js)进行引入使用

import { rewriteRouter } from './rewrite-router'
//创建_apps变量储存主应用传递进来的子应用配置
 let _apps = []
//子应用配置列表获取函数
 export const getApps = () => _apps

 //微前端框架核心逻辑-----注册函数
 export const registerMicroApps = (apps) =>{
    _apps = apps
 }
 
 
 //微前端核心逻辑------微前端的运行原理
 export const start = () =>{
    //1.监视路由的变化
    rewriteRouter()

    //2.匹配子应用

    //3.加载子应用

    //4.渲染子应用

} 

根据路由变化,匹配子应用

创建路由匹配处理函数

在监听到路由的变化后,我们应该做一些处理

window.addEventListener('popstate',()=>{
    // 路由处理相关逻辑
})

window.history.pushState = (...args) =>{
    // 路由处理相关逻辑
})
   
window.history.repalceState = (...args) =>{
    // 路由处理相关逻辑  
})

为了结构更加清晰,我们将这些逻辑封装成一个函数,在rewrite-router.js文件里进行引入

在src\micro-fe\文件夹下创建handle-router.js文件

//处理路由变化
//获取子应用的配置列表,用于路由匹配
import { getApps } from "./index";

export const handleRouter = async () => {
    console.log('2.路由已经发生变化,现在进行处理');
}
//处理路由相关变化逻辑
import { handleRouter } from "./handle-router";
export const rewriteRouter = () =>{
    history.go、history.back、history.forword 使用popstate事件:window.onpopstate 
    window.addEventListener('popstate',()=>{
        console.log("1.路由history.go等方法被触发");
        handleRouter()
    })
    const rawPushState = window.history.pushState
    window.history.pushState = (...args) =>{
        rawPushState.apply(window.history,args)
        handleRouter()
    }
    const rawReplaceState = window.history.replaceState
    window.history.repalceState = (...args) =>{
        rawReplaceState .apply(window.history,args)
        handleRouter()
    }
}

根据当前路由匹配子应用

匹配子应用的逻辑大致如下:

  • 获取当前的路由路径
  • 去 apps 里面查找
//处理路由变化的
import { getApps } from "./index";
import { importHTML } from './import-html'
export const handleRouter = async () => {
    console.log('2.路由已经发生变化,现在进行处理');
    //2.匹配子应用
    //  2.1获取当前的路由路径
    console.log('2.1获取当前路由路径',window.location.pathname);
    //  2.2去 apps 里面查找
    const apps = getApps()
    const app = apps.find(item => window.location.pathname.startsWith(item.activeRule))
    if(!app){
        return
    }
}

加载子应用

匹配到子应用后,我们可以获取到子应用的配置对象,像这样

   {
      name: 'vue1', // app name registered
      entry: 'http://localhost:9005/',
      container: '#micro-container',
      activeRule: '/vue1',
    },

现在,我们需要加载子应用的所有资源,如HTML、CSS及javasrcipt。

获取并渲染html内容

  • 使用ajax获取子应用对应域名下的html
  • 获取要渲染html的dom元素
  • 将获取的html挂载在获取的dom元素上
//mainApp\src\micro-fe\handle-router.js
import { getApps } from "./index";
import { importHTML } from './import-html'
export const handleRouter = async () => {
   
    //2.匹配子应用
    const apps = getApps()
    const app = apps.find(item => window.location.pathname.startsWith(item.activeRule))
    if(!app){
        return
    }
  
    //3.加载子应用
    // 使用 fetch 获取子应用对应域名下的html
    html = await fetch(app.entry).then(res => res.text())
    //获取要渲染html的dom元素
    const container = document.querySelector(app.container)
    //将获取的html挂载在获取的dom元素上
    container.innerHTML = html
}

观察控制台,我们发现html已经被成功挂载在主应用的容器里,但是,页面并没有如我们预期的那样显示出来


原因在于,页面的渲染需要通过执行 js代码来生成内容,浏览器出于安全考虑,innerHTML中生成的script不会被加载。因此,script标签的解析及处理是微前端框架的第一步核心操作。

资源请求函数封装

在乾坤框架里,对于html的资源请求,阿里做了单独的插件。这个插件包含三个核心对象

  • template:用于获取html模板
  • getExternalScripts()函数:获取所有的script标签代码
  • execScripts()函数:获取并执行所有的js脚本

类似的,我们也可以封装一个这样的函数。由于html和js内容都是通过fetch进行请求的,我们可以先将fetch函数进行封装。

  1. 在src\micro-fe\文件夹下创建fetch-resource.js文件
export const fetchResource = url => fetch(url).then(res => res.text())
  1. 我们在src\micro-fe\文件夹下创建import-html.js文件。
import { fetchResource } from "./fetch-resource"

export const importHTML = async (url) =>{
    //获取html模板
    const html = await fetch(url).then(res => res.text())
    const template = document.createElement('div')
    template.innerHTML = html

    //获取所有的script标签代码
    function getExternalScripts () {
        
    }
    //执行所有的js脚本
    function execScripts () {

    }

    return {
        template,
        getExternalScripts,
        execScripts
    }
}
  1. 最后,我们将封装好的 importHT ML函数导出在src\micro-fe\handle-router.js中引入使用
//处理路由变化的

import { getApps } from "./index";
import { importHTML } from './import-html'
export const handleRouter = async () => {
    //2.匹配子应用
    const apps = getApps()
    const app = apps.find(item => window.location.pathname.startsWith(item.activeRule))
    if(!app){
        return
    }
  
    //3.加载子应用
    // 请求子应用的资源:HTML、CSS、js
    const {  template,getExternalScripts,execScripts } = await importHTML(app.entry)
    const container = document.querySelector(app.container)
    container.appendChild(template)
}

和之前一样,这时的html可以被挂载到html页面上,但js不能被正确引入处理。

获取所有的script标签及js代码

  1. 获取所有的script标签:
 constscripts = template.querySelectorAll('script')

注:获取到的constscripts是一个类数组,我们可以使用ES6的Array.from()方法将其转换成真数组

  1. 解析scripts报签内的js内容

对于scripts报签,有两种形式

//行内标签
<script>console.log('行内标签');</script>

//外链标签
<script src="./a.js"></script>
  • 行内标签解析:直接使用innerHTML获取内容
  • 外链标签解析:使用fecth函数请求内容

import-html.js文件内添加相应逻辑:

import { fetchResource } from "./fetch-resource"

export const importHTML = async (url) =>{
    //--------------------------------------获取子应用html内容
    const html = await fetchResource(url)
    const template = document.createElement('div')
    template.innerHTML = html

    //--------------------------------------获取子应用所有js内容
    const scripts = template.querySelectorAll('script')
    
    //获取所有的script标签代码:返回一个[代码,代码]的数组
    function getExternalScripts () {
        return Promise.all(Array.from(scripts).map(script => {
            //获取标签的src属性
            const src  = script.getAttribute('src')
            //加载script标签的js代码
            if(!src){
                return Promise.resolve(script.innerHTML)
            }else{
                return fetchResource(
                    //有的标签没有域名,我们需要添加域名
                    src.startsWith('http') ? src : `${url}${src}`
                )
            }
        }))
    }
  
    //打印获取的scrip标签内容
    getExternalScripts().then((scripts) =>{
        console.log('3.2获取所有scripts代码',scripts);
    })
  
  
    //获取并执行所有的js脚本
    function execScripts () { }
    return { template, getExternalScripts, execScripts }
}

观察浏览器控制台,可以看到,所有的js内容已经获取到了,虽然目前只是字符串形式

渲染子应用

现在,我们已经知道,执行完获取的子应用js代码后,页面上就应该能够渲染出我们的子应用界面。

执行获取的js内容

我们使用eval函数来执行这些脚本内容。

//mainApp\src\micro-fe\import-html.js
import { fetchResource } from "./fetch-resource"

export const importHTML = async (url) =>{
    const html = await fetchResource(url)
    const template = document.createElement('div')
    template.innerHTML = html

    const scripts = template.querySelectorAll('script')
    function getExternalScripts () {
        return Promise.all(Array.from(scripts).map(script => {
            const src  = script.getAttribute('src')
            if(!src){
                return Promise.resolve(script.innerHTML)
            }else{
                return fetchResource(
                    src.startsWith('http') ? src : `${url}${src}`
                )
            }
        }))
    }

    //----------------------------------执行所有的js脚本
    async function execScripts () {
        const script = await getExternalScripts()
        script.forEach(code => {
            eval(code)
        })
    }

    return {
        template,
        getExternalScripts,
        execScripts
    }
}

execScripts函数我们需要在handle-router.js内手动执行

//处理路由变化
import { getApps } from "./index";
import { importHTML } from './import-html'
export const handleRouter = async () => {
    const apps = getApps()
    const app = apps.find(item => window.location.pathname.startsWith(item.activeRule))
    if(!app){
        return
    }
    // 加载子应用
    const {  template,getExternalScripts,execScripts } = await importHTML(app.entry)
    const container = document.querySelector(app.container)
    container.appendChild(template)
    // 渲染子应用
    execScripts()
}

切换路由,我们会发现,我们的页面被成功渲染出来!!!但是,页面整体被替换成了vue2的内容,而不是渲染在我们指定的容器里。

这是为什么呢?其实很简单,当前我们的全局没有微前端的全局变量,vue2子应用在渲染时没有走微前端框架,直接渲染在了id = app这个容器里,而这个容器也是主应用的容器名称。

因此,解决办法也很简单,要么直接更换主应用的容器,或者我们使用我们的微前端框架来手动调用子应用的render函数。

使用微前端框架执行render函数

要使子应用运行在微前端框架里,我们只需要在handle-router.js中添加变量window.MICRO_WEB = true即可

//处理路由变化
import { getApps } from "./index";
import { importHTML } from './import-html'
export const handleRouter = async () => {

    const apps = getApps()
    const app = apps.find(item => window.location.pathname.startsWith(item.activeRule))
    if(!app){
        return
    }

    const {  template,getExternalScripts,execScripts } = await importHTML(app.entry)
    const container = document.querySelector(app.container)
    container.appendChild(template)

    //配置全局变量
    window.__MICRO_WEB__ = true

    // 3.2执行获取的js内容
    execScripts()
}

此时,切换路由就不会出现app根节点被替换的问题,但由于vue2子应用的render函数没有执行,所以页面依旧不会有内容。我们需要做的就是拿到子应用的生命周期函数,手动执行render函数。

那现在核心的问题应该是我们如何拿到子应用的生命周期函数。

umd

子打包的时候,我们做了如下配置

    output: {
      // 把子应用打包成 umd 库格式
      libraryTarget: 'umd',                          //umd格式  支持comm.js引入  浏览器、node可以识别
      filename: 'vue2.js',                           //打包出的文件名称
      library: 'vue2',                               //全局可以通过window.vue2拿到的应用
      jsonpFunction: `webpackJsonp_${name}`,          
    },

我们这里将文件打包成了一个umd模块化的js文件

什么是UMD

UMD (Universal Module Definition),就是一种javascript通用模块定义规范,让你的模块能在javascript所有运行环境中发挥作用。

我们来学习一下umd模块化后的代码,我们在vue2的子应用配置文件vue.config.js中配置mode:‘development’

module.exports = {
  configureWebpack: {
    mode:'development',//这样打包可以看到未压缩的代码
    ...
  },
    ...
}

然后,执行 npm run build 进行打包

观察打包后的vue2.js的代码,它的简化逻辑如下

vue2.js打包后的代码,这里实际是一个自执行函数,主要功能是兼容不同的模块规范

//umd的模块格式
(function webpackUniversalModuleDefinition(root, factory) {
  //兼容Commojs模块规范   node环境
	if(typeof exports === 'object' && typeof module === 'object')
		module.exports = factory();
  //兼容amd规范
	else if(typeof define === 'function' && define.amd)
		define([], factory);
  //兼容ES6
	else if(typeof exports === 'object')
		exports["vue2"] = factory();
	else
    //挂载到去局Window对象上
		root["vue2"] = factory();
})(window, function() {

  //这里是自定义的内部代码
  //最后会返回结果

});

简化逻辑

//umd的模块格式
function webpackUniversalModuleDefinition(root, factory) {
	
})

webpackUniversalModuleDefinition(window, factory)

//factory是框架内部的代码

根据以上知识,通过umd模块化打包后的js文件,我们可以通过window.拿到子应用的内容

获取子应用的生命周期函数

通过umd,我们在微前端框架里可以使用window对象来获取子应用的生命周期函数。

    //3.2----------------------------------执行所有的js脚本
    async function execScripts () {
        const script = await getExternalScripts()
        console.log('3.2获取所有scripts代码',script);
        script.forEach(code => {
            eval(code)
        })
        //子应用的所有信息
        console.log(window['vue2']);
    }

由于每个子应用打包后的名称不一样,这样获取子应用的属性需要知道打包后的子应用名称,因此,我们通过别的方式来获取这个子应用的对象。

通过umd的代码,我们可以知道子应用的信息也可以通过CommnJs的方式拿到,因此,我们手动创建个CommnJs环境。

    //3.2----------------------------------执行所有的js脚本
    async function execScripts () {
        const script = await getExternalScripts()
        console.log('3.2获取所有scripts代码',script);

        //手动构建一个commonJs环境
        const module = { exports:{}}
        const exports = module.exports
        
        script.forEach(code => {
            //eval执行的代码可以访问外部变量
            eval(code)
        })
    }

结合umd的封装函数

if(typeof exports === 'object' && typeof module === 'object')
		module.exports = factory();

我们可以知道,通过 module.exports 就可以拿到所有的子应用属性。为了代码的简洁性,我们通过execScripts函数把这个对象暴露出去,在handle-router.js中获取这个值

import { fetchResource } from "./fetch-resource"

export const importHTML = async (url) =>{
    const html = await fetchResource(url)
    const template = document.createElement('div')
    template.innerHTML = html

    //--------------------------------------获取子应用所有js内容
    const scripts = template.querySelectorAll('script')
    //获取所有的script标签代码:返回一个[代码,代码]的数组
    function getExternalScripts () {
        return Promise.all(Array.from(scripts).map(script => {
            const src  = script.getAttribute('src')
            if(!src){
                return Promise.resolve(script.innerHTML)
            }else{
                return fetchResource(
                    src.startsWith('http') ? src : `${url}${src}`
                )
            }
        }))
    }

    //3.2----------------------------------执行所有的js脚本
    async function execScripts () {
        const script = await getExternalScripts()
        //手动构建一个commonJs环境
        const module = { exports:{}}
        const exports = module.exports
        script.forEach(code => {
            eval(code)
        })
        return module.exports
    }
    return {
        template,
        getExternalScripts,
        execScripts
    }
}

mainApp\src\micro-fe\handle-router.js

//处理路由变化
import { getApps } from "./index";
import { importHTML } from './import-html'
export const handleRouter = async () => {
    //2.匹配子应用
    const apps = getApps()
    const app = apps.find(item => window.location.pathname.startsWith(item.activeRule))
    if(!app){
        return
    }
    //3.加载子应用
    const {  template,getExternalScripts,execScripts } = await importHTML(app.entry)
    const container = document.querySelector(app.container)
    container.appendChild(template)
    //配置全局变量
    window.__MICRO_WEB__ = true
    // 4.渲染子应用
    const appExPports = await execScripts()   
}

可以看到,这里的值我们是正常拿到的。

这时,只要执行mount周期函数里的render函数,我们就可以实现子应用的挂载渲染。

执行子应用的生命周期函数

对获取到的生命周期函数信息,我们可以将其绑定在我们的配置列表app里,然后使用app.mount 对这个生命周期函数进行调用即可。为了便于函数逻辑的添加,我们可以把生命周期函数的调用都封装成函数。

  • 获取子应用生命周期函数
const appExPports = await execScripts()
  • 将函数绑定在app上
app.bootstrap = appExPports.bootstrap
app.mount = appExPports.mount
app.unmount = appExPports.unmount
  • 封装相应生命周期执行函数
async function bootstrap (app) {
    app.bootstrap && (await app.bootstrap())
}
async function mount(app){
    app.mount && (await app.mount({
        container:document.querySelector(app.container)
    }))
}
async function unmount(app){
    app/unmount && (await app.unmount())
}
  • 执行封装的函数
await bootstrap(app)
await mount(app)

handle-router.js完成代码

//处理路由变化
import { getApps } from "./index";
import { importHTML } from './import-html'
export const handleRouter = async () => {
    console.log('2.路由已经发生变化,现在进行处理');
    //2.匹配子应用
    //  2.1获取当前的路由路径
    console.log('2.1获取当前路由路径',window.location.pathname);
    //  2.2去 apps 里面查找
    const apps = getApps()
    const app = apps.find(item => window.location.pathname.startsWith(item.activeRule))
    if(!app){
        return
    }
    //3.加载子应用
    // 3.1将获取的HTML插入容器里
    const {  template,getExternalScripts,execScripts } = await importHTML(app.entry)
    const container = document.querySelector(app.container)
    container.appendChild(template)

    //配置全局变量
    window.__MICRO_WEB__ = true

    // 3.2执行获取的js内容;返回子应用的生命周期函数
    const appExPports = await execScripts()
   
    //3.3将获取的生命周期函数储存在app上
    app.bootstrap = appExPports.bootstrap
    app.mount = appExPports.mount
    app.unmount = appExPports.unmount

    //3.4执行生命周期函数
    await bootstrap(app)
    await mount(app)
    
}

async function bootstrap (app) {
    app.bootstrap && (await app.bootstrap())
}
async function mount(app){
    app.mount && (await app.mount({
        container:document.querySelector(app.container)
    }))
}
async function unmount(app){
    app/unmount && (await app.unmount())
}

切换路由,发现我们的基本功能已经实现了!!

卸载前一个子应用

当我们切换路由时,会发现页面内容不断变长,原因在于原先的子应用并没有被卸载。因此,我们需要实现应用切换时,卸载上一个子应用,实现这个功能,我们就需要得到浏览器的历史记录。

浏览器由于安全问题,没有提供路由的历史记录获取API,我们需要自己在路由监视的代码中自己维护一个路由记录。

  1. 创建两个变量,分别用于储存路由变化前的值及变化后的值
let prevRoute = ''                        //上一个路由
let nextRoute = window.location.pathname  //下一个路由
  1. 变量赋值
  • 对于pushState和replaceState的路由变化

在路由的真正历史记录前后分别对两个值进行赋值

    const rawPushState = window.history.pushState
    window.history.pushState = (...args) =>{
        prevRoute = window.location.pathname
        rawPushState.apply(window.history,args)   //真正的改变历史记录
        nextRoute = window.location.pathname   
        handleRouter()
    }
    const rawReplaceState = window.history.replaceState
    window.history.repalceState = (...args) =>{
        prevRoute = window.location.pathname
        rawReplaceState .apply(window.history,args)
        nextRoute = window.location.pathname   
        handleRouter()
    }
  • 对于popstate事件,我们需要将nextRoute的值赋给prevRoute,nextRoute = window.location.pathname
prevRoute = nextRoute
nextRoute = window.location.pathname
  1. 暴露出这两个变量,供其他地方使用
export const getPrevRoute = () => prevRoute
export const getNextRoute = () => nextRoute
  1. 根据获取的上一次子应用路由,匹配子应用信息,卸载子应用
const apps = getApps()
//------------------卸载上一个路由
const prevApp = apps.find(item => {
  return getPrevRoute().startsWith(item.activeRule)
})

mainApp\src\micro-fe\rewrite-router.js

//处理路由相关变化逻辑
import { handleRouter } from "./handle-router";

let prevRoute = ''                        //上一个路由
let nextRoute = window.location.pathname  //下一个路由

export const getPrevRoute = () => prevRoute
export const getNextRoute = () => nextRoute


export const rewriteRouter = () =>{

    //1.监视路由的变化
    //  hash路由 window.onhashchange
    //  history路由
    //      history.go、history.back、history.forword 使用popstate事件:window.onpopstate 
    window.addEventListener('popstate',()=>{
        prevRoute = nextRoute
        nextRoute = window.location.pathname
        handleRouter()
    })

    //pushState、及repalceState  popstate事件监听不到,我们需要重写pushState、及repalceState的原生方法
    const rawPushState = window.history.pushState
    window.history.pushState = (...args) =>{
        //导航前
        prevRoute = window.location.pathname
        rawPushState.apply(window.history,args)   //真正的改变历史记录
        //导航后
        nextRoute = window.location.pathname   
        handleRouter()
    }
    const rawReplaceState = window.history.replaceState
    window.history.repalceState = (...args) =>{
        //导航前
        prevRoute = window.location.pathname
        rawReplaceState .apply(window.history,args)
        //导航后
        nextRoute = window.location.pathname   
        handleRouter()
    }

}//处理路由变化
import { getApps } from "./index";
import { importHTML } from './import-html'
import { getNextRoute, getPrevRoute } from "./rewrite-router";
export const handleRouter = async () => {
    //1.获取子路由配置信息
    const apps = getApps()
    //2.匹配子应用
    //------------------卸载上一个路由
    const prevApp = apps.find(item => {
        return getPrevRoute().startsWith(item.activeRule)
    })
    if(prevApp) await unmount(prevApp)
    //------------------加载下一个路由
    const app = apps.find(item => getNextRoute().startsWith(item.activeRule))
    
    if(!app) return
    //3.加载子应用
    const {  template,getExternalScripts,execScripts } = await importHTML(app.entry)
    const container = document.querySelector(app.container)
    container.appendChild(template)

    //配置全局变量
    window.__MICRO_WEB__ = true

    // 3.2获取子应用的生命周期函数
    const appExPports = await execScripts()
   
    //4.渲染子应用
    //将获取的生命周期函数储存在app上
    app.bootstrap = appExPports.bootstrap
    app.mount = appExPports.mount
    app.unmount = appExPports.unmount
    //执行生命周期函数
    await bootstrap(app)
    await mount(app)
}

async function bootstrap (app) {
    app.bootstrap && (await app.bootstrap())
}
async function mount(app){
    app.mount && (await app.mount({
        container:document.querySelector(app.container)
    }))
}
async function unmount(app){
    app.unmount && (await app.unmount({
        container:document.querySelector(app.container)
    }))
}

mainApp\src\micro-fe\handle-router.js

//处理路由变化
import { getApps } from "./index";
import { importHTML } from './import-html'
import { getNextRoute, getPrevRoute } from "./rewrite-router";
export const handleRouter = async () => {
    //1.获取子路由配置信息
    const apps = getApps()
    //2.匹配子应用
    //------------------卸载上一个路由
    const prevApp = apps.find(item => {
        return getPrevRoute().startsWith(item.activeRule)
    })
    if(prevApp) await unmount(prevApp)
    //------------------加载下一个路由
    const app = apps.find(item => getNextRoute().startsWith(item.activeRule))
    
    if(!app) return
    //3.加载子应用
    const {  template,getExternalScripts,execScripts } = await importHTML(app.entry)
    const container = document.querySelector(app.container)
    container.appendChild(template)

    //配置全局变量
    window.__MICRO_WEB__ = true

    // 3.2获取子应用的生命周期函数
    const appExPports = await execScripts()
   
    //4.渲染子应用
    //将获取的生命周期函数储存在app上
    app.bootstrap = appExPports.bootstrap
    app.mount = appExPports.mount
    app.unmount = appExPports.unmount
    //执行生命周期函数
    await bootstrap(app)
    await mount(app)
}

async function bootstrap (app) {
    app.bootstrap && (await app.bootstrap())
}
async function mount(app){
    app.mount && (await app.mount({
        container:document.querySelector(app.container)
    }))
}
async function unmount(app){
    app.unmount && (await app.unmount({
        container:document.querySelector(app.container)
    }))
}

样式资源文件的加载

在微前端框架里的子应用图片资源是加载不到的。

因为图片的路径是子应用ip + url (9004 + url)

在主应用请求后就变成了主应用ip+ url (8080 + url)

要解决此问题,我们可以在子应用里把路径写死,像这样

module.exports = {
    publicPath:'http://localhost:9004',
}

这当然不是个好办法。

webpack支持运行时的publicPath

import './public-path.js'
module.exports = {
    publicPath:webpack_public_path,
}
//webpack在运行时生成的路径会自动拼接上这个全局变量,如果有的话
webpack_public_path = http://localhost:9004

我们使用微前端框架来提供

if (window.__POWERED_BY_QIANKUN__) {
  __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
    //配置全局变量
    window.__MICRO_WEB__ = true
    window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__ = app.entry + "/"

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

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

相关文章

Java微服务金融项目智牛股-基础知识一(CAT链路监控)

CAT链路监控 背景&#xff1a;从单体架构到微服务架构的演变&#xff0c; 一个业务请求往往会流转多个服务&#xff0c; 大型互联网产品服务架构尤为复杂&#xff0c;腾讯的抢红包服务&#xff0c; 阿里的交易支付服务&#xff0c; 可能就流转成百上千个服务节点&#xff0c; 面…

HashMap夺命14问

1. HashMap的底层数据结构是什么&#xff1f; 在JDK1.7中和JDK1.8中有所区别&#xff1a; 在JDK1.7中&#xff0c;由”数组链表“组成&#xff0c;数组是HashMap的主体&#xff0c;链表则是主要为了解决哈希冲突而存在的。 在JDK1.8中&#xff0c;有“数组链表红黑树”组成。当…

【软考网络管理员】2023年软考网管初级常见知识考点(19)-防火墙与入侵检测系统IDS

涉及知识点 防火墙有哪些及其功能&#xff0c;防火墙的区域划分及工作模式&#xff0c;IDS是什么及其作用&#xff1f;入侵检测系统的分类及原理&#xff0c;软考网络管理员常考知识点&#xff0c;软考网络管理员网络安全&#xff0c;网络管理员考点汇总。 原创于&#xff1a;…

java.sql.Time 字段时区问题 Jackson 源码分析 意想不到的Time处理类

java.sql.Time 字段时区问题 系列文章目录 第一章 初步分析 第二章 Mybatis 源码分析 第三章 Jackson 源码分析 意想不到的Time处理类 文章目录 java.sql.Time 字段时区问题 系列文章目录前言Jackson 源码阅读1. 先找 JsonFormat.class 打断点一步步跟踪2. 跟踪进入实际处理类…

RTSP视频流相关的一些操作

播放rtsp camera 内容 端口554在网络通信中用于Real Time Streaming Protocol(RTSP)。 gst-launch-1.0 playbin urirtsp://admin:WANGfengtu1210.0.20.190:554/client0x gst-launch-1.0 playbin urirtsp://admin:WANGfengtu1210.0.20.61:554/client1xgst-launch-1.0 rtspsrc …

基于Arduino UNO的循迹小车

目录 1.analogWrite函数的使用 2.红外循迹模块介绍 3.循迹小车代码实现 4.实物示例 1.analogWrite函数的使用 用analogWrite来替换digitalWrite 说明 将一个模拟数值写进Arduino引脚。这个操作可以用来控制LED的亮度, 或者控制电机的转速. 在Arduino UNO控制器中&#…

关于二叉树的操作,详细操作与实现方法

树是数据结构中的重中之重&#xff0c;尤其以各类二叉树为学习的难点。在面试环节中&#xff0c;二叉树也是必考的模块。本文主要讲二叉树操作的相关知识&#xff0c;梳理面试常考的内容。一起来复习吧。 本篇针对面试中常见的二叉树操作作个总结&#xff1a; 前序遍历&#x…

Kubernetes(k8s)容器编排控制器使用

目录 1 Pod控制器1.1 Pod控制器是什么1.2 Pod和Pod控制器1.3 控制器的必要性1.4 常见的控制器1.4.1 ReplicaSet1.4.2 Deployment1.4.3 DaemonSet 2 ReplicaSet控制器2.1 ReplicaSet概述2.2 ReplicaSet功能2.2.1 精确反应期望值2.2.2 保证高可用2.2.3 弹性伸缩 2.3 创建ReplicaS…

专项练习12

目录 一、选择题 1、JavaScript中定义var a"40",var b7,则执行a%b会得到&#xff1f; 2、下面哪个选项中的对象与浏览列表有关&#xff08; &#xff09; 3、下面哪一个语句可以实现在jQuery中找到所有元素的同辈元素&#xff1f; 4、如何阻止IE和各大浏览器默认行为…

CVSS4.0将于2023年底正式发布

通用漏洞评分系统(CVSS)是一种流行的、标准化的方法&#xff0c;用于评估数字系统安全漏洞的严重程度。由事件反应和安全小组论坛(FIRST)开发&#xff0c;它为安全专业人员提供了评估和优先排序风险的一致方法。 目前的CVSS v3.0已经运行了十多年&#xff0c;但因其复杂性和灵…

Jenkins 持续集成:Linux 系统 两台机器互相免密登录

背景知识 我们把public key放在远程系统合适的位置&#xff0c;然后从本地开始进行ssh连接。 此时&#xff0c;远程的sshd会产生一个随机数并用我们产生的public key进行加密后发给本地&#xff0c;本地会用private key进行解密并把这个随机数发回给远程系统。 最后&#xf…

ModaHub魔搭社区:向量数据库MIlvus服务端配置(四)

目录 常见问题 常见问题 除了配置文件外&#xff0c;怎样可以判断我确实在使用 GPU 做搜索&#xff1f; 有以下三种方式&#xff1a; 使用 nvidia-smi 命令查看 GPU 使用情况。用 Prometheus 配置&#xff0c;详见 使用 Grafana 展示监控指标 > 系统运行指标。使用 Milv…

一文教你Mysql如何性能优化

Mysql性能优化 Mysql性能优化 性能优化维度 数据库优化思路 应急调优的思路&#xff1a; 针对突然的业务办理卡顿&#xff0c;无法进行正常的业务处理&#xff01;需要立马解决的场景&#xff01; show processlist&#xff08;查看连接session状态&#xff09; explain(分…

hudi系列-timeline service

Timeline Service(时间线服务)是hudi的一个组件,用于暴露文件系统视图接口给客户端,是一个基于Javalin+Jetty实现的web服务。当客户端使用远程文件系统视图(RemoteHoodieTableFileSystemView)时,就是访问时间线服务http接口 默认情况下,如果开启了时间线服务,则它运行在…

支付中心“收银台“设计方案

01.收银台的产品架构 重点收银台架构的三个方面&#xff1a; 1.公司所支持的收银台种类以未来拓展倾向 2.支付方式的枚举及根据业务发展预判拓展倾向 3.收银台服务端的能力建设规划和选择 02.收银台的业务架构 收银台&#xff0c;是支付的起点&#xff0c;所以无论是何种…

接口自动化测试学习笔记分享(附上视频教程供你学习)

目录 接口自动化测试框架介绍 目录 接口测试场景 自动化测试场景 接口测试在分层测试中的位置 接口自动化测试与 Web/App 自动化测试对比 接口自动化测试与 Web/App 自动化测试对比 接口测试工具类型 为什么推荐 Requests Requests 优势 Requests 环境准备 接口请求…

Java安全——应用安全

Java安全 Java 应用安全 JCE&#xff08;Java Cryptography Extension&#xff09;java加密扩展包 Java Cryptography Extension&#xff08;JCE&#xff09;是一个可选的Java标准扩展&#xff0c;提供了一组用于加密、密钥生成和密钥协商等功能的类和接口。JCE包含了导入、生…

【ChatGpt】解决视频框交换中的平滑过渡的问题

【ChatGpt】解决视频框交换中的平滑过渡的问题 问题抽象chatgpt 看看直接给参考代码 解决效果 问题 在视频的播放中&#xff0c;我们想调换下容器的位置 &#xff0c;在互调的过程中&#xff0c;如果需要重新进行数据的初始化&#xff0c;获取与加载&#xff0c;就会很慢&…

RocketMQ --- 原理篇

一、专业术语 Producer 消息生产者&#xff0c;负责产生消息&#xff0c;一般由业务系统负责产生消息。 Consumer 消息消费者&#xff0c;负责消费消息&#xff0c;一般是后台系统负责异步消费。 Push Consumer Consumer 的一种&#xff0c;应用通常向 Consumer 对象注册一个…

基于JavaScript的百度AI的人脸识别微信小程序(深度学习+机器视觉)含全部工程源码及视频演示(仅供学习)

目录 前言总体设计系统整体结构图系统流程图 运行环境模块实现1. Access token 获取2. 人脸注册3. 人脸删除4. 人脸识别 系统测试工程源代码下载其它资料下载 前言 本项目采用了百度AI的训练模型&#xff0c;利用图像识别接口返回结果&#xff0c;旨在实现人脸在库中的判断&am…