曝光前东家的微前端体系技术规划

2,391 阅读21分钟

前言

文章内容主要介绍我做的微前端的规划和调研。在前公司凉透之后,征得同意,之前没能说的分享,现在说一次。囊括“初步规划和过程问题”以及“进一步规划和过程问题”,是一个微前端的技术的规划探索。

今整理下,当做业务处理的经验的分享。 这个规划有着强烈的业务结合,但不妨提供一种思路,让人知道“有这么一回事”。

不过多讲述api,重描述想法,吹一下umi可以做到这些事。总的来说,是一开始“优化用户体验和开发体验”,到“兼容拓展和拓展优化”的实践和思考过程

需要解决的业务问题

  1. 当时(现在没有了将来)觉得将来的项目会越来越多,会面临十多个几十个项目。这么多项目要写登录代码,内部依赖库一改全部全部都要改。
  2. 业务原因,跳转会有白屏,交互不好。下图中原本Home项目不是真的父应用,点击菜单其实是跳转到旧运营项目,会白屏一下子,同时旧运营项目也记载了一次一样的侧边栏代码。
  1. 旧项目臃肿,维护难。无论是打包速度还是dev启动速度,都是难忍受级别。
  2. 各项目顶部栏长的类似,有的一样,有的就一两个按钮的差别,往往一个需求改N个项目。
  3. 代码规范之类配置、npm源、以及各种内部构建配置需到处复制。

立项之初的硬性理由其实不多,基本的微前端方案足够解决上面问题

后来想尝鲜和走远,我有了立项会议之外的越来越多的设想。从“尝试调研微前端 -> 完善问题的微前端 -> 建立脚手架模板 -> 更多的技术方案”这么一过程。

为什么是qiankun
  1. qiankun相对简单,号称开箱即用
  2. 当时相比其他方案更火
  3. create react app的webpack配置很烦,umi有很好的集成,自带路由和redux等功能集合
  4. 有umi旧项目,与qiankun是一家人。

后两点也是我坚持用umi的原因,效率至上。

规划思考

立项会议本质是想解决上面业务问题的前3点,以及一些畅想,剩下自由发挥。负面说法就是:我的自嗨专场。

先看最后的功能架构的图示

点我跳过去看大图

图片描述(可以做到的事):

  1. 红色父应用(home)展示各个系统平台入口,包含顶部栏,以及展示不同子应用的侧边菜单栏
  2. 跳到"紫色子应用"时候,顶部栏渲染"紫色项目"的代码按钮
  3. 若跳到"黄色子应用",顶部栏紫色按钮消失,渲染"黄色项目"的代码按钮
  4. 蓝色是另外独立的父应用,进行渲染模块级子应用(不改变url)的业务
  5. 蓝色父应用定义登录的插件按钮,可跳转到一个子应用页面

初步规划

为了更方便接入父应用,同时减少项目内登录配置、gitlab配置、编辑规范等等繁琐事情,写个base项目,当做基座,给同事fork用的,专注业务开发。

但结合已有业务和案例,解决原本问题之外,给这个基座带入其他想法目的

  1. 希望开发子应用时候,不想额外打开父应用,也不想改host或复制cookie的进行身份绑定。 -> 基座可分别开发父子应用。子可以脱离父独立运行。需要一个新的umi-layout插件做这个事,活用Umi提供的插件方式,强约定式结构

灵感来源:发现ant design pro的款式中是叫umi-layout插件,我也就跟着用umi插件了。事实证明跟着写是有必要的,让我了解到了这个插件体系,从而有了更多的拓展想法

  1. 为了着重解决“上述项目问题”的第一点,把登录模块需要新依赖、以及需要一个父子间的通讯库。 -> 方便我维护,采用lerna管理

灵感来源:umi的源码本身也是采用这种形式。

  1. 组装的概念,子应用更简便地出现在不同主应用里面 —> 配置菜单,父应用菜单,实际是子应用中的某些页面

灵感来源:大概是追求业务响应速度的经验,“配置化”思维。

技术细节

父子通讯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插件类似,是个函数,接受的参数是一个上下文

umi详备插件原理讲解

抄的layout插件大概运转流程如下:

  1. 被umi配置引入
  2. api.describeapi.addRuntimePluginKey,可以理解为同步阶段,各种注册给umi
  3. 重要的是异步阶段,api.onGenerateFilesapi.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
  });
};

  1. 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…

消极回顾

  1. 碰到的开发问题远远超过这里汇总(链接路由处理、资源处理、测试排查、有些库在Umi插件中不能随意用等等),有时很蠢的问题,没人能一起持续询问,耗费很多时间。有一些还是写demo时候解决的,有问题多搜issue,各种表述地去搜。
  2. 时间跨度是很大,旧项目改动跟不上迭代。
  3. 性能、代码量变大啊,这种事我是没多考虑的。无论我外面的布局插件重渲染多少次,就那么点东西,影响不大。反正有的系统页面真的卡,渲染几M的接口数据。
  4. 子应用的问题有的也没解决,特别是nextjs子应用的非路由级渲染情况,比如一些style-component警告、或者顶部栏添加东西会闪一下等等。

正向思考

  1. 我觉得做基建,设计和易用是根本。
    1. 设计。工程化是追求,解决维护性问题,和产生好用的东西。没有想法还做什么规划?
    2. 易用。封装是基本素养,好的抛出以及简易的使用方法,是对得起使用的同事。
  2. 文章牵扯写出来的东西,看似没有深度的,纯经验类,知道了就是知道。但是在平时真正开发过程中,很多事是可以去层层追溯到有深度的,途中自然就有不理解的东西,比如:
    1. CRA子应用空白 -> 资源是否正确形式传达(网络) -> 是否qiankun配置有误(webpack配置、模块识别)
    2. 抽象“遍历渲染antd的Menu.Item”,失败 -> Menu和Menu.Item的源码结构(React.Context)
    3. 各种没见过的hook警告和报错 -> {renderFn()}、{React.createElement(renderFn)}某些情况的差异
    4. ...
  3. 碰到的问题,项目的独特性决定了大概率以后都不会再见,但排查问题的思维有了不少的拓展以及心性的沉淀,奇奇怪怪的问题已经见怪不怪,因为真可能很离谱。

回想起开会立项的探讨和请教别人时的情景,一点的提示或者设计形式,都让我豁然开朗,可惜规划仅仅是规划。