項目忙完,這次上新,寫一個前端系列,採用vue3來開發一個微信公眾號商城。 前言: 1. 微信公眾號商城本質也是一個網站,由一個個網頁組成,只不過這些網頁運行在手機端,能響應手指的點擊、長按、拖拽等操作。 2. 既然是網頁,當然可以用3件套(js+html+css)來寫,但象vue這樣的前端框架比3 ...
背景
針對目前團隊自己開發的組件庫,對當前系統內引用組件庫占比進行統計分析,以實現對當前進度的總結以及後續的覆蓋度目標制定。
主要思路
目前找到的webpack
分析插件,基本都是針對打包之後的分析打包之後的chunk
進行分析,但是我希望的是分析每個頁面中的import
數,對比一下在所有頁面中的import
數中有多少是使用了組件庫的。所以就在網上看了一些相關資料以及webpack
的api
文檔。主要是利用webpack
的importCall
、import
、importSpecifier
三個鉤子來實現,它們的作用直接跟著代碼看一下。
完整代碼實現
import fs from 'fs';
import path from 'path';
import resolve from 'enhanced-resolve';
let myResolve;
/**
* 通過source獲取真實文件路徑
* @param parser
* @param source
*/
function getResource(parser, source) {
if (!myResolve) {
myResolve = resolve.create.sync(parser.state.options.resolve);
}
let result = '';
try {
result = myResolve(parser.state.current.context, source);
} catch (err) {
console.log(err);
} finally {
return result;
}
}
class WebpackImportAnalysisPlugin {
constructor(props) {
this.pluginName = 'WebpackCodeDependenciesAnalysisPlugin';
// 文件數組
this.files = [];
// 當前編譯的文件
this.currentFile = null;
this.output = props.output;
}
apply(compiler) {
compiler.hooks.compilation.tap(this.pluginName, (compilation, { normalModuleFactory }) => {
const collectFile = parser => {
const { rawRequest, resource } = parser.state.current;
if (resource !== this.currentFile) {
this.currentFile = resource;
this.files.push({
name: rawRequest,
resource,
children: []
});
}
};
const handler = parser => {
// 用來捕獲import(xxx)
parser.hooks.importCall.tap(this.pluginName, expr => {
collectFile(parser);
let ast = {};
const isWebpack5 = 'webpack' in compiler;
// webpack@5 has webpack property, webpack@4 don't have the property
if (isWebpack5) {
// webpack@5
ast = expr.source;
} else {
//webpack@4
const { arguments: arg } = expr;
ast = arg[0];
}
const { type, value } = ast;
if (type === 'Literal') {
const resource = getResource(parser, value);
this.files[this.files.length - 1].children.push({
name: value,
resource,
importStr: `import ('${value}')`
});
}
});
// 用來捕獲 import './xxx.xx';
parser.hooks.import.tap(this.pluginName, (statement, source) => {
// 由於statement.specifiers.length大於0的時候同時會被importSpecifier鉤子捕獲,所以需要在這個地方攔截一下,這個地方只處理單獨的引入。
if (statement.specifiers.length > 0) {
return;
}
collectFile(parser);
this.files[this.files.length - 1].children.push({
name: source,
resource: getResource(parser, source),
importStr: `import '${source}'`
});
});
// 用來捕獲 import xx from './xxx.xx';
parser.hooks.importSpecifier.tap(
this.pluginName,
(statement, source, exportName, identifierName) => {
collectFile(parser);
let importStr = '';
if (exportName === 'default') {
importStr = `import ${identifierName} from '${source}'`;
} else {
if (exportName === identifierName) {
importStr = `import { ${identifierName} } from '${source}'`;
} else {
importStr = `import { ${exportName}: ${identifierName} } from '${source}'`;
}
}
this.files[this.files.length - 1].children.push({
name: source,
exportName,
identifierName,
importStr,
resource: getResource(parser, source)
});
}
);
};
normalModuleFactory.hooks.parser.for('javascript/auto').tap(this.pluginName, handler);
});
compiler.hooks.make.tap(this.pluginName, compilation => {
compilation.hooks.finishModules.tap(this.pluginName, modules => {
// 過濾掉深度遍歷的node_modules中的文件,只分析業務代碼中的文件
const needFiles = this.files.filter(
item => !item.resource.includes('node_modules') && !item.name.includes('node_modules')
);
fs.writeFile(this.output ?? path.resolve(__dirname, 'output.json'), JSOn.stringify(needFiles, null, 4), err => {
if (!err) {
console.log(`${path.resolve(__dirname, 'output.json')}寫入完成`);
}
});
});
});
}
}
export default WebpackImportAnalysisPlugin;
// 以文件為基準,扁平化輸出所有的import
[
{
"name": "./src/routes",
"resource": "/src/routes.tsx",
"children": [
{
"name":"react",
"exportName":"lazy",
"identifierName":"lazy",
"importStr":"import { lazy } from 'react'",
"resource":"/node_modules/.pnpm/[email protected]/node_modules/react/index.js"
},
...
]
},
...
]
後續
上面拿到的數據是扁平化的數據,如果針對需要去分析整體的樹狀結構,可以直接將扁平化數據處理一下,定義一個主入口去尋找它的子級,這樣可以自己去生成一顆樹狀的import
關係圖。