首页 > IT基础架构 > 服务器/存储 > 效率前端微应用推进之微前端研发提效|得物技术
目录
一、背景
1. 业务背景
2. 名词解释
二、研发费力度痛点
1. 费力点 1
2. 费力点 2
3. 费力点 3
三、技术调研
1. Shared 通信
2. Mock 父应用环境
3. Chrome 代理插件
四、Chrome 代理插件
1. 产品设计
2. 技术设计
五、推进情况
1. 项目覆盖率
2. 研发效率
3. 用户反馈
六、总结
一
背景
业务背景
在之前的微信公众号《得物效率前端微应用推进过程与思考》中详细介绍了效率前端推进微应用落地的思路和部分效果。
这篇文章将着重介绍得物效率前端微应用推进中,微前端的研发效率遇到的挑战和解决方案。
名词解释
微应用
「微应用」是得物效率前端内部称谓,是一个基于“monorepo & 微前端 & 基座与业务分离”的、包括“文档 & 工具”的一套体系化降低研发成本和提升用户体验的技术产品。
微前端
「微前端」是得物效率前端微应用推进的重要一环,尤其是父子应用技术栈不同时,利用 iframe / qiankun / wujie / micro-app 等工具进行微前端化改造,能显著增强业务扩展性。

基座应用/父应用
微前端中,基座应用(父应用)、子应用是常见概念,本文描述中,“基座应用”又名“父应用”,为简化文案,“基座应用与子应用”也被称为“父子应用”。
浏览器插件/扩展
chrome://extensions/ 页面中对第三方工具称为“扩展”,在本文语境下,又称为“插件”。
二
研发费力度痛点
费力点1
大大多数情况下,项目需要启动 2 个本地服务(基座应用和子应用)才能进行日常开发,因为子应用通常依赖基座应用透传一些依赖数据。
而非微前端场景下,启动 1 个应用就可以了,这反而引入了降低了研发效率。虽然微前端方案更注重业务效率,但研发效率也是必须要考虑的。
如果电脑性能一般的话,卡顿问题就随之而来了。
费力点2
如父应用需要区分本地环境和生产环境类似这样的代码,虽然代码量不大,但还是需要关注的:
// 基座代码if (isLocal) { return <microapppage src="localhost://8000/a/b/c" />} else if (isProd) { return <microapppage src="https://www.abc.com/a/b/c">}费力点3
在 Qiankun 微前端框架且本地开发环境下,基座应用与子应用页面均需要 WebSocket 与其自身本地服务进行通信。
在同时启动基座与子应用的本地服务后,修改子应用代码以及偶发的,页面会触发 Reload,而不是局部更新,开发体验很差。针对这个问题我们做了一些分析。
本地开发过程会启动 Webpack-Dev-Server 服务,其会监听业务文件变化,浏览器通过 WebSocket 与 Webpack-Dev-Server 进行通信。
当发现文件内容改变时 Webpack-Dev-Server 会根据更新的文件内容生成 Hash 信息传递给浏览器,如图:

当浏览器收到信息时,会根据收到的信息和配置进行判断是刷新操作还是热更新操作。热更新时,Webpack-Dev-Serer 通过 Jsonp 拉取最新的 JS 模块代码,并进行模块替换,如图:

若此过程异常,则会降级为页面刷新。
基座应用(端口 8010)热更新时返回的文件 Json 和 JS 文件(分别是 **:8010/update.json 和 **:8010/update.js)内容如下:


但在嵌套在基座应用中的本地子应用(端口 8020)热更新时,两个同类文件并没有返回内容(**:8020/update.json 和 **:8020/update.js)


若浏览器单独打开两个文件的地址(**:8020/update.json 和 **:8020/update.js),有内容返回。

但是在基座与子应用嵌套的情况下,子应用的任何请求(包括 update.json / update.js )都会被基座应用代理,开发环境下很容易出现子应用的更新探针请求被基座代理后,出现内容丢失的情况。
子应用 HMR 逻辑检测到更新探针请求内容异常,局部更新失效,降级为页面刷新。
即使该问题解决,微前端应用开发者依然面临同时启动 2 个应用才能启动开发的问题,所以我们不过度投入精力关注这个问题。
三
技术调研
解决「默认情况下,父子应用需要分别独立启动,并指定关联关系」的问题,最好的方式是回归到非微前端场景下的常规开发方式,即只启动 1 个应用进行本地开发。
通常情况下,我们开发的是子应用(也就是业务页面),那先实现子应用单独启动即可开启项目开发吧,以下是面向该需求的技术调研。
Shared 通信
同时,子应用需要单独维护一份 Shared 实例,在独立运行时使用自身的 Shared 实例,在嵌入主应用时使用主应用的 Shared 实例,这样就可以保证在使用和表现上的一致性。
Shared 通信方案要求父子应用都各自维护一份属于自己的 Shared 实例,同样会增加项目的复杂度。同时,在子应用独立运行时,Shared 只能获取本地缓存数据,无法真正做到完全独立于子应用运行。

Mock 父应用环境
这个方案和 Shared 通信有类似之处。
用户体验
// 这是子应用代码import { MicroLayout } from '@abc/components';// 组件内使用<MicroLayout title="Layout 内容">{children}</MicroLayout>

流程设计

其他
Chrome 代理插件
用户体验

其他

四
Chrome 代理插件
产品设计


技术设计
Proxy 和 Popup

功能流转

无感更新(Seamless Update)

这么做需要在 chrome 插件的 csp 安全策略配置中加入 cdn 域名白名单,允许插件访问外部 cdn 资源。
{..."permissions": ["webRequest", // 允许浏览器开放http请求劫持的功能"storage", // 允许使用浏览器缓存"activeTab","background","webRequestBlocking","<all_urls>"],// 允许特定域名可以访问的安全策略"content_security_policy":"script-src 'self' https://cdn.xxxxx.com 'sha256-G7YAg/PQDo8GYc/fSYvWtXP98kXS7iqT7K4QZgyhUIE='; object-src 'self'","content_scripts": [{"matches": ["*://*.xxxxxxx.net/*"],"js": ["contentScript.bundle.js"]}]}// ==========attention===========//v2配置,v2版本中可以配置script等通过外部引入,这个content_security_policy配置参数不加或者加上之后相应的值填none"content_security_policy": "script-src 'none'; object-src 'none'",//v3配置,v3版本中安全政策配置script引入等信息,都必须填写self,即只允许script标签引用当前插件内部文件,不允许引用外部链接,如果不填写self的话,插件添加到扩展程序时会报错"content_security_policy": {//原文:此政策涵盖您的扩展程序中的页面,包括 html 文件和服务人员;"extension_pages": "script-src 'self'; object-src 'self'",//原文:此政策涵盖您的扩展程序使用的任何[沙盒扩展程序页面](https://developer.chrome.com/docs/extensions/mv3/manifest/sandbox/)。;"sandbox": "sandbox allow-scripts; script-src 'self'; object-src 'self'"},
<html lang="en"><head><meta charset="UTF-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>Popup</title><script>var scriptEl = document.createElement('script');scriptEl.defer = "defer";scriptEl.src = "https://cdn.xxxxxxxxxxx/popup.js?timestamp=" + Date.now();document.getElementsByTagName('head')[0].appendChild(scriptEl);</script></head><body><div id="app-container"></div></body></html>

Proxy
chrome.webRequest.onBeforeRequest.addListener( worker.getRequest.bind(worker), {urls: worker.getFilterUrls(worker.replaceRules)}, ['blocking']);// Class background class Background { constructor() { this.replaceRules = [] } getRequest(details) { if (!details) { // chrome未返回任何request return false } const {url} = details const returnObj = {} if (this.replaceRules.length) { // 存在替换规则 this.replaceRules.map((item) => { if (url.includes(item.from)) { // 更新 icon 状态 this.setBadgeInfo(true) // 代理替换 returnObj.redirectUrl = url.replace(item.from, item.to) // 缓存更新当前代理域名 chrome.storage.sync.get(null, function (data) { if (data.messageProxyingData) { // 存在被代理的数据,合并数据 const isExistMessageProxyingData = data.messageProxyingData chrome.storage.sync.set({messageProxyingData: Object.assign(isExistMessageProxyingData, {[item.key]: true})}) } else { // 未存在缓存,使用新数据 chrome.storage.sync.set({messageProxyingData: {[item.key]: true}}) } }) } }) } return returnObj }}chrome.webRequest.onBeforeRequest/ getRequest 配合拦截获取请求地址,然后拦截替换目标路径,当然这是比较简单的逻辑,复杂的可以参考 glob 写法代理链接。Popup

const sendMessage = (data: DataType[]) => { // 保留数据中已开启的数据,进行数据传输 const finalData = data.filter.((item) => item.is_open); chrome.runtime.sendMessage({ data: finalData, type: 'rule' });};const handleSaveData = ( data: DataType[], isNeedSendMsg: boolean, key?: number[]) => { setDataSource([...data]); // 缓存配置 chrome.storage.sync.set({ popupData: data }); if (isNeedSendMsg) { sendMessage(data); if (key?.length) { chrome.storage.sync.get(null, function (data) { const messageProxyingData = data.messageProxyingData || {}; if (messageProxyingData) { key.forEach.((item) => { delete messageProxyingData[item]; }); } chrome.storage.sync.set({ messageProxyingData }); }); } }};ContentScript
const scrpit = document.createElement('script')scrpit.textContent = `window.__REACT_ERROR_OVERLAY_GLOBAL_HOOK__ = window.__REACT_ERROR_OVERLAY_GLOBAL_HOOK__ || { iframeReady: function () { var overlay = document.querySelector( 'iframe[style*="z-index: 2147483647;"]', ); if (overlay) { overlay.style.display = 'none'; } }, };`document.body.appendChild(scrpit)五
推进情况
项目覆盖率
研发效率
用户反馈
六
思考
来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/70027824/viewspace-2996693/ ,如需转载,请注明出处,否则将追究法律责任。