- A+
所属分类:Web前端
背景
针对目前团队自己开发的组件库,对当前系统内引用组件库占比进行统计分析,以实现对当前进度的总结以及后续的覆盖度目标制定。
主要思路
目前找到的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/react@17.0.2/node_modules/react/index.js" }, ... ] }, ... ]
后续
上面拿到的数据是扁平化的数据,如果针对需要去分析整体的树状结构,可以直接将扁平化数据处理一下,定义一个主入口去寻找它的子级,这样可以自己去生成一颗树状的import
关系图。