见素包朴,少私寡欲,绝学无忧
github: miniapp-shaking
上一章我们介绍了遍历js文件的方法,接下来我们介绍其他文件的遍历。
1. 遍历JSON文件
对于json文件,我们直接读取json文件,然后转化为json对象来处理。json文件中我们主要处理的是组件的json和app.json,微信小程序中有一些特殊的字段会对外产生依赖:pages
,usingComponents
, componentGenerics
,componentPlaceholder
,这里需要特别注意后面两个,即抽象节点组件和分包异步化之后的占位组件。另外我还添加了一个自定义的字段replaceComponents
用于处理组名的问题,在上一章中我曾经介绍过config里面有一个字段叫groupName
,这里就是用于定义不同业务组中组件的地方。
/**
* 收集json文件依赖
* @param file
* @returns {[]}
*/
jsonDeps(file) {
const deps = [];
const dirName = path.dirname(file);
// json中有关依赖的关键字段
const { pages, usingComponents, replaceComponents, componentGenerics, componentPlaceholder} = fse.readJsonSync(file);
// 处理有pages的json,一般是主包
if (pages && pages.length) {
pages.forEach(page => {
this.addPage(page);
});
}
// 处理有usingComponents的json,一般是组件
if (usingComponents && typeof usingComponents === 'object' && Object.keys(usingComponents).length) {
// 获取改组件下的wxml的所有标签,用于下面删除无用的组件
const tags = this.getWxmlTags(file.replace('.json', '.wxml'));
Object.keys(usingComponents).forEach(key => {
// 对于没有使用的组件,不需要依赖
if (tags.size && !tags.has(key.toLocaleLowerCase())) return;
let filePath;
// 如有需要,替换组件
const rcomponents = replaceComponents ? replaceComponents[this.config.groupName] : null;
const component = getReplaceComponent(key, usingComponents[key], rcomponents);
if (component.startsWith('../') || component.startsWith('./')) {
// 处理相对路径
filePath = path.resolve(dirName, component);
} else if (component.startsWith('/')) {
// 处理绝对路径
filePath = path.join(this.config.sourceDir, component.slice(1));
} else {
// 处理npm包
filePath = path.join(this.config.sourceDir, 'miniprogram_npm', component);
}
// 对于json里面依赖的组价,每一个路径对应组件的四个文件: .js,.json,.wxml,wxss
this.config.fileExtends.forEach((ext) => {
const temp = this.replaceExt(filePath, ext);
if (this.isFile(temp)) {
deps.push(temp);
} else {
const indexPath = this.getIndexPath(temp);
if (this.isFile(indexPath)) {
deps.push(indexPath);
}
}
});
});
}
// 添加抽象组件依赖
const genericDefaultComponents = this.getGenericDefaultComponents(componentGenerics, dirName);
// 添加分包异步化占用组件
const placeholderComponents = this.getComponentPlaceholder(componentPlaceholder, dirName);
deps.push(...genericDefaultComponents);
deps.push(...placeholderComponents);
return deps;
}
一般来说,pages
只存在于app.json
中,usingComponents
存在于组件的json中,因此这两者在同一个文件中是互斥的,但我们这里用同一个函数来处理。对于pages的处理后面再统一说明,我们先说usingComponents
的处理。
1.1 删除不使用的组件
对于usingComponents
的处理,我先调用了一个getWxmlTags
的方法来获取组件对应的wxml
文件的标签,为什么要多于做这一步?因为在公司中我发现很多在json
中定义的组件并没有真正的用于wxml
中,或许是由于疏忽还是在迭代中忘记了,而且一个组件可能依赖很多其他的组件,依赖组件在依赖其他组件,这种深层次的递归所引用的文件是很庞大的 。因此在这里我会把在usingComponents
中定义但没有实际在wxml
中使用的组件给删除掉,这样就可以节省很大的空间,算是细节点之一。
/**
* 获取wxml所有的标签,包括组件泛型
* @param filePath
* @returns {Set<unknown>}
*/
getWxmlTags(filePath) {
let needDelete = true;
const tags = new Set();
if (fse.existsSync(filePath)) {
const content = fse.readFileSync(filePath, 'utf-8');
const htmlParser = new htmlparser2.Parser({
onopentag(name, attribs = {}) {
if ((name === 'include' || name === 'import') && attribs.src) {
// 不删除具有include和import的文件,因为不确定依赖的wxml文件是否会包含组件
needDelete = false;
}
tags.add(name);
// 特别处理泛型组件
const genericNames = getGenericName(attribs);
genericNames.forEach(item => tags.add(item.toLowerCase()));
},
});
htmlParser.write(content);
htmlParser.end();
}
if (!needDelete) {
tags.clear();
}
return tags;
}
获取wxml
的标签需要使用到htmlparser2
包,在这里我对于使用了include
或import
导入外部文件的wxml
不做递归的深入处理了,直接跳过偷下懒吧,以后有时间在做。在这里我们还要特别注意范型组件,json中定义的组件可能并不是作为一个标签使用,而是作为范型组件使用,这里也算是一个细节点之一。要想做好一个东西,需要考虑的东西实在太多了,cry。
/**
* 解析泛型组件名称
* @param attribs
* @returns {[]}
*/
function getGenericName(attribs = {}) {
let names = [];
Object.keys(attribs).forEach(key => {
if (/generic:/.test(key)) {
names.push(attribs[key]);
}
});
return names;
}
1.2 取代业务组的组件
上面我们说到了groupName
和replaceComponents
字段,为什么我要新增这样一个字段呢?如果你的公司很庞大,有很多的业务组,为了减少重复开发和复用逻辑或者是客开逻辑,他们的代码往往是可能放在同一个页面或者组件的,例如你可能会做一个详情页,这个详情页有不同业务组的详情组件,通常你会通过wx:if
来判断使用哪个详情组件,但这样也把其他业务组的组件打包进来了,实际上别的业务的组件对你来说毫无用处,这些代码是应该去掉的。举个的例子:
detail.wxml
<detail-a wx:if="{{ flag === 'A' }}"></detail-a>
<detail-b wx:elif="{{ flag === 'B' }}"></detail-b>
<detail-c wx:else></detail-c>
detail.json
{
"usingComponents": {
'detail-a': A,
'detail-b': B,
'detail-c': C
}
}
如果你是业务组A,那么你就把业务组B和业务组C的代码都打包进来了。当然这是对于大公司的复杂逻辑而言,一般情况不必考虑这么复杂的场景。
现在我们换一种写法,添加一个replaceComponents字段:
detail.wxml
<detail></detail>
detail.json
{
"usingComponents": {
"detail": detail-c,
},
"replaceComponents": {
"A": {
"detail": detail-a
},
"B": {
"detail": detail-c
}
}
}
在这里我们只有一个详情页,打包工具在打包的时候对于groupName
为A
的会直接使用detail-a
,对于B
同理。即打包之后会变成:
{
"usingComponents": {
"detail": detail-a,
}
}
其他组B
、组C
的代码会被忽略,从而大大减少包的大小,对于大公司的复杂业务逻辑超包的问题尤其有用。但是有利也有弊,在开发的时候由于默认使用的是detail-c
,所以A
开发的时候需要暂时替换成detail-a
,但是相对于超包发布不了或者提高加载性能而言,这些好像也能够接受,全看自己取舍吧。
1.3 一个路径4个文件
接下来就是路径的判断了,和js的处理差不多。
对于json中定义的组件,我们知道微信的组件是由4个文件组成的js
,json,wxml,wxss,所以接下来我们对于每个组件,需要生成四个依赖,即这段代码:
// 对于json里面依赖的组价,每一个路径对应组件的四个文件: .js,.json,.wxml,wxss
this.config.fileExtends.forEach((ext) => {
const temp = this.replaceExt(filePath, ext);
if (this.isFile(temp)) {
deps.push(temp);
} else {
const indexPath = this.getIndexPath(temp);
if (this.isFile(indexPath)) {
deps.push(indexPath);
}
}
});
/**
*
* @param filePath
* @param ext
* @returns {string}
*/
replaceExt(filePath, ext = '') {
const dirName = path.dirname(filePath);
const extName = path.extname(filePath);
const fileName = path.basename(filePath, extName);
return path.join(dirName, fileName + ext);
}
/**
* 获取index文件的路径
* @param filePath
* @returns {string}
*/
getIndexPath(filePath) {
const ext = path.extname(filePath);
const index = filePath.lastIndexOf(ext);
return filePath.substring(0, index) + path.sep + 'index' + ext;
}
1.4 处理范型默认组件
需要注意范型组件支持默认组件,我们也同样需要处理,这也是细节之一。
/**
* 处理泛型组件的默认组件
* @param componentGenerics
* @param dirName
* @returns {[]}
*/
getGenericDefaultComponents(componentGenerics, dirName) {
const deps = [];
if (componentGenerics && typeof componentGenerics === 'object') {
Object.keys(componentGenerics).forEach(key => {
if (componentGenerics[key].default) {
let filePath = componentGenerics[key].default;
if (filePath.startsWith('../') || filePath.startsWith('./')) {
filePath = path.resolve(dirName, filePath);
} else if (filePath.startsWith('/')) {
filePath = path.join(this.config.sourceDir, filePath.slice(1));
} else {
filePath = path.join(this.config.sourceDir, 'miniprogram_npm', filePath);
}
this.config.fileExtends.forEach((ext) => {
const temp = this.replaceExt(filePath, ext);
if (this.isFile(temp)) {
deps.push(temp);
} else {
const indexPath = this.getIndexPath(temp);
if (this.isFile(indexPath)) {
deps.push(indexPath);
}
}
});
}
});
}
return deps;
}
1.5 处理分包异步化的占位组件
分包异步化之后,又有了占位组件,也需要同样处理。
/**
* 处理分包异步化的站位组件
* @param componentPlaceholder
* @param dirName
* @returns {[]}
*/
getComponentPlaceholder(componentPlaceholder, dirName) {
const deps = [];
if (componentPlaceholder && typeof componentPlaceholder === 'object' && Object.keys(componentPlaceholder).length) {
Object.keys(componentPlaceholder).forEach(key => {
let filePath;
const component = componentPlaceholder[key];
// 直接写view的不遍历
if (component === 'view' || component === 'text') return;
if (component.startsWith('../') || component.startsWith('./')) {
// 处理相对路径
filePath = path.resolve(dirName, component);
} else if (component.startsWith('/')) {
// 绝对相对路径
filePath = path.join(this.config.sourceDir, component.slice(1));
} else {
// 处理npm包
filePath = path.join(this.config.sourceDir, 'miniprogram_npm', component);
}
this.config.fileExtends.forEach((ext) => {
const temp = this.replaceExt(filePath, ext);
if (this.isFile(temp)) {
deps.push(temp);
} else {
const indexPath = this.getIndexPath(temp);
if (this.isFile(indexPath)) {
deps.push(indexPath);
}
}
});
});
}
return deps;
}
到此为止json的处理基本讲完了,有点长,细节也非常多,做这种工具需要平心静气,认真思考,不然一招不慎满盘皆输。还剩下一个pages
字段的处理留到最后再说,这涉及到入口的遍历。
欲知后文请关注下一章。
连载文章链接:
手写小程序摇树工具(一)——依赖分析介绍
手写小程序摇树工具(二)——遍历js文件
手写小程序摇树工具(三)——遍历json文件
手写小程序摇树工具(四)——遍历wxml、wxss、wxs文件
手写小程序摇树工具(五)——从单一文件开始深度依赖收集
手写小程序摇树工具(六)——主包和子包依赖收集
手写小程序摇树工具(七)——生成依赖图
手写小程序摇树工具(八)——移动独立npm包
手写小程序摇化工具(九)——删除业务组代码