当前位置: 首页 > Web前端 > HTML

node_modules瘦身

时间:2023-03-28 13:02:57 HTML

原因场景一:当前项目经过刀耕火种开发,然后接入cli工具进行集中管理打包,那么项目中的依赖与cli工具中的依赖有哪些重合,是否它们的版本相同,是否存在冗余?场景二:项目中某个库升级,依赖A库的V3版本。同时,当前项目依赖A库V2版本。这时候打包就很明显了,会同时打包这个包的不同版本。进入场景三:当前deps中有对应的依赖库,但是业务代码没有使用到。由于以上场景,我们需要一个工具来解决这些情况。想想这些场景怎么解决,场景3的解决方案是什么比如已经有一个库:depcheck的简单原理:通过检测项目中的文件import或require并与依赖进行比较,最后生成一个需要某种配置的依赖列表(通过实际调用发现还有一些问题:子模块中的代码没有检测到,依赖中的babel配置插件的检测也是如此)和场景1和场景2与场景3不同,是已有的库,只是略有重复,都是必需的目前检测库的方案是运行node脚本,检查同一个库是否有多个版本node_modules或锁定文件。node_modules文件层数太多,lock文件就是它的层映射。考虑从这里入手,确保锁文件是最新的(这一层比较麻烦,没有标识来保证,只要确定这个文件是否存在即可)打开本地网站,可视化结果(实际运行后,这一幕已废弃,具体原因详见下文)development这里我们先解决场景1的问题,场景1在上面的思路中有针对这个场景的解决方案,即depcheck场景,但是需要重写其配置:checkconfigurationupdateconstoptions={ignoreBinPackage:false,//忽略带有bin条目的包skipMissing:false,//跳过缺失依赖项的计算ignorePatterns:[//匹配这些模式的文件将被忽略'sandbox','dist','bower_components','tsconfig.json'],ignoreMatches:[//忽略匹配这些globs'grunt-*',],parsers:{//目标解析器'**/*.js':depcheck.parser.es6,'**/*.jsx':depcheck.parser.jsx,'**/*.ts':depcheck.parser.typescript,//ts可能有问题在这里打字,但实际操作和文档后没问题'**/*.tsx':[depcheck.parser.typescript,depcheck.parser.jsx],},detectors:[//目标检测器depcheck.detector.requireCallExpression,depcheck.detector.requireResolveCallExpression,depcheck.detector.importDeclaration,depcheck.detector.exportDeclaration,depcheck.detector.gruntLoadTaskCallExpression,depcheck.detector.importCallExpression,depcheck.detector.typescriptImportEqualsDeclaration,depcheck.detector.typescriptImportType,]],//特价:[////部门检查API在选项中公开特殊属性,这些属性接受一个数组来指定特定的分析器。//],//这将覆盖原来的package.json解析//package:{//},};然后调用configuration://默认为当前路径constcheck=(path=process.cwd())=>depcheck(path,options)最后加上打印结果:console.log('Unuseddependencies:')unused。dependencies.forEach(name=>{console.log(chalk.greenBright(`*${name}`))})console.log('UnuseddevDependencies:');unused.devDependencies.forEach(name=>{console.log(chalk.greenBright(`*${name}`))})调用结果示例展示:场景2命令技术选择:Commander推荐最多,也是下载最多.锁文件的下载量为8kw+package-lock.json。默认是npm及其对应的解析。现在有yarn,pnpm比较流行。但是一般在服务器上打包的时候,都会用到开发计划中使用npmcommand命令的命令。check//默认场景1操作checkjson//解析.lock文件,同时打印占用空间的包checkjson-d//保存结果打印文件中第一步命令的定义:constmain=()=>{constprogram=newcommander.Command();program.command('check').description('checklibrary').action((options)=>{//显示加载constspinner=ora('Loadingcheck').start();//checkcheck()}).command('json').description('分析锁文件').option('-d,--doc','分析锁文件并保存结果').action(async(options)=>{//显示加载constspinner=ora('Loadingcheck').start();//执行脚本//附加判断options.opendeepCheck(spinner,options)})program.parse();}第二步是解析文件。首先我们使用fs获取文件内容:constlockPath=path.resolve('package-lock.json')constdata=fs.readFileSync(lockPath,'utf8')进行锁数据解析:constallPacks=newMap();Object.keys(allDeps).forEach(name=>{constitem=allDeps[name]if(item.dev){//暂时忽略dev的return}if(item.requires){//item.dependencies中的操作类似到setCommonPack(item.requires,name,item.dependencies)}if(item.dependencies){Object.keys(item.dependencies).forEach(depsName=>{constdepsItem=item.dependencies[depsName]if(!allPacks.has(depsName)){allPacks.set(depsName,[])}constpackArr=allPacks.get(depsName);packArr.push({location:`${name}/node_modules/${depsName}`,version:depsItem.version,label:'reDeps',//标记为重复依赖size:getFileSize(`./node_modules/${name}/node_modules/${depsName}`)})allPacks.set(depsName,packArr)})}})最后循环计算临时空间最大的包://创建一个排序好的数据,推送后自动按大小排序lettopSizeIns=createTopSize()allPacks.forEach((arr,name,index)=>{if(arr.length<=1){return}letlocalSize=0arr.forEach((item,itemIndex)=>{constsize=Number(item.size)localSize+=size})topSizeIns.push({items:arr,size:localSize})})//最后打印结果,输出可选文档if(options.doc){fs.writeFileSync(`deepCheck.json`,`${JSON.stringify(mapChangeObj(allPacks),null,2)}`,{encoding:'utf-8'})}//打印top5console.log(chalk.yellow('5个最大的重复库占用空间:'))topSizeIns.arr.forEach(itemObj=>{constcommon=itemObj.items.find(it=>it.label==='common')console.log(chalk.cyan(`${common.location}--${itemObj.size.toFixed(2)}KB`));itemObj.items.forEach(it=>{console.log(`*${it.location}@${it.version}--size:${它.size}KB`)})})第三步是图形方案(已弃用)。首先说一下实现方案:将json生成的数据转换成图表启动本地服务所需的数据,参考echart和data数据转换:letnodes=[]letedges=[]packs.forEach((arr,name,index)=>{letlocalSize=0arr.forEach((item,itemIndex)=>{constsize=Number(item.size)nodes.push({x:Math.random()*1000,y:Math.random()*1000,id:item.location,name:item.location,symbolSize:size>max?max:size,itemStyle:{color:getRandomColor(),},})localSize+=size})topSizeIns.push({items:arr,size:localSize})常量common=arr.find(it=>it.label==='common')if(common){arr.forEach(item=>{if(item.label==='common''){return}edges.push({attributes:{},size:1,source:common.location,target:item.location,})})}})启动服务:服务没有使用第三方库,但是添加了节点http服务:varmineTypeMap={html:'text/html;charset=utf-8',htm:'text/html;charset=utf-8',xml:"text/xml;charset=utf-8'8",//省略其他}constcreateServer=()=>{constchartData=fs.readFileSync(getFile('deepCheck.json'),'utf8')http.createServer(function(request,response){//解析请求,包括文件名//request.urlif(request.url==='/'){//从文件系统读取请求的文件内容constdata=fs.readFileSync(getFile('tools.html'))response.writeHead(200,{'Content-Type':'text/html'});//这里是类似服务端数据的解决方案,当然也可以使用引入json的方案const_data=data.toString().replace(newRegExp('<%chartData%>'),chartData)//响应文件内容response.write(_data);response.end();}else{consttargetPath=decodeURIComponent(getFile(request.url));//目标地址是文件的引用路径和相对路径的拼接,decodeURIComponent()是解码其中的汉字路径console.log(request.method,request.url,baseDir,targetPath)constextName=path.extname(targetPath).substr(1);if(fs.existsSync(targetPath)){//判断是否是本地文件如果(mineTypeMap[extName]){response.setHeader('Content-Type',mineTypeMap[分机名]);}varstream=fs.createReadStream(targetPath);stream.pipe(响应);}else{response.writeHead(404,{'Content-Type':'text/html'});响应.end();}}}).listen(8080);console.log('服务器运行在http://127.0.0.1:8080/');opener(`http://127.0.0.1:8080/`);}导出defaultcreateServer效果图:通过这张图可以看出大致问题:依赖包太多,导致数据显示混乱。圆圈根据包裹的实际大小显示。差距太大了,大的几万kb,小的几万kb。图片有几十kb,最大尺寸200暂时闲着,所以暂时不启用这个功能。其他解决方案在使用pnpm的时候,我发现它可以解决冗余包的大小问题,所以我也在这里列出来总结一下目前构建的包。:@grewer/deps-check你可以尝试使用文章开头提出的三种常见场景。这个包基本可以解决然后提出一些优化点,比如一些包的替换(moment替换dayjs,lodash和lodash.xx包不能同时存在等)这些都需要长期的维护和管理。看完这篇文章,如果你有什么好的建议,可以留言告诉我