多单页应用的构建优化-按entry拆分构建
一、目的
多单页应用的仓库体积相对较大,而往往开发分支涉及的改动仅会影响其中的少数页面,因此无论是开发时还是生产时,都有必要去做选择性的前端代码构建。
二、原理
以webpack为例,在构建过程中,会根据entry配置,在指定目录下寻找main文件作为入口文件进行依赖树构建以及打包。当应用中存在多个单页也就是多个main文件时,排除其中不想打包的main文件,或者指定想打包的main文件,即可实现开发时按enrty拆分构建。
三、实现方案
使用IgnorePlugin,排除非指定main入口
module.exports = {
plugins: [
new webpack.IgnorePlugin({
checkResource(resource, context) {
// 获取绝对路径
const absolutePath = path.resolve(context, resource);
// 排除所有/pages下非目标页面的main文件
return (
absolutePath.includes('/pages/') &&
absolutePath.includes('main') &&
!absolutePath.includes(targetPage)
);
},
}),
],
};
直接修改entry,构建指定main入口
module.exports = {
entry: [
{
files: entryFiles,
},
],
}
四、应用场景
开发时构建
开发时,确定开发影响的单页,以命令行参数的形式传给webpack.config.js即可
实例代码:
# 命令行去指定 entry
yarn dev --entry=./src/anotherEntry.js
const path = require('path');
// 消费 entry
const argv = process.argv.slice(2);
const entryIndex = argv.findIndex(arg => arg.startsWith('--entry='));
const entry = entryIndex !== -1 ? argv[entryIndex].split('=')[1] : './src/index.js';
module.exports = {
mode: 'development',
entry: path.resolve(__dirname, entry)
};
运行时构建
运行时构建,可以通过获取变更文件,匹配引用到变更文件的main入口来实现。引用分析,可以通过babel的ast能力去识别import语句去做递归处理,构建引用树,作为变更文件溯源main的依据。
主要步骤如下:
- 获取当前分支改动所影响的main入口
- 获取当前分支改动文件(git rev-parse & git log)
- 构建main文件依赖树(babel的ast去做import语句匹配)
- 根据改动文件进行溯源,获取有影响的main
- 对有影响的main进行打包(用entry收窄或者ignorePlugin过滤均可实现)
- 将打包资源与线上资源进行增量替换(上传cdn,并且修改资源映射表)
实例代码:
# 获取当前分支
git rev-parse --abbrev-ref HEAD
# 获取相对master有变更的文件
git log --name-only --pretty=format: master..currentBranch
# 获取文件存档
git archive --remote=git@gitlab.qima-inc.com:fe/scrm-b-pc-dist.git master -- packages/pc-node/dist/config/${fileName} | tar -xO
/*
* 分析文件的引用关系,从main入口开始递归分析
*/
const handleFileReferenceAnalyse = (targetFile, referenceFile, mainFilePath) => {
const relativeFilePath = getRelativeFilePath(targetFile);
let fileEntity = allFileEntityMap.get(relativeFilePath);
if (allFileEntityMap.get(relativeFilePath)) {
fileEntity.setExportPath(referenceFile);
fileEntity.addToRelativeMainFile(mainFilePath);
return;
}
fileEntity = new FileEntity(relativeFilePath, allFileEntityMap);
if (referenceFile) {
fileEntity.setExportPath(referenceFile);
}
fileEntity.addToRelativeMainFile(mainFilePath);
if (fileEntity.isEscapeAst) {
return;
}
const parseAst = getAstBody(targetFile);
const importPaths = parseNodeImportPath(parseAst, targetFile, relativeFilePath);
fileEntity.setImportPath(importPaths);
allFileEntityMap.set(relativeFilePath, fileEntity);
importPaths.forEach((importPath) => {
handleFileReferenceAnalyse(importPath, relativeFilePath, mainFilePath);
});
};