作者:许仙
随着业务的发展,很多程序应用变成一个 Monolithic-Applications(巨石应用)。同时由于维护的团队人员都比较分散,工程大,导致开发调试效率低,上线困难(代码合并相互依赖),成为阻塞业务发展的一个重要因素。
于是行业创建了微服务框架,它主要解决两个问题:
微前端(Micro Frontends)是一种类似于微服务的架构,是一种由独立交付的多个前端应用组成整体的架构风格,将前端应用分解成一些更小、更简单的能够独立开发、测试、部署的应用,而在用户看来仍然是内聚的单个产品。
Techniques, strategies and recipes for building a modern web app with multiple teams that can ship features independently –Micro Frontends
多个团队通过独立发布功能的方式共同构建现代Web应用程序的技术、策略和方法
看完概念可能还是对微前端的理解有些模糊,简单来说,如果有一个内容和应用功能丰富的前端项目,而这个项目随着时间和需求的推移,项目会变得不再简单而是越来越庞大,不太容易能在项目中添加新的功能,往往会牵一发而动全身,开发和维护成本也越来越高。微前端呢,就可以将项目拆分和细化成多个独立的子应用,子应用之间可以独立进行开发和维护。又或者说自己的项目中存在一些历史项目,而这些项目使用的是老框架,但是这些项目需要结合到新框架中来使用还不能放弃,但是也没有足够的精力和时间重写旧的的逻辑,微前端就可以将这些系统进行整合,在几乎不修改逻辑的同时能够兼容新旧两套系统并行运行。这也是我所在部门现在遇到的相同情况
当前微前端主要采用的是组合式应用路由方案,它的核心是主从思想,也就是包括一个基座(MainApp)应用和若干个微应用(MicroApp)应用。基座应用基本上是一个前端Spa项目,主要功能是负责应用注册、路由映射以及消息分发等,而微应用是一个个独立的前端项目,这些项目不限于采用React、Vue或者Angular等技术栈开发,每个微应用都被注册到基座应用中,由基座进行管理,基本流程如下图所示:
想必看到这里对微前端有了一定的认识,接下来让我们来看看微前端架构Qiankun
Qiankun是一个蚂蚁金服基于single-spa的微前端实现库。它的核心设计理念一个是简单一个是解耦或者是技术栈无关。简单,是对于用户而言只是一个类似Jquery的裤,用户只需要调用几个Qiankun提供的Api即可完成应用的微前端改造。同时由于Qiankun的一些特性,使得微应用的接入比较简单。而解耦/技术栈无关,是为了确保微应用真正具备独立开发、独立运行的能力,设计了HTML entry、沙箱、应用通信等功能。
single-spa 是一个将多个单页面应用聚合为一个整体应用的 JavaScript 微前端框架,虽然Qiankun是基于single-spa开发,但是在此基础上新设计了很多解决single-spa问题的一些功能
js
的方法,而Qiankun不需要,只需要传入响应的apps的配置即可html
字符串获取静态资源地址的解析依赖库import-html-entry
,方便资源的预加载
css
打包进js
,生成一个json
的配置文件(标识了子应用资源文件的相对路径地址)script
标签src
属性的方式加载子应用资源文件css
打包进 js
中,也增加了编译后的包体积html
中插入行内 script
代码__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__
。二是可以通过使用webpack静态publicPath配置,需要在webpack的publicPath配置设置成一个绝对路径的urlif (!window.__POWERED_BY_QIANKUN__) {
render();
}
export const mount = async () => render();
history.pushState()
,也可以将主应用的路由实例通过props传给微应用,通过这个实例进行跳转 HTML entry 是通过import-html-empty直接获取子应用html文件,解析html文件的资源,加载入主应用
// 使用
import importHTML from 'import-html-entry'
/* 核心功能
调用fetch请求html资源
调用processTpl处理资源
调用getEmbedHtml对processTpl处理后的资源中链接的远程js、css资源去到本地并嵌入html中
*/
importHTML(url, opts = {}) {
// 核心伪代码
const html = fetch(url)
const {
template,
scripts,
styles,
entry
} = processTpl(html, ...rest) // 对象1
return getEmbedHtml().then(() => {
// 对象2
return {
template,
assetPublicPath,
getExternalScripts,
getExternalStyleSheets,
execScripts
}
})
}
// 获取到的对象1
{
template: 经过处理的html,
scripts: [脚本],
styles: [样式],
entry: 入口脚本的地址
}
// 获取到的对象2
{
template // template 是 link 替换为 style 后的 template,
assetPublicPath // 静态资源地址,
getExternalScripts: // 获取外部脚本,最终得到所有脚本的代码内容,
getExternalStyleSheets // 获取外部样式文件的内容,
execScripts: // 脚本执行器,让 JS 代码(scripts)在指定 上下文 中运行
}
Garfish是字节跳动推出的一款微前端框架,包含构建微前端系统时所需要的基本能力,任意前端框架均可使用。接入简单,可轻松将多个前端应用组合成内聚的单个产品。
设计理念也是采取基座+子应用分治的形势,部署平台负责进行服务发现和服务注册,将注册的应用列表信息发至基座,通过基座来动态控制子应用的渲染和销毁,并提供应用之间的通信和公共依赖管理
Garfish底层使用Garfish.loader来进行资源加载,并且可以用来添加资源加载过程中的回调以及资源的缓存策略。
html
内容, 然后 执行 provider
中的渲染函数。因为子应用的真实执行环境并未被销毁,而是通过 render
和 destroy
控制对应应用的渲染和销毁 基于 ShadowDom 实现样式隔离, 将容器节点变为 shadowdom,子应用节点操作转发到容器内,动态增加的样式和节点都会放置容器内,查询节点操作转发到容器内,事件向上传播,避免 React
依赖事件委托的库失效。能够提供代码执行能力、手机执行代码时的副作用,以及销毁收集副作用的能力,能够完全支持浏览器主子应用的样式隔离
主流的前端框架基本上都是可以通过路由驱动的,开发者只需要配置路由的规则,即可在进入制定路由后载入子应用,这降低了单页应用的复杂度,我们还可以借用单页应用的路由驱动模式,将每个子应用作为组件,并且只托管子应用的根路由,二级及以下路由交由子应用自己负责。Garfish主要提供了一下三条策略:
Router Map
,减少典型中台应用下的开发者理解成本basename
用于隔离应用间的路由抢占问题 Garfish.channel为Garfish的实例属性,用于应用间的通信问题。我在实际的使用Garfish过程中,这一功能主要用于路由的监听以及登录信息的监听
// 子应用
useEffect(() => {
if (window.Garfish) {
// moduleRouteConfig为子应用的路由配置
window?.Garfish.channel.emit("router", moduleRouteConfig);
}
return () => {
handleCleanMainInfo();
};
}, []);
// 主应用
useEffect(() => {
// 获取到子应用传递的路由配置,并且进行一些路由计算逻辑
window.Garfish.channel.on("router", handleRouterChange)
return () => {
window.Garfish.channel.removeListener("router", handleRouterChange)
}
})
// 子应用
useEffect(() => {
// mainInfo 为子应用下用户的登录信息
if (JSON.stringify(mainInfo) !== "{}") {
window?.Garfish.channel.emit("loginExpired", mainInfo);
}
}, [mainInfo]);
// 主应用
useEffect(() => {
// 接收子应用传递的登录信息,并且进行登录信息过期的逻辑处理
window.Garfish.channel.on("loginExpired", handleLoginExpired)
return () => {
window.Garfish.channel.removeListener("loginExpired", handleLoginExpired)
}
}, [])
Garfish提供实例Garfish.setExternal用于实现主子应用间的依赖共享。因为前端的一些依赖包(比如lodash)比较大而且内容更新的不频繁,这样如果主子应用都使用了这个相同的依赖包,下载多份便显得不太合适,因此提供了这个依赖共享,可以只用下载一份依赖包,供主子应用的共同使用。而且使用这个功能还有一些注意事项:
jsonpFunction
唯一// 主应用 webpack.config.js
module.exports = {
output: {
// 需要配置成 umd 规范
libraryTarget: 'umd',
// 请求确保该值与子应用的值不相同避免与子应用发生影响
jsonpFunction: 'main-app-jsonpFunction'
},
};
// 主应用 index.jsx
import React from 'react';
import * as lodash from 'lodash';
import Garfish from 'garfish';
Garfish.setExternal({
react: React,
'lodash': lodash,
});
// 子应用 webpack.config.js
module.exports = {
output: {
// 需要配置成 umd 规范
libraryTarget: 'umd',
// 修改不规范的代码格式,避免逃逸沙箱
globalObject: 'window',
// 请求确保每个子应用该值都不相同,否则可能出现 webpack chunk 互相影响的可能
jsonpFunction: 'vue-app-jsonpFunction',
// 保证子应用的资源路径变为绝对路径,避免子应用的相对资源在变为主应用上的相对资源,因为子应用和主应用在同一个文档流,相对路径是相对于主应用而言的
publicPath: 'http://localhost:8000'
},
externals: {
react: 'react',
'lodash': 'lodash',
}
};
模块热更新也叫 HMR,全称是 Hot Module Replacement,指当你在更改并保存代码时,构建工具将会重新进行编译打包,并将新的包模块发送至浏览器端,浏览器用新的包模块替换旧的,从而可以在不刷新浏览器的前提下达到修改的功能
// webpack.config.js
module.exports = {
devServer: {
hot: true,
},
}
// index.tsx
import ReactDOM from 'react-dom';
import App from './components/App';
// 设置 hot.module 回调开启热更新:
if ((module as any).hot) {
(module as any).hot.accept(['./components/App'], () => {
ReactDOM.render(<App />, document.getElementById('root'));
});
}
module.exports = {
entry: './index.js',
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
loader: 'babel-loader',
options: {
presets: ['@babel/env', '@babel/preset-react'],
plugins: [require.resolve('react-refresh/babel')], // 为 react-refresh 添加
},
},
],
},
plugins: [
isDevelopment && new ReactRefreshPlugin(), // 为 react-refresh 添加
new HtmlWebpackPlugin({
template: './index.html',
}),
],
};
// webpack.config.js
module.exports = {
output: {
// 开发环境设置 true 将会导致热更新失效
clean: process.env.NODE_ENV === 'production' ? true : false,
filename: '[name].[contenthash].js',
chunkFilename: '[name].[contenthash].js',
// 需要配置成 umd 规范
libraryTarget: 'umd',
// 修改不规范的代码格式,避免逃逸沙箱
globalObject: 'window',
jsonpFunction: 'garfishDemo',
// 保证子应用的资源路径变为绝对路径
publicPath: 'http://localhost:8080',
},
};
在实际使用Garfish与Qiankun后,遇到了一个比较棘手的问题。Qiankun的子应用互相切换时,由于自己的生命周期以及渲染方法,会导致子应用切换后会出现样式丢失的问题,而Garfish在子应用切换时并没有这个问题,而且页面的渲染和展示也会比Qiankun更加快和流畅。
Qiankun和Garfish这两款微前端架构都各有自己的一些优点和特性,但是在使用的过程中发现,Qiankun的子应用切换导致的样式丢失问题降低了用户体验度,而Garfish的使用以及页面跳转显得更加的流畅。而且Garfish的应用通信相比于Qiankun来说较为简单,而且对于全局变量来说方便管理,防止数据管理的混乱。从这些角度来看,Garfish的使用是更加符合我们项目目前的需求以及应用。
https://www.garfishjs.org/ Garfish官网
https://qiankun.umijs.org/zh Qiankun官网