用微前端的方式搭建类单页应用

前言

微前端由ThoughtWorks 2016年提出,将后端微服务的理念应用于浏览器端,即将 Web 应用由单一的单体应用转变为多个小型前端应用聚合为一的应用。

美团已经是一家拥有几万人规模的大型互联网公司,提升整体效率至关重要,这需要很多内部和外部的管理系统来支撑。由于这些系统之间存在大量的连通和交互诉求,因此我们希望能够按照用户和使用场景将这些系统汇总成一个或者几个综合的系统。

我们把这种由多个微前端聚合出来的单页应用叫做“类单页应用”,美团HR系统就是基于这种设计实现的。美团HR系统是由30多个微前端应用聚合而成,包含1000多个页面,300多个导航菜单项。对用户来说,HR系统是一个单页应用,整个交互过程非常顺畅;对开发者同学来说,各个应用均可独立开发、独立测试、独立发布,大大提高了开发效率。

接下来,本文将为大家介绍“微前端构建类单页应用”在美团HR系统中的一些实践。同时也分享一些我们的思考和经验,希望能够对大家有所启发。

HR系统的微前端设计

因为美团的HR系统所涉及项目比较多,目前由三个团队来负责。其中:OA团队负责考勤、合同、流程等功能,HR团队负责入职、转正、调岗、离职等功能,上海团队负责绩效、招聘等功能。这种团队和功能的划分模式,使得每个系统都是相对独立的,拥有独立的域名、独立的UI设计、独立的技术栈。但是,这样会带来开发团队之间职责划分不清、用户体验效果差等问题,所以就迫切需要把HR系统转变成只有一个域名和一套展示风格的系统。

为了满足公司业务发展的要求,我们做了一个HR的门户页面,把各个子系统的入口做了链接归拢。然而我们发现HR门户的意义非常小,用户跳转两次之后,又完全不知道跳到哪里去了。因此我们通过将HR系统整合为一个应用的方式,来解决以上问题。

一般而言,“类单页应用”的实现方式主要有两种:

  1. iframe嵌入
  2. 微前端合并类单页应用

其中,iframe嵌入方式是比较容易实现的,但在实践的过程中带来了如下问题:

  • 子项目需要改造,需要提供一组不带导航的功能
  • iframe嵌入的显示区大小不容易控制,存在一定局限性
  • URL的记录完全无效,页面刷新不能够被记忆,刷新会返回首页
  • iframe功能之间的跳转是无效的
  • iframe的样式显示、兼容性等都具有局限性

考虑到这些问题,iframe嵌入并不能满足我们的业务诉求,所以我们开始用微前端的方式来搭建HR系统。

在这个微前端的方案里,有几个我们必须要解决的问题:

  1. 一个前端需要对应多个后端
  2. 提供一套应用注册机制,完成应用的无缝整合
  3. 构建时集成应用和应用独立发布部署

只有解决了以上问题,我们的集成才是有效且真正可落地的,接下来详细讲解一下这几个问题的实现思路。

一个前端对应多个后端

HR系统最终线上运行的是一个单页应用,而项目开发中要求应用独立,因此我们新建了一个入口项目,用于整合各个应用。在我们的实践中,把这个项目叫做“Portal项目”或“主项目”,业务应用叫做“子项目”,整个项目结构图如下所示:

项目结构图

“Portal项目”是比较特殊的,在开发阶段是一个容器,不包含任何业务,除了提供“子项目”注册、合并功能外,还可以提供一些系统级公共支持,例如: * 用户登录机制 * 菜单权限获取 * 全局异常处理 * 全局数据打点

“子项目”对外输出不需要入口HTML页面,只需要输出的资源文件即可,资源文件包括js、css、fonts和imgs等。

HR系统在线上运行了一个前端服务(Node Server),这个Server用于响应用户登录、鉴权、资源的请求。HR系统的数据请求并没有经过前端服务做透传,而是被Nginx转发到后端Server上,具体交互如下图所示:

前后端分离图

转发规则上限制数据请求格式必须是 系统名+Api做前缀 这样保障了各个系统之间的请求可以完全隔离。其中,Nginx的配置示例如下:

server {listen          80;server_name     xxx.xx.com;location  /project/api/ {set $upstream_name "server.project";proxy_pass  http://$upstream_name;}...location  / {set $upstream_name "web.portal";proxy_pass  http://$upstream_name;}
}

我们将用户的统一登录和认证问题交给了SSO,所有的项目的后端Server都要接入SSO校验登录状态,从而保障业务系统间用户安全认证的一致性。

在项目结构确定以后,应用如何进行合并呢?因此,我们开始制定了一套应用注册机制。

应用注册机制

“Portal项目”提供注册的接口,“子项目”进行注册,最终聚合成一个单页应用。在整套机制中,比较核心的部分是路由注册机制,“子项目”的路由应该由自己控制,而整个系统的导航是“Portal项目”提供的。

路由注册

路由的控制由三部分组成:权限菜单树、导航和路由树,“Portal项目”中封装一个组件App,根据菜单树和路由树生成整个页面。路由挂载到DOM树上的代码如下:

let Router = <RouterfetchMenu = {fetchMenuHandle}routes = {routes}app = {App}history = {history}>
ReactDOM.render(Router,document.querySelector("#app"));

Router是在react-router的基础上做了一层封装,通过menu和routes最后生成一个如下所示的路由树:

  <Router><Route path="/" component={App}><Route path="/namespace/xx" component={About} /><Route path="inbox" component={Inbox}><Route path="messages/:id" component={Message} /></Route></Route></Router>

具体注册使用了全局的window.app.routes,“Portal项目”从window.app.routes获取路由,“子项目”把自己需要注册的路由添加到window.app.routes中,子项目的注册如下:

let app = window.app = window.app || {}; 
app.routes = (app.routes || []).concat([
{code:'attendance-record',	path: '/attendance-record',component: wrapper(() => async(require('./nodes/attendance-record'), 'kaoqin')),
}]);

路由合并的同时也把具体的功能做了引用关联,再到构建时就可以把所有的功能与路由管理起来。项目的作用域要怎么控制呢?我们要求“子项目”间是彼此隔离,要避免样式污染,要做独立的数据流管理,我们用项目作用域的方式来解决这些问题。

项目作用域控制

在路由控制的时候我们提到了 window.app,我们也是通过这个全局App来做项目作用域的控制。window.app包含了如下几部分:

let app = window.app || {};
app = {require:function(request){...},define:function(name,context,index){...},routes:[...],init:function(namespace,reducers){...}       
};

window.app主要功能:

  • define 定义项目的公共库,主要用来解决JS公共库的管理问题
  • require 引用自己的定义的基础库,配合define来使用
  • routes 用于存放全局的路由,子项目路由添加到window.app.routes,用于完成路由的注册
  • init 注册入口,为子项目添加上namesapce标识,注册上子项目管理数据流的reducers

子项目完整的注册,如下所示:

import reducers from './redux/kaoqin-reducer';
let app = window.app = window.app || {}; 
app.routes = (app.routes || []).concat([
{code:'attendance-record',	path: '/attendance-record',component: wrapper(() => async(require('./nodes/attendance-record'), 'kaoqin')),// ... 其他路由
}]);function wrapper(loadComponent) {let React = null;let Component = null;let Wrapped = props => (<div className="namespace-kaoqin"><Component {...props} /></div>);return async () => {await window.app.init('namespace-kaoqin',reducers);React = require('react');Component = await loadComponent();return Wrapped;};
}

其中做了这几件事情:

  1. 把路由添加到window.app中
  2. 业务第一次功能被调用的时候执行 window.app.init(namespace,reducers),注册项目作用域和数据流的reducers
  3. 对业务功能的挂载节点包装一个根节点:Component挂载在classNamenamespace-kaoqindiv下面

这样就完成了“子项目”的注册,“子项目”的对外输出是一个入口文件和一系列的资源文件,这些文件由webpack构建生成。

CSS作用域方面,使用webpack在构建阶段为业务的所有CSS都加上自己的作用域,构建配置如下:

//webpack打包部分,在postcss插件中 添加namespace的控制
config.postcss.push(postcss.plugin('namespace', () => css =>css.walkRules(rule => {if (rule.parent && rule.parent.type === 'atrule' && rule.parent.name !== 'media') return;rule.selectors = rule.selectors.map(s => `.namespace-kaoqin ${s === 'body' ? '' : s}`);})
));

CSS处理用到postcss-loader,postcss-loader用到postcss,我们添加postcss的处理插件,为每一个CSS选择器都添加名为.namespace-kaoqin的根选择器,最后打包出来的CSS,如下所示:

.namespace-kaoqin .attendance-record {height: 100%;position: relative
}.namespace-kaoqin .attendance-record .attendance-record-content {font-size: 14px;height: 100%;overflow: auto;padding: 0 20px
}
... 

CSS样式问题解决之后,接下来看一下,Portal提供的init做了哪些工作。

let inited = false;
let ModalContainer = null;
app.init = async function (namespace,reducers) {if (!inited) {inited = true;let block = await new Promise(resolve => {require.ensure([], function (require) {app.define('block', require.context('block', true, /^\.\/(?!dev)([^\/]|\/(?!demo))+\.jsx?$/));resolve(require('block'));}, 'common');});ModalContainer = document.createElement('div');document.body.appendChild(mtfv3ModalContainer);let { Modal} = block;Modal.getContainer = () => ModalContainer;}ModalContainer.setAttribute('class', `${namespace}`);mountReducers(namepace,reducers)
};

init方法主要做了两件事情:

  1. 挂载“子项目”的reducers,把“子项目”的数据流挂载了redux上
  2. “子项目”的弹出窗全部挂载在一个全局的div上,并为这个div添加对应的项目作用域,配合“子项目”构建的CSS,确保弹出框样式正确

上述代码中还看到了app.define的用法,它主要是用来处理JS公共库的控制,例如我们用到的组件库Block,期望每个“子项目”的版本都是统一的。因此我们需要解决JS公共库版本统一的问题。

JS公共库版本统一

为了不侵入“子项目”,我们采用构建过程中替换的方式来做,“Portal项目”把公共库引入进来,重新定义,然后通过window.app.require的方式引用,在编译“子项目”的时候,把引用公共库的代码从require('react')全部替换为window.app.require('react'),这样就可以将JS公共库的版本都交给“Portal项目”来控制了。

define 的代码和示例如下:

/**
* 重新定义包
* @param name  引用的包名,例如 react
* @param context 资源引用器 实际上是 webpackContext(是一个方法,来引用资源文件)
* @param index 定义的包的入口文件
*/
app.define = function (name, context, index) {let keys = context.keys();for (let key of keys) {let parts = (name + key.slice(1)).split('/');let dir = this.modules;for (let i = 0; i < parts.length - 1; i++) {let part = parts[i];if (!dir.hasOwnProperty(part)) {dir[part] = {};}dir = dir[part];}dir[parts[parts.length - 1]] = context.bind(context, key);}if (index != null) {this.modules[name]['index.js'] = this.modules[name][index];}
};
//定义app的react 
//定义一个react资源库:把原来react根目录和lib目录下的.js全部获取到,绑定到新定义的react中,并指定react.js作为入口文件
app.define('react', require.context('react', true, /^.\/(lib\/)?[^\/]+\.js$/), 'react.js');
app.define('react-dom', require.context('react-dom', true, /^.\/index\.js$/));

“子项目”的构建,使用webpack的externals(外部扩展)来对引用进行替换:

/*** 对一些公共包的引用做处理 通过webpack的externals(外部扩展)来解决*/
const libs = ['react', 'react-dom', "block"];module.exports = function (context, request, callback) {if (libs.indexOf(request.split('/', 1)[0]) !== -1) {//如果文件的require路径中包含libs中的 替换为 window.app.require('${request}'); //var在这儿是声明的意思 callback(null, `var window.app.require('${request}')`);} else {callback();}
};

这样项目的注册就完成了,还有一些需要“子项目”自己改造的地方,例如本地启动需要把“Portal项目”的导航加载进来,需要做mock数据等等。

项目的注册完成了,我们如何发布部署呢?

构建后集成和独立部署

在HR系统的整合过程中,开发阶段对“子项目”是“零侵入”,而在发布阶段,我们也希望如此。

我们的部署过程,大概如下:

第一步:在发布机上,获取代码、安装依赖、执行构建; 第二步:把构建的结果上传到服务器; 第三步:在服务器执行 node index.js 把服务启动起来。

“Portal项目”构建之后的文件结构如下:

“子项目”构建后的文件结构如下:

线上运行的文件结构如下:

把“子项目”的构建文件上传到服务器对应的“子项目”文件目录下,然后对“子项目”的资源文件进行集成合并,生成.dist目录中的文件,提供给用户线上访问使用。

每次发布,我们主要做以下三件事情:

  1. 发布最新的静态资源文件
  2. 重新生成entry-xx.js和index.html(更新入口引用)
  3. 重启前端服务

如果是纯静态服务,完全可以做到热部署,动态更新一下引用关系即可,不需要重启服务。因为我们在Node服务层做了一些公共服务,所以选择了重启服务,我们使用了公司的基础服务和PM2来实现热启动。

对于历史文件,我们需要做版本控制,以保障之前的访问能够正常运行。此外,为了保证服务的高可用性,我们上线了4台机器,分别在两个机房进行部署,最终来提高HR系统的容错性。

总结

以上就是我们使用React技术栈和微前端方式搭建的“类单页应用”HR业务系统,回顾一下这个技术方案,整个框架流程如下图所示:

在产品层面上,“微前端类单页应用”打破了独立项目的概念,我们可以根据用户的需求自由组装我们的页面应用,例如:我们可以在HR门户上把考勤、请假、OA审批、财务报销等高频功能放在一起。甚至可以让用户自己定制功能,让用户真的感受到我们是一个系统。

“微前端构建类单页应用”方案是基于React技术栈开发,如果把路由管理机制和注册机制抽离出来作为一个公共的库,就可以在webpack的基础上封装成一个业务无关性的通用方案,而且使用起来非常的友好。

截止目前,HR系统已经稳定运行了1年多的时间,我们总结了以下三个优点:

  1. 单页应用的体验比较好,按需加载,交互流畅
  2. 项目微前端化,业务解耦,稳定性有保障,项目的粒度易控制
  3. 项目的健壮性比较好,项目注册仅仅增加了入口文件的大小,30多个项目目前只有12K

作者简介

  • 贾召,2014年加入美团,先后主导了OA、HR、财务等企业项目的前端搭建,自主研发React组件库Block,在Block的基础上统一了整个企业平台的前端技术栈,致力于提高研发团队的工作效率。

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

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

相关文章

12种NumpyPandas高效技巧

文 | Kunal Dhariwal本文分享给大家 12 种 Numpy 和 Pandas 函数&#xff0c;这些高效的函数会令数据分析更为容易、便捷。最后&#xff0c;读者也可以在 GitHub 项目中找到本文所用代码的 Jupyter Notebook。项目地址&#xff1a;https://github.com/kunaldhariwal/12-Amazing…

LeetCode 1002. 查找常用字符(哈希)

1. 题目 给定仅有小写字母组成的字符串数组 A&#xff0c;返回列表中的每个字符串中都显示的全部字符&#xff08;包括重复字符&#xff09;组成的列表。例如&#xff0c;如果一个字符在每个字符串中出现 3 次&#xff0c;但不是 4 次&#xff0c;则需要在最终答案中包含该字符…

抖音算法推荐机制详解

抖音算法推荐机制详解&#xff01;&#xff08;科普向&#xff09; 众所周知抖音的流量分配是去中心化的&#xff0c;这种去中心化算法&#xff0c;让每个人都有机会爆红&#xff0c;可为什么别人几个粉玩抖音&#xff0c;就能轻松获得10w点赞?而你怒拍几十条也枉然? 抖音的…

论文浅尝 - ICLR2020 | 用于半监督分类的图形推理学习

论文笔记整理&#xff1a;周虹廷&#xff0c;浙江大学研究生。研究方向&#xff1a;知识图谱&#xff0c;图表示学习等。论文链接&#xff1a;https://arxiv.org/pdf/2001.06137.pdf本文是发表在ICLR2020上针对图数据做节点半监督分类任务的论文。现有的算法解决图上节点分类问…

WMRouter:美团外卖Android开源路由框架

WMRouter是一款Android路由框架&#xff0c;基于组件化的设计思路&#xff0c;功能灵活&#xff0c;使用也比较简单。 WMRouter最初用于解决美团外卖C端App在业务演进过程中的实际问题&#xff0c;之后逐步推广到了美团其他App&#xff0c;因此我们决定将其开源&#xff0c;希望…

Android官方开发文档Training系列课程中文版:管理系统UI之变暗系统条

原文地址&#xff1a;http://android.xsoftlab.net/training/system-ui/index.html 引言 系统条(System Bars)是屏幕上的一块显示区域&#xff0c;专门用来显示通知&#xff0c;设备的通讯状态以及设备的导向。典型的System Bars与APP同时显示在屏幕上。APP展示了具体的内容&…

实话实说:中文自然语言处理的N个真实情况

文 | Liu Huanyong按语中文自然语言处理&#xff0c;目前在AI泡沫之下&#xff0c;真假难辨&#xff0c;实战技术与PPT技术往往存在着很大的差异。目前关于AI或者自然语言处理&#xff0c;做的人与讲的人往往是两回事。作者简介Liu Huanyong&#xff0c;就职于中国科学院软件研…

Android官方开发文档Training系列课程中文版:管理系统UI之隐藏状态条

原文地址&#xff1a;http://android.xsoftlab.net/training/system-ui/status.html 这节课将会介绍如何隐藏不同的版本的状态条。隐藏状态条可以使内容展示区域更大&#xff0c;因此可以提供一种更强的身临其境的用户体验。 含有状态条的APP&#xff1a; 隐藏状态条的APP&am…

论文浅尝 - ACL2020 | 用于回答知识库中的多跳复杂问题的查询图生成方法

论文笔记整理&#xff1a;谭亦鸣&#xff0c;东南大学博士。来源&#xff1a;ACL 2020链接&#xff1a;https://www.aclweb.org/anthology/2020.acl-main.91.pdf1.介绍在以往的工作中&#xff0c;知识图谱复杂问答一般被分为两种类型分别处理&#xff1a;其一是带有约束的问题&…

深入理解JSCore

背景 动态化作为移动客户端技术的一个重要分支&#xff0c;一直是业界积极探索的方向。目前业界流行的动态化方案&#xff0c;如Facebook的React Native&#xff0c;阿里巴巴的Weex都采用了前端系的DSL方案&#xff0c;而它们在iOS系统上能够顺利的运行&#xff0c;都离不开一个…

全球44家机构,55位大佬,历时两年,打造最强NLG评测基准!

文 | 小轶&#xff08;大家好&#xff0c;我是已经鸽了夕总仨月没写文章了的小轶&#xff08;y&#xff09;&#xff01;新的一年一定改过自新&#xff0c;多读paper多写稿&#xff0c;望广大读者敦促(ง •̀_•́)ง&#xff09;今天要和大家分享的是卖萌屋学术站上的本月最热…

LeetCode 171. Excel表列序号(26进制转10进制)

1. 题目 给定一个Excel表格中的列名称&#xff0c;返回其相应的列序号。 例如&#xff0c;A -> 1B -> 2C -> 3...Z -> 26AA -> 27AB -> 28 输入: "A" 输出: 1输入: "AB" 输出: 28输入: "ZY" 输出: 701来源&#xff1a;力扣&…

用户评论标签的抽取

原文链接&#xff1a;https://blog.csdn.net/shijing_0214/article/details/71036808 无意中在知乎中看到一个问题&#xff1a;淘宝的评论归纳是如何做到的&#xff1f; 了解之后觉得较为容易实现&#xff0c;就简单实现了一个对用户评论的标签抽取功能&#xff0c;纯属兴趣所致…

开源开放|数据地平线通过OpenKG开放全行业因果事理、大规模实时事理等7类常识知识库...

本期介绍开放中文简称、中文同义、中文抽象、全行业因果事理、实体概念描述、实时事理知识库、军事武器装备知识等七个事理相关知识图谱。截至目前&#xff0c;该七个数据集规模达数千万、累计下载次数达两千余次&#xff0c;可用于底层事理推理、查询扩展、数据增强等多个自然…

写给工程师的十条精进原则

引言 时间回到8年前&#xff0c;我人生中第一份实习的工作&#xff0c;是在某互联网公司的无线搜索部做一个C工程师。当时的我可谓意气风发&#xff0c;想要大干一场&#xff0c;结果第一次上线就写了人生中第一个Casestudy。由于对部署环境的不了解&#xff0c;把SVN库里的配置…

我删掉了Transformer中的这几层…性能反而变好了?

文 | chaos编 | 小轶基于Transformer结构的各类语言模型&#xff08;Bert基于其encoder,Gpt-2基于其decoder&#xff09;早已经在各类NLP任务上大放异彩&#xff0c;面对让人眼花缭乱的transformer堆叠方式&#xff0c;你是否也会感到迷茫&#xff1f;没关系&#xff0c;现在让…

LeetCode 821. 字符的最短距离

1. 题目 给定一个字符串 S 和一个字符 C。返回一个代表字符串 S 中每个字符到字符串 S 中的字符 C 的最短距离的数组。 示例 1:输入: S "loveleetcode", C e 输出: [3, 2, 1, 0, 1, 0, 0, 1, 2, 2, 1, 0]来源&#xff1a;力扣&#xff08;LeetCode&#xff09; 链…

论文浅尝 - ESWC2020 | ESBM:一个面向实体摘要的评测集

本文转载自公众号&#xff1a;南大Websoft。实体摘要&#xff08;Entity Summarization&#xff09;&#xff0c;是知识图谱研究与应用中的一个关键问题。南京大学Websoft团队为此制作了一个评测集&#xff0c;称作ESBM&#xff0c;是目前可以公开获取的规模最大的评测集。这项…

美团在O2O场景下的广告营销

美团作为中国最大的在线本地生活服务平台&#xff0c;覆盖了餐饮、酒店、旅行、休闲娱乐、外卖配送等方方面面生活场景&#xff0c;连接了数亿用户和数百万商户。如何帮助本地商户开展在线营销&#xff0c;使得他们能快速有效地触达目标用户群体提升经营效率&#xff0c;是美团…

LeetCode 202. 快乐数(快慢指针)

1. 题目 2. 解题 一个数经过若干次各位数平方和后&#xff0c;会等于它自己使用类似环形链表的快慢指针法&#xff0c;最终快慢指针相遇&#xff0c;若不为1则是不快乐数 class Solution { public:int bitSquareSum(int n) {int sum 0, bit;while(n > 0){bit n % 10;su…