三千年读史,无非功名利禄。九万里悟道,终归诗酒田园。
github: miniapp-shaking
在前面的章节中,我们已经摇树掉了所有未使用的文件,现在我将要做一个更为高级的事情。
在项目中我们可能会遇到某个子包需要一个或者N个npm包,但这些npm包并不会被主包和其他的子包使用,由于微信小程序的机制,这些npm只能算做是主包的代码,从而增大了主包的体积。我们都知道微信小程序的体积超包最严重的就是主包,特别是对于大型项目而言。现在我要做的就是通过工具把这些npm包(我称之为独立npm包
)自动化的移动到子包中,从而缩小主包的体积。
为了做到这一点,我需要解决以下问题:
- 如何找到每个子包的独立npm包
- 独立npm包移动到子包后需要修改子包中所有对其引用路径
- 其他的npm包也可能依赖这些独立npm包,需要同步修改其他npm包的引用路径
我们先来解决第一个问题
1. 找到每个子包的独立npm包
在前面的章节中,我们通过遍历已经得到了主包和子包所依赖的npm
包,这些npm
可能会出现重叠,你可能还对以下代码有印象。
class BaseDepend {
constructor(config, rootDir = '') {
// 当前分包依赖的npm包名称
this.npms = new Set();
}
现在我们在子包类中添加一个属性isolatedNpms
来存储子包的独立npm包
class SubDepend extends BaseDepend {
constructor(config, rootDir, mainDepend) {
super(config, rootDir);
// 改子包所以依赖的独立npm包
this.isolatedNpms = new Set();
}
接着在容器类中添加一个收集独立npm包的方法,通过比对每个包的npm包来找出每个子包的独立npm包:
class DependContainer {
splitIsolatedNpmForSubPackage() {
const mainNpm = this.mainDepend.npms;
const subDepends = this.subDepends;
const interDependNpms = new Set();
subDepends.forEach(item => {
let otherNpm = subDepends.reduce((sum, it) => {
if (it !== item) {
this.appendSet(sum, it.npms);
}
return sum;
}, new Set());
Array.from(item.npms).forEach(npm => {
if (otherNpm.has(npm) || this.config.excludeNpms.includes(npm)) {
interDependNpms.add(npm);
} else if (!mainNpm.has(npm)) {
item.isolatedNpms.add(npm);
}
});
});
console.log('mainNpm', Array.from(this.appendSet(mainNpm, interDependNpms)));
subDepends.forEach(item => {
console.log(`${item.rootDir}_npm`, Array.from(item.isolatedNpms));
});
}
appendSet(set1, set2) {
for (let item of set2.values()) {
set1.add(item);
}
return set1;
}
}
你可以在config
中配置excludeNpms
来排除一些不需要移动的npm包,这可以解决一些特殊的情况。如果一个npm包不在其他的子包中也不在主包中,则可以判断它是一个独立npm包。
2. 修改子包路径
移动了npm包之后,我们需要修改子包中所有文件对其引用路径,我们新建一个ReplaceSubPackagesPath
类来解决这个问题,这里直接填代码就不多解释了。
const path = require('path');
const fse = require('fs-extra');
const { parse } = require('@babel/parser');
const { default: traverse } = require('@babel/traverse');
const {default: generate} = require('@babel/generator');
const htmlparser2 = require('htmlparser2');
class ReplaceSubPackagesPath {
constructor(pathMap, config, subPackageName) {
this.pathMap = pathMap;
this.config = config;
this.subPackageName = subPackageName;
this.replaceAll(pathMap);
}
replaceAll() {
const pathMap = this.pathMap;
if (pathMap.size === 0) return;
for (let [key, value] of pathMap.entries()) {
const ext = path.extname(key);
switch (ext) {
case '.js':
this.replaceJs(key, value);
break;
case '.json':
this.replaceJson(key, value);
break;
case '.wxml':
this.replaceWXML(key, value);
break;
case '.wxss':
this.replaceWXSS(key, value);
break;
case '.wxs':
this.replaceWxs(key, value);
break;
default:
throw new Error(`don't know type: ${ext} of ${key}`);
}
}
}
replaceJs(file, npms) {
if (!npms.length) return;
// 读取js内容
let content = fse.readFileSync(file, 'utf-8');
const contentMap = {};
// 将代码转化为AST
const ast = parse(content, {
sourceType: 'module',
plugins: ['exportDefaultFrom'],
});
// 遍历AST
traverse(ast, {
ImportDeclaration: ({ node }) => {
// 获取import from 地址
const { value } = node.source;
node.source.value = this.transformScript(value, npms, file);
},
ExportNamedDeclaration: ({ node }) => {
// 获取export form地址
if (!node.source) return;
const { value } = node.source;
node.source.value = this.transformScript(value, npms, file);
},
CallExpression: ({ node }) => {
if (
(node.callee.name && node.callee.name === 'require')
&& node.arguments.length >= -1
) {
const [{ value }] = node.arguments;
if (!value) return;
node.arguments[0].value = this.transformScript(value, npms, file);
}
},
ExportAllDeclaration: ({ node }) => {
if (!node.source) return;
const { value } = node.source;
node.source.value = this.transformScript(value, npms, file);
},
});
fse.outputFile(file, generate(ast).code);
}
transformScript(src, npms, file) {
for (let i = 0; i < npms.length; i++) {
const index = src.indexOf(npms[i]);
if (index !== -1) {
if (src.startsWith('/miniprogram_npm/')) {
return this.getRelativePath(file, src.replace('/miniprogram_npm/', ''));
} else if (index === 0) {
return this.getRelativePath(file, src);
} else if (!fse.existsSync(this.getResolvePath(file, src))) {
return src.replace('miniprogram_npm', `${this.subPackageName}/${this.subPackageName}_npm`);
}
break;
}
}
return src;
}
replaceWxs(file, npms) {
if (!npms.length) return;
// 读取js内容
let content = fse.readFileSync(file, 'utf-8');
// 将代码转化为AST
const ast = parse(content, {
sourceType: 'module',
plugins: ['exportDefaultFrom'],
});
// 遍历AST
traverse(ast, {
CallExpression: ({ node }) => {
if (
(node.callee.name && node.callee.name === 'require')
&& node.arguments.length >= -1
) {
const [{ value }] = node.arguments;
if (!value) return;
node.arguments[0].value = this.transformScript(value, npms, file);
}
},
});
fse.outputFile(file, generate(ast).code);
}
replaceJson(file, npms) {
if (!npms.length) return;
const content = fse.readJsonSync(file);
const usingComponents = content.usingComponents;
if (usingComponents && Object.keys(usingComponents).length) {
Object.keys(usingComponents).forEach(key => {
let value = usingComponents[key];
for (let i = 0; i < npms.length; i++) {
const index = value.indexOf(npms[i]);
if (index !== -1) {
if (value.startsWith('/miniprogram_npm/')) {
usingComponents[key] = this.getRelativePath(file, value.replace('/miniprogram_npm/', ''));
} else if (index === 0) {
usingComponents[key] = this.getRelativePath(file, value);
} else if (!fse.existsSync(this.getResolvePath(file, value))) {
usingComponents[key] = value.replace('miniprogram_npm', `${this.subPackageName}/${this.subPackageName}_npm`);
}
break;
}
}
});
}
fse.writeJsonSync(file, content);
}
getResolvePath(file, src) {
return path.resolve(path.dirname(file), src);
}
getRelativePath(file, component) {
const relativePath = path.relative(path.join(this.config.targetDir, `${this.subPackageName}`), file);
const pathArr = relativePath.split(path.sep);
let filePath = `${this.getDotPath(pathArr)}${this.subPackageName}_npm/${component}`;
try {
const stats = fse.statSync(path.resolve(file, path.join('../', filePath)));
if (stats.isDirectory()) {
filePath += '/index';
}
} catch (e) {}
return filePath;
}
getDotPath(len) {
let result = '';
if (len === 1) {
result = './';
} else {
for (let i = 0; i < len - 1; i++) {
result += '../';
}
}
return result;
}
replaceWXML(file, npms) {
if (!npms.length) return;
let content = fse.readFileSync(file, 'utf-8');
const contentMap = {};
const htmlParser = new htmlparser2.Parser({
onopentag(name, attribs = {}) {
if (name !== 'import' && name !== 'include' && name !== 'wxs') {
return;
}
const { src } = attribs;
if (!src) return;
for (let i = 0; i < npms.length; i++) {
const index = src.indexOf(npms[i]);
if (index !== -1) {
if (src.startsWith('/miniprogram_npm/')) {
contentMap[src] = this.getRelativePath(file, src.replace('/miniprogram_npm/', ''));
} else if (index === 0) {
contentMap[src] = this.getRelativePath(file, src);
} else if (!fse.existsSync(this.getResolvePath(file, src))) {
contentMap[src] = src.replace('miniprogram_npm', `${this.subPackageName}/${this.subPackageName}_npm`);
}
break;
}
}
},
});
Object.keys(contentMap).forEach(key => {
const reg = new RegExp(key, 'g');
content = content.replace(reg, contentMap[key]);
});
htmlParser.write(content);
htmlParser.end();
}
replaceWXSS(file, npms) {
if (!npms.length) return;
let content = fse.readFileSync(file, 'utf-8');
const importRegExp = /@import\s+['"](.*)['"];?/g;
const npmMap = {};
let matched;
while ((matched = importRegExp.exec(content)) !== null) {
const str = matched[1];
if (str) {
for (let i = 0; i < npms.length; i++) {
const index = str.indexOf(npms[i]);
if (index !== -1) {
if (str.startsWith('/miniprogram_npm/')) {
npmMap[str] = this.getRelativePath(file, str.replace('/miniprogram_npm/', ''));
} else if (index === 0) {
npmMap[str] = this.getRelativePath(file, str);
} else if (!fse.existsSync(this.getResolvePath(file, str))) {
npmMap[str] = str.replace('miniprogram_npm', `${this.subPackageName}/${this.subPackageName}_npm`);
}
break;
}
}
}
}
Object.keys(npmMap).forEach(key => {
const reg = new RegExp(key, 'g');
content = content.replace(reg, npmMap[key]);
});
fse.outputFile(file, content);
}
}
module.exports = {
ReplaceSubPackagesPath,
};
3. 修改其他独立npm的的路径。
npm包之间可能会存在相互依赖现象,我们需要同步修改其路径。注意这只存在于独立npm之间,如果一个主包代码依赖了一个npm包,那么这个npm包就不是一个独立npm包,他应该是一个主包代码才对。我们新建一个ReplaceNpmPackagesPath
类来解决这个问题。
const path = require('path');
const fse = require('fs-extra');
const { parse } = require('@babel/parser');
const { default: traverse } = require('@babel/traverse');
const {default: generate} = require('@babel/generator');
const htmlparser2 = require('htmlparser2');
class ReplaceNpmPackagesPath {
constructor(pathMap, config, subDepend) {
this.pathMap = pathMap;
this.config = config;
this.subDepend = subDepend;
this.invalidPathMap = new Map();
}
replaceAll() {
const pathMap = this.pathMap;
if (pathMap.size === 0) return;
for (let [key, value] of pathMap.entries()) {
const ext = path.extname(key);
switch (ext) {
case '.js':
this.replaceJs(key, value);
break;
case '.json':
this.replaceJson(key, value);
break;
case '.wxml':
this.replaceWXML(key, value);
break;
case '.wxss':
this.replaceWXSS(key, value);
break;
case '.wxs':
this.replaceWxs(key, value);
break;
default:
throw new Error(`don't know type: ${ext} of ${key}`);
}
}
// 打印非法路径
this.printInvalidPathMap();
}
replaceJs(file, npms) {
if (!npms.length) return;
// 读取js内容
let content = fse.readFileSync(file, 'utf-8');
// 将代码转化为AST
const ast = parse(content, {
sourceType: 'module',
plugins: ['exportDefaultFrom'],
});
// 遍历AST
traverse(ast, {
ImportDeclaration: ({ node }) => {
// 获取import from 地址
const { value } = node.source;
node.source.value = this.transformScript(value, npms, file);
},
ExportNamedDeclaration: ({ node }) => {
// 获取export form地址
if (!node.source) return;
const { value } = node.source;
node.source.value = this.transformScript(value, npms, file);
},
CallExpression: ({ node }) => {
if (
(node.callee.name && node.callee.name === 'require')
&& node.arguments.length >= 1
) {
const [{ value }] = node.arguments;
if (!value) return;
node.arguments[0].value = this.transformScript(value, npms, file);
}
},
ExportAllDeclaration: ({ node }) => {
if (!node.source) return;
const { value } = node.source;
node.source.value = this.transformScript(value, npms, file);
},
});
fse.outputFile(file, generate(ast).code);
}
transformScript(src, npms, file) {
const result = file.match(this.config.SPLIT_NPM_REGEXP);
const currentNpmName = result[1];
if (src.indexOf(currentNpmName) !== -1) {
this.addInvalidPathMap(file, src);
}
for (let i = 0; i < npms.length; i++) {
const index = src.indexOf(npms[i]);
if (index !== -1) {
if (src.startsWith('/miniprogram_npm/')) {
if (this.subDepend.isIsolatedNpm(npms[i])) {
return this.getRelativePath(file, src.replace('/miniprogram_npm/', ''));
} else {
return src.substring(index);
}
} else if (!fse.existsSync(this.getResolvePath(file, src))){
if (this.subDepend.isIsolatedNpm(npms[i])) {
if (index === 0) {
return this.getRelativePath(file, src);
}
} else if (index > 0) {
return src.substring(index);
}
}
break;
}
}
return src;
}
addInvalidPathMap(file, src) {
let arr = this.invalidPathMap.get(file);
if (!arr) {
arr = [];
}
arr.push(src);
this.invalidPathMap.set(file, arr);
}
printInvalidPathMap() {
if (this.invalidPathMap.size) {
console.log('存在自引用包文件,请必须修改:');
for (let [key, value] of this.invalidPathMap) {
console.log(key + ':');
console.log(value);
}
}
}
getResolvePath(file, src) {
return path.resolve(path.dirname(file), src);
}
getRelativePath(file, src) {
const relativePath = path.relative(path.join(this.config.targetDir, `${this.subDepend.rootDir}/${this.subDepend.rootDir}_npm`), file);
const pathArr = relativePath.split(path.sep);
let filePath = `${this.getDotPath(pathAr)}${src}`;
try {
const stats = fse.statSync(path.resolve(file, path.join('../', filePath)));
if (stats.isDirectory()) {
filePath += '/index';
}
} catch (e) {}
return filePath;
}
getDotPath(len) {
let result = '';
for (let i = 0; i < len - 1; i++) {
result += '../';
}
return result;
}
replaceWxs(file, npms) {
if (!npms.length) return;
// 读取js内容
let content = fse.readFileSync(file, 'utf-8');
// 将代码转化为AST
const ast = parse(content, {
sourceType: 'module',
plugins: ['exportDefaultFrom'],
});
// 遍历AST
traverse(ast, {
CallExpression: ({ node }) => {
if (
(node.callee.name && node.callee.name === 'require')
&& node.arguments.length >= -1
) {
const [{ value }] = node.arguments;
for (let i = 0; i < npms.length; i++) {
const index = value.indexOf(npms[i]);
if (index > 0) {
if (value.startsWith('/miniprogram_npm/')) {
if (this.subDepend.isIsolatedNpm(npms[i])) {
node.arguments[0].value = this.getRelativePath(file, value.replace('/miniprogram_npm/', ''));
}
} else if (!fse.existsSync(this.getResolvePath(file, value))) {
node.arguments[0].value = '/miniprogram_npm/' + value.substring(index);
}
break;
}
}
}
},
});
fse.outputFile(file, generate(ast).code);
}
replaceJson(file, npms) {
if (!npms.length) return;
const content = fse.readJsonSync(file);
const usingComponents = content.usingComponents;
if (usingComponents && Object.keys(usingComponents).length) {
Object.keys(usingComponents).forEach(key => {
let value = usingComponents[key];
for (let i = 0; i < npms.length; i++) {
const index = value.indexOf(npms[i]);
if (index !== -1) {
if (value.startsWith('/miniprogram_npm/')) {
if (this.subDepend.isIsolatedNpm(npms[i])) {
usingComponents[key] = this.getRelativePath(file, value.replace('/miniprogram_npm/', ''));
} else {
usingComponents[key] = value.substring(index);
}
} if (!fse.existsSync(this.getResolvePath(file, value))) {
if (this.subDepend.isIsolatedNpm(npms[i])) {
if (index === 0) {
usingComponents[key] = this.getRelativePath(file, value);
}
} else if (index > 0) {
usingComponents[key] = value.substring(index);
}
}
break;
}
}
});
}
fse.writeJson(file, content);
}
replaceWXML(file, npms) {
if (!npms.length) return;
let content = fse.readFileSync(file, 'utf-8');
const contentMap = {};
const htmlParser = new htmlparser2.Parser({
onopentag(name, attribs = {}) {
if (name !== 'import' && name !== 'include' && name !== 'wxs') {
return;
}
const { src } = attribs;
if (!src) return;
for (let i = 0; i < npms.length; i++) {
const index = src.indexOf(npms[i]);
if (index > 0) {
if(src.startsWith('/miniprogram_npm/')) {
if (this.subDepend.isIsolatedNpm(npms[i])) {
contentMap[src] = this.getRelativePath(file, src.replace('/miniprogram_npm/', ''));
}
} else if(!fse.existsSync(this.getResolvePath(file, src))) {
if (!this.subDepend.isIsolatedNpm(npms[i])) {
contentMap[src] = '/miniprogram_npm/' + value.substring(index);
}
}
break;
}
}
},
});
Object.keys(contentMap).forEach(key => {
content = content.replace(new RegExp(key, 'g'), contentMap[key]);
});
htmlParser.write(content);
htmlParser.end();
}
replaceWXSS(file, npms) {
if (!npms.length) return;
let content = fse.readFileSync(file, 'utf-8');
const importRegExp = /@import\s+['"](.*)['"];?/g;
const npmMap = {};
let matched;
while ((matched = importRegExp.exec(content)) !== null) {
const str = matched[1];
if (str) {
for (let i = 0; i < npms.length; i++) {
const index = str.indexOf(npms[i]);
if (index > 0) {
if(str.startsWith('/miniprogram_npm/')) {
if (this.subDepend.isIsolatedNpm(npms[i])) {
npmMap[str] = this.getRelativePath(file, str.replace('/miniprogram_npm/', ''));
}
} else if(!fse.existsSync(this.getResolvePath(file, str))) {
if (!this.subDepend.isIsolatedNpm(npms[i])) {
npmMap[str] = '/miniprogram_npm/' + value.substring(index);
}
}
break;
}
}
}
}
Object.keys(npmMap).forEach(key => {
content = content.replace(new RegExp(key, 'g'), npmMap[key]);
});
fse.outputFile(file, content);
}
}
module.exports = {
ReplaceNpmPackagesPath: ReplaceNpmPackagesPath,
};
现在我们已经解决了上面的三个问题,接下来我们就要使用这些类。
4. 移动独立npm包
在我们的容器类中添加一个移动独立npm包的方法,我们将把这些独立npm包移动到子包下的一个目录,并把它命名为:“子包名”_npm。
class DependContainer {
moveIsolatedNpm() {
console.log('正在移动独立npm包...');
this.subDepends.forEach(sub => {
Array.from(sub.isolatedNpms).forEach(npm => {
const source = path.join(this.config.targetDir, `miniprogram_npm/${npm}`);
const target = path.join(this.config.targetDir, `${sub.rootDir}/${sub.rootDir}_npm/${npm}`);
fse.moveSync(source, target);
});
});
}
}
5. 在子包中处理路径
接着在子包添加处理路径的方法:
class SubDepend extends BaseDepend {
/**
* 修复npm包的路径
*/
replaceNpmDependPath() {
const instance = new ReplaceNpmPackagesPath(this.getIsolatedNpmDepend(), this.config, this);
instance.replaceAll();
}
/**
* 修复子包正常文件的路径
*/
replaceNormalFileDependPath() {
const instance = new ReplaceSubPackagesPath(this.getSubPackageDepend(), this.config, this.rootDir);
instance.replaceAll();
}
/**
* 获取独立npm包的依赖
* @returns {Map<any, any>}
*/
getIsolatedNpmDepend() {
const isolatedNpmDepends = new Map();
if (this.isolatedNpms.size !== 0) {
const dependsMap = this.dependsMap;
const npmFiles = Array.from(this.files).filter(file => file.indexOf('miniprogram_npm') !== -1);
for (let file of npmFiles) {
const value = dependsMap.get(file);
if (value.length) {
for (let key of this.isolatedNpms.keys()) {
if (file.indexOf(`miniprogram_npm${path.sep}${key}`) !== -1) {
const depends = value.reduce((sum, item) => {
const result = item.match(this.config.npmRegexp);
if (result && result[1] && result[1] !== key) {
sum.add(result[1]);
}
return sum;
}, new Set());
const filePath = file.replace(`${this.config.sourceDir}${path.sep}miniprogram_npm`, `${this.config.targetDir}${path.sep}${this.rootDir}${path.sep}${this.rootDir}_npm`);
isolatedNpmDepends.set(filePath, Array.from(depends));
break;
}
}
}
}
}
return isolatedNpmDepends;
}
/**
* 获取子包的依赖
* @returns {Map<any, any>}
*/
getSubPackageDepend() {
const isolatedNpmDepends = new Map();
if (this.isolatedNpms.size !== 0) {
const normalFiles = Array.from(this.files).filter(item => item.indexOf('miniprogram_npm') === -1);
for (let file of normalFiles) {
const value = this.dependsMap.get(file);
if (value.length) {
const depends = value.reduce((sum, item) => {
const result = item.match(this.config.npmRegexp);
if (result && result[1] && this.isolatedNpms.has(result[1]) ) {
sum.add(result[1]);
}
return sum;
}, new Set());
const filePath = file.replace(this.config.sourceDir, this.config.targetDir);
isolatedNpmDepends.set(filePath, Array.from(depends));
}
}
}
return isolatedNpmDepends;
}
}
6. 在容器类中调用这些方法
class DependContainer {
replacePath() {
console.log('正在修复路径映射...');
this.subDepends.forEach(sub => {
sub.replaceNpmDependPath();
sub.replaceNormalFileDependPath();
});
}
}
接着我们在初始化函数里面增加这些方法:
class DependContainer {
async init() {
this.clear();
this.initMainDepend();
this.initSubDepend();
this.splitIsolatedNpmForSubPackage();
const allFiles = await this.copyAllFiles();
this.replaceComponentsPath(allFiles);
if (this.config.isSplitNpm) {
this.moveIsolatedNpm();
this.replacePath();
}
if (this.config.analyseDir) {
this.createTree();
}
console.log('success!');
}
}
现在我们的移动独立npm包就完成了,有一点复杂。
到目前为止为了介绍如何手写一个小程序摇树工具我花了整整八个章节,不是我不想写简单些,而是实在是有很多细节需要处理,而且很多是我实际所遇到的问题。要想做好这个工具,你需要对小程序的文件,语法等有着深刻和全面的理解,如果少算了一种情况就会导致摇树出来的代码出现问题,轻则文件报错,重则功能被删除。
舍得,舍得,望有缘人得之,也算成就了我的一段善缘。
连载文章链接:
手写小程序摇树工具(一)——依赖分析介绍
手写小程序摇树工具(二)——遍历js文件
手写小程序摇树工具(三)——遍历json文件
手写小程序摇树工具(四)——遍历wxml、wxss、wxs文件
手写小程序摇树工具(五)——从单一文件开始深度依赖收集
手写小程序摇树工具(六)——主包和子包依赖收集
手写小程序摇树工具(七)——生成依赖图
手写小程序摇树工具(八)——移动独立npm包
手写小程序摇化工具(九)——删除业务组代码