项目背景
业务的快速发展,越来越多的接入渠道(百度、快应用等等),人员增加,开发成本与管理成本都上升,效率反而越来越低,团队的人员重复造轮子,毫无挑战,当然市面上也有多端解决方案,但是不太适用目前的业务,所以前端聚合成“微前端”的概念。
单体应用与「微前端」架构
在传统的软件开发当中,大多数软件都是单体式应用架构的。为了适应我们这个时代的不确定性。快速试验,快速失败。更快地推出新产品和有效地改进当前产品,从而为客户提供有意义的数字体验。
而单体应用这种软件架构对于企业来说的致命缺点就是,对于市场的响应速度变慢。由于依赖关系,其响应周期往往会变得非常漫长。此时前端工程化越来越复杂,每次的整站编译耗时严重,根据用户漏斗原则,有一部分无效的消耗。
「微前端」的好处
- 安全高效:服务分离,降低整体风险与测试等成本;
- 灵活扩展:为每一个服务选择最合适的技术和基础架构;
- 独立敏捷:每一个服务可以独立开发,测试和部署。
「微前端」的四种可选实践方案
使用后端模板引擎插入 HTML (方案A
)
方案会增加后端复杂度,并且又将前端的渲染控制权交回了后端服务器。作为一个前端开发人员,一般不会选择该方案,但并不是完全没用,需要看使用场景,如果只是纯展示的,可以适当考虑。
客户端 JavaScript 异步加载 (方案B
)
这种方式可以通过前端模块化方式在开发整个「微前端」方案(AMD,CMD等),对开发效率有一定影响。
WebComponents 整合所有功能模块 (方案C
)
使用了较新的技术方案,在较老的浏览器上,可能兼容性欠缺。我们需要在整个 Web 应用程序上做出改变,把它们全部转换成 Web Components。
使用 iframe 隔离运行 (方案D
)
优点
- 最强大的是隔离了组件和应用程序部分的运行时环境,因为每个模块都可以独立开发,并且可以与其他部分的技术无关;
- 因为每个模块都可以独立开发,我们可以使用我们最熟悉的框架开发「微前端」模块;
- 可以各自使用完全不同的前端框架,可以在 React 中开发一部分,在 Vue 中开发一部分,然后使用原生 JavaScript 开发其他部分或任何其他技术;
- 消息传递也就相当强大。(Window.postMessageAPI)。
缺点
- Bundle的大小非常明显,因为应用程序是分开的,所以在构建时也不能提取公共依赖关系;
- 考虑到浏览器性能问题,尽量避免iframe的多层调用;
- 处理移动端中的iframe时,将变得相当痛苦。
「微前端」的技术选型
需要接入的「微前端」的应用
「微前端」开发要求
- 准:快速找出当前应用的痛点,给出「微前端」解决方案;
- 快:快速迭代开发上线;
- 稳:保证线上代码兼容性以及稳定运行。
「微前端」的演变过程
前端单体应用痛点
- 每个站点中,都有一个通用的模块《常旅》;
- 这么多站点也不是一次性完成,每次需要开发新的站点,都通过Copy的方式,将《常旅》复制一份;
- 当站点积累到一定程度时,突然《常旅》有个新的需求需要开发;
- 此时,我们的开发量就成倍增加(1 x N),同时测试的工作量也一样。
单体应用到的「微前端」进化
「微前端」优劣
- 前端单体应用中的常旅模块分离,增加通用的「微前端」服务模组;
- 常旅模块独立为一个单独的「微前端」应用,为每个站点提供”常旅模块“服务;
- 此时,如果需要做「常旅功能迭代」,工作量永远只需要一份就可以;
- 我们需要保证「微前端」稳定运行,一旦奔溃,将影响所有引入的单体应用。
「微前端」架构方案
主框架:vue + vue-router + vuex
- 通讯方案:url入参 + postMessage + vue-unicom(内部广播机制)
- 滚屏:iscroll
- 布局方案:px2rem (750)布局
- 承载方式:iframe + webview
- 静态资源加载优化:asset-cache-webpack-plugin(h5离线缓存)
- 宿主(iframe)优化方案 「微前端」预加载
「微前端」数据通讯方案
「微前端」数据通讯方案
「微前端」部分数据通讯代码
// routerimport router from '../../router' // 获得一个唯一的值import getSole from 'rimjs/sole'// 全局unicom事件触发import { unicomEmit } from 'rimjs/vueUnicom' // 当前环境的变量数据,这里主要是设置身份import { env } from '../env'let win = window// 默认为 iframe 需要兼容其他的,可以这里兼容function postMessage(data, source = window.parent) {source.postMessage(data, '*')}// 临时存放 Promise 的resolve的对象let postMessageResolveFns = {} // 信息输入数据let messageInData = null// 信息输出数据 暂存let messageOutData = {}let query = router.queryif (query._in != null) {
messageInData = {}}if (messageInData) {// 是否为类微信小程序内(webview和小程序无法实时双向通讯)// 传入的参数需要一次性输入try {Object.assign(messageInData, JSON.parse(query._in) || {})} catch (e) {}}// 用户同步用户身份信息的函数function setUser(inEnv) {if (!inEnv) {return}env.userId = inEnv.userId || ''}// 从宿主获取用户身份async function getUser() {let inEnv = await bridge.postMessage('env:get')setUser(inEnv)}// 接收消息// type:类型 fnKey:回调方法 insruct:指令 data:数据function onMessage({ type, insruct, data } = {}, source) {let backData = nullif (type == 'unicom') {
backData = data == null ? {} : data// 事件广播, 通过这个方法,宿主环境可以向「微前端」端发送广播消息unicomEmit(insruct, backData)return}if (type == 'system') {// 一些系统设置if (insruct == 'font') {// rem布局,根节点 字体大小// 「微前端」如果为预加载,无法正确计算出根节点字体大小,需要宿主通知document.documentElement.style.fontSize = datareturn}if (insruct == 'href') {setUser(data.env)// iframe 预加载,重新定位 当前路由if (data.replace) {window.location.replace(window.location.pathname + '#' + data.href)} else {window.location.href = window.location.pathname + '#' + data.href}return}if (insruct == 're_init') {// 重新初始化页面,一般在iframe中,返回宿主环境后使用window.location.replace(window.location.pathname + '#/init?re=1')return}return}if (type == 'back') {// 找到回调的Promiselet resolveFn = postMessageResolveFns[insruct]if (resolveFn) {// 运行 resolveFnresolveFn(data)// console.log("data", data)delete postMessageResolveFns[insruct]}return}if (fnKey) {// 数据回调postMessage({ type: 'back', from: 'microservice', insruct: fnKey, data: backData }, source)}}// 注册接收的消息// 如果需要兼容其他渠道,可以这里写不同的window.addEventListener('message', function (ev) {let opt = ev.data || {}let { from } = optif (from != 'microservice') {// 非目标,舍弃return}onMessage(opt, ev.source)})// 对象定义export let bridge = {// postMessage 发送消息// 可以通过 Promise 获取到宿主的回调函数的值// bridge.postMessage("xxx") 同 bridge.postMessage("unicom:xxx")// data 为发送的参数postMessage(opt, data) {if (typeof opt == 'string') {let x = opt.match(/^([^:]*):*(.*)$/)if (!x) {
x = [opt]}if (!x[2]) {// type 默认为 unicom
x[2] = x[1]
x[1] = 'unicom'}
opt = {
data,type: x[1],insruct: x[2]}}return new Promise(function (resolve) {// 回调唯一的keylet fnKey = 'ms:' + getSole()
postMessageResolveFns[fnKey] = resolveif (messageInData) {// 此处无法直接使用 postMessagelet key = opt.type + ':' + opt.insruct// 寄存需要发送出去的数据
messageOutData[key] = data// console.log(messageOutData, key, args)setTimeout(function () {// 模拟接收到 back 事件onMessage({type: 'back',insruct: fnKey,data: messageInData[key] || null})}, 0)return}// 发送下次postMessage(Object.assign({ from: 'microservice', fnKey }, opt))})},// 结束「微前端」endBack(opt, data) {if (opt) {// 如果在「微前端」中有对身份信息做一些修改,需要同步到宿主this.postMessage(opt, data)}// 退出「微前端」界面// 不同载体,需要不同的方法// 比如 微信小程序,就需要 win.wx.miniProgram.navigateBack()if (messageInData) {// 此处微信小程序内嵌webview处理// 微信返回win.wx.miniProgram.navigateBack()// 微信发送寄存的数据win.wx.miniProgram.postMessage({ data: messageOutData })return}// 历史记录后退window.history.back()}}// 初始化时,尝试获取用户身份信息getUser()
滚屏为什么要使用iscroll?
问题 iframe引入时,body上的滚屏失效。
解决方案: 引入 iscrollbar5.2 实现自定义滚动条。
"less">
.cp-layout {
position: fixed;
left: 0;
right: 0;
top: 0;
bottom: 0;
z-index: 2;
overflow: hidden;
.iScrollVerticalScrollbar {
position: absolute;
z-index: 9999;
width: 3px;
bottom: 2px;
top: 2px;
right: 4px;
overflow: hidden;
pointer-events: none;
}
.iScrollIndicator {
box-sizing: border-box;
position: absolute;
border-radius: 3px;
width: 100%;
transition-duration: 0ms;
transform: translate(0px, 0px);
transition-timing-function: cubic-bezier(0.1, 0.57, 0.1, 1);
background-color: rgba(0, 0, 0, 0.2);
}
}
class="cp-layout">
通讯方案使用了postMessage为什么还要使用url入参?
问题:小程序webview对postMessage受限。解决方案: 使用url传参一次性传入所有参数。完成后,通过postMessage将数据一次性全部发送到宿主。
布局中,为什么要使用px2rem?
问题:iframe引入时,viewport 设置布局模式无效。解决方案: 引入 px2rem ,用rem来做整体布局。
「微前端」优化
如何减少「微前端」加载白屏时间?
- 通过骨架屏,减少白屏时间;
- 通过引入 asset-cache-webpack-plugin 插件,为资源加载做离线缓存(减少或消除白屏时间);
- iframe时,使用单列模式开发,避免同时初始化多个「微前端」,造成性能损耗;
- iframe加载微服务时,对「微前端」预加载。当需要使用时,可以快速展现。
引入 px2rem
和 asset-cache-webpack-plugin
vue.config.js
const AssetCachePlugin = require('asset-cache-webpack-plugin')module.exports = {devServer: {port: 9001
},// 样式配置css: {// css不单独一个文件编译extract: false,loaderOptions: {postcss: {plugins: [require('postcss-plugin-px2rem')({rootValue: 100, //换算基数, 默认100 ,这样的话把根标签的字体规定为1rem为50px,这样就可以从设计稿上量出多少个px直接在代码中写多上px了。exclude: /(node_module)/,mediaQuery: false, //(布尔值)允许在媒体查询中转换px。minPixelValue: 0 //设置要替换的最小像素值(3px会被转rem)。默认 0
}) ]
conf.plugin('asset-cache').use(AssetCachePlugin, [
总结与思考:「微前端」的优缺点
优点
- 敏捷性 - 独立开发和更快的部署周期;
- 快捷测试 - 每一个小的变化不必再触碰整个应用程序的回归测试;
- 有助于持续集成、持续部署以及持续交付;
- 维护简单,每个团队都熟悉所维护特定的区域。
缺点
- 复杂的集成,「微前端」需要面对的多种环境,需要做多种兼容性验证;
- 第三方模块重叠,依赖冗余增加了管理的复杂性。在团队之间共享公共资源的机制;
- 避免影响最终用户的体验,「微前端」初始化可能会增加不必要的等待时间。
使用场景
- 在单体应用中那些重复试用的模块可以抽取为「微前端」;
- 这些模块也较稳定,代码迭代相对较少;
- 在单体应用中的那些主流程,不适用于「微前端」。