前言
文章内容主要介绍我做的微前端的规划和调研。在前公司凉透之后,征得同意,之前没能说的分享,现在说一次。囊括“初步规划和过程问题”以及“进一步规划和过程问题”,是一个微前端的技术的规划探索。
今整理下,当做业务处理的经验的分享。 这个规划有着强烈的业务结合,但不妨提供一种思路,让人知道“有这么一回事”。
不过多讲述api,重描述想法,吹一下umi可以做到这些事。总的来说,是一开始“优化用户体验和开发体验”,到“兼容拓展和拓展优化”的实践和思考过程
需要解决的业务问题
- 当时(现在没有了将来)觉得将来的项目会越来越多,会面临十多个几十个项目。这么多项目要写登录代码,内部依赖库一改全部全部都要改。
- 业务原因,跳转会有白屏,交互不好。下图中原本
Home项目
不是真的父应用,点击菜单其实是跳转到旧运营
项目,会白屏一下子,同时旧运营
项目也记载了一次一样的侧边栏代码。
- 旧项目臃肿,维护难。无论是打包速度还是dev启动速度,都是难忍受级别。
- 各项目顶部栏长的类似,有的一样,有的就一两个按钮的差别,往往一个需求改N个项目。
- 代码规范之类配置、npm源、以及各种内部构建配置需到处复制。
立项之初的硬性理由其实不多,基本的微前端方案足够解决上面问题
后来想尝鲜和走远,我有了立项会议之外的越来越多的设想。从“尝试调研微前端 -> 完善问题的微前端 -> 建立脚手架模板 -> 更多的技术方案”这么一过程。
为什么是qiankun
- qiankun相对简单,号称开箱即用
- 当时相比其他方案更火
create react app
的webpack配置很烦,umi有很好的集成,自带路由和redux等功能集合- 有umi旧项目,与qiankun是一家人。
后两点也是我坚持用umi的原因,效率至上。
规划思考
立项会议本质是想解决上面业务问题的前3点,以及一些畅想,剩下自由发挥。负面说法就是:我的自嗨专场。
先看最后的功能架构的图示
图片描述(可以做到的事):
- 红色父应用(home)展示各个系统平台入口,包含顶部栏,以及展示不同子应用的侧边菜单栏
- 跳到"紫色子应用"时候,顶部栏渲染"紫色项目"的代码按钮
- 若跳到"黄色子应用",顶部栏紫色按钮消失,渲染"黄色项目"的代码按钮
- 蓝色是另外独立的父应用,进行渲染模块级子应用(不改变url)的业务
- 蓝色父应用定义登录的插件按钮,可跳转到一个子应用页面
初步规划
为了更方便接入父应用,同时减少项目内登录配置、gitlab配置、编辑规范等等繁琐事情,写个base项目,当做基座,给同事fork用的,专注业务开发。
但结合已有业务和案例,解决原本问题之外,给这个基座带入其他想法目的:
- 希望开发子应用时候,不想额外打开父应用,也不想改host或复制cookie的进行身份绑定。 -> 基座可分别开发父子应用。子可以脱离父独立运行。需要一个新的umi-layout插件做这个事,活用Umi提供的插件方式,强约定式结构
灵感来源:发现
ant design pro
的款式中是叫umi-layout
插件,我也就跟着用umi插件了。事实证明跟着写是有必要的,让我了解到了这个插件体系,从而有了更多的拓展想法
- 为了着重解决“上述项目问题”的第一点,把登录模块需要新依赖、以及需要一个父子间的通讯库。 -> 方便我维护,采用lerna管理
灵感来源:umi的源码本身也是采用这种形式。
- 组装的概念,子应用更简便地出现在不同主应用里面 —> 配置菜单,父应用菜单,实际是子应用中的某些页面
灵感来源:大概是追求业务响应速度的经验,“配置化”思维。
技术细节
父子通讯sdk
开发这个东西一开始原因:因为当时qiankun还没有提供通讯的api。登录插件数据传递和后来react组件的传递,基本都是依靠它。
本质就是window对象上开荒,以及运用3个设计模式:
- 单例模式:qiankun的js沙盒
window对象
和iframe的window对象
一样,top属性能拿到顶层的父window对象
。在父的window对象
中的开一个对象,进行存储和数据传递。理论上也可以用在同域的子iframe之间通讯。 - 订阅发布模式:vue2的数据响应那套,有“Dep”、“Watcher”和“Observer”,抄的最初版本的已经找不到了,放两个类似的参考。只是我把get和set的拦截改成
Proxy
。参考1,参考2 - 观察者模式:就普通的事件巴士
umi布局插件
前置知识:
以ant design pro
为例,在src
目录下写一个layout
文件夹,并放一个装布局的JSX,会给全局套上这个布局。
实质是启动阶段会在"src/.umi/*"内,生成一个缓存react文件,并require
读取"src/layout"文件,最后这个react文件会被“src/.umi/core/routes.ts”引用,当做根路由的组件。
umi插件大部分都是在
src/.umi/**
内,比如dva、useModel、以及qiankun(非umi项目也可以在这里参考一些封装)。
插件底层是tapable,也有点和webpack插件类似,是个函数,接受的参数是一个上下文
抄的layout插件大概运转流程如下:
- 被umi配置引入
api.describe
和api.addRuntimePluginKey
,可以理解为同步阶段,各种注册给umi- 重要的是异步阶段,
api.onGenerateFiles
和api.writeTmpFile
将写个文件到src/.umi/中
api.onGenerateFiles(() => {
...
api.writeTmpFile({
path: join(DIR_NAME, 'Layout.tsx'),
content: getLayoutContent(
layoutOpts,
currentLayoutComponentPath,
{
// 顶部栏右侧按钮,约定的src文件位置:src/mlayout/headerMenu
headerMenu: pathMap.headerMenuPath,
// 顶部栏logo右侧空间,约定的src文件位置:src/mlayout/headerTabs
headerTabs: pathMap.headerTabsPath,
// 头像的登录插件的插件名,登录插件将插入顶部栏最右侧,头像下拉的子菜单约定位置: src/mlayout/headerSubMenuForLogin
injectMenuPlugin,
headerTitle,
// 侧边栏菜单
menus,
},
{},
),
});
})
getLayoutContent 获取了各种配置和参数,返回模板字符串,umi的api将把这堆字符串 "src/.umi/" 中,成为缓存tsx文件,缓存tsx文件又读取打包后真正的layout文件的路径,并渲染。
// src/.umi/xx-mlayout/Layout.tsx
import React from "react";
// 解析其他插件
import Item1 from "@@/xx-layout-login/MenuItemLogin";
export default props => {
...
const userComp = {
headerMenu: require("/xxxx/example/mainapp/src/mlayout/headerMenu").default,
headerTabs: require("/xxxx/example/mainapp/src/mlayout/headerTabs").default,
pluginItems: [Item1]
};
//插件主渲染逻辑文件的路径,并渲染
return React.createElement(require("/xxxx/packages/plugin-mlayout/lib/layout/index.js").default, {
userConfig,
userComp,
umircConfig: {...},
...props
});
};
api.modifyRoutes
在有了.umi里面的缓存tsx文件后,将其侵入路由
其他比较有用的api 比如:addUmiExports。
我这有三个插件
1. plugin-mlayout
外观整体是antd案例差不多,主要多了上面代码中的几个文件夹约定协议。
主要依据qiankun提供的两个变量(window.__POWERED_BY_QIANKUN__
和window.__isMainApp__
),判断当前处于什么环境,隐藏了"子-Sider"和"子-Header"。
主要是为了保持原ant pro项目的原设计,使用了dva。数据主要源自项目定义的dva的"microLayout"文件,并根据约定的src文件路径渲染顶部栏的东西。
因此可以同一个脚手架做父应用或者子应用,子可以改成父应用
不同的父应用返回不同的侧边栏菜单。侧边栏的设计大概下面,记不清,原定是有界面配置,所以就设计成树,对于配置化渲染子应用菜单,就前面几个字段有用。接口获取当前应用渲染什么。
CREATE TABLE `menu` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`app` varchar(11) DEFAULT NULL COMMENT '在哪个应用里面使用',
`micro_app` varchar(11) DEFAULT NULL COMMENT '渲染哪个子应用的',
`title` varchar(11) NOT NULL DEFAULT '' COMMENT '菜单文案',
`route` varchar(11) DEFAULT NULL COMMENT '应用的路由,有子菜单将置空',
`parent` varchar(11) DEFAULT NULL COMMENT '上级菜单id,无则为空',
`icon` varchar(11) DEFAULT NULL COMMENT '使用的icon',
`path` varchar(11) DEFAULT NULL COMMENT '菜单路径, parent_id/id',
`is_leaf` int(2) DEFAULT NULL COMMENT '1 是叶子节点,0不是',
`status` int(2) DEFAULT NULL COMMENT '软删',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
2. plugin-layout-login
封装一些业务强相关的身份逻辑:跳转登录,顶部栏头像模块,抛出获取身份的hook。 数据存储在了通讯库里面,因为是父子可共用的数据。
3. plugin-sub-utils
抽象一些函数给前面两个插件使用
过程问题
1. 接口按需获取侧边栏菜单的处理
菜单数据是一个数组。
侧边菜单栏是接口获取的,但是菜单栏渲染是"mlayout插件"的事,各项目菜单可能又不一样,决定应该由项目里面获取“异步的菜单数组”,利用某个手段传递给"mlayout插件"进行渲染。
这里直接抄了的antd pro layout插件方式,和umi的redux插件(dva)绑定起来。所以脚手架也强约定要启动dva,不需要格外引入数据流存储的东西。
这里设计有3个串联点,
- 数据api:
fetchMenus
函数,请求需要渲染的各子应用和侧边栏菜单。 是一个带缓存的fetch,两个地方各调用一次实际是发起一次network。叫“Promise cache”,随便搜搜就有,挺多写法,或者用个fetch库。
- 注册子应用:在父应用调用
fetchMenus
函数进行注册,详情文档。必须先注册了,才能保证侧边栏点击时候能打开子应用。 - 侧边栏数据的渲染:起一个全局state 的属性。叫
microLayout
,初始化调用fetchMenus函数
并存储结果,以及存其他配置。将由插件获取这个模块数据。
<!--mlayout 插件-->
// @ts-ignore
import { connect } from 'umi';
const BaseLayout = (props) => {...}
// 开启了dva就直接联通,没有启动dva要防止报错
const __connect = connect
? connect
: (props: any) => {
return (BaseComp: any) => {
return BaseComp;
};
};
// dva协定预定这个命名“microLayout”处理微前端数据
export default __connect(({microLayout}) => {
if (microLayout) {
const dvaConfig = {...};
return { dvaConfig };
}
return {};
})(BaseLayout);
--------我是分割线--------
<!--父应用 App.tsx-->
export const qiankun = fetchMenus()/** 按需返回各子应用和侧边栏菜单 */
.then(res => {
return res;
/**
* {
// 注册子应用
apps: [
{
name: 'subapp1', // 唯一 id
entry: '//localhost:8100', // html entry
props: {},
},
],
// 子应用路由配置
routes: [
{
path: '/portal',
microApp: 'course_portal',
microAppProps: {
autoSetLoading: true,
},
},
{
path: '/subapp1',
microApp: 'subapp1',
microAppProps: {
autoSetLoading: true,
},
},
],
};
*/
})
1. 插件包引用出问题,没有引用到build后的新代码
大概率是我不熟悉的原因,不应该乱在文件夹中yarn,排查手段主要是看各个文件里面node_modules的文件代码,排查umi插件用了哪里的引用。
需要经常删掉各个demo里面的node_modules文件夹重新安装依赖。反复rm,反复build,反复yarn。
有时候直接改node_modules里面的插件代码会更快捷,同时注意重启项目,因为webpack不会热更新node_modules里面东西
2. antd版本不一样,以及外观不同
按钮的3版本是圆角,4是偏直角。主要手段是给旧版本的项目加上antd前缀。
就算不是微前端,内部组件库也会涉及到这个问题(组件库使用antd4编写)。一个组件想给3和4版本的两个项目使用。
此外内部组件库要写好rollup配置,打包两份,一份是正常的es模式,另一份是给ant3项目使用的umd模式,按需打包并编译出原生JS文件。
3. 各项目的url处理
某些原因,不同项目url不靠域名区分,而是靠前缀,如portal.guorou.net/{protjectName}
子项目CRA项目则某些情况,需要根据window.__isMainApp__
去处理路由的path
4. 子应用CRA项目的配置重改
creat react app
需要第三方库改动webpack,这里使用react-app-rewired,因为试过了其他的不行,可能webpack配置还被一起改动了其他内容,不好排查,就试着换库。
此外,js、css资源有问题,可能是 Content-type 为 text/html导致,有可能是主应用没有启动代理。
// config-overrides.js
devServer: function(configFunction) {
return function(proxy, allowedHost) {
const config = configFunction(proxy, allowedHost);
// 关闭主机检查,使微应用可以被 fetch
config.disableHostCheck = true;
// 配置跨域请求头,解决开发环境的跨域问题
config.headers = {
'Access-Control-Allow-Origin': '*',
};
config.historyApiFallback = true;
config.hot = false;
config.watchContentBase = false;
config.liveReload = false;
return config;
};
},
webpack: config => {
config.output.library = `${name}-[name]`;
config.output.libraryTarget = 'umd';
config.output.jsonpFunction = `webpackJsonp_${name}`;
config.output.globalObject = 'window';
return config;
},
5. 热更新报错
开一个路由子应用,打开过子应用页面的话,父应用的热更新,整个网页会崩
来自devScript.js, 搜出个问题,感觉是没解决好,源自沙盒问题。
最快解决办法就是禁止父的热更新,HMR=none yarn dev
。
也有曲线救国的方法:
react-error-overlay这个库就是显示运行时错误的,猜测沙盒处理的时候报错了,显示这个提示错误的iframe。
点进去定位问题,window.__REACT_ERROR_OVERLAY_GLOBAL_HOOK__
是这个东西是undefined。排查发现,一开始是有的,关掉子应用就没有了,这是关键1。同时注意页面不是变白,是变灰色,并没有崩掉,这是一层这个库的iframe,没有报错却触发了iframe,删除节点后发现父应用的热更新是成功了,这是关键2。
解决关键1:window.__REACT_ERROR_OVERLAY_GLOBAL_HOOK__
切走子应用时候复原它,所以要先存出来,子路由关掉时候再赋值一次。
解决关键2:这个库无法配置,也没有暴露window变量。那就是想办法关掉这个iframe,使用MutationObserver
<!--app.tsx-->
...
// 缓存react-error-overlay的window属性
let fixErrorCache: any = null;
const devFixHotUpdateListener = () => {
if (!location.host.includes('localhost')) return;
fixErrorCache = window.__REACT_ERROR_OVERLAY_GLOBAL_HOOK__;
// 监听body下面是不是多了个iframe,是则删除
const mutationObserver = new MutationObserver(mutations => {
mutations.forEach(mutation => {
const dom = mutation.addedNodes[0];
// dom?.clientWidth == window.innerWidth
if (dom && dom.nodeName.toLowerCase() === 'iframe') {
document.querySelector('body')!.removeChild(dom);
}
});
});
mutationObserver.observe(document.querySelector('body')!, {
childList: true,
});
};
devFixHotUpdateListener();
// 切走子应用时候,复原这个变量
const devFixHotUpdateError = () => {
if (!location.host.includes('localhost')) return;
// 也可以考虑把其内部属性“iframeReady”赋值成空函数
window.__REACT_ERROR_OVERLAY_GLOBAL_HOOK__ =
window.__REACT_ERROR_OVERLAY_GLOBAL_HOOK__ || fixErrorCache;
};
...
export const qiankun = {
...
lifeCycles: {
afterUnmount: () => {
devFixHotUpdateError();
},
},
}
实践证明,热更新也没有出现,“iframe被删,页面闪一下”的情况。
相关东西问题,github.com/umijs/umi/i…
6. umi导出的东西的TS处理
插件导入的东西给Umi抛出,暂时不支持。这个还是打开状态:github.com/umijs/umi/i…
某些情况umi插件都报错,删掉.umi文件夹,点进去第一层看不到什么的,插件往往是export * from时候 '/xx'
,重跑几次就好了,当然也有不好的情况,我是忽略掉。
插件导出函数只能ts文件手动补充了
// typing.d.ts
declare module 'umi' {
export function xxx
}
进一步规划
到这一步,更多是我个人想花里胡哨一点,能做到更多的可能性,希望开发和给产品有更多的可操作性。也算是这个基础建设的展望,在原设计之外,开发过程中的部分想法的尝试实现。
比如在项目的redux中全局传递组件,那么也想着父子通讯也可试图这样做。
又或者把组件脱离npm包范畴,生成url渲染的就是一个可用组件,但是又比iframe有更方便的数据操作。
技术细节
顶部栏传递组件
由于通讯库的实现,但实际子应用除了身份信息,并没有什么特别需要通讯的,想找些事给这个库做。这边的项目基本都是react,而且版本是16以上,所以考虑顶部栏那些按钮,交给子项目实现。
老早前接一个CRA项目时候,把react16版本的简单组件对象丢给17去渲染,好像没有问题。不确定,写这个文章demo时候并不想试这个事情。顶部栏东西不会复杂多框式,直接原生JS也是可尝试方法。
实行对通讯库的变量监听,将原有的顶部栏数组(antd Menu组件)放到这个变量里面,顶部栏的组件的state将依据这个变量变动,父子都可以改变这个数组。
就双向绑定那么一回事,原设计是使用事件巴士处理,对我来说会更简单实现,但是考虑别的开发,使用上会觉得麻烦,所以放弃了事件巴士。
// mlayout插件的顶部栏
const PublicHeader = (props) => {
...
const [headerMenuListForRender, setHeaderMenuListForRender] = useState([]);
useEffect(() => {
const layoutStoreKey = '__layout__';
const store = new MicroStore({
state: {
headerMenuList: [],
},
name: layoutStoreKey,
});
// 监听通讯库的变动,实时设置渲染的menu
store.watch(layoutStoreKey + '/headerMenuList', (v, v2) => {
setHeaderMenuListForRender(v);
});
store.set('headerMenuList', headerMenuList);
}, []);
...
return <div>
...
<Menu
className="mlayout-publicHeader__Menu"
mode="horizontal"
style={{
lineHeight: `${globalHeaderHeight}px`,
height: `${globalHeaderHeight}px`,
}}
selectedKeys={[]}
>
{/* 渲染约定的文件夹内or通讯库里面的Menu */}
{deliveryMenuNode(renderCustomMenu, headerMenuListForRender)}
{/* 渲染登录插件的menu,插件写法问题不适合被hook处理,分离开,否则报错 */}
{pluginItemsMenuList.map((fn: any) =>
fn({
MenuItem: Menu.Item,
MenuSubMenu: Menu.SubMenu,
MenuDivider: Menu.Divider,
}),
)}
</Menu>
<div>
}
// 子应用,app.tsx
// mlayout导出个修改顶部栏的工具函数,参数是一个函数
import { setTopMenuList } from 'umi';
export function rootContainer(container) {
// 选择就触发加顶部栏菜单
setTopMenuList(menuListByShallowCloning => {
menuListByShallowCloning.unshift({
node: () => (
<span
onClick={() => {
alert(1);
}}
style={{ border: '1px solid gray' }}
>
sub1添加的顶部栏btn
</span>
),
key: 'sub1',
});
// 返回修改的结果
return menuListByShallowCloning;
});
return <ConfigProvider locale={zhCN}>{container}</ConfigProvider>;
}
export const qiankun = {
// 应用卸载之后触发
async unmount(props) {
setTopMenuList(menuListByShallowCloning => {
menuListByShallowCloning.shift();
return menuListByShallowCloning;
});
},
};
最后数据使用的分布
图片海报iframe优化
灵感来源自FOLLWME网站的微前端的远程组件。
公司内业务,有个这么的nextjs项目,理论是有这样的流程,当做渲染海报并或者oss url的工具项目。
除了海报页面特定数据接口,其他的逻辑是集合在一个HOC内部,开发者只在pages目录里面写html+css以及简单JS数据逻辑。
业务项目想要制作一个海报,传入对应的海报url到iframe组件,然后得到一个url。
实际开发的问题出自这个iframe组件,写的不好,勉强能用的状态,通讯麻烦,LocalStorage处理麻烦等等问题。所以考虑了使用微前端优化。
这也可以当做是远端组件的可能性。
过程问题
1. malyout的动态顶部栏开发问题
业务中一般是简单的按钮跳转或者是打开个弹窗,足够应付不少场景,再不济写原生JS进行传递都可。
更多问题是暴露了通讯的监听写的不好,以及ant Menu的动态渲染问题。是将就用的状态。
还有就是插件直接嵌套,间接使用了{renderPlugin()}
,而不是React.createElement(renderPlugin)
,造成一些Hook报错。
以及路由链接和侧边栏的匹配等问题。
都比较细节,搜谷歌和不断注释可以排查,此处略。
2. nextjs接入主应用
nextjs也是有webpack,但是配下来各种不畅,最后当它是非webpack应用进行配置。在"pages/_document.tsx"中写个sciprt标签,补充qiankun的生命周期导出。(一开始也是不行的,后来qiankun的升级又可以这个方法了)
qiankun 封装 singleSpa,singleSpa 适配HTML文件最后一个script标签。
此外有2个问题
- 不能热更新。父加载过子之后,改父的代码,不能再次加载了,需手动刷新。
- nextjs可能已经渲染完了才触发qiankun的生命钩子,因为ssr本身就是有内容的html。不过不影响,这个工具项目本身只是自动返回一个oss url
格外需要改动上图黄色框的内容。本身这个轮训参照了 tti-polyfill。
内部使用了MutationObserver
,原监听document,现在是子应用了,应该只监听自己的dom节点。所以要事前存储住父容器
<!--抛出qiankun 周期的自执行函数,此时的window已经是沙盒的-->
(global => {
global.nextAppList = global.nextAppList || []
global['next-app' + global.nextAppList.length] = {
bootstrap: function() {
return Promise.resolve();
},
mount: function(props, b) {
global.imgCache = props.store
// 存父容器
global.imgCache = props.cache
global.topWin = props.topWin
global.__fatherDom__ = props.container
// global.topWin.nextAppList = global.topWin.nextAppList || []
// global.topWin.nextAppList.push(global['next-app'])
global.imgCache.fatherList.push(props.container)
},
unmount: function() {
console.log('home unmount');
return Promise.resolve();
}
};
global.nextAppList.push(global['next-app' + global.nextAppList.length])
})(window);
<!--高阶函数的tti代码-->
mutationObserver.observe(window.__fatherDom__ || document, {
attributes: true,
childList: true,
subtree: true,
attributeFilter: ['href', 'src'],
});
3. 相继打开2个nextjs,第二个的hook会不执行
就是上一个gif演示的demo。这是个非常诡异的问题,A和B两个子的nextjs应用,先加载A再到B,B的useEffect
不会执行。但是先加载B,再到A就没问题。
至今也未知原因,换链接,换展示内容,换子应用顺序都试过。但没有好的解决方案,总是会出现一个子应用的Hook无法执行。
最后碰巧勉强解决。尝试改变沙盒的配置,发觉可以解决这情况。
const sandbox = {
// strictStyleIsolation: true,
experimentalStyleIsolation: true,
loose: true,
};
loadMicroApp(
{
name: 'next-app',
entry: '/image/prize/level',
container: '#firstNext',
props: {
cache: window.imgCache,
topWin: window,
},
},
{ sandbox },
);
sandbox的loose属性,在文档是没有的,是我点进去ts里面发现。目测是JS沙盒变了一些。
去看ts不是偶然举动,这个文档不全,一早发现的,去年的调试远不及现在顺利,当时是直接看qiankun源码排查,问题没解决,倒是发现文档和里面的代码不对应。
同理,子应用的沙盒应该根据实际情况,是可以特定情况考虑关闭的,当然关了也可能会有全局变量污染之外的奇奇怪怪渲染问题。
还有next/Image也要按顺序才触发,没有排查的头绪。盲猜是nextjs有单例形式的代码,后来的nextjs总是拿到之前的js在跑,因为某些情况下,第二个子应用的useEffect里面的异步回调,拿了第一个子应用的dom,解决办法就是把这个dom存在异步外面的一个变量里面。强烈建议:qiankun里面别碰nextjs。
可能qiankun写法问题,第一个应用的hook的unmount,会在第二个应用激活时触发。无头绪。
总结
经验类文章,算是整个过程中的总结。
github: github.com/Ele-Lee/umi…
消极回顾
- 碰到的开发问题远远超过这里汇总(链接路由处理、资源处理、测试排查、有些库在Umi插件中不能随意用等等),有时很蠢的问题,没人能一起持续询问,耗费很多时间。有一些还是写demo时候解决的,有问题多搜issue,各种表述地去搜。
- 时间跨度是很大,旧项目改动跟不上迭代。
- 性能、代码量变大啊,这种事我是没多考虑的。无论我外面的布局插件重渲染多少次,就那么点东西,影响不大。反正有的系统页面真的卡,渲染几M的接口数据。
- 子应用的问题有的也没解决,特别是nextjs子应用的非路由级渲染情况,比如一些style-component警告、或者顶部栏添加东西会闪一下等等。
正向思考
- 我觉得做基建,设计和易用是根本。
- 设计。工程化是追求,解决维护性问题,和产生好用的东西。没有想法还做什么规划?
- 易用。封装是基本素养,好的抛出以及简易的使用方法,是对得起使用的同事。
- 文章牵扯写出来的东西,看似没有深度的,纯经验类,知道了就是知道。但是在平时真正开发过程中,很多事是可以去层层追溯到有深度的,途中自然就有不理解的东西,比如:
- CRA子应用空白 -> 资源是否正确形式传达(网络) -> 是否qiankun配置有误(webpack配置、模块识别)
- 抽象“遍历渲染antd的Menu.Item”,失败 -> Menu和Menu.Item的源码结构(React.Context)
- 各种没见过的hook警告和报错 -> {renderFn()}、{React.createElement(renderFn)}某些情况的差异
- ...
- 碰到的问题,项目的独特性决定了大概率以后都不会再见,但排查问题的思维有了不少的拓展以及心性的沉淀,奇奇怪怪的问题已经见怪不怪,因为真可能很离谱。
回想起开会立项的探讨和请教别人时的情景,一点的提示或者设计形式,都让我豁然开朗,可惜规划仅仅是规划。