背景

针对目前团队自己开发的组件库,对当前系统内引用组件库占比进行统计分析,以实现对当前进度的总结以及后续的覆盖度目标制定。

主要思路

目前找到的webpack分析插件,基本都是针对打包之后的分析打包之后的chunk进行分析,但是我希望的是分析每个页面中的import数,对比一下在所有页面中的import数中有多少是使用了组件库的。所以就在网上看了一些相关资料以及webpackapi文档。主要是利用webpackimportCallimportimportSpecifier三个钩子来实现,它们的作用直接跟着代码看一下。

完整代码实现

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关系图。