微前端与项目实施方案研究

一、前言

微前端(micro-frontends)是近几年在前端领域出现的一个新概念,主要内容是将前端应用分解成一些更小、更简单的能够独立开发、测试、部署的小块,而在用户看来仍然是内聚的单个产品。微前端的理念源于微服务,是将庞大的整体拆成可控的小块,并明确它们之间的依赖关系,而它的价值在于能将低耦合的代码与组件进行组合,基座+基础协议模式能接入大量应用,进行统一的管理和输出,许多公司与团队也都在不断尝试和优化相关解决技术与设计方案,为这一概念的落地和推广添砖加瓦。结合自身遇到的问题,适时引用微前端架构能起到明显的提效赋能作用。

二、背景

目前我司拥有大量的内部系统,这些系统采用相同的技术栈,在实际开发和使用过程中,逐渐暴露出如下几个问题:

1.有大量可复用的部分,虽然有组件库,但是依赖版本难统一;
2.静态资源体积过大,影响页面加载和渲染速度;
3.应用切换目前是通过链接跳转的方式实现,会有白屏和等待时长的问题,对用户体验不够友好;
针对上述几个问题,决定采用微前端架构对内部系统进行统一的管理,本文也是围绕微前端落地的技术预研方案。

三、方案调研

目前业界有多种解决方案,有各自的优缺点,具体如下:

  • 路由转发:路由转发严格意义上不属于微前端,多个子模块之间共享一个导航即可 简单,易实现 体验不好,切换应用整个页面刷新;

  • 嵌套 iframe:每个子应用一个 iframe 嵌套 应用之间自带沙箱隔离 重复加载脚本和样式;

  • 构建时组合:独立仓储,独立开发,构建时整体打包,合并应用 方便依赖管理,抽取公共模块 无法独立部署,技术栈,依赖版本必须统一;

  • 运行时组合:每个子应用独立构建,运行时由主应用负责应用管理,加载,启动,卸载,通信机制 良好的体验,真正的独立开发,独立部署 复杂,需要设计加载,通信机制,无法做到彻底隔离,需要解决依赖冲突,样式冲突问题;

    开源微前端框架也有多种,例如阿里出品的qiankun,icestark,还有针对angular提出的mooa等,都能快速接入项目,但结合公司内部系统的特点,直接采用会有有些限制,例如要实现定制界面,无刷新加载应用,且不能对现有项目的开发和部署造成影响,因此决定自研相关技术。

四、架构设计

4.1 应用层

应用层包括所有接入微服务工作台的内部系统,他们各自开发与部署,接入前后没有多大影响,只是需要针对微服务层单独输出打包一份静态资源;

4.2 微服务层

微服务层作为核心模块,拥有资源加载、路由管理、状态管理和用户认证管理几大功能,具体内容将在后面详细阐述,架构整体工作流程如下:

4.3 基础支撑层

基础支撑层作为基座,提供微服务运行的环境和容器,同时接入其他后端服务,丰富实用场景和业务功能;

五、技术重难点

要实现自定义微前端架构,难点在于需要管理和整合多个应用,确保应用之间独立运行,彼此不受影响,需要解决如下几个问题:

5.1 资源管理

5.1.1资源加载

每个应用有一个应用资源管理和注册的文件(app.regiser.js),其中包含路由信息,应用配置信息(configs.js)和静态资源清单,当首次切换到某应用时,首先加载app.register.js文件,完成路由和应用信息的注册,然后根据当前浏览器路由地址加载对应的静态文件,完成页面渲染,从而将各应用的静态资源串联起来,其中注册入口文件通过webpack插件来实现,具体实现如下:

FuluAppRegisterPlugin.prototype.apply = function(compiler) {appId = extraAppId();var entry = compiler.options.entry;if (isArray(entry)) {for (var i = 0; i < entry.length; i++) {if (isIndexFile(entry[i])) { // 入口文件indexFileEdit(entry[i]);entry[i] = entry[i].replace(indexEntryRegx, indeEntryTemp); // 替换入口文件i = entry.length;}}} else {if (isIndexFile(entry)) { // 入口文件indexFileEdit(entry); // 重新生成和编辑入口文件compiler.options.entry = compiler.options.entry.replace(indexEntryRegx, indeEntryTemp); // 替换入口文件}}compiler.hooks.done.tap('fulu-app-register-done', function(compilation) {fs.unlinkSync(tempFilePath); // 删除临时文件return compilation;});compiler.hooks.emit.tap('fulu-app-register', function(compilation) {var contentStr = 'window.register("'+ appId + '", {\nrouter: [ \n ' + extraRouters() + ' \n],\nentry: {\n'; // 全局注册方法var entryCssArr = [];var entryJsArr = [];for (var filename in compilation.assets) {if (filename.match(mainCssRegx)) { // 提取css文件entryCssArr.push('\"' + filename + '\"');} else if (filename.match(mainJsRegx) || filename.match(manifestJsRegx) || filename.match(vendorsJsRegx)) { // 提取js文件entryJsArr.push('\"' + filename + '\"');}}contentStr += ('css: ['+ entryCssArr.join(', ') +'],\n'); // css资源清单contentStr += ('js: ['+ entryJsArr.join(', ') +'],\n }\n});\n'); // js资源清单compilation.assets['resources/js/' + appId + '-app-register.js'] = { // 生成appid-app-register.js入口文件source: function() {return contentStr;},size: function() {return contentStr.length;}};return compilation;});};
5.1.2资源文件名

微服务输出打包模式下,静态资源统一打包形式以项目id开头,形如10000092-main.js,
文件名称的修改通过webpack的插件实现;

核心实现代码如下:

FuluAppRegisterPlugin.prototype.apply = function(compiler) {......compiler.options.output.filename = addIdToFileName(compiler.options.output.filename, appId);compiler.options.output.chunkFilename = addIdToFileName(compiler.options.output.chunkFilename, appId);compiler.options.plugins.forEach((c) => {if (c.options) {if (c.options.filename) {c.options.filename = addIdToFileName(c.options.filename, appId);}if (c.options.chunkFilename) {c.options.chunkFilename = addIdToFileName(c.options.chunkFilename, appId);}}});......};

5.2 路由管理

路由分为应用级和菜单级两大类,应用类以应用id为前缀,将各应用区分开,避免路由地址重名的情况,菜单级的路由由各应用的路由系统自行管理,结构如下:

5.3 状态分隔

前端项目通过状态管理库来进行数据的管理,为了保证各应用彼此间独立,因此需要修改状态库的映射关系,这一部分需要借助于webpack插件来进行统一的代码层面调整,包括model和view两部分代码,model定义了状态对象,view借助工具完成状态对象的映射,调整规则为【应用id+旧状态对象名称】,下面来讲解一下插件的实现;

插件的实现原理是借助AST的搜索语法匹配源代码中的状态编写和绑定的相关代码,然后加上应用编号前缀,变成符合预期的AST,最后输出成目标代码:

module.exports = function(source) {var options = loaderUtils.getOptions(this);stuff = 'app' + options.appId;isView = !!~source.indexOf('React.createElement'); // 是否是视图层allFunc = [];var connectFn = "function connect(state) {return Object.keys(state).reduce(function (obj, k) { var nk = k.startsWith('"+stuff+"') ? k.replace('"+stuff+"', '') : k; obj[nk] = state[k]; return obj;}, {});}";connctFnAst = parser.parse(connectFn);const ast = parser.parse(source, { sourceType: "module", plugins: ['dynamicImport'] });traverse(ast, {CallExpression: function(path) {if (path.node.callee && path.node.callee.name === 'connect') { // export default connext(...)if (isArray(path.node.arguments)) {var argNode = path.node.arguments[0];if (argNode.type === 'FunctionExpression') { // connect(() => {...})traverseMatchFunc(argNode);} else if (argNode.type === 'Identifier' && argNode.name !== 'mapStateToProps') { // connect(zk)var temp_node = allFunc.find((fnNode) => {return fnNode.id.name === argNode.name;});if (temp_node) {traverseMatchFunc(temp_node);}}}} else if (path.node.callee && path.node.callee.type === 'SequenceExpression') {if (isArray(path.node.callee.expressions)) {for (var i = 0; i < path.node.callee.expressions.length; i++) {if (path.node.callee.expressions[i].type === 'MemberExpression'&& path.node.callee.expressions[i].object.name === '_dva'&& path.node.callee.expressions[i].property.name === 'connect') {traverseMatchFunc(path.node.arguments[0]);i = path.node.callee.expressions.length;}}}}},FunctionDeclaration: function(path) {if (path.node.id.name === 'mapStateToProps' && path.node.body.type === 'BlockStatement') {traverseMatchFunc(path.node);}allFunc.push(path.node);},ObjectExpression: function(path) {if (isView) {return;}if (isArray(path.node.properties)) {var temp = path.node.properties;for (var i = 0; i < temp.length; i++) {if (temp[i].type === 'ObjectProperty' && temp[i].key.name === 'namespace') {temp[i].value.value = stuff + temp[i].value.value;i = temp.length;}}}}});return core.transformFromAstSync(ast).code;};

5.4 框架容器渲染

完成以上步骤的改造,就可以实现容器中的页面渲染,这一部分涉及到组件库框架层面的调整,大流程如下图:

六、构建流程

6.1 使用插件

构建过程中涉及到两款自开发的插件,分别是fulu-app-register-plugin和fulu-app-loader;

6.1.1 安装
npm i fulu-app-register-plugin fulu-app-loader -D;
6.1.2 配置

webpack配置修改:

const FuluAppRegisterPlugin = require('fulu-app-register-plugin');module: {rules: [{test: /\.jsx?$/,loader: 'fulu-app-loader',}]}plugins: [new FuluAppRegisterPlugin(),......]

6.2.编译

编译过程与目前项目保持一致,相比以前,多输出了一份微前端项目编译代码,流程如下:

七、遗留问题

7.1 js环境隔离

由于各应用都加载到同一个运行环境,因此如果修改了公共的部分,则会对其他系统产生不可预知的影响,目前没有比较好的办法来解决,后续将持续关注这方面的内容,逐渐优化达到风险可制的效果。

7.2.获取token

目前应用切换使用重定向来完成token获取,要实现如上所述的微前端效果,需要放弃这种方式,改用接口调用异步获取,或者其他解决方案。

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

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

相关文章

ASP.NET Core分布式项目实战(集成ASP.NETCore Identity)--学习笔记

任务24&#xff1a;集成ASP.NETCore Identity之前在 Index 页面写了一个 strong 标签&#xff0c;需要加个判断再显示&#xff0c;不然为空没有错误的时候也会显示if (!ViewContext.ModelState.IsValid) {<strong>Error""</strong><div asp-validatio…

[数据结构]链表中销毁和清空的区别

链表是一个很基本的数据结构&#xff0c;其他的数据结构&#xff08;如&#xff1a;栈、队列、二叉树等&#xff09;都可以用链表来实现。 销毁&#xff1a;是先销毁了链表的头&#xff0c;然后接着一个一个的把后面的销毁了&#xff0c;这样这个链表就不能再使用了&#xff0…

java——泛型

文章目录Java 泛型泛型方法实例有界的类型参数:实例泛型类实例类型通配符1、类型通配符一般是使用?代替具体的类型参数。例如 List<?> 在逻辑上是List,List 等所有List<具体类型实参>的父类。实例2、类型通配符上限通过形如List来定义&#xff0c;如此定义就是通…

Dotnet core基于ML.net的销售数据预测实践

ML.net已经进到了1.5版本。作为Microsoft官方的机器学习模型&#xff0c;你不打算用用&#xff1f;一、前言ML.net可以让我们很容易地在各种应用场景中将机器学习加入到应用程序中。这是这个框架很重要的一点。通过ML.net&#xff0c;我们可以使用手中的可用数据&#xff0c;进…

Java中关于省略作用域报错问题分析

这个是很典型的作用域问题&#xff0c;if后如果省略那么if只作用于其后面的第一行代码 这时候如果这行代码只是个变量声明语句的话&#xff0c;这个变量是没有其他任何逻辑可以访问到的&#xff0c;因为作用域问题(如果有/&#xff0c;那么声明语句中声明的变量只在这个个内可用…

java进阶之注解篇

文章目录注解基本语法定义注解元注解编写注解处理器注解元素默认值限制生成外部文件替代方案注解不支持继承实现处理器注解 注解&#xff08;也被称为元数据&#xff09;为我们在代码中添加信息提供了一种形式化的方式&#xff0c;使我们可以在稍后的某个时刻更容易的使用这些…

C#9就这么来了,.NET开发者该做点什么?

就在上周三10号&#xff0c;.NET5.0发布了第5个预览版&#xff0c;同时支持了C#9-preview&#xff01;是的&#xff0c;你没看错&#xff0c;虽然C# 8.0还未正式发布&#xff0c;但是通往C&#xff03;9的漫长道路却已经开始&#xff0c;这发展速度简直了&#xff01;C#语言的快…

C++,Java编程空指针的一个小细节

//判断是否击中了敌人坦克if (hero.shot!null && hero.shot.isLive ){for (int i 0;i<enemyTanks.size();i){EnemyTank enemyTank enemyTanks.get(i);hitTank(hero.shot,enemyTank);}}上面代码的if条件不能写成这样&#xff1a; if ( hero.shot.isLive &&am…

Magicodes.IE在.NET Core中通过请求头导出多种格式文件

原文作者&#xff1a;HueiFeng前言在2.2里程碑中我们增加了一些新的功能,正如标题所写通过请求头进行导出我们不同格式的文件.下面我们来看一下如何使用.通过这种方式无论是对我们的数据多用途&#xff0c;还是说对我们的数据校验都做到了轻松易配。同时我们也将在本周发布2.3版…

使用DQL查询数据

文章目录DQL语言SELECT语法制定查询字段AS 子句作为别名DISTINCT关键字的使用使用表达式的列where条件语句逻辑操作符模糊查询 &#xff1a; 比较操作符连接查询JION自连接排序和分页子查询DQL语言 DQL( Data Query Language 数据查询语言 ) 查询数据库数据 , 如SELECT语句简…

基于 abp vNext 和 .NET Core 开发博客项目 - Blazor 实战系列(九)

系列文章使用 abp cli 搭建项目给项目瘦身&#xff0c;让它跑起来完善与美化&#xff0c;Swagger登场数据访问和代码优先自定义仓储之增删改查统一规范API&#xff0c;包装返回模型再说Swagger&#xff0c;分组、描述、小绿锁接入GitHub&#xff0c;用JWT保护你的API异常处理和…

关于-32768补码的问题

首先要知道的是计算机中补码的表示是唯一且连续的&#xff01;我想这是计算机为什么不用原码来表示的一个原因&#xff01;另外&#xff0c;以补码形式来运算的话&#xff0c;设计的逻辑电路会简单很多&#xff0c;会少很多逻辑运算器件&#xff0c;所以计算机采用补码的形式来…

TCP(发消息:简易代码实现)

文章目录客户端服务器review&#xff1a;查询IP和端口发送文件客户端 链接服务器Socket发送消息 package com.ayv.try02;import java.io.IOException; import java.io.OutputStream; import java.net.InetAddress; import java.net.Socket;//客户端 public class TcoClientDe…

记实现TDengine时序数据库支持 .Net Windows 32位系统踩坑

TDengine是一个高效的存储、查询、分析时序大数据的平台&#xff0c;专为物联网、车联网、工业互联网、运维监测等优化而设计的数据库&#xff0c; 官方目前没有提供完整的.Net Core 解决方案&#xff0c; 因此闲来无事&#xff0c; 从基于restful api 到现在使用官方编译的动态…

SPDY, WebSocket, WebDAV概念

SPDY&#xff08;读作“SPeeDY”&#xff09;是Google开发的基于TCP的应用层协议&#xff0c;用以最小化网络延迟&#xff0c;提升网络速度&#xff0c;优化用户的网络使用体验。SPDY并不是一种用于替代HTTP的协议&#xff0c;而是对HTTP协议的增强。新协议的功能包括数据流的多…

UDP(发短信:简单模拟)

发短信&#xff1a;不用连接&#xff0c;需要知道对方地址 文章目录UDP发送消息实现互相聊天UDP多线程实现聊天UDP发送消息 UDP传输 我们要用UDP传输数据时&#xff0c;怎么用Socket建立连接呢&#xff1f; DatagramSocket与DatagramPacket 建立发送端&#xff0c;接收端。 建…

SilkierQuartz 1.0.21 发布, 是一个 Quartz.NET 的强大且简单的Web管理工具和承载组件...

SilkierQuartz 是一个新的合并了 Quartzmin 和 QuartzHostedService的组件!Quartz.NET 是一个完整的开源的任务规划系统&#xff0c;从小应用至大型企业级应用都可以适用.Quartzmin Quartzmin 是一个 Quartz.NET 的强大且简单的Web管理工具QuartzHostedService QuartzHostedSer…

超文本运输协议HTTP概念

超文本传输协议&#xff08;HTTP&#xff0c;HyperTextTransfer Protocol)是互联网上应用最为广泛的一种网络传输协议&#xff0c;所有的WWW文件都必须遵守这个标准。设计HTTP最初的目的是为了提供一种发布和接收HTML页面的方法。 1960年美国人Ted Nelson构思了一种通过计算机处…

多线程(简单实现)

继承Thread类 两条线程交替进行创建线程方式一&#xff1a;继承Theard类&#xff0c;重写run方法&#xff0c;调用start开启线程总结:注意&#xff0c;线程开启不一定立即执行&#xff0c;由CPU调度执行 //两条线程交替进行//创建线程方式一&#xff1a;继承Theard类&#xf…

用C#在STM32上写第一个Hello world

随着微软放弃.Net MF ,通过C#编写STM32 平台上的程序变得渺茫&#xff0c; 但是&#xff0c; 别着急&#xff0c; 目前至少有两个社区在做这件事情&#xff0c; 传承了微软的.Net MF , 一家是 nanoframework,另外一家比较封闭的是 GHI Electronics 地址是: https://github.com/…