Handwritten micro-front-end qiankun framework from scratch [super detailed 10,000-character long text]

project creation

We create several folders as shown in the figure

  • main: the main application (using vue3 as the technology stack)
  • react: sub-application 1
  • vue2: child application 2
  • vue3: child application 3
  • service: server code

vue2 sub-application:

We write a little bit in 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>

Then, we need to do some configuration and create a vue.config.js configuration file

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 transformation

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);
}

Master application development

Interface development of the main application central controller

interface effect

When the button is clicked, the route is switched

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')

Sub application registration

To implement a micro-frontend, we must need a configuration file that contains information about all sub-applications, such as sub-application names, access paths, corresponding routes, and so on.

Then, store this information in our main application. Of course, this is not optimal. We should create a micro-frontend framework, pass our configuration information into the micro-frontend framework, and then call the method in the micro-frontend framework in the main application.

Creation of sub-application configuration files

First, we need to create a file for storing sub-application configuration information. This file must be defined in our main application.

Create a store folder in the sr directory and create a sub.js file

//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',
    },
  ];

Micro frontend framework creation

We first create a micro front-end micro folder in the main application main (of course, this folder and main are the most reasonable), and then create an index.js entry file.

The logic of the index file must be a file that exposes some of the logic we encapsulate to the outside world. Therefore, our main logic should be written in other files, and the index file should be imported.

Then, we create a start.js file in the main\micro\ file, which is mainly used to write some logic of our framework. Let's first define and expose a function for registering sub-applications in start.js. We don't know what this function does.

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

Then, we can introduce and expose this function in the index

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

We created a configuration file in the main application. This configuration file must be put into the micro front-end framework. Therefore, our registerMicroApps function can be used to accept such a configuration list first, and call registerMicroApps(list) at the appropriate position Just implement the configuration file injection into the micro-frontend framework.

In order to achieve modularization, our configuration information can not be stored in main\micro\start.js, we can put all this information in a single file for unified management. We create main\micro\const\subApps.js file

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

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

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

In this way, we can write information with setList(list)

Read information with getList()

We write the following logic in start.js

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

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

In this way, when we call the registerMicroApps() function externally, we can store the configuration file in our framework.

We can introduce this function in main.js and pass in the configuration file

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, now our sub-application configuration information has been registered in our micro frontend framework.

Micro Frontend Framework

To realize the micro front end, our general idea should be as follows:

  1. Monitor page routing changes
  2. Match sub-applications based on the current page route
  3. The main application loads sub-applications
  4. Container-specific rendering sub-applications

Preparation

In qinkun, two functions registerMicroApps and start are introduced into the main.js of the main application. Like qinkun, we create our own micro-frontend framework folder and name it micro-fe. We make the following in the main.js of the main application, just like qiankun configuration

//引入的不再是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')

There should be an entry index.js file in the src\micro-fe folder of our main application. According to the import logic of the main application, we should expose two functions.

mainApp\src\micro-fe\index.js

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

When the registerMicroApps function in the main application main.js is executed, the configuration file apps of each sub-application is passed in. Therefore, in the registerMicroApps function, we can store the obtained apps configuration list. At the same time, we can define a function to get the configuration list, so that we can get the apps configuration list we passed in later.

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

Monitor route changes

In our satrt function, it should be the core logic of our micro-frontend framework. It needs to implement four core functions : monitoring page routing changes, matching sub-applications according to current page routing, loading sub-applications from the main application, and rendering sub-applications with specific containers .

First, we implement routing monitoring, which is divided into hash routing and history routing.

hash routing

For changes in hash routing, we can use window.onhashchange to monitor, which is relatively easy. In this example, we will not consider it for now.

history route

method illustrate
back() Referring to the current page, return to the previous record in the history (that is, return to the previous page). You can also click the ← button in the browser toolbar to achieve the same effect.
forward() Referring to the current page, go to the next record in the history (that is, advance to the next page). You can also click the → button in the browser toolbar to achieve the same effect.
go() Refer to the current page and open the specified history record according to the given parameters, for example -1 means return to the previous page, 1 means return to the next page.
pushState() Insert a new entry into the browser's history.
replaceState() Replaces the current history with the specified data, name and URL.

For the history route, we can use the popstate event to monitor its history.go, history.back, history.forword and other methods. (The browser's forward and backward will trigger the history.go and history.back methods).

Therefore, after our routing changes, the core processing function should be

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

However, the pushState and replaceState methods of the browser history will not be monitored by the popstate event, so we need to rewrite the native methods of pushState and replaceState.

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, to implement routing monitoring, these three methods should be executed first in our start function. In order to make the logic clearer, we encapsulate these functions into a js file and import and execute them in satrt.

Create rewrite-router.js under the src\micro-fe\ folder

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事件");
    }

}

Then, import and use it in the start function (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.渲染子应用

} 

Match sub-applications according to routing changes

Create a route matching handler

After listening to the change of the route, we should do some processing

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

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

For a clearer structure, we encapsulate these logics into a function and introduce them in the rewrite-router.js file

Create the handle-router.js file under the src\micro-fe\ folder

//处理路由变化
//获取子应用的配置列表,用于路由匹配
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()
    }
}

Match sub-applications based on the current route

The logic of matching sub-applications is roughly as follows:

  • Get the current routing path
  • Go to the apps to find
//处理路由变化的
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
    }
}

load sub-app

After matching the sub-application, we can get the configuration object of the sub-application, like this

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

Now, we need to load all the resources of the child application, such as HTML, CSS and javascript.

Get and render html content

  • Use ajax to get the html under the domain name corresponding to the sub-application
  • Get the dom element to render html
  • Mount the obtained html on the obtained dom element
//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
}

Observing the console, we found that html has been successfully mounted in the container of the main application, but the page is not displayed as we expected


The reason is that the rendering of the page needs to generate content by executing js code, and the browser will not load the script generated in innerHTML for security reasons. Therefore, the parsing and processing of script tags is the first core operation of the micro-frontend framework.

Resource request function encapsulation

In the Qiankun framework, Ali made a separate plug-in for html resource requests. This plugin contains three core objects

  • template: used to get the html template
  • getExternalScripts() function: Get all script tag codes
  • execScripts() function: get and execute all js scripts

Similarly, we can also encapsulate such a function. Since both html and js content are requested through fetch, we can encapsulate the fetch function first.

  1. Create the fetch-resource.js file under the src\micro-fe\ folder
export const fetchResource = url => fetch(url).then(res => res.text())
  1. We create the import-html.js file under the src\micro-fe\ folder.
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. Finally, we export the encapsulated importHTML function and import it into 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)
}

As before, the html at this time can be mounted on the html page, but the js cannot be imported and processed correctly.

Get all script tags and js code

  1. Get all script tags:
 constscripts = template.querySelectorAll('script')

Note: The obtained constscripts is a class array, we can use ES6's Array.from() method to convert it into a real array

  1. Parse the js content in the scripts sign

There are two forms for scripts sign-up

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

//外链标签
<script src="./a.js"></script>
  • Inline tag parsing: directly use innerHTML to get content
  • Analysis of external link tags: use the fecth function to request content

Add the corresponding logic in the import-html.js file:

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 }
}

Observing the browser console, you can see that all js content has been obtained, although it is currently only in the form of a string

Render sub-apps

Now, we already know that after executing the obtained sub-application js code, our sub-application interface should be rendered on the page.

Execute the obtained js content

We use the eval function to execute these script contents.

//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 function we need to manually execute in 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()
}

Switching the route, we will find that our page is successfully rendered!!! However, the page as a whole is replaced with the content of vue2 instead of being rendered in the container we specified.

Why is this? In fact, it is very simple. At present, our global environment does not have the global variables of the micro-frontend. The vue2 sub-application does not have the micro-frontend framework when rendering, and is directly rendered in the container with id = app, which is also the container name of the main application.

Therefore, the solution is also very simple, either directly replace the container of the main application, or we use our micro-frontend framework to manually call the render function of the sub-application.

Use the micro frontend framework to execute the render function

To make the sub-application run in the micro-frontend framework, we only need to add the variable window.MICRO_WEB = true in 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)

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

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

At this point, the root node of the app will not be replaced when the route is switched, but since the render function of the vue2 sub-application is not executed, the page still has no content. All we need to do is to get the lifecycle function of the sub-application and execute the render function manually.

The core question now should be how we get the life cycle function of the sub-application.

umd

When subpackaging, we made the following configuration

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

Here we package the file into a umd modularized js file

What is UMD

UMD (Universal Module Definition) is a javascript universal module definition specification that allows your modules to function in all javascript runtime environments.

Let's learn about the modularized code of umd. We configure mode:'development' in the sub-application configuration file vue.config.js of vue2

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

Then, execute npm run build to package

Observe the packaged vue2.js code, its simplified logic is as follows

The packaged code of vue2.js is actually a self-executing function, the main function is to be compatible with different module specifications

//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() {

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

});

simplified logic

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

webpackUniversalModuleDefinition(window, factory)

//factory是框架内部的代码

According to the above knowledge, through the js file packaged by umd modularization, we can get the content of the sub-application through window.

Get the life cycle function of the sub-application

Through umd, we can use the window object in the micro-frontend framework to obtain the life cycle function of the sub-application.

    //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']);
    }

Since the packaged name of each sub-application is different, obtaining the properties of the sub-application requires knowing the name of the packaged sub-application. Therefore, we obtain the object of the sub-application in other ways.

Through the umd code, we can know that the sub-application information can also be obtained through CommnJs, so we manually create a CommnJs environment.

    //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)
        })
    }

Package function combined with umd

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

We can know that all sub-application attributes can be obtained through module.exports. For the simplicity of the code, we expose this object through the execScripts function, and get this value in 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()   
}

As you can see, we get the value here normally.

At this time, as long as the render function in the mount cycle function is executed, we can realize the mount rendering of the sub-application.

Execute the life cycle function of the sub-application

For the obtained life cycle function information, we can bind it in our configuration list app, and then use app.mount to call this life cycle function. In order to facilitate the addition of function logic, we can encapsulate the calls of life cycle functions into functions.

  • Get sub application life cycle function
const appExPports = await execScripts()
  • Bind the function to the app
app.bootstrap = appExPports.bootstrap
app.mount = appExPports.mount
app.unmount = appExPports.unmount
  • Encapsulate the corresponding lifecycle execution function
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())
}
  • Execute the encapsulated function
await bootstrap(app)
await mount(app)

handle-router.js completion code

//处理路由变化
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())
}

Switch the route and find that our basic functions have been realized! !

Uninstall the previous sub-app

When we switch routes, we will find that the content of the page keeps getting longer, because the original sub-application has not been uninstalled. Therefore, when we need to implement application switching, uninstall the previous sub-application. To achieve this function, we need to get the history of the browser.

Due to security issues, the browser does not provide an API for obtaining route history records. We need to maintain a route record in the route monitoring code.

  1. Create two variables to store the value before and after the route change
let prevRoute = ''                        //上一个路由
let nextRoute = window.location.pathname  //下一个路由
  1. variable assignment
  • Routing changes for pushState and replaceState

Assign two values ​​before and after the real history of the route

    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()
    }
  • For the popstate event, we need to assign the value of nextRoute to prevRoute, nextRoute = window.location.pathname
prevRoute = nextRoute
nextRoute = window.location.pathname
  1. Expose these two variables for use elsewhere
export const getPrevRoute = () => prevRoute
export const getNextRoute = () => nextRoute
  1. According to the obtained last sub-application route, match the sub-application information, and uninstall the sub-application
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)
    }))
}

Loading of style resource files

The sub-application image resources in the micro-frontend framework cannot be loaded.

Because the path of the picture is sub-application ip + url (9004 + url)

After the main application requests, it becomes the main application ip+ url (8080 + url)

To solve this problem, we can hard-code the path in the sub-application, like this

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

This is of course not a good idea.

webpack supports publicPath at runtime

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

We use a micro frontend framework to provide

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 + "/"

Guess you like

Origin blog.csdn.net/weixin_46769087/article/details/131406750