业务侧代码解耦实践产生的动态构建思考

86 阅读4分钟

背景

最近自己参与了旧代码从巨石应用剥离分仓,自己在过程中也是负责解耦架构的搭建与侧页面的迁移,当然,这个专项的核心目的是稳定性, 但我也从中看到了很多可能性,基于模块化的架构,对于存量业务的动态构建产生了一些思考。

以下的思考的基础是Module Federation,不了解的同学可以看看Webpack Module federation

模块化

项目拆分

目前后台的主流微前端架构是模块联邦,一方面满足了渐进式迁移,另一方面不同的子应用具备共享依赖的能力,从本次业务侧解耦来看,对于两个团队来说,除了稳定性,还有一点就是终于可以摆脱巨石应用了,巨石应用由于历史原因存在着大量的冗余代码,由于两个团队同时开发,大家都是倾向于不动那些自己看不懂的代码,哪怕这个代码看起来是没用的。现在迁移之后,大家也只用关注自身负责的有效的代码,带来了构建效率与开发体验的提升,其实细想,与其说是微前端分仓,本质上是多个模块分开构建了。

image.png

构建的少了

可以回顾下在基于模块联邦与大仓模式的巨石应用拆分实践解耦专项之前我们是如何迭代的,Stark几十万行代码,一位同学因为一个小需求改了几行代码,发布至线上时,需要将整个庞大的仓库再次构建一遍,这个过程是低效的。分仓解耦后实现了分开构建,我改到哪个模块就构建哪个模块,速度和开发效率当然是有提升的。因为构建的业务代码少了

暂时无法在飞书文档外展示此内容

这里我们将多个同一业务类的页面(可以看做一个MF模块)聚合在一个子应用中,通过entry的形式在stark基座上进行动态加载

More Assets, Less dev

从开发者角度来看,当然希望改到那个模块(注意,是模块,不是子应用)就构建哪个模块,这样就能打破代码总量和构建时间的正比关系,当然要做到这一点,需要具备几个条件

  • 子应用支持按需dev

  • 未被构建的代码需要有Assets

  • 加载控制器,如果加载不到dev模块,就加载assets

局部结合兜底加载

现状

目前子应用本地开发是通过代理的方式将线上远程的子应用转发到本地,实现了线上多模块预览的同时单模块本地调试

  generateOnlineUrlByLocal({
    originUrl,
    outsideUrl,
  }: {
    originUrl: string;
    outsideUrl?: string;
  }) {
    const microLoadLocation = new URL(originUrl);
    let needBeReplacedUrl = microLoadLocation.origin;
    let microAppData = this.microPortMap[microLoadLocation.port];
    if (!microAppData) return;
    const appName = microAppData["appName"];

    const microLoadInfo = this.microLoadData[appName] || {};
    const microConfigEnvValue = microLoadInfo["envValue"];
    if (outsideUrl) {
      return outsideUrl;
    }
    if (microConfigEnvValue === this.currentEnv) {
      return null;
    }
    if (microConfigEnvValue) {
      let microProxyUrl = "";
      if (!outsideUrl) {
        microProxyUrl = `http://${microConfigEnvValue}-stark.dewu.net/${appName
          }${isVueMicro ? '' : `/${MICRO_ONLINE_LOAD_PATH}`}`;
      }
      return originUrl.replace(needBeReplacedUrl, microProxyUrl);
    } else {
      return null;
    }
  }

局部构建

目前模块remote端通过重写部分module federation的方式实现了自动化expose,通过扫描构建文件,若加了标志位,则创建ContainerEntryDependency的形式指定文件expose,这使得项目仅构建expose出的组件,既然具备动态 expose的能力,那么我们可以将谁可以被expose的选择权交给开发者,比如开发者正在开发首页,那么他可以通过类似dx dev --page ./pages/databoard/index的方式,仅将这个页面(也可以称作为组件)暴露出去,那么之后的开发哪怕项目页面数增至上百个,我们也可以做到单模块调试

    compiler.hooks.make.tapAsync('mf-veact-plugin', (compilation, callback) => {
      this.generateProxyFile();
      const name = APP_NAME;
      const filename = `${MICRO_REMOTE_ENTRY_FILE_NAME}.js`;
      let reactComponentsPathMap = {};
      try {
        const reactComponentsPathMapFileContent = readFileSync(
          join(process.cwd(), 'src', '.mf-veact', 'reactComponentsPathMap.json'),
        );
        if (reactComponentsPathMapFileContent) {
          reactComponentsPathMap = JSON.parse(reactComponentsPathMapFileContent.toString());
        }
      } catch (error) {
        console.log({ error });
      }
      const exposes = reactComponentsPathMap;
      const formatExposes = parseOptions(
        exposes,
        (item) => ({
          import: Array.isArray(item) ? item : [item],
          name: undefined,
        }),
        (item) => ({
          import: Array.isArray(item.import) ? item.import : [item.import],
          name: item.name || undefined,
        }),
      );
      const dep = new ContainerEntryDependency(name, formatExposes, 'default');
      dep.loc = { name };
      compilation.addEntry(
        compilation.options.context,
        dep,
        {
          name,
          filename,
          runtime: null,
          library: { type: 'var', name },
        },
        (error) => {
          if (error) return callback(error);
          callback();
        },
      );
    });

那么问题来了,做到了单模块调试,子应用内的其他页面就不管了吗?如果业务在单个需要里需要在其他页面操作后再回到需要开发的页面呢?举个例子,业务需求是在首页内点击更多任务出现的任务抽屉内过改动,由于首页和更多任务抽屉是在一个子应用的,那么局部构建的模块就变成一座孤岛,开发体验大打折扣

兜底加载

那么如何解决上面的问题,可知通过上面的技术方案,我们已经拥有了本地与线上两类模块集,在子应用里我们通过重写Remote Micro的加载方式,将线上子应用模块加载逻辑作为本地模块的加载兜底

  if (process.env.NODE_ENV === 'development') {
    return `promise new Promise((resolve,reject) => {
      ...
      const script = document.createElement('script')
      script.src = newRemoteUrl
      script.onload = () => {
        const proxy = {
          get: (request) => { 
let loadFunc = window.local_ ${MICRO_NAME} .get(request)
if(!loadState.__zone_symbol__state) {
return window. ${MICRO_NAME} .get(request) },
}
          init: (arg) => {
            ...
          }
        }
        resolve(proxy)
      }
      try {
        document.head.appendChild(script);
      } catch(e) {
        console.error('appendChild error:', e)
        reject(e);
      }
    })
    `
  }

线上页面渲染时,根据插件配置优先加载本地micro,若加载失败则默认加载线上逻辑,这样我们就实现了本地加载单个模块的同时,线上仍然是全量功能可用

总结

以上思考仅是从Module Feration的原理上推理局部构建的可行性,本文的阐述其实也是在说明对于基础架构库的不断学习不仅仅可以用在官方声称的场景中,也可以被作为解决业务问题的工具。当然,Actions speak louder than words,最近刚做完拆分工作,只能说在做了在做了,后续落地将出一篇更为详近的数据与技术复盘,让团队技术提效更进一步。