乐趣区

关于微前端:基于-qiankun-的-CMS-应用微前端实践

图片起源:https://zhuanlan.zhihu.com/p/…
本文作者:史志鹏

前言

LOOK 直播经营后盾工程是一个迭代了 2+ 年,累计超过 10+ 位开发者参加业务开发,页面数量多达 250+ 的“巨石利用”。代码量的宏大,带来了构建、部署的低效,此外该工程依赖外部的一套 Regularjs 技术栈也曾经实现了历史使命,相应的 UI 组件库、工程脚手架也被举荐停止使用,走向了少保护或者不保护的阶段。因而,LOOK 直播经营后盾基于 React 新建工程、做工程拆分被提上了工作日程。一句话形容指标就是:新的页面将在基于 React 的新工程开发,React 工程能够独立部署,而 LOOK 直播经营后盾对外输入的拜访地址冀望维持不变。
本文基于 LOOK 直播经营后盾的微前端落地实际总结而成。次要介绍在既有“巨石利用”、Regularjs 和 React 技术栈共存的场景下,应用微前端框架 qiankun,实现 CMS 利用的微前端落地历程。
对于 qiankun 的介绍,请移步至官网查阅,本文不会侧重于介绍无关微前端的概念。

一. 背景

1.1 现状

  1. 如上所述,存在一个如下图所示的 CMS 利用,这个利用的工程咱们称之为 liveadmin,拜访地址为:https://example.com/liveadmin,拜访如下图所示。

  1. 咱们心愿不再在 liveadmin 旧工程新增新业务页面,因而咱们基于外部的一个 React 脚手架新建了一个称为 increase 的新工程,新的业务页面都举荐应用这个工程开发,这个利用能够独立部署独立拜访,拜访地址为:https://example.com/lookadmin,拜访如下图所示:

1.2 指标

咱们心愿应用微前端的形式,集成这两个利用的所有菜单,让用户无感知这个变动,仍旧依照原有的拜访形式 https://example.com/liveadmin,能够拜访到 liveadmin 和 increase 工程的所有页面。
针对这样一个指标,咱们须要解决以下两个外围问题:

  1. 两个零碎的菜单合成展现;
  2. 应用原有拜访地址拜访两个利用的页面。

对于第 2 个问题,置信对 qiankun 理解的同学能够和咱们一样达成共识,至于第 1 个问题,咱们在实际的过程中,通过外部的一些计划失去解决。下文在实现的过程会加以形容。这里咱们先给出整个我的项目落地的效果图:

能够看到,increase 新工程的一级菜单被追加到了 liveadmin 工程的一级菜单前面,原始地址能够拜访到两个工程的所有的菜单。

1.3 权限治理

说到 CMS,还须要说一下权限管理系统的实现,下文简称 PMS。

  1. 权限:目前在咱们的 PMS 里定义了两种类型的权限:页面权限(决定用户是否能够看到某个页面)、性能权限(决定用户是否能够拜访某个性能的 API)。前端负责页面权限的实现,性能权限则由服务端进行管控。
  2. 权限治理:本文仅论述页面权限的治理。首先每个前端利用都关联一个 PMS 的权限利用,比方 liveadmin 关联的是 appCode = live_backend 这个权限利用。在前端利用工程部署胜利后,通过后门的形式推送前端工程的页面和页面关联的权限码数据到 PMS。风控经营在 PMS 零碎中找到对应的权限利用,依照角色粒度调配页面权限,领有该角色的用户即可拜访该角色被调配的页面。
  3. 权限管制:在前端利用被拜访时,最外层的模块负责申请以后用户的页面权限码列表,而后依据此权限码列表过滤出能够拜访的无效菜单,并注册无效菜单的路由,最初生成一个以后用户权限下的非法菜单利用。

二. 实现

2.1 lookcms 主利用

  1. 首先,新建一个 CMS 根底工程,定义它为主利用 lookcms,具备根本的申请权限和菜单数据、渲染菜单的性能。

入口文件执行以下申请权限和菜单数据、渲染菜单的性能。

// 应用 Redux Store 解决数据
const store = createAppStore(); 
// 查看登录状态
store.dispatch(checkLogin());
// 监听异步登录状态数据
const unlistener = store.subscribe(() => {unlistener();
 const {auth: { account: { login, name: userName} } } = store.getState();
 if (login) { // 如果已登录,依据以后用户信息申请以后用户的权限和菜单数据
 store.dispatch(getAllMenusAndPrivileges({ userName}));
 subScribeMenusAndPrivileges();} else {injectView(); // 未登录则渲染登录页面
 }
});
// 监听异步权限和菜单数据
const subScribeMenusAndPrivileges = () => {const unlistener = store.subscribe(() => {unlistener();
 const {auth: { privileges, menus, allMenus, account} } = store.getState();
 store.dispatch(setMenus(menus)); // 设置主利用的菜单,据此渲染主利用 lookcms 的菜单
 injectView(); // 挂载登录态的视图
 // 启动 qiankun,并将菜单、权限、用户信息等传递,用于后续传递给子利用,拦挡子利用的申请
 startQiankun(allMenus, privileges, account, store); 
 });
};
// 依据登录状态渲染页面
const injectView = () => {const { auth: { account: { login} } } = store.getState();
 if (login) {new App().$inject('#j-main');
 } else {new Auth().$inject('#j-main');
 window.history.pushState({}, '', `${$config.rootPath}/auth?redirect=${window.location.pathname}`);
 }
};
  1. 引入 qiankun,注册 liveadmin 和 increase 这两个子利用。

定义好子利用,依照 qiankun 官网的文档,确定 name、entry、container 和 activeRule 字段,其中 entry 配置留神辨别环境,并接管上一步的 menus,privileges 等数据,根本代码如下:

// 定义子利用汇合
const subApps = [{ // liveadmin 旧工程
 name: 'music-live-admin', // 取子利用的 package.json 的 name 字段
 entrys: { // entry 辨别环境
 dev: '//localhost:3001',
 // liveadmin 这里定义 rootPath 为 liveadminlegacy,便于将原有的 liveadmin 开释给主利用应用,以达到应用原始拜访地址拜访页面的目标。test: `//${window.location.host}/liveadminlegacy/`,
 online: `//${window.location.host}/liveadminlegacy/`,
 },
 pmsAppCode: 'live_legacy_backend', // 权限解决相干
 pmsCodePrefix: 'module_livelegacyadmin', // 权限解决相干
 defaultMenus: ['welcome', 'activity']
}, { // increase 新工程
 name: 'music-live-admin-react',
 entrys: {
 dev: '//localhost:4444',
 test: `//${window.location.host}/lookadmin/`,
 online: `//${window.location.host}/lookadmin/`,
 },
 pmsAppCode: 'look_backend',
 pmsCodePrefix: 'module_lookadmin',
 defaultMenus: []}];
// 注册子利用
registerMicroApps(subApps.map(app => ({
 name: app.name,
 entry: app.entrys[$config.env], // 子利用的拜访入口
 container: '#j-subapp', // 子利用在主利用的挂载点
 activeRule: ({pathname}) => { // 定义加载以后子利用的路由匹配策略,此处是依据 pathname 和以后子利用的菜单 key 比拟来做的判断
 const curAppMenus = allMenus.find(m => m.appCode === app.pmsAppCode).subMenus.map(({name}) => name);
 const isInCurApp = !!app.defaultMenus.concat(curAppMenus).find(headKey => pathname.indexOf(`${$config.rootPath}/${headKey}`) > -1);
 return isInCurApp;
 },
 // 传递给子利用的数据:菜单、权限、账户,能够使得子利用不再申请相干数据,当然子利用须要做好判断
 props: {menus: allMenus.find(m => m.appCode === app.pmsAppCode).subMenus, privileges, account }
})));
// ...
start({prefetch: false});
  1. 主利用菜单逻辑

咱们基于已有的 menus 菜单数据,应用外部的 UI 组件实现了菜单的渲染,对每一个菜单绑定了点击事件,点击后通过 pushState 的形式,变更窗口的门路。比方点击 a-b 菜单,对应的路由便是 http://example.com/liveadmin/a/b,qiankun 会响应路由的变动,依据定义的 activeRule 匹配到对应的的子利用,接着子利用接管路由,加载子利用对应的页面资源。具体的实现过程能够参考 qiankun 源码,根本的思维是荡涤子利用入口返回的 html 中的 <script> 标签,fetch 模块的 Javascript 资源,而后通过 eval 执行对应的 Javascript。

2.2 liveadmin 子利用

  1. 依照 qiankun 官网文档的做法,在子利用的入口文件中导出相应的生命周期钩子函数。
if (window.__POWERED_BY_QIANKUN__) { // 注入 Webpack publicPath, 使得主利用正确加载子利用的资源
 __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
if (!window.__POWERED_BY_QIANKUN__) { // 独立拜访启动逻辑
 bootstrapApp({});
}
export const bootstrap = async () => { // 启动前钩子
 await Promise.resolve(1);
};
export const mount = async (props) => { // 集成拜访启动逻辑,接手主利用传递的数据
 bootstrapApp(props);
};
export const unmount = async (props) => {  // 卸载子利用的钩子
 props.container.querySelector('#j-look').remove();};
  1. 批改 Webpack 打包配置。
output: {
 path: DIST_PATH,
 publicPath: ROOTPATH,
 filename: '[name].js',
 chunkFilename: '[name].js',
 library: `${packageName}-[name]`,
 libraryTarget: 'umd', // 指定打包的 Javascript UMD 格局
 jsonpFunction: `webpackJsonp_${packageName}`,
},
  1. 解决集成拜访时,暗藏子利用的头部和侧边栏元素。
const App = Regular.extend({
 template: window.__POWERED_BY_QIANKUN__
 ? `
 <div class="g-wrapper" r-view></div>
 `
 : `
 <div class="g-bd">
 <div class="g-hd mui-row">
 <AppHead menus={headMenus}
 moreMenus={moreMenus}
 selected={selectedHeadMenuKey}
 open={showSideMenu}
 on-select={actions.selectHeadMenu($event)}
 on-toggle={actions.toggleSideMenu()}
 on-logout={actions.logoutAuth}></AppHead>
 </div>
 <div class="g-main mui-row">
 <div class="g-sd mui-col-4" r-hide={!showSideMenu}>
 <AppSide menus={sideMenus} 
 selected={selectedSideMenuKey}
 show={showSideMenu}
 on-select={actions.selectSideMenu($event)}></AppSide>
 </div>
 <div class="g-cnt" r-class={cntClass}>
 <div class="g-wrapper" r-view></div>
 </div>
 </div> 
 </div> 
 `,
 name: 'App',
 // ...
})
  1. 解决集成拜访时,屏蔽权限数据和登录信息的申请,改为接管主利用传递的权限和菜单数据,防止冗余的 HTTP 申请和数据设置。
if (props.container) { // 集成拜访时,间接设置权限和菜单
 store.dispatch(setMenus(props.menus))
 store.dispatch({
 type: 'GET_PRIVILEGES_SUCCESS',
 payload: {
 privileges: props.privileges,
 menus: props.menus
 }
 });
} else { // 独立拜访时,申请用户权限,菜单间接读取本地的配置
 MixInMenus(props.container);
 store.dispatch(getPrivileges({ userName: name}));
}
if (props.container) {  // 集成拜访时,设置用户登录账户
 store.dispatch({
 type: 'LOGIN_STATUS_SUCCESS',
 payload: {
 user: props.account,
 loginType: 'OPENID'
 }
 });
} else { // 独立拜访时,申请和设置用户登录信息
 store.dispatch(loginStatus());
}
  1. 解决集成拜访时,路由 base 更改

因为集成拜访时要对立 rootPath 为 liveadmin,所以集成拜访时注册的路由要批改成主利用的 rootPath 以及新的挂载点。

const start = (container) => {
 router.start({
 root: config.base,
 html5: true,
 view: container ? container.querySelector('#j-look') : Regular.dom.find('#j-look')
 });
};

2.3 increase 子利用

同 liveadmin 子利用做的事相似。

  1. 导出相应的生命周期钩子。
if (window.__POWERED_BY_QIANKUN__) {__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;}
const CONTAINER = document.getElementById('container');
if (!window.__POWERED_BY_QIANKUN__) {const history = createBrowserHistory({ basename: Config.base});
 ReactDOM.render(<Provider store={store()}>
 <Symbol />
 <Router path="/" history={history}>
 {routeChildren()}
 </Router>
 </Provider>,
 CONTAINER
 );
}
export const bootstrap = async () => {await Promise.resolve(1);
};
export const mount = async (props) => {const history = createBrowserHistory({ basename: Config.qiankun.base});
 ReactDOM.render(<Provider store={store()}>
 <Symbol />
 <Router path='/' history={history}>
 {routeChildren(props)}
 </Router>
 </Provider>,
 props.container.querySelector('#container') || CONTAINER
 );
};
export const unmount = async (props) => {ReactDOM.unmountComponentAtNode(props.container.querySelector('#container') || CONTAINER);
};
  1. Webpack 打包配置。
output: {
 path: DIST_PATH,
 publicPath: ROOTPATH,
 filename: '[name].js',
 chunkFilename: '[name].js',
 library: `${packageName}-[name]`,
 libraryTarget: 'umd',
 jsonpFunction: `webpackJsonp_${packageName}`,
},
  1. 集成拜访时,去掉头部和侧边栏。
if (window.__POWERED_BY_QIANKUN__) { // eslint-disable-line
 return (<BaseLayout location={location} history={history} pms={pms}>
 <Fragment>
 {
 curMenuItem && curMenuItem.block
 ? blockPage
 : children
 }
 </Fragment>
 </BaseLayout>
 );
}
  1. 集成拜访时,屏蔽权限和登录申请,接管主利用传递的权限和菜单数据。
useEffect(() => {if (login.status === 1) {history.push(redirectUrl);
 } else if (pms.account) { // 集成拜访,间接设置数据
 dispatch('Login/success', pms.account);
 dispatch('Login/setPrivileges', pms.privileges);
 } else { // 独立拜访,申请数据
 loginAction.getLoginStatus().subscribe({next: () => {history.push(redirectUrl);
 },
 error: (res) => {if (res.code === 301) {
 history.push('/login', {
 redirectUrl,
 host
 });
 }
 }
 });
 }
});
  1. 集成拜访时,更改 react-router base。
export const mount = async (props) => {const history = createBrowserHistory({ basename: Config.qiankun.base});
 ReactDOM.render(<Provider store={store()}>
 <Symbol />
 <Router path='/' history={history}>
 {routeChildren(props)}
 </Router>
 </Provider>,
 props.container.querySelector('#container') || CONTAINER
 );
};

2.4 权限集成(可选步骤)

  1. 上文提到,一个前端利用关联一个 PMS 权限利用,那么如果通过微前端的形式组合了每个前端利用,而每个前端子利用如果还仍然对应本人的 PMS 权限利用的权限,那么站在权限管理人员的角度而言,就须要关注多个 PMS 权限利用,进行调配权限、治理角色,操作起来都很麻烦,比方两个子利用的页面辨别,两个子利用同一权限的角色治理等。因而,须要思考将子利用对应的 PMS 权限利用也对立起来,这里仅形容咱们的解决形式,仅供参考。
  2. 要尽量维持原有的权限治理形式(权限管理人员通过前端利用后门推送页面权限码到 PMS,而后到 PMS 进行页面权限调配),则微前端场景下,权限集成须要做的事件能够形容为:

    1. 各个子利用先推送本工程的菜单和权限码数据到到各自的 PMS 权限利用。
    2. 主利用加载各子利用的菜单和权限码数据,批改每个菜单和权限码的数据为主利用对应的 PMS 权限利用数据,而后对立推送到主利用对应的 PMS 权限利用,权限管理人员能够在主利用对应的 PMS 权限利用内进行权限的统一分配治理。
  3. 在咱们的实际中,为了使权限管理人员仍旧不感知这种拆分利用带来的变动,仍旧应用原 liveadmin 利用对应的 appCode = live_backend PMS 权限利用进行权限调配,咱们须要把 liveadmin 对应的 PMS 权限利用更改为 lookcms 主利用对应的 PMS 权限利用,而为 liveadmin 子利用新建一个 appCode = live_legacy_backend 的 PMS 权限利用,新的 increase 子利用则持续对应 appCode = look_backend 这个 PMS 权限利用。以上两个子利用的菜单和权限码数据依照上一步形容的第 2 点各自上报给对应的 PMS 权限利用。最初 lookcms 主利用同时获取 appCode = live_legacy_backend 和 appCode = look_backend 这两个 PMS 权限利用的前端子利用菜单和权限码数据,批改为 appCode = live_backend 的 PMS 权限利用数据,推送到 PMS,整体的流程如下图所示,右边是原有的零碎设计,左边是革新的零碎设计。

2.5 部署

  1. liveadmin 和 increase 各自应用云音乐的前端动态部署零碎进行独立部署,主利用 lookcms 也是独立部署。
  2. 解决好主利用拜访子利用资源跨域的问题。在咱们的实际过程中,因为都部署在同一个域下,资源打包遵循了同域规定。

2.6 小结

自此,咱们曾经实现了基于 qiankun LOOK 直播经营后盾的微前端的实现,次要是新建了主工程,划分了主利用的职责,同时批改了子工程,使得子利用能够被集成到主利用被拜访,也能够放弃原有独立拜访性能。整体的流程,能够用下图形容:

三. 依赖共享

qiankun 官网并没有举荐具体的依赖共享解决方案,咱们对此也进行了一些摸索,论断能够总结为:对于 Regularjs,React 等 Javascript 公共库的依赖的能够通过 Webpack 的 externals 和 qiankun 加载子利用生命周期函数以及 import-html-entry 插件来解决,而对于组件等须要代码共享的场景,则能够应用 Webapck 5 的 module federation plugin 来解决。具体计划如下:
3.1. 咱们整顿出的公共依赖分为两类
3.1.1. 一类是根底库,比方 Regularjs,Regular-state,MUI,React,React Router 等冀望在整个拜访周期中不要反复加载的资源。
3.1.2. 另一类是公共组件,比方 React 组件须要在各子利用之间相互共享,不须要进行工程间的代码拷贝。
3.2. 对于以上两类依赖,咱们做了一些本地的实际,因为还没有迫切的业务需要以及 Webpack 5 暂为公布稳定版(截至本文公布时,Webpack 5 曾经公布了 release 版本,后续看具体的业务需要是否上线此局部 feature),因而还没有在生产环境验证,但在这里能够分享下解决形式和后果。
3.2.1. 对于第一类公共依赖,咱们实现共享的冀望的是:在集成拜访时,主利用能够动静加载子利用强依赖的库,子利用本身不再加载,独立拜访时,子利用自身又能够自主加载本身须要的依赖。这里就要解决好两个问题:a. 主利用怎么收集和动静加载子利用的依赖 b. 子利用怎么做到集成和独立拜访时对资源加载的不同体现。
3.2.1.1. 第一个问题,咱们须要保护一个公共依赖的定义,即在主利用中定义每个子利用所依赖的公共资源,在 qiankun 的全局微利用生命周期钩子 beforeLoad 中通过插入 <script> 标签的形式,加载以后子利用所需的 Javascript 资源,参考代码如下。

// 定义子利用的公共依赖
const dependencies = {live_backend: ['regular', 'restate'],
 look_backend: ['react', 'react-dom']
};
// 返回依赖名称
const getDependencies = appName => dependencies[appName];
// 构建 script 标签
const loadScript = (url) => {const script = document.createElement('script');
 script.type = 'text/javascript';
 script.src = url;
 script.setAttribute('ignore', 'true'); // 防止反复加载
 script.onerror = () => {Message.error(` 加载失败 ${url},请刷新重试 `);
 };
 document.head.appendChild(script);
};
// 加载某个子利用前加载以后子利用的所需资源
beforeLoad: [(app) => {console.log('[LifeCycle] before load %c%s', 'color: green;', app.name);
 getDependencies(app.name).forEach((dependency) => {loadScript(`${window.location.origin}/${$config.rootPath}${dependency}.js`);
 });
 }
],

这里还要留神通过 Webpack 来生产好相应的依赖资源,咱们应用的是 copy-webpack-plugin 插件将 node_modules 下的 release 资源转换成包成能够通过独立 URL 拜访的资源。

// 开发
plugins: [
 new webpack.DefinePlugin({
 'process.env': {NODE_ENV: JSON.stringify('development')
 }
 }),
 new webpack.NoEmitOnErrorsPlugin(),
 new CopyWebpackPlugin({
 patterns: [{ from: path.join(__dirname, '../node_modules/regularjs/dist/regular.js'), to: '../s/regular.js' },
 {from: path.join(__dirname, '../node_modules/regular-state/restate.pack.js'), to: '../s/restate.js' },
 {from: path.join(__dirname, '../node_modules/react/umd/react.development.js'), to: '../s/react.js' },
 {from: path.join(__dirname, '../node_modules/react-dom/umd/react-dom.development.js'), to: '../s/react-dom.js' }
 ]
 })
],
// 生产
new CopyWebpackPlugin({
 patterns: [{ from: path.join(__dirname, '../node_modules/regularjs/dist/regular.min.js'), to: '../s/regular.js' },
 {from: path.join(__dirname, '../node_modules/regular-state/restate.pack.js'), to: '../s/restate.js' },
 {from: path.join(__dirname, '../node_modules/react/umd/react.production.js'), to: '../s/react.js' },
 {from: path.join(__dirname, '../node_modules/react-dom/umd/react-dom.production.js'), to: '../s/react-dom.js' }
 ]
})

3.2.1.2. 对于子利用集成和独立拜访时,对公共依赖的二次加载问题,咱们采纳的办法是,首先子利用将主利用曾经定义的公共依赖通过 copy-webpack-plugin 和 html-webpack-externals-plugin 这两个插件应用 external 的形式独立进去,不打包到 Webpack bundle 中,同时通过插件的配置,给 <script> 标签加上 ignore 属性,那么在 qiankun 加载这个子利用时应用,qiankun 依赖的 import-html-entry 插件剖析到 <script> 标签时,会疏忽加载有 ignore 属性的 <script> 标签,而独立拜访时子利用自身能够失常加载这个 Javascript 资源。

plugins: [
 new CopyWebpackPlugin({
 patterns: [{ from: path.join(__dirname, '../node_modules/regularjs/dist/regular.js'), to: '../s/regular.js' },
 {from: path.join(__dirname, '../node_modules/regular-state/restate.pack.js'), to: '../s/restate.js' },
 ]
 }),
 new HtmlWebpackExternalsPlugin({
 externals: [{
 module: 'remoteEntry',
 entry: 'http://localhost:3000/remoteEntry.js'
 }, {
 module: 'regularjs',
 entry: {
 path: 'http://localhost:3001/regular.js',
 attributes: {ignore: 'true'}
 },
 global: 'Regular'
 }, {
 module: 'regular-state',
 entry: {
 path: 'http://localhost:3001/restate.js',
 attributes: {ignore: 'true'}
 },
 global: 'restate'
 }],
 })
],

3.2.2. 针对第二类共享代码的场景,咱们调研了 Webpack 5 的 module federation plugin,通过利用之间援用对方导入导出的 Webpack 编译公共资源信息,来异步加载公共代码,从而实现代码共享。
3.2.2.1. 首先,咱们实际所定义的场景是:lookcms 主利用同时提供基于 Regularjs 的 RButton 组件和基于 React 的 TButton 组件别离共享给 liveadmin 子利用和 increase 子利用。
3.2.2.2. 对于 lookcms 主利用,咱们定义 Webpack5 module federation plugin 如下:

plugins: [// new BundleAnalyzerPlugin(),
 new ModuleFederationPlugin({
 name: 'lookcms',
 library: {type: 'var', name: 'lookcms'},
 filename: 'remoteEntry.js',
 exposes: {TButton: path.join(__dirname, '../client/exports/rgbtn.js'),
 RButton: path.join(__dirname, '../client/exports/rcbtn.js'),
 },
 shared: ['react', 'regularjs']
 }),
],

定义的共享代码组件如下图所示:

3.2.2.3. 对于 liveadmin 子利用,咱们定义 Webpack5 module federation plugin 如下:

plugins: [new BundleAnalyzerPlugin(),
 new ModuleFederationPlugin({
 name: 'liveadmin_remote',
 library: {type: 'var', name: 'liveadmin_remote'},
 remotes: {lookcms: 'lookcms',},
 shared: ['regularjs']
 }),
],

应用形式上,子利用首先要在 html 中插入源为 http://localhost:3000/remoteEntry.js 的主利用共享资源的入口,能够通过 html-webpack-externals-plugin 插入,见上文子利用的公共依赖 external 解决。
对于内部共享资源的加载,子利用都是通过 Webpack 的 import 办法异步加载而来,而后插入到虚构 DOM 中,咱们冀望参考 Webapck 给出的 React 计划做 Regularjs 的实现,很遗憾的是 Regularjs 并没有相应的根底性能帮咱们实现 Lazy 和 Suspense。
通过一番调研,咱们抉择基于 Regularjs 提供的 r-component API 来条件渲染异步加载的组件。
根本的思维是定义一个 Regularjs 组件,这个 Regularjs 组件在初始化阶段从 props 中获取要加载的异步组件 name,在构建阶段通过 Webpack import 办法加载 lookcms 共享的组件 name,并依照 props 中定义的 name 增加到 RSuspense 组件中,同时批改 RSuspense 组件 r-component 的展现逻辑,展现 name 绑定的组件。
因为 Regularjs 的语法书写受限,咱们不便将上述 RSuspense 组件逻辑形象进去,因而采纳了 Babel 转换的形式,通过开发人员定义一个组件的加载模式语句,应用 Babel AST 转换为 RSuspense 组件。最初在 Regularjs 的模版中应用这个 RSuspense 组件即可。

// 反对定义一个 fallback
const Loading = Regular.extend({template: '<div>Loading...{content}</div>',
 name: 'Loading'
});
// 写成一个 lazy 加载的模式语句
const TButton = Regular.lazy(() => import('lookcms/TButton'), Loading);
// 模版中应用 Babel AST 转换好的 RSuspense 组件
`<RSuspense origin='lookcms/TButton' fallback='Loading' />`

通过 Babel AST 做的语法转换如下图所示:

理论运行成果如下图所示:

3.2.2.4. 对于 increase 子利用,咱们定义 Webpack 5 module federation plugin 如下:

plugins: [
 new ModuleFederationPlugin({
 name: 'lookadmin_remote',
 library: {type: 'var', name: 'lookadmin_remote'},
 remotes: {lookcms: 'lookcms',},
 shared: ['react']
 }),
],

应用形式上,参考 Webpack 5 的官网文档即可,代码如下:

const RemoteButton = React.lazy(() => import('lookcms/RButton'));
const Home = () => (
 <div className="m-home">
 欢送
 <React.Suspense fallback="Loading Button">
 <RemoteButton />
 </React.Suspense>
 </div>
);

理论运行成果如下图所示:

  1. 总结

四. 注意事项

  1. 跨域资源
    如果你的利用内通过其余形式实现了跨域资源的加载,请留神 qiankun 是通过 fetch 的形式加载所有子利用资源的,因而跨域的资源须要通过 CORS 实现跨域拜访。
  2. 子利用的 html 标签
    可能你的某个子利用的 html 标签上设置了某些属性或者附带了某些性能,要留神 qiankun 理论解决中剥离掉了子利用的 html 标签,因而如果由设置 rem 的需要,请留神应用其余形式适配。

五. 将来

  1. 自动化
    子利用的接入通过平台的形式接入,当然这须要子利用恪守的标准行程。
  2. 依赖共享
    Webpack 5 曾经公布了其正式版本,因而对于 module federation plugin 的应用能够提上工作日程。

六. 总结

LOOK 直播经营后盾基于理论的业务场景,应用 qiankun 进行了微前端形式的工程拆分,目前在生产环境安稳运行了近 4 个月,在实际的过程中,的确在需要确立和接入 qiankun 的实现以及部署利用几个阶段碰到了一些难点,比方开始的需要确立,咱们对要实现的主菜单性能有过斟酌,在接入 qiankun 的过程中常常碰到报错,在部署的过程中也遇到外部部署零碎的抉择和妨碍,好在共事们给力,我的项目能顺利的上线和运行。

参考资料

  • qiankun
  • Regularjs
  • Module Federation Plugin

本文公布自 网易云音乐大前端团队,文章未经受权禁止任何模式的转载。咱们长年招收前端、iOS、Android,如果你筹备换工作,又恰好喜爱云音乐,那就退出咱们 grp.music-fe(at)corp.netease.com!

退出移动版