Loading [MathJax]/jax/output/CommonHTML/config.js
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >深度:从零编写一个微前端框架

深度:从零编写一个微前端框架

作者头像
Peter谭金杰
发布于 2020-05-18 09:28:45
发布于 2020-05-18 09:28:45
1.4K00
代码可运行
举报
运行总次数:0
代码可运行

对于微前端,最近好像很火,之前我公众号也发过比较多微前端框架文章

那么现在我们需要手写一个微前端框架,首先得让大家知道什么是微前端,现在微前端模式分很多种,但是大都是一个基座+多个子应用模式,根据子应用注册的规则,去展示子应用。

这是目前的微前端框架基座加载模式的原理,基于single-spa封装了一层,我看有不少公司是用Vue做加载器(有天然的keep-alive),还有用angular和web components技术融合的


首先项目基座搭建,这里使用parcel

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
mkdir pangu 
yarn init 
//输入一系列信息
yarn add parcel@next

然后新建一个index.html文件,作为基座

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    
</body>
</html>

新建一个index.js文件,作为基座加载配置文件

新建src文件夹,作为pangu框架的源码文件夹,

新建example案例文件夹

现在项目结构长这样


既然是手写,就不依赖其他任何第三方库

我们首先需要重写hashchange popstate这两个事件,因为微前端的基座,需要监听这两个事件根据注册规则去加载不同的子应用,而且它的实现必须在React、vue子应用路由组件切换之前,单页面的路由源码原理实现,其实也是靠这两个事件实现,之前我写过一篇单页面实现原理的文章,不熟悉的可以去看看

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
https://segmentfault.com/a/1190000019936510
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
const HIJACK_EVENTS_NAME = /^(hashchange|popstate)$/i;
const EVENTS_POOL = {
  hashchange: [],
  popstate: [],
};

window.addEventListener('hashchange', loadApps);
window.addEventListener('popstate', loadApps);

const originalAddEventListener = window.addEventListener;
const originalRemoveEventListener = window.removeEventListener;
window.addEventListener = function (eventName, handler) {
  if (
    eventName &&
    HIJACK_EVENTS_NAME.test(eventName) &&
    typeof handler === 'function'
  ) {
    EVENTS_POOL[eventName].indexOf(handler) === -1 &&
      EVENTS_POOL[eventName].push(handler);
  }
  return originalAddEventListener.apply(this, arguments);
};
window.removeEventListener = function (eventName, handler) {
  if (eventName && HIJACK_EVENTS_NAME.test(eventName)) {
    let eventsList = EVENTS_POOL[eventName];
    eventsList.indexOf(handler) > -1 &&
      (EVENTS_POOL[eventName] = eventsList.filter((fn) => fn !== handler));
  }
  return originalRemoveEventListener.apply(this, arguments);
};

function mockPopStateEvent(state) {
  return new PopStateEvent('popstate', { state });
}

// 拦截history的方法,因为pushState和replaceState方法并不会触发onpopstate事件,所以我们即便在onpopstate时执行了reroute方法,也要在这里执行下reroute方法。
const originalPushState = window.history.pushState;
const originalReplaceState = window.history.replaceState;
window.history.pushState = function (state, title, url) {
  let result = originalPushState.apply(this, arguments);
  reroute(mockPopStateEvent(state));
  return result;
};
window.history.replaceState = function (state, title, url) {
  let result = originalReplaceState.apply(this, arguments);
  reroute(mockPopStateEvent(state));
  return result;
};

// 再执行完load、mount、unmout操作后,执行此函数,就可以保证微前端的逻辑总是第一个执行。然后App中的Vue或React相关Router就可以收到Location的事件了。
export function callCapturedEvents(eventArgs) {
  if (!eventArgs) {
    return;
  }
  if (!Array.isArray(eventArgs)) {
    eventArgs = [eventArgs];
  }
  let name = eventArgs[0].type;
  if (!HIJACK_EVENTS_NAME.test(name)) {
    return;
  }
  EVENTS_POOL[name].forEach((handler) => handler.apply(window, eventArgs));
}

上面代码很简单,创建两个队列,使用数组实现

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
const EVENTS_POOL = {
  hashchange: [],
  popstate: [],
};

如果检测到是hashchange popstate两种事件,而且它们对应的回调函数不存在队列中时候,那么就放入队列中。(相当于redux中间件原理)

然后每次监听到路由变化,调用reroute函数:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
function reroute() {
  invoke([], arguments);
}

这样每次路由切换,最先知道变化的是基座,等基座同步执行完(阻塞)后,就可以由子应用的vue-Rourer或者react-router-dom等库去接管实现单页面逻辑了。


那,路由变化,怎么加载子应用呢?

像一些微前端框架会用import-html之类的这些库,我们还是手写吧

逻辑大概是这样,一共四个端口,nginx反向代理命中基座服务器监听的端口(用户必须首先访问到根据域名),然后去不同子应用下的服务器拉取静态资源然后加载。


提示:所有子应用加载后,只是在基座的一个div标签中加载,实现原理跟ReactDom.render()这个源码一样,可参考我之前的文章

原创:从零实现一个简单版React (附源码)


那么我们先编写一个registrApp方法,接受一个entry参数,然后去根据url变化加载子应用(传入的第二个参数activeRule

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
/**
 *
 * @param {string} entry
 * @param {string} function
 */
const Apps = [] //子应用队列
function registryApp(entry,activeRule) {
    Apps.push({
        entry,
        activeRule
    })
}

注册完了之后,就要找到需要加载的app

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
export async function loadApp() {
  const shouldMountApp = Apps.filter(shouldBeActive);
  console.log(shouldMountApp, 'shouldMountApp');
  //   const res = await axios.get(shouldMountApp.entry);
  fetch(shouldMountApp.entry)
    .then(function (response) {
      return response.json();
    })
    .then(function (myJson) {
      console.log(myJson, 'myJson');
    });
}

shouldBeActive根据传入的规则去判断是否需要此时挂载:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
export function shouldBeActive(app){
    return app.activeRule(window.location)
}

此时的res数据,就是我们通过get请求获取到的子应用相关数据,现在我们新增subapp1和subapp2文件夹,模拟部署的子应用,我们把它用静态资源服务器跑起来

subapp1.js作为subapp1的静态资源服务器

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
const express = require('express');

subapp2.js作为subapp2的静态资源服务器

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
const express = require('express');
const app = express();
const { resolve } = require('path');
app.use(express.static(resolve(__dirname, '../subapp1')));

app.listen(8889, (err) => {
  !err && console.log('8889端口成功');
});

现在文件目录长这样:

基座index.html运行在1234端口,subapp1部署在8889端口,subapp2部署在8890端口,这样我们从基座去拉取资源时候,就会跨域,所以静态资源服务器、webpack热更新服务器等服务器,都要加上cors头,允许跨域。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
const express = require('express');
const app = express();
const { resolve } = require('path');
//设置跨域访问
app.all('*', function (req, res, next) {
  res.header('Access-Control-Allow-Origin', '*');
  res.header('Access-Control-Allow-Headers', 'X-Requested-With');
  res.header('Access-Control-Allow-Methods', 'PUT,POST,GET,DELETE,OPTIONS');
  res.header('X-Powered-By', ' 3.2.1');
  res.header('Content-Type', 'application/json;charset=utf-8');
  next();
});
app.use(express.static(resolve(__dirname, '../subapp1')));

app.listen(8889, (err) => {
  !err && console.log('8889端口成功');
});

⚠️:如果是dev模式,记得在webpack的热更新服务器中配置允许跨域,如果你对webpack不是很熟悉,可以看我之前的文章:

万字硬核 从零实现webpack热更新HMR

原创:如何自己实现一个简单的webpack构建工具 【附源码】


这里我使用nodemon启用静态资源服务器,简单为主,如果你没有下载,可以:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
npm i nodemon -g 
或
yarn add nodemon global 

这样我们先访问下8889,8890端口,看是否能访问到。

访问8889和8890都可以访问到对应的资源,成功


正式开启启用我们的微前端框架pangu.封装start方法,启用需要挂载的APP。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
export function start(){
    loadApp()
}

注册子应用subapp1,subapp2,并且手动启用微前端

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
import { registryApp, start } from './src/index';
registryApp('localhost:8889', (location) => location.pathname === '/subapp1');
registryApp('localhost:8890', (location) => location.pathname === '/subapp2');
start()

修改index.html文件:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>
    <div>
        <h1>基座</h1>
        <div class="subapp">
            <div>
                <a href="/subapp1">子应用1</a>
            </div>
            <div>
                <a href="/subapp2">子应用2</a>
            </div>
        </div>
        <div id="subApp"></div>
    </div>
</body>
<script src="./index.js"></script>

</html>

ok,运行代码,发现挂了,为什么会挂呢?因为那边返回的是html文件,我这里用的fetch请求,JSON解析不了

那么我们去看看别人的微前端和第三方库的源码吧,例如import-html-entry这个库

由于之前我解析过qiankun这个微前端框架源码,我这里就不做过度讲解,它们是对fetch做了一个text()。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
export async function loadApp() {
  const shouldMountApp = Apps.filter(shouldBeActive);
  console.log(shouldMountApp, 'shouldMountApp');
  //   const res = await axios.get(shouldMountApp.entry);
  fetch(shouldMountApp.entry)
    .then(function (response) {
      return response.text();
    })
    .then(function (myJson) {
      console.log(myJson, 'myJson');
    });
}

然后我们已经可以得到拉取回来的html文件了(此时是一个字符串)

由于现实的项目,一般这个html文件会包含js和css的引入标签,也就是我们目前的单页面项目,类似下面这样:

于是我们需要把脚本、样式、html文件分离出来。用一个对象存储

本想照搬某个微前端框架源码的,但是觉得它写得也就那样,今天又主要讲原理,还是自己写一个能跑的把,毕竟html的文件都回来了,数据处理也不难

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
export async function loadApp() {
  const shouldMountApp = Apps.filter(shouldBeActive);
  console.log(shouldMountApp, 'shouldMountApp');
  //   const res = await axios.get(shouldMountApp.entry);
  fetch(shouldMountApp[0].entry)
    .then(function (response) {
      return response.text();
    })
    .then(function (text) {
      const dom = document.createElement('div');
      dom.innerHTML = text;
      console.log(dom, 'dom');
    });
}

先改造下,打印下DOM

发现已经能拿到dom节点了,那么我先处理下,让它展示在基座中

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
export async function loadApp() {
  const shouldMountApp = Apps.filter(shouldBeActive);
  console.log(shouldMountApp, 'shouldMountApp');
  //   const res = await axios.get(shouldMountApp.entry);
  fetch(shouldMountApp[0].entry)
    .then(function (response) {
      return response.text();
    })
    .then(function (text) {
      const dom = document.createElement('div');
      dom.innerHTML = text;
      const content = dom.querySelector('h1');
      const subapp = document.querySelector('#subApp-content');
      subapp && subapp.appendChild(content);
    });
}

此时,我们已经可以加载不同的子应用了。

乞丐版的微前端框架就完成了,后面会逐步完善所有功能,向主流的微前端框架靠拢,并且完美支持IE11.记住它叫:pangu

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
H3C配置常用命令
超级用户终端登陆交换机 SYS进入                              //sys进入系统视图 输入以下命令 #  sysname lyz_xmc_bgl                    //sysname 系统名称 #  super password level 3 cipher pass          //superpassword 管理员密码,level3权限为3级,cipher输入密码为隐藏,缺省状态为可见,pass密码字符
py3study
2020/01/07
1.7K0
电信联通负载均衡,NQA联动,实现链路故障自动切换
之前有客户要求,把电信链路配置为上网主要链路,联通链路则作为备份使用,我虽然觉得很浪费,但还是照做了,因为客户总会有自己的考虑。
IT狂人日志
2022/05/18
7500
电信联通负载均衡,NQA联动,实现链路故障自动切换
Linux 集群总结 + LVS(负载均衡器)原理及配置
相信你已经对集群分类有了大致了解了,那么我们现在详细说说使用LVS来实现负载均衡集群。
小土豆Yuki
2020/06/15
3.2K0
Linux 集群总结 + LVS(负载均衡器)原理及配置
网络安全实验07 部署防火墙负载分担双击热备
该为某企业组网拓扑,两台防火墙都工作在网络互连层,上行链接路由器,下行连接二层交换机。防火墙与路由器间运行OSPF协议。现在希望两台防火墙以负载分担模式工作。正常情况下,防火墙A和B共同转发流量。当其中一台防火墙出现故障时,另一台防火墙转发全部业务。
90后小陈老师
2024/04/19
7940
网络安全实验07 部署防火墙负载分担双击热备
网络工程师必知:35个思科设备show命令,排障利器!
在网络工程领域,熟练掌握各种命令是网络工程师必备的技能之一。而在思科设备的管理和维护过程中,show命令是排障、配置和监视网络状态的重要工具。本文将深入介绍35个常用的思科设备show命令,涵盖了配置管理、网络状态查看、邻居和协议状态、安全和监控、高可用性和性能优化、以及VLAN和交换机配置等方面。
网络技术联盟站
2025/01/14
1.2K0
网络工程师必知:35个思科设备show命令,排障利器!
CISCO思科路由器配置命令详解及实例
路由器处于用户命令状态,这时用户可以看路由器的连接状态,访问其它网络和主机,但不能看到和更改路由器的设置内容。
网络技术联盟站
2021/04/23
6.8K0
h3c路由器配置命令大全_h3c命令手册
6、 port link-type Access|Trunk|Hybrid 设置端口访问模式
全栈程序员站长
2022/11/02
5.1K0
基于LVS-NAT模型的负载均衡调度
首先,需要确定的一点是,LVS-NAT模型中,所有的网络流量都需要流经DS,即包括请求报文和回应报文。当Client端从浏览器或其他客户端请求http或其它网络服务时,先由我们的DS服务器公网网卡接收,然后通过LVS调度挑选一个RS服务器,并通过内部转发机制从其内网网口转发给选中的RS服务器,最后将RS返回的响应报文通过相同的路线反向转回Client端。
用户1456517
2019/03/05
7550
基于LVS-NAT模型的负载均衡调度
中小型网络思路规划配置分享,H3C HCL模拟器
采用三层网络结构,核心、汇聚三层互联,堆叠采用40G网络,汇聚10G,接入1G,网关下放到汇聚,交换机采用独立管理VLAN,模拟某工厂真实网络情况。
王忘杰
2022/09/22
8410
中小型网络思路规划配置分享,H3C HCL模拟器
二层交换机与防火墙对接上网配置示例
二层交换机指的是仅能够进行二层转发,不能进行三层转发的交换机。也就是说仅支持二层特性,不支持路由等三层特性的交换机。
知孤云出岫
2023/12/22
5940
二层交换机与防火墙对接上网配置示例
linux route命令的使用详解
route命令用于显示和操作IP路由表。要实现两个不同的子网之间的通信,需要一台连接两个网络的路由器,或者同时位于两个网络的网关来实现。在Linux系统中,设置路由通常是 为了解决以下问题:该Linux系统在一个局域网中,局域网中有一个网关,能够让机器访问Internet,那么就需要将这台机器的IP地址设置为 Linux机器的默认路由。要注意的是,直接在命令行下执行route命令来添加路由,不会永久保存,当网卡重启或者机器重启之后,该路由就失效了;要想永久保存,有如下方法:
墨文
2020/02/28
4.3K0
思科模拟器常用命令
计算机命令: PCA login: root                                  :使用root用户 password: linux                                  :口令是linux shutdown -h now                               :同init 0 关机 logout login ifconfig                                        :显示IP地址
阿七日记
2021/12/28
2.8K0
2022年山东省职业院校技能大赛高职组“网络系统管理”赛项
2.所有交换机和无线控制器开启SSH服务,用户名密码分别为admin、admin1234。密码为明文类型,特权密码为admin。
青灯古酒
2023/10/16
5130
2022年山东省职业院校技能大赛高职组“网络系统管理”赛项
烽火2640路由器命令行手册-04-网络协议配置命令
配置静态ARP映射,静态ARP映射会永久保留在ARP缓存中。如果要删除配置的静态ARP映射的话,使用 no arp 命令。
landv
2018/12/17
1.3K0
2023年华三H3C HCL新版模拟器防火墙、AC、AP、Phone新功能使用
当前H3C最新版模拟器加入了防火墙、AC、AP、Phone等新设备,本文重点介绍新设备的使用
王忘杰
2023/08/21
9290
2023年华三H3C HCL新版模拟器防火墙、AC、AP、Phone新功能使用
搭建LVS-DR负载均衡集群、Keepalived-LVS高可用负载均衡集群
在浏览器访问VIP:192.168.8.100,刷新网页,访问结果由real1、real2交替回复。
阿dai学长
2019/04/03
9290
H3C AC+FIT完全设置
       用户采用PON线路,动态分配地址,无固定IP,每月1088元,如果带有固定IP,则需要每月7088元,采用较经济的方式,每次用户查询ip138得到公网IP后远程管理。
py3study
2020/01/08
1.6K0
一个H3CNE测试的配置
                                        H3CNE的配置
py3study
2020/01/08
4630
一个H3CNE测试的配置
HCIA数通RS综合实验,附详细配置命令
华三HCL全版本、华为ENSP、Wireshark、VirtualBox全版本、SecureCRT下载!
网络技术联盟站
2023/03/13
1K0
HCIA数通RS综合实验,附详细配置命令
思科模拟器配置指北
本文将详细介绍如何在思科模拟器中完成以下任务:配置VLAN、IP地址、路由、访问控制列表(ACL)和网络地址转换(NAT)。本文假设读者没有任何基础知识,将详细解释每个命令的作用。
曈曈too
2023/11/17
6380
推荐阅读
相关推荐
H3C配置常用命令
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验