本文做者:张延卿javascript
云音乐广告 Dsp(需求方平台)平台分为合约平台(Vue 框架)和竞价平台(React 框架),因历史缘由框架选型未能统一,最近来了新需求,须要同时在两个平台增长同样的模块,由于都是 Dsp 平台,后期这样的需求可能会不少,因此考虑到组件复用以及下降维护成本,在想怎么统一技术栈,把 React 系统塞到 Vue 项目中进行呈现。html
系统是传统的左右布局,左侧侧边栏展现菜单栏,头部导航展现基础信息,应用内容所有填充到蓝色的内容区。前端
说实话,第一反应我直接想嵌套 iframe ,可是应用过 iframe 技术的,你们都知道它的痛:vue
postMessage
了
iframe
应用更新上线后,打开系统会发现系统命中缓存显示旧内容,须要用时间戳方案解决或强制刷新
另外就是使用 MPA + 路由分发,当用户访问页面时,由 Nginx 等负责根据路由分发到不一样的业务应用,由各个业务应用完成资源的组装后返回给浏览器,这种方式就须要把界面、导航都作成相似的样子。
java
还有就是目前比较主流的几种微前端方案:react
总的来讲,iframe 主要用于简单而且性能要求不高的第三方系统;MPA 不管在实现成本和体验上面都不能知足当前业务需求;基座模式和 EMP 都是不错的选择,因 qiankun 在业内使用比较广,较为成熟,最后仍是选择了 qiankunwebpack
qiankun(乾坤)是由蚂蚁金服推出的基于Single-Spa实现的前端微服务框架,本质上仍是路由分发式的服务框架,不一样于本来 Single-Spa 采用 JS Entry 加载子应用的方案,qiankun 采用 HTML Entry 方式进行了替代优化。git
JS Entry
的使用限制要求:github
对比 JS Entry, HTML Entry 使用就方便太多了,项目配置给定入口文件后,qiankun 会自行 Fetch 请求资源,解析出 JS 和 CSS 文件资源后,插入到给定的容器中,完美~
web
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 组件
<section class="app-main">
<transition v-show="$route.name" name="fade-transform" mode="out-in">
<!-- 主应用渲染区,用于挂载主应用路由触发的组件 -->
<router-view />
</transition>
<!-- 子应用渲染区,用于挂载子应用节点 -->
<div id="frame" />
</section>
复制代码
import startQiankun, { registerApp } from "../../../micro";
export default {
name: "AppMain",
mounted() {
// 初始化配置
registerApp();
startQiankun();
},
};
复制代码
这里会用到 qiankun 的两个重要的 API :
注意点:咱们选择在 mounted 生命周期中进行初始化配置,是为了保证挂载容器必定存在
咱们来经过图示具体理解一下 qiankun 注册子应用的过程:
xx_QIANKUN__
,用于子应用判断所处环境等activeRule
的规则来判断是否激活子应用
activeRule
为字符串时,以路由拦截方式进行自主拦截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
时,会激活子应用,可是若是咱们的子应用路由配置是下面这样的:
<Router>
<Route exact path="/" component={Home} />
<Route path="/list" component={List} />
</Router>
复制代码
咱们怎么实现域名 /react
能正确加载对应的组件呢?你们必定经历过用域名二级目录访问的需求,这里是同样的,咱们判断是否在 qiankun 环境下,调整下 base 便可,以下:
const BASE_NAME = window.__POWERED_BY_QIANKUN__ ? "/react" : "";
...
<Router base={BASE_NAME}>
...
</Router>
复制代码
增长生命周期函数
子应用的入口文件加入生命周期函数初始化,方便主应用调用资源完成后按应用名称调用子应用的生命周期
/** * 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
监听并处理异常状况(例如子应用加载失败),当子应用监听到跳转路由时,加载子应用(上述<Router>
组件中)定义的 component,完成主应用到子应用的跳转。
{
path: '/xxx',
component: Layout,
children: [
{
path: '/xxx',
component: () => import('@/micro/app/react'),
meta: { title: 'xxx', icon: 'user' }
}
]
},
复制代码
一、子应用未成功加载
若是项目启动完成后,发现子应用系统没有加载,咱们应该打开控制台分析缘由:
二、基座应用路由模式
基座应用项目是 hash 模式路由,这种状况下子应用的路由模式必须跟主应用保持一致,不然会加载异常。缘由很简单,假设子应用采用 history 模式,每次切换路由都会改变 pathname,这个时候很难再经过激活规则去匹配到子应用,形成子应用 unmount
三、CSS 样式错乱
因为默认状况下 qiankun 并不会开启 CSS 沙箱进行样式隔离,当主应用和子应用产生样式错乱时,咱们能够启用 { strictStyleIsolation: true }
配置开启严格隔离样式,这个时候会用 Shadow Dom 节点包裹子应用,相信你们看到这个也很熟悉,和微信小程序中页面和自定义组件的样式隔离方案一致。
四、另外,在接入过程当中,总结了几个须要注意的点
onClick
或 addEventListener
给 <body>
添加了一个点击事件,JS 沙箱并不能消除它的影响,还得靠平时的代码规范
五、将来可能须要考虑一些问题
其实写下来整个项目,最大的感觉 qiankun 的开箱可用性很是强,须要更改的项目配置基本不多,固然遇到的一些坑点也确定是踩过才能更清晰。
若是文章有什么问题或者错误,欢迎指正交流,谢谢!
本文发布自 网易云音乐大前端团队,文章未经受权禁止任何形式的转载。咱们常年招收前端、iOS、Android,若是你准备换工做,又刚好喜欢云音乐,那就加入咱们 grp.music-fe (at) corp.netease.com!