一、业务背景
近期的一个业务涉及对管理系统统进行重构,涉及业务主要是后端监控,包括服务器的CPU,内存、磁盘、QPS、QPM,JVM监控等,同时也包括用户和权限管理模块;前后端未分离,页面总数达到50+,整个项目加载后将近20M,相当庞大,我们要支持的既有之前代码的重构,又有新业务需求,工作量不小
二、待解决的研发痛点
本次技术选型,主要是解决之前开发时遇到的痛点:
- 工程越来越复杂,打包越来越慢,构建产出物越来越大
- 团队人员多,产品功能复杂,代码冲突多、影响面大
- 前后端未分离,或分离不彻底
- 界面和逻辑混杂
- 发布流程耗时、繁琐
核心目的都是提高用户体验、理顺开发流程、提高工程效率;经过筛选,我们最终选择了微前端的开发方案。
三、微前端简介和实践
微前端简单地说,就是将一个大的前端工程拆分成一个一个的小工程。别小看这些小工程,它们也都是可以独立开发、构建、运行、发布的;整个系统就是由这些小工程协同合作,实现整个系统的展示与交互。我们运用一种新技术,并不是因技术新老,而是因为他们能够真正有效的解决我们当前页面遇到的问题,接下来详细讲解一下微前端框架icestark在我们项目中的应用和最佳实践。
原理简介
微前端分为框架应用和子应用,框架应用内部维护了所有子应用的配置信息,包括路由规则、bundle 地址等,同时劫持了 window.history
相关的几个跳转事件,当捕获到页面跳转事件时,框架会根据跳转的路由获取对应的子应用信息,然后跟之前的子应用信息进行对比,如果是同一个子应用,则什么都不做,如果是不同的子应用,则将前一个子应用的 bundle 卸载,同时加载新的子应用 bundle 资源,加载完成后子应用 bundle 会执行自身的渲染逻辑。一个系统只有一个框架应用,框架应用负责整体的 Layout 以及子应用的管理与注册;子应用通常是一个单页面应用,可能包含一个或多个页面,子应用负责自身相关页面代码。
整体方案
-
项目目录
│───dist ----------------------项目打包目录 ├── package-lock.json ├── package.json ├── src -------------------------开发目录 │ ├── common │ │ ├── components │ ├── consolePro │ ├── frameIndex ---------------主应用 │ ├── homePro │ ├── screenPro ---------------子应用 │ │ ├── api │ │ │ └── index.js │ │ ├── components ----------当前子应用公共组件 │ │ │ └── Func │ │ │ ├── index.jsx │ │ │ └── index.scss │ │ ├── index.html -----------html │ │ ├── index.js ------------入口js │ │ ├── store ------------store │ │ ├── action │ │ │ └── index.js │ │ ├── action-types.js │ │ ├── index.js │ │ └── reducer │ │ └── index.js │ ├── settingPro -------------子应用
-
主应用
实际项目中主应用包括整体Layout,Header、Footer、左侧菜单、子应用的注册。比较核心的部分是路由注册机制,我们要处理好本地、预发、生产不同的环境下的子应用注册,这里应该从构建角度统一处理,避免打包时到处手动修改配置
import React from "react"; import { AppRouter, AppRoute } from "@ice/stark"; class MainFrame extends Component { getSubAppResource(app){ let appConfig = { dashboard: { development: [ "http://localhost/js/main.js", "http://localhost/css/index.css" ], beta: [ "http://beta.com/css/index.css", "http://beta.com/js/main.js", ], production: [ "https://pro.com/css/index.css", "https://pro.com/js/main.js", ] } } return appConfig[app][process.env.NODE_ENV] } render() { return ( <AppRouter onRouteChange={this.handleRouteChange} onAppLeave={this.handleAppLeave} onAppEnter={this.handleAppEnter} > <AppRoute path="/dashboard" basename="/dashboard" hashType={true} title="数据大屏" url={this.getSubAppResource('dashboard')} /> <!--其他子应用--> </AppRouter> ); } }
-
子应用
子应用的划分主要依据业务的归类,例如:设置子应用,数据大屏子应用、控制台子应用等,子应用都会在中间模块展示。由于本次是同一团队开发,全部项目都放到一个大的工程里,如果多团队协作,子应用也可以分离出去,子应用目录结构和主应用一致。
-
共享模块
多个应用通用模块可独立出来,防止代码冗余;例如:搜索组件、404页面、公用工具类等
构建优化
-
整体方案
提取公共Webpack配置文件common.js,框架和子应用都有自己的配置文件,一方面继承公共配置,另一方面配置自己的启动端口和publicpath等差异化属性。另外npm script需要针对每个应用和各个环境定制,例如主框架本地启动:npm run start;主框架预发环境构建:npm run build-beta;主框架生产环境构建:npm run build-pro;其他子应用也一样,核心目的是通过不同的命令构建适配不同环境的代码,避免发布时手动到处修改代码适配各个环境。
-
构建优化
-
node_modules共享:多个子应用共享node_modules,较少体积,提高效率
-
pre-build cdn + bundless:通过DllPlugin提前打包公共类库并发布到cdn,通过script引入,剩余要构建的只包含业务代码,优化过程中可通过webpack-bundle-analyzer插件分析bundle的结构,我们最终每个子应用构建后大小在200kb左右。
-
作用域控制
多个子应用css和js可能相互影响,我们可通过以下方式避免:
-
CSS 隔离
-
尽量少使用基础组件(防止子应用有不同版本造成冲突)
-
尽量使用独有的 class,可以添加统一的前缀或者开启 CSS Module
-
-
js隔离
-
全局性的变量、函数需要添加统一前缀,避免重复
-
各个应用的开发团队之间,确定一些开发公约
-
-
最后如果是接入已有代码且评估影响较大,还是建议回退到iframe方式接入,减少冲突和影响
前后端分离
-
未分离的痛点
- 本地开发:强依赖后端,需要启动后端项目才能运行前端,后端环境搭建复杂。
- 发布:两端耦合,一端发布必须连带另一端。
- 代码管理:某个分支到底是处理前端问题还是后端问题?如果是主要处理前端问题但又必须携带后端代码,导致git仓库臃肿。
-
怎样分离
- 开发分离: 使用Mock;后端支持cros;使用devServer的proxy实现接口代理。
- 发布分离:我们要实现全部分离,包括html、js、css、图片等所有静态资源,发布项目下包括主框架和各个子应用,可以分别分布,互不影响。
-
延伸
- 前端发布也是整个前端工程化的重要组成部分,我们的目标是:a、高效发布(预发问题调试非常需要频繁改动代码后立即编译、发布、多终端观察效果) b、可随时回滚任意版本 c、线上发布需审批 d、节省资源 e、可支持独立域名 f、高并发。如果使用公司发布平台发布一个应用,预发环境至少一台2C2G,线上环境双机房至少两台4C4G,这样的成本,想想都心疼,我们的最佳实践是基于 OSS + Domain Proxy 的自研前端发布系统,配合发布脚本可以在预发环境一键构建并发布多个项目,Domain Proxy服务器4C4G单台可达3万tps,彻底解决前端发布难题,非常适合微前端应用发布。
四、总结
以上就是我们使用微前端的最佳实践,最终我们追求的目标是:
- 更好的用户体验
- 前后端彻底分离解耦,包括开发阶段 + 发布阶段
- 方便多人甚至多团队协作
- 界面和逻辑分离
- 提高构建速度,较小构建产物
- 可快速独立部署
由于本人水平有限,希望大家多提意见建议,共同探讨微前端最佳实践,推动微前端项目的落地。