律师事务所网站案例,天下第一社区在线观看 welcome,竞价推广网络推广运营,高端建站选哪家前言 微前端由ThoughtWorks 2016年提出#xff0c;将后端微服务的理念应用于浏览器端#xff0c;即将 Web 应用由单一的单体应用转变为多个小型前端应用聚合为一的应用。 美团已经是一家拥有几万人规模的大型互联网公司#xff0c;提升整体效率至关重要#xff0c;这需要很… 前言 微前端由ThoughtWorks 2016年提出将后端微服务的理念应用于浏览器端即将 Web 应用由单一的单体应用转变为多个小型前端应用聚合为一的应用。 美团已经是一家拥有几万人规模的大型互联网公司提升整体效率至关重要这需要很多内部和外部的管理系统来支撑。由于这些系统之间存在大量的连通和交互诉求因此我们希望能够按照用户和使用场景将这些系统汇总成一个或者几个综合的系统。 我们把这种由多个微前端聚合出来的单页应用叫做“类单页应用”美团HR系统就是基于这种设计实现的。美团HR系统是由30多个微前端应用聚合而成包含1000多个页面300多个导航菜单项。对用户来说HR系统是一个单页应用整个交互过程非常顺畅对开发者同学来说各个应用均可独立开发、独立测试、独立发布大大提高了开发效率。 接下来本文将为大家介绍“微前端构建类单页应用”在美团HR系统中的一些实践。同时也分享一些我们的思考和经验希望能够对大家有所启发。 HR系统的微前端设计 因为美团的HR系统所涉及项目比较多目前由三个团队来负责。其中OA团队负责考勤、合同、流程等功能HR团队负责入职、转正、调岗、离职等功能上海团队负责绩效、招聘等功能。这种团队和功能的划分模式使得每个系统都是相对独立的拥有独立的域名、独立的UI设计、独立的技术栈。但是这样会带来开发团队之间职责划分不清、用户体验效果差等问题所以就迫切需要把HR系统转变成只有一个域名和一套展示风格的系统。 为了满足公司业务发展的要求我们做了一个HR的门户页面把各个子系统的入口做了链接归拢。然而我们发现HR门户的意义非常小用户跳转两次之后又完全不知道跳到哪里去了。因此我们通过将HR系统整合为一个应用的方式来解决以上问题。 一般而言“类单页应用”的实现方式主要有两种 iframe嵌入微前端合并类单页应用其中iframe嵌入方式是比较容易实现的但在实践的过程中带来了如下问题 子项目需要改造需要提供一组不带导航的功能iframe嵌入的显示区大小不容易控制存在一定局限性URL的记录完全无效页面刷新不能够被记忆刷新会返回首页iframe功能之间的跳转是无效的iframe的样式显示、兼容性等都具有局限性考虑到这些问题iframe嵌入并不能满足我们的业务诉求所以我们开始用微前端的方式来搭建HR系统。 在这个微前端的方案里有几个我们必须要解决的问题 一个前端需要对应多个后端提供一套应用注册机制完成应用的无缝整合构建时集成应用和应用独立发布部署只有解决了以上问题我们的集成才是有效且真正可落地的接下来详细讲解一下这几个问题的实现思路。 一个前端对应多个后端 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最后生成一个如下所示的路由树 RouterRoute path/ component{App}Route path/namespace/xx component{About} /Route pathinbox component{Inbox}Route pathmessages/: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 classNamenamespace-kaoqinComponent {...props} //div);return async () {await window.app.init(namespace-kaoqin,reducers);React require(react);Component await loadComponent();return Wrapped;};
} 其中做了这几件事情 把路由添加到window.app中业务第一次功能被调用的时候执行 window.app.init(namespace,reducers)注册项目作用域和数据流的reducers对业务功能的挂载节点包装一个根节点Component挂载在className为namespace-kaoqin的div下面这样就完成了“子项目”的注册“子项目”的对外输出是一个入口文件和一系列的资源文件这些文件由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-loaderpostcss-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方法主要做了两件事情 挂载“子项目”的reducers把“子项目”的数据流挂载了redux上“子项目”的弹出窗全部挂载在一个全局的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目录中的文件提供给用户线上访问使用。 每次发布我们主要做以下三件事情 发布最新的静态资源文件重新生成entry-xx.js和index.html更新入口引用重启前端服务如果是纯静态服务完全可以做到热部署动态更新一下引用关系即可不需要重启服务。因为我们在Node服务层做了一些公共服务所以选择了重启服务我们使用了公司的基础服务和PM2来实现热启动。 对于历史文件我们需要做版本控制以保障之前的访问能够正常运行。此外为了保证服务的高可用性我们上线了4台机器分别在两个机房进行部署最终来提高HR系统的容错性。 总结 以上就是我们使用React技术栈和微前端方式搭建的“类单页应用”HR业务系统回顾一下这个技术方案整个框架流程如下图所示 在产品层面上“微前端类单页应用”打破了独立项目的概念我们可以根据用户的需求自由组装我们的页面应用例如我们可以在HR门户上把考勤、请假、OA审批、财务报销等高频功能放在一起。甚至可以让用户自己定制功能让用户真的感受到我们是一个系统。 “微前端构建类单页应用”方案是基于React技术栈开发如果把路由管理机制和注册机制抽离出来作为一个公共的库就可以在webpack的基础上封装成一个业务无关性的通用方案而且使用起来非常的友好。 截止目前HR系统已经稳定运行了1年多的时间我们总结了以下三个优点 单页应用的体验比较好按需加载交互流畅项目微前端化业务解耦稳定性有保障项目的粒度易控制项目的健壮性比较好项目注册仅仅增加了入口文件的大小30多个项目目前只有12K作者简介 贾召2014年加入美团先后主导了OA、HR、财务等企业项目的前端搭建自主研发React组件库Block在Block的基础上统一了整个企业平台的前端技术栈致力于提高研发团队的工作效率。