本文已参与好文召集令活动,点击查看:后端、大前端双赛道投稿,2万元奖池等你挑战!
前言
本文是我终于把你送进了大厂的另一个版本,准确的说是女生视角下的故事。本文感情线和技术线会更加细腻,请细细体会。
0. 商业解决方案
我叫白依依。
“小白,了解一些商业化的解决方案就走吧。”
我看着我桌上那份 《小白的商业解决方案》 郁闷了很久。老大告诉我那句话差点没让我难过不安1小时,直到我看到桌上那份打印好的资料。你说封面上写着“小白”二字就是意外着开玩笑对不对?说吗商业解决方案,一听就是业界大佬才有资格触及的区域。
我,小白,和真小白也差不多……
我暗自懊恼,心里吐槽老大,说个话就不能说清楚。我翻开第一页,一行加粗大字:微前端架构。
微前端架构
先来看张图举个美团的栗子。
看起来这么复杂的吗?
- 基础能力层
- 基础能力。例如权限管理(拥有什么角色就能看见什么内容)
- 产品功能层
- 具体的需求功能实现。例如 数据分析师角色进入你的平台要看分析报告
- 业务方案层
- 以业务划分出不同区域
- 相关系统
- 你的平台集成了多少系统?
(到这,看不太懂没关系。先记住一个概念,看看别人家做的微前端架构师基于哪些内容。
)
重新来总结一下,到底什么是微前端架构。
- 和业务不相关,对访问产品进行辅助作用的内容放入一个容器。
- 实现产品功能相关,可以是多个容器。
- 对不同的产品做一个功能集合,形成不同业务方案。
- 1-3可实现为一个系统。多个系统请重复执行1-3.
(到这,还没明白也没关系。重新再说一遍人话。
)
-
假如你有一个前端项目(主应用),其中啥事也不干,只做一件事情。那就是把访问不同系统的能力集成在一起,搭配角色即可成功访问相对应的内容。
-
然后你有很多前端项目(微应用),其中除了产品功能啥也不干,不管权限也不管角色正确。举个栗子,你会有 商城系统、到店系统、供应链系统等。 然后每个系统又可以拥有很多个产品,产品又分布在不同微应用当中。
(到这,觉得依然不够说人话程度也没关系。重新分析一下
)
各种方案调研
方案 | 介绍 | 优势 | 劣势 | |
---|---|---|---|---|
1. iframe | </div> | 1. 开发和引入成本低。 | 1. 主/微应用通信困难 | |
2. 兼容性高 | 2. 内部跳转不友好,例如登录跳转 | |||
3. 隔离性好 | 3. 组件、模块无法复用 | |||
2. npm集成 | 把子项目抽打成包发布到npm,谁要用谁引入依赖便好。 | 1.编译过程、项目运行过程无需额外加载资源,所以体验会比较流畅 | 1. 主应用的编译速度和打包后的体积容易过大 | |
2.开发接入成本低 | 2. 不支持动态下发,按需加载,npm包更新后,主应用需要重新发布 | |||
3. single-spa/qiankun | 通过路由变化驱动主应用加载微应用的bundle,将其渲染至指定节点 | 1. 隔离机制完善,例如沙箱。可隔离样式、js脚本。 | 1. 微应用间通信成本较高 | |
2. 主应用不限制接入应用的技术栈 | 2.需要对微应用作额外管理 | |||
4. 模块联邦 | webpack5 新增功能插件,实现模块联邦、组件联邦 | 最大程度实现模块、组件自由化,去中心化 | 太过自由,依然缺乏管理 |
所以看得出来,qiankun更适合应用级的集成。模块联邦。模块/组件颗粒度更大的集成
qiankun 简单分析源码
- 初始化全局配置
- 注册子应用 registerMicroApps(apps, lifeCycles?)
- 获取子应用资源 使用了 import-html-entry 库从 entry 进入加载子应用,加载完成后将返回一个对象: {}
const {template,execScript,assetPublicPath,getExternalScripts,getExternalStyleSheets} = await importEntry(entry,{ getTemplate:flow(getTemplate,getDefaultTplWrapper(appName)), ...settings }) console.log(template()); // => <!DOCTYPE html> <html>...</html> console.log(getExternalScripts()); // 得到js脚本 list [ /******/(function(modules){/* webpackBootstrap... /*******/})(), /******/(this["webpackJsonP_react"] = this['webpackJsonP.r' /******/]), .... ] * */ console.log(getExternalStyleSheets()) ; // => [] 样式表 css // 然后我们再来分析 execScripts 方法,该方法的作用就是指定一个 proxy(默认是 window)对象,然后执行该模板文件中所有的 JS,并返回 JS 执行后 proxy 对象的最后一个属性 // 在微前端架构中,这个对象一般会包含一些子应用的生命周期钩子函数,主应用可以通过在特定阶段调用这些生命周期钩子函数,进行挂载和销毁子应用的操作。
- 主应用挂载子应用 HTML 模板
- 沙箱运行环境 - genSandbox
// genSandbox 部分代码如下 export function genSandbox(appName: string,singular: boolean){ ... let sandbox: SandBox; if(window.Proxy){ sandbox = singular ? new ProxySandBox(appName): new LegacySandBox(appName) }else{ sandox = new SnapshotSandbox(appName) } ... } // 可以看出 genSandbox 内部的沙箱主要是通过是否支持 window.Proxy 分为 ProxySandbox 和 SnapshotSandbox 两种(多实例还有一种 LegacySandbox 沙箱) /** * 基于 diff 方式实现的沙箱,用于不支持 Proxy 的低版本浏览器 */ export default class SnapshotSandbox implements SandBox { proxy: WindowProxy; name: string; type: SandBoxType; sandboxRunning: boolean; private windowSnapshot; private modifyPropsMap; constructor(name: string); active(): void; inactive(): void; } // SnapshotSandbox 的沙箱环境主要是通过激活时记录 window 状态快照,在关闭时通过快照还原 window 对象来实现的
- 挂载沙箱 - mountSandbox
- 计时器劫持 - patchTimer
- 动态添加样式表和脚本文件劫持 - patchDynamicAppend
- 卸载沙箱 - unmountSandbox
- 不写了,再下下去你们也不会看 😂
- 注册内部生命周期函数
- 进入到 mount 挂载流程
- 进入到 unmount 卸载流程
- 启动主应用 start(opts?)
所依,你看懂qiankun了吗?
step 1: 主应用先获取微应用的HTML模版、JS脚本、CSS样式表 三大法宝。
step 2: 挂载微应用资源
step 3: 注册生命周期
模块联邦
ModuleFederationPlugin 的用法
new ModuleFederationPlugin({
name: "app1",
library: { type: "var", name: "app1" },
filename: "remoteEntry.js",
remotes: {
app2: 'app2',
app3: 'app3',
},
remoteType: 'var',
exposes: {
antd: './src/antd',
button: './src/button',
},
shared: ['react', 'react-dom'],
shareScope: 'default'
})
配置属性:
name
,必须,唯一 ID,作为输出的模块名(容器),使用的时通过name/{name}/name/{expose}
的方式使用;library
,可选,打包方式,默认{ type: "var", name: options.name }
,其中这里的name
为作为umd
的name
,是挂载在全局下的变量名;filename
,可选,打包后的文件名;remotes
,可选,表示当前应用是一个 Host,可以引用 Remote 中 expose 的模块;remoteType
,可选,默认 var,("var"|"module"|"assign"|"this"|"window"|"self"|"global"|"commonjs"|"commonjs2"
等远程容器的外部类型;exposes
,可选,表示当前应用是一个Remote
,exposes
内的模块可以被其他的Host
引用,引用方式为import(name/{name}/name/{expose})
;shared
,可选,主要是用来避免项目出现多个公共依赖,若是配置了这个属性,webpack在加载的时候会先判断本地应用是否存在对应的包,若是不存在,则加载远程应用的依赖包;shareScope
,可选,用于所有共享模块的共享作用域名称
// 公共依赖shared的配置项
Shared = string[] | {
[string]: {
eager?: boolean; // 是否立即加载模块而不是异步加载
import?: false | SharedItem; // 应该提供给共享作用域的模块。如果在共享范围中没有发现共享模块或版本无效,还充当回退模块。默认为属性名
packageName?: string; // 设置包名称以查找所需的版本。只有当包名不能根据请求自动确定时,才需要这样做(如要禁用自动推断,请将requiredVersion设置为false)。
requiredVersion?: false | string; // 共享范围内模块的版本要求
shareKey?: string; // 用这个名称在共享范围中查找模块
shareScope?: string; // 共享范围名称
singleton?: boolean; // 是否在共享作用域中只允许共享模块的一个版本 (单例模式).
strictVersion?: boolean; // 如果版本无效则不接受共享模块(默认为true,如果本地回退模块可用且共享模块不是一个单例,否则为false,如果没有指定所需的版本则无效)
version?: false | string; // 所提供模块的版本,将替换较低的匹配版本
}[]
}
敲黑板,划重点!
在使用 Module Federation
的时候一定要记得,将公共依赖配置到 shared
中。另外,一定要两个项目同时配置 shared
,否则会报错
ModuleFederationPlugin 的原理
ModuleFederationPlugin主要做了三件事:
-
如何共享依赖:使用
SharePlugin
。 该插件使公共依赖可共享 -
如何公开模块:使用
ContainerPlugin
。 该插件为指定的公开模块创建entry
。entry.js``执行后会在window
上挂一下对象,该对象有两个方法,get和init
。get
方法用来获取模块。init
方法用来初始化容器,它可以提供共享模块。// remote对象里的get和init方法 var get = (module, getScope) => { __webpack_require__.R = getScope; getScope = ( __webpack_require__.o(moduleMap, module) ? moduleMap[module]() : Promise.resolve().then(() => { throw new Error('Module "' + module + '" does not exist in container.'); }) ); __webpack_require__.R = undefined; return getScope; }; var init = (shareScope, initScope) => { if (!__webpack_require__.S) return; var oldScope = __webpack_require__.S["default"]; var name = "default" if(oldScope && oldScope !== shareScope) throw new Error("Container initialization failed as it has already been initialized with a different share scope"); __webpack_require__.S[name] = shareScope; return __webpack_require__.I(name, initScope); }; // This exports getters to disallow modifications __webpack_require__.d(exports, { get: () => get, init: () => init });
在使用 Remote
的模块时候,通过 init
将自身 shared
写入 Remote
中,再通过 get
获取Remote
中 expose
的组件,而作为 Remote
时,判断Host中是否有可用的共享依赖,若有,则加载Host
的这部分依赖,若无,则加载自身依赖。
ContainerReferencePlugin:
该插件将特定的引用添加到作为外部资源(externals
)的容器中,并允许从这些容器中导入远程模块。在导入时会调用容器使用者提供的remote进行重载。
通过remotes定义的模块,也会在 __webpack_modules__
中声明但不会有具体实现,这就和异步导入类似。
webpack5
中新增了 __webpack_require__.e
方法,对通过次方法导入的模块执行一下三个函数,并且全部成功才返回。
__webpack_require__.f.consumes
用来判断和消费shared模块,如果当前环境下已经有这个模块就不向远程请求__webpack_require__.f.remotes
用来连接容器__webpack_require__.f.j
用来加载JS
1. 线上还是出问题了,紧急发布。
那天,风很大。下午3、4点的天乌黑的,简直吓死了宝宝。
“呦呵,来凑热闹啦?带伞木有啦?要不要喝一杯我新买的咖啡啦?”
老大慈眉善目的样子简直不要太招人恨。我就学习了一个礼拜,天天晚上看源码,眼黑圈都重了好几轮。我在想,这么苦的吗? 周一早上,老大公开宣布我们要做渐进式微前端架构,我是负责人。 我简直要坏掉了。
排期就5个人日,我每天都在极度焦虑当中渡过,真的简直从头到脚都要坏掉了!这礼拜刚好来例假,脑袋昏昏沉沉只想睡觉,看到人就想发脾气。你说我放弃?有用吗?老大真的就是那种人,这活儿要干不好,他会让我自己滚蛋的……
发布那天,意外的发现测试一点麻烦都没找我。连续加了一个星期的班,我终于做出了成功,然而例假根本没有走的情况。我有点不安……
急诊室。
“老大,成功上线了吗?”
“嗯。”
“老大,没有bug吗?”
“没。”
“老大,医院的味道好难闻。”
“消毒水的味道。”
“老大,为什么你要赶我走?”
“赶你走? 什么意思?”
“你说的啊:小白,了解一些商业化的解决方案就走吧。”
“噢。我那时让你自己离开茶水间,别被人事抓到你在摸鱼。”
哇…… 555555.
我还是没忍住崩溃了。一方面是感觉委屈,另一方面是缓了一口气。
“你这有可能是子宫内膜炎症,得做一下妇科检查。小小白马上就来了,你平时注意休息。”
无情!冷漠!老大一句安慰都木有,出去接小小白了。
小小白一来,脸色铁青。 我知道肯定是出问题了,而今晚能出问题的就是我。
“小白,你还好吧? 天呐,你也太不爱惜自己了。哪能在例假期间不注意休息,疯狂加班的?”
“老大交给我的任务没完成啊!”
……
小小白不沉默了。我很清楚,老大肯定是回去处理紧急发布的事情了。
前端可视化解决方案
未完待续。 来例假了,写不动了。