云音乐广告 Dsp(需求方平台)平台分为合约平台(Vue 框架)和竞价平台(React 框架),因历史原因框架选型未能统一,最近来了新需求,需要同时在两个平台增加一样的模块,因为都是 Dsp 平台,后期这样的需求可能会很多,所以考虑到组件复用以及降低维护成本,在想怎么统一技术栈,把 React 系统塞到 Vue 项目中进行呈现。
系统是传统的左右布局,左侧侧边栏展示菜单栏,头部导航展示基础信息,应用内容全部填充到蓝色的内容区。
说实话,第一反应我直接想嵌套 iframe ,但是应用过 iframe 技术的,大家都知道它的痛:
postMessage
了iframe
应用更新上线后,打开系统会发现系统命中缓存显示旧内容,需要用时间戳方案解决或强制刷新
另外就是使用 MPA + 路由分发,当用户访问页面时,由 Nginx 等负责根据路由分发到不同的业务应用,由各个业务应用完成资源的组装后返回给浏览器,这种方式就需要把界面、导航都做成类似的样子。
还有就是目前比较主流的几种微前端方案:
总的来说,iframe 主要用于简单并且性能要求不高的第三方系统;MPA 无论在实现成本和体验上面都不能满足当前业务需求;基座模式和 EMP 都是不错的选择,因 qiankun 在业内使用比较广,较为成熟,最后还是选择了 qiankun
qiankun(乾坤)是由蚂蚁金服推出的基于Single-Spa实现的前端微服务框架,本质上还是路由分发式的服务框架,不同于原本 Single-Spa 采用 JS Entry 加载子应用的方案,qiankun 采用 HTML Entry 方式进行了替代优化。
JS Entry
的使用限制要求:
对比 JS Entry, HTML Entry 使用就方便太多了,项目配置给定入口文件后,qiankun 会自行 Fetch 请求资源,解析出 JS 和 CSS 文件资源后,插入到给定的容器中,完美~
JS Entry 的方式通常是子应用将资源打成一个 Entry Script, 类似 Single-Spa 的 例子;
HTML Entry 则是使用 HTML 格式进行子应用资源的组织,主应用通过 Fetch html 的方式获取子应用的静态资源,同时将 HTML Document 作为子节点塞到主应用的容器中。可读性和维护性更高,更接近最后页面挂载后的效果,也不存在需要双向转义的问题。
由于 Vue 项目已经开发完成,我们需要在原始项目中进行改造,很明显选定 Vue 项目作为基座应用,新需求开发采用 Create React App 搭建 React 子应用,接下来我们看一下具体实现
基座(main)采用是的 vue-cli 搭建的,我们保持其原本的代码结构和逻辑不变,在此基础上单独为子应用提供一个挂载的容器 DIV,同样填充在相同的内容展示区域。
qiankun 只需要在基座应用中引入,为了方便管理,我们新增目录,命名为 micro ,标识目录里面是微前端改造代码,进行全局配置初始化,改造如下:
路由配置文件 app.js
// 路由配置
const apps = [
{
name: 'ReactMicroApp',
entry: '//localhost:10100',
container: '#frame',
activeRule: '/react'
}
];
应用配置注册函数
import { registerMicroApps, start } from "qiankun";
import apps from "./apps";
// 注册子应用函数,包装成高阶函数,方便后期如果有参数注入修改app配置
export const registerApp = () => registerMicroApps(apps);
// 导出 qiankun 的启动函数
export default start;
Layout 组件
import startQiankun, { registerApp } from "../../../micro";
export default {
name: "AppMain",
mounted() {
// 初始化配置
registerApp();
startQiankun();
},
};
这里会用到 qiankun 的两个重要的 API :
注意点:我们选择在 mounted 生命周期中进行初始化配置,是为了保证挂载容器一定存在
我们来通过图示具体理解一下 qiankun 注册子应用的过程:
xx_QIANKUN__
,用于子应用判断所处环境等activeRule
的规则来判断是否激活子应用
我们基于 Create React App 创建一个 React 项目应用,由上述的流程描述,我们知道子应用得向外暴露一系列生命周期函数供 qiankun 调用,在 index.js 文件中进行改造:
增加 public-path.js 文件
目录外层添加 `public-path.js` 文件,当子应用挂载在主应用下时,如果我们的一些静态资源沿用了 `publicPath=/` 的配置,我们拿到的域名将会是主应用域名,这个时候就会造成资源加载出错,好在 Webpack 提供了修改方法,如下:
if (window.__POWERED_BY_QIANKUN__) {
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
路由 base 设置
因为通常来说,主应用会拦截浏览器路由变化以激活加载子应用。比如,上述的代码里我们的路由配置,激活规则写了 `activeRule: /react`,这是什么意思呢?这意味着,当浏览器 `pathname` 匹配到 `/react` 时,会激活子应用,但是如果我们的子应用路由配置是下面这样的:
我们怎么实现域名 /react
能正确加载对应的组件呢?大家一定经历过用域名二级目录访问的需求,这里是一样的,我们判断是否在 qiankun 环境下,调整下 base 即可,如下:
const BASE_NAME = window.__POWERED_BY_QIANKUN__ ? "/react" : "";
...
...
增加生命周期函数
子应用的入口文件加入生命周期函数初始化,方便主应用调用资源完成后按应用名称调用子应用的生命周期
/**
* bootstrap 只会在微应用初始化的时候调用一次,下次微应用重新进入时会直接调用 mount 钩子,不会再重复触发 bootstrap。
* 通常我们可以在这里做一些全局变量的初始化,比如不会在 unmount 阶段被销毁的应用级别的缓存等。
*/
export async function bootstrap() {
console.log("bootstraped");
}
/**
* 应用每次进入都会调用 mount 方法,通常我们在这里触发应用的渲染方法
*/
export async function mount(props) {
console.log("mount", props);
render(props);
}
/**
* 应用每次切出/卸载 会调用的方法,通常在这里我们会卸载微应用的应用实例
*/
export async function unmount() {
console.log("unmount");
ReactDOM.unmountComponentAtNode(document.getElementById("root"));
}
注意:所有的生明周期函数都必须是 Promise
修改打包配置
module.exports = {
webpack: (config) => {
// 微应用的包名,这里与主应用中注册的微应用名称一致
config.output.library = `ReactMicroApp`;
// 将你的 library 暴露为所有的模块定义下都可运行的方式
config.output.libraryTarget = "umd";
// 按需加载相关,设置为 webpackJsonp_ReactMicroApp 即可
config.output.jsonpFunction = `webpackJsonp_ReactMicroApp`;
config.resolve.alias = {
...config.resolve.alias,
"@": path.resolve(__dirname, "src"),
};
return config;
},
devServer: function (configFunction) {
return function (proxy, allowedHost) {
const config = configFunction(proxy, allowedHost);
// 关闭主机检查,使微应用可以被 fetch
config.disableHostCheck = true;
// 配置跨域请求头,解决开发环境的跨域问题
config.headers = {
"Access-Control-Allow-Origin": "*",
};
// 配置 history 模式
config.historyApiFallback = true;
return config;
};
},
};
注意:配置的修改为了达到两个目的,一个是暴露生命周期函数给主应用调用,第二点是允许跨域访问,修改的注意点可以参考代码的注释。
小结:跳转流程梳理,在主应用 router 中定义子应用跳转 path ,如下图,在调用组件 mounted 生命周期中使用 qiankun 暴露的loadMicroApp
方法加载子应用,跳转到子应用定义的路由,同时使用addGlobalUncaughtErrorHandler
和removeGlobalUncaughtErrorHandler
监听并处理异常情况(例如子应用加载失败),当子应用监听到跳转路由时,加载子应用(上述组件中)定义的 component,完成主应用到子应用的跳转。
{
path: '/xxx',
component: Layout,
children: [
{
path: '/xxx',
component: () => import('@/micro/app/react'),
meta: { title: 'xxx', icon: 'user' }
}
]
},
1、子应用未成功加载
如果项目启动完成后,发现子应用系统没有加载,我们应该打开控制台分析原因:
2、基座应用路由模式
基座应用项目是 hash 模式路由,这种情况下子应用的路由模式必须跟主应用保持一致,否则会加载异常。原因很简单,假设子应用采用 history 模式,每次切换路由都会改变 pathname,这个时候很难再通过激活规则去匹配到子应用,造成子应用 unmount
3、CSS 样式错乱
由于默认情况下 qiankun 并不会开启 CSS 沙箱进行样式隔离,当主应用和子应用产生样式错乱时,我们可以启用 { strictStyleIsolation: true }
配置开启严格隔离样式,这个时候会用 Shadow Dom 节点包裹子应用,相信大家看到这个也很熟悉,和微信小程序中页面和自定义组件的样式隔离方案一致。
4、另外,在接入过程中,总结了几个需要注意的点
onClick
或 addEventListener
给 添加了一个点击事件,JS 沙箱并不能消除它的影响,还得靠平时的代码规范
5、未来可能需要考虑一些问题
其实写下来整个项目,最大的感受 qiankun 的开箱可用性非常强,需要更改的项目配置基本很少,当然遇到的一些坑点也肯定是踩过才能更清晰。