适配 CRA 项目为微前端应用

前期准备

  • 一个使用 CRA 建立的新项目或者旧项目
  • 了解 publicPath 是什么
  • 了解 single-spa 中关于微前端应用的概念(及 qiankun 中 html-entry  的概念)

解决如何覆盖 CRA 配置的问题

一般状况下,覆盖 CRA 配置的解决方案有两种:css

  • 直接 npm run eject 
  • 使用 react-app-rewired 或者 rescripts 等第三方工具

这里使用第二种方式,缘由也比较简单,由于魔改 webpack 是须要很大的勇气的,且 npm run eject 不可逆(虽然能够经过其余方式恢复,但太麻烦了),而且对于须要覆盖的配置,咱们也是有针对性的,因此使用第三方工具会更好一些。html

我这里使用 rescripts 这个库,它与 create-react-rewired 大同小异。前端

覆盖 webpack 的打包模式

首先,对于 single-spa 中要加载的微前端应用,咱们须要提供诸如 bootstrap 、 mount 以及 unmount 等若干生命周期钩子,但 CRAwebpack 的默认打包方式不会将这些方法暴露出来,因此声明配置以下:node

config.output.library = `${name}-[name]`;
config.output.libraryTarget = 'umd';
config.output.jsonpFunction = `webpackJsonp_${name}`;
config.output.globalObject = 'window';

让咱们来挨个分析下每行配置的做用及意义:react

  • library 和 libraryTarget 是共同生效的,默认状况下,libraryTarget 的值是 var ,即在 entry file 在执行后,会返回一个变量,咱们这里使用 umd 的缘由是由于,在主应用中,咱们仍然会使用 module system,不管是 webpack 仍是 Systemjs,所以 umd 是一个最佳选择,由于它适配全部的 module system
  • jsonpFunction 是用来按需加载 chunk 的工具函数,因为微前端应用中,一个页面中,会同时存在多个 webpack 运行时环境,因此可能会存在命名冲突,致使加载 chunk 时出现意想不到的后果,手动设置一个惟一的命名能够解决这个冲突
  • globalObject 自己属性的默认值便是 window ,但因为 libraryTarget 咱们设置成了 umd ,对于 nodejs 环境,全局对象时 global 而非 window ,这里显示地声明它是 window 证实微前端应用只是针对 browser 而言的

移除 CRA 中内置的 HMR 功能

HMR 功能通常是针对开发环境而言的,对于为何微前端应用在开发环境要关闭 HMR,我尚未深刻研究,但关闭它是官方代码库示例中提供的最佳实践。webpack

在 CRA 中,HMR 功能是分两部分存在的,一个是 webpack.config.devServer 提供的,另外一个是 CRA 本身实现的 webpackHotDevClient,咱们需依次移除或者关闭它们。git

首先关闭 webpack.devServer 的 HMR 功能,很简单,添加以下配置:github

config.hot = false;
config.watchContentBase = false;
config.liveReload = false;

再来移除 webpackHotDevClient,这个会稍微麻烦一些,由于它是直接声明在 webpack.config.entry 中的,因此使用下面的代码移除它:web

config.entry = config.entry.filter(
   (e) => !e.includes('webpackHotDevClient')
);

同时还有 HotModuleReplacementPlugin 插件,它提供 css 的 HMR 功能,利用相同的代码移除它:npm

config.plugins = config.plugins.filter(
   (p) => !(p instanceof webpack.HotModuleReplacementPlugin)
);

这样就彻底从 CRA 中移除了 HMR 的功能。

对 devServer 添加 CORS 配置以支持跨域访问

html-entry 为前提实现的微前端框架,构建前提既是微前端应用要支持跨域访问,对于部署阶段,咱们能够在 web server 或代理层完成该步骤,对于开发阶段,咱们则须要对 devServer 进行一些调整,由于它默认是不支持跨域访问的。

解决跨域问题除了配置反向代理以外,还可使用 CORS 来解决,在 devServer 中,显示使用后者更加快捷,添加以下代码便可:

config.headers = {
   'Access-Control-Allow-Origin': '*',
};

这样既实现了最简单的 CORS 配置,但知足开发环境中对于跨域访问的支持,足够了。

对于使用 history 做为路由模式的应用作适配

很简单,声明以下配置便可:

config.historyApiFallback = true;

推荐使用 history 做为微前端子应用的路由模式,由于在全局路由解析中,针对 hash 的匹配并不像 url 那样灵活,同时也存在一些微妙的 bug。

使 devServer 监听微前端子应用相对应的端口

也十分简单,使用以下代码:

config.port = 7101;

这里的 7101,是微前端子应用监听的接口,建议不论在开发阶段,仍是在部署阶段,都使用相同的接口以减小分辨接口的心智负担。

为微前端应用指定单独的 publicPath

须要单独指定 publicPath 的缘由是由于,当前咱们的微前端架构依赖于 html entry,每一个路径所对应的 entry 所加载的微前端应用,必然会有一些从 publicPath 加载资源的代码。

但在项目中,除非将全部子应用项目中的静态资源目录集合到一块儿,托管在主应用中,或者使用 CDN,否则在项目启动时,会遇到不少 404 的错误,其根本缘由是由于,以前请求的静态资源,并非托管在主应用的服务器上,而是子应用的,所以如何在主应用或者代理层中映射这些静态资源的加载请求,是必需要解决的事情。

默认状况下,CRA 的 publicPath/ ,即相对于当前服务器的域名,子应用在主应用中加载时,所相对的是主应用服务器的域名,因此这里须要对每一个子应用声明不一样的 publicPath

在 CRA 中声明 publicPath 有两种,

  • 经过在 package.json 中添加 homepage 字段
  • 经过 PUBLIC_PATH  环境变量

当前我使用的方式是第一种,由于第二种在当前的 CRA 版本中不生效(感受像是一个 bug),以下:

{
  "name": "vcapp-login",
  "homepage": "/login",
  ...
}

除了针对静态资源设置单独的 publicPath 以外,还须要在应用中,针对动态使用 publicPath 的地方作出修改,这个在 qiankun 中已经有响应的解决方案,以下:

if (window.__POWERED_BY_QIANKUN__) {
  // eslint-disable-next-line no-undef
  __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}

简单原理就是动态的注入了 __INJECTED_PUBLIC_PATH_BY_QIANKUN__ 这个全局变量,它是经过 html entry 导入 entry 时,动态解析出来的。

适配 CRA 支持多 entry 启动模式

众所周知,CRA 启动的 entry 文件是 src/index.jsx ,若是一个项目须要适配为微前端应用,势必须要在 index.jsx 中实现各类微前端模块的生命周期函数。

在微前端应用的架构中,很重要的一点既是解耦,若是咱们在开发子应用时,且没有用到任何和主应用或者其余子应用相关的模块或者状态时,仍然须要其余它们显示不是一种理想的开发模式。

咱们理想的模式应该是,咱们仍然能够按照传统 SPA 的启动方式,开发子应用,当须要与主应用集成,或者与其余子应用调试时,又能够以微前端模块的方式启动它。这实际上是在说,咱们当前的子应用要支持多 entry 启动模式。

在 CRA 中,虽然能够经过覆盖 webpack 的方式来解决这个问题,可是我认为有更简便的方法。考虑到不管是传统启动方式,仍是微前端模块的启动方式,这两种启动方式在同一时间,咱们只会使用一种,那咱们移花接木式的变动 index.jsx 的内容,在 CRA 加载 entry 以前欺骗它岂不是更好?这里咱们能够利用如下两点来实现相似的效果:

  • 使用 npm scripts 中的 hook 前缀来截止启动指令
  • 在 hook 中执行一些脚本,动态修改 index.jsx  的内容

因为涉及到的代码较多,这里就简单贴一个 npm scripts 的截图好了,以下:

image.png

能够发现,对于 start 、 build ,均支持两种模式的指令,从而适配不一样开发模式下的构建需求。

最后说一点,对于 index.jsx 内容的更改,最简单的方式即时经过软连接的方式来实现,提早提供两份被连接的目标文件,好比:

image.png

micro.tsx 和 standalone.tsx 均对应不一样的 entry 入口,使用 CRA 启动应用前,动态地建立软连接将它们和 index.tsx 文件连接起来便可(因为项目中使用了 ts,后缀为 .tsx ,js 项目同理)。

引入主应用

以后咱们就能够愉快地在主应用中引入咱们的子应用了,主要配置有两个,一是注册子应用,以下:

registerMicroApps(
  [
   // 其余子应用
   ...,
    {
      name: "vcapp-login",
      entry: "//localhost:7101/login",
      container: "#subapp-container",
      activeRule: "/trade-login/",
    },
  ],
)

二是增长对于子应用的 publicPath 的配置,这儿会分为两部分,一个是部署环境下的,一个是开发环境下的,这里分享开发环境下的。我主应用项目使用的打包器是 parcel ,所以能够直接对它内部的 web server 增长中间件来完成这部分工做,以下:

app.use(
  createProxyMiddleware("/login", {
    target: "http://localhost:7101",
  })
);

注意这里的 7101,与上文中的 7101 对应,若是它们不一致,会形成子应用加载失败。

其余的坑

  • CRA 对于 svg 格式的图片,没有写在 url-loader  的匹配规则中,若是子应用使用了 svg 图片,须要覆盖 url-loader 配置已适配 publicPath 变动形成的影响

最后

因为仓库代码在公司内网,不太方面直接拷贝出来,往后有时间会单另在 github 建立一个示例项目。

同时因为该微前端应用的架构基于 single-spaqiankun,对于 CRA 项目向微前端项目的迁移所作的一些工做并不具备通用性。

对于微前端这种架构,我更多地将它做为一种可以渐进式地重构项目的手段在使用,对于大型复杂项目,并无太多的经验,一是由于没机会作相似的复杂度极高的中台项目,二是由于不少巨石应用,大可能是旧项目,因此将它用做重构项目的一种手段也许更能发挥它的用处。

若有错误,还望指出。

相关文章
相关标签/搜索