publicPath
是什么通常情况下,覆盖 CRA 配置的解决方案有两种:
npm run eject
react-app-rewired
或者 rescripts
等第三方工具这里使用第二种方式,原因也比较简单,因为魔改 webpack
是需要很大的勇气的,且 npm run eject
不可逆(虽然可以通过其他方式恢复,但太麻烦了),并且对于需要覆盖的配置,我们也是有针对性的,所以使用第三方工具会更好一些。
我这里使用 rescripts
这个库,它与 create-react-rewired
大同小异。
首先,对于 single-spa
中要加载的微前端应用,我们需要提供诸如 bootstrap
、 mount
以及 unmount
等若干生命周期钩子,但 CRA
中 webpack
的默认打包方式不会将这些方法暴露出来,所以声明配置如下:
config.output.library = `${name}-[name]`; config.output.libraryTarget = 'umd'; config.output.jsonpFunction = `webpackJsonp_${name}`; config.output.globalObject = 'window';
让我们来挨个分析下每行配置的作用及意义:
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 而言的HMR 功能一般是针对开发环境而言的,对于为什么微前端应用在开发环境要关闭 HMR,我还没有深入研究,但关闭它是官方代码库示例中提供的最佳实践。
在 CRA 中,HMR 功能是分两部分存在的,一个是 webpack.config.devServer
提供的,另一个是 CRA 自己实现的 webpackHotDevClient
,我们需依次移除或者关闭它们。
首先关闭 webpack.devServer
的 HMR 功能,很简单,添加如下配置:
config.hot = false; config.watchContentBase = false; config.liveReload = false;
再来移除 webpackHotDevClient
,这个会稍微麻烦一些,因为它是直接声明在 webpack.config.entry
中的,所以使用下面的代码移除它:
config.entry = config.entry.filter( (e) => !e.includes('webpackHotDevClient') );
同时还有 HotModuleReplacementPlugin
插件,它提供 css 的 HMR 功能,利用相同的代码移除它:
config.plugins = config.plugins.filter( (p) => !(p instanceof webpack.HotModuleReplacementPlugin) );
这样就完全从 CRA 中移除了 HMR 的功能。
以 html-entry
为前提实现的微前端框架,构建前提既是微前端应用要支持跨域访问,对于部署阶段,我们可以在 web server
或代理层完成该步骤,对于开发阶段,我们则需要对 devServer
进行一些调整,因为它默认是不支持跨域访问的。
解决跨域问题除了配置反向代理之外,还可以使用 CORS
来解决,在 devServer
中,显示使用后者更加快捷,添加如下代码即可:
config.headers = { 'Access-Control-Allow-Origin': '*', };
这样既实现了最简单的 CORS
配置,但满足开发环境中对于跨域访问的支持,足够了。
很简单,声明如下配置即可:
config.historyApiFallback = true;
推荐使用 history
作为微前端子应用的路由模式,因为在全局路由解析中,针对 hash
的匹配并不像 url
那样灵活,同时也存在一些微妙的 bug。
也十分简单,使用如下代码:
config.port = 7101;
这里的 7101
,是微前端子应用监听的接口,建议不论在开发阶段,还是在部署阶段,都使用相同的接口以减少分辨接口的心智负担。
需要单独指定 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
文件是 src/index.jsx
,如果一个项目需要适配为微前端应用,势必需要在 index.jsx
中实现各种微前端模块的生命周期函数。
在微前端应用的架构中,很重要的一点既是解耦,如果我们在开发子应用时,且没有用到任何和主应用或者其他子应用相关的模块或者状态时,仍然需要其他它们显示不是一种理想的开发模式。
我们理想的模式应该是,我们仍然可以按照传统 SPA 的启动方式,开发子应用,当需要与主应用集成,或者与其他子应用调试时,又可以以微前端模块的方式启动它。这其实是在说,我们当前的子应用要支持多 entry
启动模式。
在 CRA 中,虽然可以通过覆盖 webpack
的方式来解决这个问题,但是我认为有更简便的方法。考虑到无论是传统启动方式,还是微前端模块的启动方式,这两种启动方式在同一时间,我们只会使用一种,那我们移花接木式的变更 index.jsx
的内容,在 CRA 加载 entry
之前欺骗它岂不是更好?这里我们可以利用以下两点来实现类似的效果:
npm scripts
中的 hook 前缀来截止启动指令index.jsx
的内容由于涉及到的代码较多,这里就简单贴一个 npm scripts
的截图好了,如下:
可以发现,对于 start
、 build
,均支持两种模式的指令,从而适配不同开发模式下的构建需求。
最后说一点,对于 index.jsx
内容的更改,最简单的方式即时通过软链接的方式来实现,提前提供两份被链接的目标文件,比如:
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
对应,如果它们不一致,会造成子应用加载失败。
svg
格式的图片,没有写在 url-loader
的匹配规则中,如果子应用使用了 svg
图片,需要覆盖 url-loader
配置已适配 publicPath
变更造成的影响由于仓库代码在公司内网,不太方面直接拷贝出来,日后有时间会单另在 github
创建一个示例项目。
同时由于该微前端应用的架构基于 single-spa
和 qiankun
,对于 CRA 项目向微前端项目的迁移所做的一些工作并不具有通用性。
对于微前端这种架构,我更多地将它作为一种能够渐进式地重构项目的手段在使用,对于大型复杂项目,并没有太多的经验,一是因为没机会做类似的复杂度极高的中台项目,二是因为很多巨石应用,大多是旧项目,所以将它用作重构项目的一种手段也许更能发挥它的用处。
如有错误,还望指出。