乐趣区

关于前端:nodemodules-瘦身

起因

场景一:
以后我的项目经验了刀耕火种地开发, 之后接入了 cli 工具集中管理打包, 那么我的项目中的依赖,
和 cli 工具中的依赖重合度是多少, 并且他的的版本是否雷同, 是否有冗余代码

场景二:
我的项目中某一个库降级了, 他依赖了 A 库的 V3 版本, 同时以后我的项目依赖的是 A 库 V2 版本, 这个时候打包很显著, 就会将这一个包的不同版本同时打入

场景三:
以后 deps 中有对应的依赖库, 然而业务代码中并未应用到

因为上述的场景, 咱们须要一个工具来解决这些状况

思考🤔

这些场景改如何解决, 解决的计划是什么

针对场景三来说, 当初曾经有一个库: depcheck

简略的原理: 通过检测我的项目中的文件 import 或者 require 和依赖进行比照, 最初生成依赖列表

想要肯定的配置
(通过理论的调用, 发现还存在肯定的问题: 在子模块中的代码未能被检测, 同时对于依赖中的 babel 配置插件检测也是同样的)

而场景一和二就和三不太一样了, 他是已有库, 然而略有反复, 所有须要针对库进行检测

目前打算是通过 node 脚本来运行

  • 查看 node_modules 或者 lock 文件中, 是否存在同一库的多个版本
  • node_modules 文件层级太多, lock 文件是他的一层映射, 思考从这里动手
  • 确保 lock 文件是最新的(这一层比拟麻烦, 没标识来保障, 明确就确保此文件是否存在即可)
  • 关上本地网站, 针对后果的可视化显示(通过理论的操作, 这一场景放弃, 具体起因放下下方详述)

开发

这里咱们首先解决场景一的问题

场景一

在下面的思考中针对此场景曾经了一解决方案了, 即 depcheck 场景, 然而他的配置须要从新编写:

check 配置更新

const options = {
    ignoreBinPackage: false, // ignore the packages with bin entry
    skipMissing: false, // skip calculation of missing dependencies
    ignorePatterns: [
        // files matching these patterns will be ignored
        'sandbox',
        'dist',
        'bower_components',
        'tsconfig.json'
    ],
    ignoreMatches: [
        // ignore dependencies that matches these globs
        'grunt-*',
    ],
    parsers: {
        // the target parsers
        '**/*.js': depcheck.parser.es6,
        '**/*.jsx': depcheck.parser.jsx,
        '**/*.ts': depcheck.parser.typescript,
        // 这里 ts 类型可能会出问题, 然而通过理论的运行和文档阐明是没问题的
        '**/*.tsx': [depcheck.parser.typescript, depcheck.parser.jsx],
    },
    detectors: [
        // the target 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,
    ],
    // specials: [
    //     // Depcheck API 在选项中裸露了非凡属性,它承受一个数组,以指定非凡分析器。// ],
    // 这里将会笼罩本来的 package.json 的解析
    // package: {//},
};

之后再调用配置:

// 默认即以后门路
const check = (path = process.cwd()) => depcheck(path ,options)

最初加上打印后果:

console.log('Unused dependencies:')
unused.dependencies.forEach(name=>{console.log(chalk.greenBright(`* ${name}`))
})
console.log('Unused devDependencies:'); 
unused.devDependencies.forEach(name=>{console.log(chalk.greenBright(`* ${name}`))
})

调用后果的例子展现:

场景二

指令技术选型:

  1. commander

举荐最多的, 同时也是下载量最多的, 下载量 8kw+

  1. package-lock.json

针对的 lock 文件, 默认 npm 及其对应的解析, 当初还有 yarn, pnpm 比拟风行, 然而
个别在服务器上打包时都用应用 npm 指令

指令的开发

打算中的指令

  • check // 默认场景一的操作
  • check json // 解析 .lock 文件, 同时打印占用空间的包
  • check json -d // 将后果打印成文件

第一步

指令的定义:

const main = () => {const program = new commander.Command();
    program.command('check')
        .description('查看应用库')
        .action((options) => {
            // 显示一个 loading
            const spinner = ora('Loading check').start();
            
            // check
            check()}).command('json').description('解析 lock 文件').option('-d, --doc', '解析 lock 文件, 将后果保留')
        .action(async (options) => {
            // 显示 loading
            const spinner = ora('Loading check').start();
            // 执行脚本
            // 额定判断 options.open
            deepCheck(spinner, options)
        })
    
    program.parse();}

第二步 解析文件

首先咱们通过 fs 来获取文件内容:

const lockPath = path.resolve('package-lock.json')

const data = fs.readFileSync(lockPath, 'utf8')

针对 lock 数据解析:

    const allPacks = new Map();
    
    Object.keys(allDeps).forEach(name => {const item = 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 => {const depsItem = item.dependencies[depsName]
                if (!allPacks.has(depsName)) {allPacks.set(depsName, [])
                }
                const packArr = 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)
            })
        }
    })

最初通过一个循环来计算出暂用空间最大的包:

    // 创立一个排序数据, push 之后主动依据 size 排序
    let topSizeIns = createTopSize()
    
    allPacks.forEach((arr, name, index) => {if(arr.length <= 1){return}
        let localSize = 0
        arr.forEach((item, itemIndex) => {const size = 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'})
    }
    
    // 打印 top5
    console.log(chalk.yellow('占用空间最大的 5 个反复库:'))
    topSizeIns.arr.forEach(itemObj => {const common = 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:${it.size}KB`)
        })
    })

第三步

图形化计划 ( 曾经弃用)

先说说实现计划:

  1. 转换 json 生成的数据至图表须要的数据
  2. 启动本地服务, 援用 echart 和数据

数据转换:

let nodes = []
let edges = []
packs.forEach((arr, name, index) => {
    let localSize = 0
    arr.forEach((item, itemIndex) => {const size = 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})
    
    const 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,
            })
        })
    }
})

启动服务:

服务并没有应用三方库, 而是增加了一个 node http 服务:


var mineTypeMap = {
    html: 'text/html;charset=utf-8',
    htm: 'text/html;charset=utf-8',
    xml: "text/xml;charset=utf-8",
    // 省略其余
}

const createServer = () => {const chartData = fs.readFileSync(getFile('deepCheck.json'), 'utf8')

    http.createServer(function (request, response) {
        // 解析申请,包含文件名
        // request.url
        if (request.url === '/') {
            // 从文件系统中读取申请的文件内容
            const data = fs.readFileSync(getFile('tools.html'))
            response.writeHead(200, {'Content-Type': 'text/html'});
            // 这里是应用的相似服务端数据的计划, 当然也能够应用引入 json 的计划来解决
            const _data = data.toString().replace(new RegExp('<%chartData%>'), chartData)
            // 响应文件内容
            response.write(_data);
            response.end();} else {const targetPath = decodeURIComponent(getFile(request.url)); // 指标地址是基准门路和文件相对路径的拼接,decodeURIComponent()是将门路中的汉字进行解码
            console.log(request.method, request.url, baseDir, targetPath)

            const extName = path.extname(targetPath).substr(1);
            if (fs.existsSync(targetPath)) { // 判断本地文件是否存在
                if (mineTypeMap[extName]) {response.setHeader('Content-Type', mineTypeMap[extName]);
                }
                var stream = fs.createReadStream(targetPath);
                stream.pipe(response);
            } else {response.writeHead(404, {'Content-Type': 'text/html'});
                response.end();}
        }
    }).listen(8080);

    console.log('Server running at http://127.0.0.1:8080/');

    opener(`http://127.0.0.1:8080/`);
}

export default createServer

效果图:

通过此图, 能够看到大略问题点所在:

  1. 依赖包太多, 导致数据显示芜杂
  2. 依据包实在尺寸大小显示圆圈, 其中的差距过大, 大的有几万 kb, 小的有几十 kb
    图中临时闲置了最大 size 200

所以临时不开启此性能

总结

以后已构建出包: @grewer/deps-check 可尝试应用

针对文章一开始提出的三种常见场景, 此包基本上可能解决了

之后还能提出一些优化点, 比方有些包的替换 (moment 替换 dayjs, lodashlodash.xx 包不能同时存在等等)
这些就须要长期保护治理了

退出移动版