package.json中的scripts脚本配置实战技巧与常见陷阱
npm run build打包后index.html被覆盖的坑
今天又踩了个scripts脚本的坑,说来还挺无语的。项目打包部署的时候发现build之后index.html文件被替换成了新的,但我想保留一些自定义的meta标签和第三方脚本引用,结果每次打包都被覆盖。
折腾了半天发现这是个挺常见的问题,特别是当你想在HTML里加一些动态配置或者第三方统计代码的时候。
各种尝试都失败了
最开始我以为直接修改build目录下的index.html就行,结果下次打包又恢复了。然后试了各种办法,包括在package.json里改scripts命令,用postbuild钩子,甚至想通过shell脚本手动复制文件,都搞不定。
这里我踩了好几个坑:
- 先是在build命令后面直接拼接cp命令,结果Windows和Mac兼容性有问题
- 用cross-env也没解决跨平台问题,因为路径分隔符不一样
- 还试过webpack插件手动注入,但配置太复杂,而且容易和其他插件冲突
折腾了两个小时,最后发现其实有几种更简单的方案。
最终解决:模板文件+构建脚本
我的方案是这样的,在public目录下创建一个index.prod.html作为生产环境模板:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<!-- 这些是我想保留的自定义配置 -->
<meta name="keywords" content="自定义关键词">
<script src="https://third-party-cdn.com/tracker.js"></script>
<!-- 其他需要保留的标签 -->
<title>我的应用</title>
</head>
<body>
<div id="root"></div>
<!-- build:js 脚本会自动插入到这里 -->
</body>
</html>
然后在package.json的scripts里这么配:
{
"scripts": {
"build": "react-scripts build && cp public/index.prod.html build/index.html",
"build:win": "react-scripts build && copy /Y public\index.prod.html build\index.html"
}
}
这样就可以根据系统选择不同的构建命令。不过为了更好的跨平台支持,后来我用了更通用的方法,安装了shelljs:
npm install --save-dev shelljs
新建一个build-script.js文件:
const fs = require('fs');
const path = require('path');
// 执行build命令
require('child_process').execSync('react-scripts build', { stdio: 'inherit' });
// 复制生产环境模板
const sourcePath = path.join(__dirname, 'public', 'index.prod.html');
const targetPath = path.join(__dirname, 'build', 'index.html');
if (fs.existsSync(sourcePath)) {
const templateContent = fs.readFileSync(sourcePath, 'utf8');
const buildIndex = path.join(__dirname, 'build', 'index.html');
if (fs.existsSync(buildIndex)) {
// 保留原有的js/css引用
const originalBuild = fs.readFileSync(buildIndex, 'utf8');
// 将新模板的内容保留,并插入原有资源引用
const modifiedContent = templateContent.replace(
'<!-- build:js 脚本会自动插入到这里 -->',
getAssetsFromOriginal(originalBuild)
);
fs.writeFileSync(buildIndex, modifiedContent);
}
}
function getAssetsFromOriginal(content) {
// 提取原有的CSS和JS引用
const cssMatches = content.match(/<link rel="stylesheet"[^>]*>/g) || [];
const jsMatches = content.match(/<script[^>]*src="[^"]*"[^>]*></script>/g) || [];
return [
...cssMatches,
...jsMatches
].join('n ');
}
然后把scripts改成:
{
"scripts": {
"build": "node build-script.js"
}
}
还有个更优雅的方案
后来试了下react-app-rewired这个库,可以自定义webpack配置而不破坏原有配置:
npm install --save-dev react-app-rewired
根目录创建config-overrides.js:
const HtmlWebpackPlugin = require('html-webpack-plugin');
const path = require('path');
module.exports = function override(config, env) {
const isEnvProduction = env === 'production';
if (isEnvProduction) {
// 自定义HTML模板
config.plugins = config.plugins.map(plugin => {
if (plugin instanceof HtmlWebpackPlugin) {
return new HtmlWebpackPlugin({
inject: true,
template: path.resolve(__dirname, 'public', 'index.prod.html'),
minify: {
removeComments: true,
collapseWhitespace: true,
removeRedundantAttributes: true,
useShortDoctype: true,
removeEmptyAttributes: true,
removeStyleLinkTypeAttributes: true,
keepClosingSlash: true,
minifyJS: true,
minifyCSS: true,
minifyURLs: true,
},
});
}
return plugin;
});
}
return config;
};
这样package.json的build命令就不用改了,依然保持原样。不过要注意,如果之前build过,可能缓存会导致配置没生效,清理一下缓存重新build就行。
线上部署的注意事项
部署到nginx服务器的时候,发现有些静态资源配置还需要注意。特别是当你的应用不在根路径下时,比如部署在 /app/ 目录下,还需要在index.prod.html里设置base标签:
<head>
<base href="/app/" />
<!-- 其他内容 -->
</head>
同时webpack配置里也要相应调整:
// 在config-overrides.js中
if (isEnvProduction) {
config.output.publicPath = '/app/';
// 其他配置...
}
还有个细节,如果用了Service Worker,记得更新sw-precache里的路径配置,不然离线缓存会出问题。
遗留的小问题
说实话这个方案还有个小问题,就是每次修改index.prod.html后,需要手动测试是否所有资源都能正常加载。特别是引入的新第三方库,有时候会出现CSP(内容安全策略)问题,需要在meta标签里调整策略。
另外,这种定制化程度比较高的情况下,团队协作时容易出现差异,最好把这套流程写成文档给组员们看。
以上是我踩坑后的总结,如果你有更好的方案欢迎评论区交流。

暂无评论