React微前端实战教程(长期缓存篇)
在初始构建React微前端
时,我是想每个微前端APP版本如何兼容发布到K8s平台上面,像后端一样部署上去就可以路由到新版本,所以,在URL Path上添加Git Revision信息。但是,这样做造成微前端APP新版本发布之后,浏览器无法重用以前版本的任何资源,没有全部遵守“缓存为王”、“客户体验至上”的设计原则。
为了重用以前版本缓存的部分资源(JS/CSS/图片等),构建微前端APP时需固定发布的URL路径,只有文件内容Hash发生变化时才使用新的文件名。我们使用Webpack构建React微前端
工程,相关配置项也是在Webpack配置脚本中修改。
文本演示如何尽可能地重用以前APP版本的资源文件。
1. Webpack配置脚本修改
Webpack配置相关内容主要在react-micro-frontend-scripts代码库中。
1.1. output.publicPath 改成固定值
output.publicPath 从原来的/rmf-{appName}/{gitRevision/
改为/rmf-{appName}/
。为了兼容以前构建脚本的输出路径,我们添加process.env.PUBLIC_DISABLE_REVISION
控制是否禁用gitRevision。同时也添加process.env.PUBLIC_UR
变量直接控制发布URL。
在scripts/internal/getPublicUrlOrPath.js
中,getPublicUrlOrPath函数改成如下:
// getPublicUrlOrPath.js
function getPublicUrlOrPath(isEnvDevelopment) {
if (process.env.PUBLIC_URL || process.env.PUBLIC_UR === '') {
return getPublicUrl(isEnvDevelopment);
}
let publicRootURL = process.env.PUBLIC_ROOT_URL || '';
const disableRevision = process.env.PUBLIC_DISABLE_REVISION === 'true';
// ensure last slash exists
if (publicRootURL) {
publicRootURL = publicRootURL.endsWith('/') ? publicRootURL : `${publicRootURL}/`;
}
if (isEnvDevelopment) {
return publicRootURL.startsWith('.') ? '/' : (publicRootURL || '/');
}
const folderName = pkgJson.getMicroFrontendFolderName();
// must add the end '/'
return disableRevision ? `${publicRootURL}${folderName}/` : `${publicRootURL}${folderName}/${getRevisionPath()}/`;
}
1.2. optimization.moduleIds 改为 ‘hashed’
默认情况下,Webpack构建js文件模块时使用数字编号,如果依赖的内容发生改变,会造成多个chunk连锁变更。optimization.moduleIds
设置成hash
就可以避免该问题。详情见: https://webpack.js.org/guides/caching/
我们的变更:
// webpack.config.js
{
optimization: {
// Long-term caching
moduleIds: isEnvProduction ? 'hashed' : false,
}
}
1.3. filename、chunkFilename要使用[contenthash]
主要关注两个地方:1、Webpack本身output配置;2、MiniCssExtractPlugin参数选项配置。
Webpack本身output:
// webpack.config.js
{
output: {
publicPath: publicUrlOrPath,
filename: (isEnvProduction && '[name].[contenthash:8].js') || (isEnvDevelopment && '[name].js'),
chunkFilename: (isEnvProduction && '[name].[contenthash:8].chunk.js') || (isEnvDevelopment && '[name].chunk.js'),
}
}
MiniCssExtractPlugin参数选项:
// webpack.config.js
isEnvProduction && new MiniCssExtractPlugin({
// Options similar to the same options in webpackOptions.output
// both options are optional
filename: '[name].[contenthash:8].css',
chunkFilename: '[name].[contenthash:8].chunk.css',
}),
1.4. 拆分CommonChunk与RuntimeChunk
// webpack.config.js
{
optimization: {
splitChunks: shouldSplitChunks && {
chunks: 'all',
name: false,
},
// Keep the runtime chunk separated to enable long term caching
runtimeChunk: shouldRuntimeChunk && {
name: (entrypoint) => `runtime-${entrypoint.name}`,
},
}
}
对框架的公共库,我们还做了更详细的配置:
// helper.js
function getSplitChunksOptions() {
return {
cacheGroups: {
'vendor-polyfill': {
test: /[\\/]node_modules[\\/](core-js|object-assign|promise|raf|regenerator-runtime|whatwg-fetch)[\\/]/,
name: 'vendor-polyfill',
chunks: 'all',
},
'vendor-react': {
test: /[\\/]node_modules[\\/](react|react-dom|react-router-dom)[\\/]/,
name: 'vendor-react',
chunks: 'all',
},
'vendor-redux': {
test: /[\\/]node_modules[\\/](redux|react-redux|redux-thunk|redux-saga|redux-observable|rxjs)[\\/]/,
name: 'vendor-redux',
chunks: 'all',
},
},
};
}
function webpackConfigCallback(config) {
const newConfig = {
...config,
optimization: {
...config.optimization,
splitChunks: (process.env.SPLIT_CHUNKS !== 'false') && getSplitChunksOptions(),
},
};
return newConfig;
}
2. Server-go修改
react-micro-frontend-server-go做了两处改变:1、在载入微前端元信息文件rmf-manifest.json
判断规则放宽;2、载如runtime-framework.xxx.js的目录层级放宽到1~3级。
2.1. rmf-manifest.json文件名放宽
我们使用正则表达式^rmf-manifest([.\-_].+)?\.json$
匹配到类似的文件就认为是元信息文件。原因,微前端APP多个版本部署到同一目录时,为了能够在启动时自动加载元信息,只能重命名为不同的文件名称。
// main.go
var manifestFileNameRegexp = regexp.MustCompile(`^rmf-manifest([.\-_].+)?\.json$`)
func matchManifestFileName(fileName string) bool {
return manifestFileNameRegexp.MatchString(fileName)
}
func walkAppFiles(rootDir string) WalkAppsResult {
...
// Find 'rmf-manifest.json' or 'rmf-manifest.xxx.json'
if !isDir && matchManifestFileName(name) {
// fmt.Printf("Find manifest: %+v\n", path)
result.ManifestFiles = append(result.ManifestFiles, path)
}
...
}
2.2. Runtime读取目录放宽1到3级深度
以前我们使用gitRevision添加到URL路径上面,JS资源文件是在第3级目录下面。现在放宽1、2级,即使所有的微前端APP的所有版本都部署到同一个路径上,也没有问题。
// manifest_cache.go
func readRuntimeContent(baseDir string, entry string) (string, error) {
entryParts := strings.Split(entry, "/")
partsLen := len(entryParts)
var content []byte
var err error
readRuntime := func(validPathParts int) bool {
start := 0
if partsLen > validPathParts {
start = partsLen - validPathParts
}
parts := append([]string{baseDir}, entryParts[start:partsLen]...)
filename := path.Join(parts...)
if exist, _ := pathExists(filename); exist {
content, err = ioutil.ReadFile(filename)
if err == nil {
return true
}
}
return false
}
ok := readRuntime(3) || readRuntime(2) || readRuntime(1)
if !ok {
log.Printf("[ERROR] Cannot read runtime content for %s\n", entry)
return "", fmt.Errorf("Cannot read runtime content")
}
return string(content[:]), nil
}
3. Nginx 配置
把JS/CSS等资源的过期时间设为max:
# Expires map
map $sent_http_content_type $micro_expires {
default epoch;
text/html epoch;
text/xml epoch;
text/css max;
application/javascript max;
application/font-woff max;
application/octet-stream 60d;
~font/ max;
~image/ max;
}
server {
# Cache control
expires $micro_expires;
}
4. 样例
我们支持以前带gitRevision路径,也支持当前的同一文件夹路径。以下tree .
中没有rmf-manifest.json
之类的版本表示已经下线,用户重新浏览时不会请求到。
样例下载,见: https://github.com/kinsprite/react-micro-frontend-server-go/releases/tag/v2.4.0
$ tree .
.
├── rmf-app-example-echarts
│ ├── app-example-echarts.3c31ae87.css
│ ├── app-example-echarts.3c31ae87.css.map
│ ├── app-example-echarts.befbc72a.js
│ ├── app-example-echarts.befbc72a.js.map
│ ├── app-example-echarts.dce2279f.css
│ ├── app-example-echarts.dce2279f.css.map
│ ├── app-example-echarts.df6483af.js
│ ├── app-example-echarts.df6483af.js.map
│ ├── rmf-manifest-v1.2.0.json
│ ├── rmf-manifest-v1.2.1.json
│ └── v1.1.0
│ ├── app-example-echarts.3c31ae87.css
│ ├── app-example-echarts.3c31ae87.css.map
│ ├── app-example-echarts.5ae95fb4.js
│ └── app-example-echarts.5ae95fb4.js.map
├── rmf-app-home
│ ├── 1ab03245
│ │ ├── app-home.740c5d55.js
│ │ ├── app-home.740c5d55.js.map
│ │ ├── app-home.83523b94.css
│ │ ├── app-home.83523b94.css.map
│ │ ├── assets
│ │ │ └── logo.103b5fa1.svg
│ │ └── rmf-manifest.json
│ ├── app-home.3f3c050e.css
│ ├── app-home.3f3c050e.css.map
│ ├── app-home.980dc961.js
│ ├── app-home.980dc961.js.map
│ ├── assets
│ │ └── logo.103b5fa1.svg
│ ├── rmf-manifest.json
│ └── v1.1.0
│ ├── app-home.77799b3a.css
│ ├── app-home.77799b3a.css.map
│ ├── app-home.8a61eaee.js
│ ├── app-home.8a61eaee.js.map
│ └── assets
│ └── logo.103b5fa1.svg
├── rmf-framework
│ ├── framework.383502c7.chunk.css
│ ├── framework.ada5bd48.chunk.js
│ ├── framework.ada5bd48.chunk.js.LICENSE.txt
│ ├── rmf-manifest.json
│ ├── runtime-framework.d8678361.js
│ ├── vendor-react.c027d328.chunk.js
│ ├── vendor-react.c027d328.chunk.js.LICENSE.txt
│ └── vendor-redux.57dd1261.chunk.js
├── rmf-polyfill
│ ├── polyfill.8920a887.js
│ ├── polyfill-ie11.fe6dce5e.js
│ ├── polyfill-ie11.fe6dce5e.js.LICENSE.txt
│ ├── polyfill-ie9.10d924a3.js
│ ├── polyfill-ie9.10d924a3.js.LICENSE.txt
│ └── rmf-manifest.json
浏览器重新打开页面,F12从开发者工具的网络中可以看到JS/CSS的响应头如下:
cache-control: max-age=315360000
expires: Thu, 31 Dec 2037 23:55:55 GMT