乐趣区

关于webpack:编写webpack插件删除没有被依赖的业务文件

背景

SCRM 我的项目须要交接给另外一个部门。领导出于一些思考,须要把对方只须要的性能保留,其余性能删除,而后把代码上传到新的仓库地址,再作交接。

和产品经理沟通之后,明确了以下需要:

  1. 以页面(性能)为单位,保留或者删除
  2. 个别页面须要删除一些性能

问题剖析

以页面(性能)为单位,保留或者删除。也就是说,依照粒度从大到细,一个路由对应着一个页面,一个页面可能蕴含多个 tab,一个页面有多个资源(导入进来的组件、api 文件、图片、utils 办法等等),而它们也分全局和部分的。

粒度越大,人工删除会比拟很不便。粒度越小,如果是人工删除,须要先确认这个资源是否别其余页面援用,是的话则须要保留,否的话能够删除。

碍于人工删除细粒度的资源须要比拟消耗工夫和精力。心愿写一个脚本来实现这个性能。想起文件依赖,就很容易联想到 webpack 原理的 构建阶段module 的依赖剖析。

webpack 在初始换编译环境之后:

  1. 内置插件 EntryPlugin 依据 entry 配置找到入口 main.js 文件,调用 compilation.addEntry 函数触发构建流程
  2. 调用对应的 loaders 转译成 javascript 文本
  3. 再通过 acorn 解析成 AST 树,进行遍历 AST 树,监听 import 对应的钩子,失去对应的资源依赖,调用 moduleaddDependency 将依赖增加到以后 module 的依赖列表
  4. 对于新增的依赖,回到第 2 步持续解决

如果咱们能够拿到 webpack 帮咱们做好的 依赖文件列表 ,再比照 src 目录上面的文件,如果文件不在 依赖文件列表,就收集起来,而后删除掉。

代码实现

const glob = require('glob');
const path = require('path');
const fs = require('fs')
class FileShaking {constructor(options) {
        this.options = {
            excludeRegex: [
                /readme\.md/i, // 不删除 readme 文件
                /utils/ // 不删除工具办法目录下的文件
            ],
            delete: false,
            ...options
        };
        this.fileDependencies = [];
        this.srcFiles = [];
        this.toDelFiles = [];}
    apply (compiler) {compiler.hooks.afterEmit.tap("FileShaking", (compilation) => {this.fileDependencies = Array.from(compilation.fileDependencies).filter(path => !path.includes('node_modules'));
            this.deleteIndependence();});
    }
    async deleteIndependence () {this.srcFiles = await this.getSrcFiles();
        this.srcFiles.forEach(filePath => {if (!this.fileDependencies.includes(filePath) && !this.matchExclude(filePath)) {this.toDelFiles.push(filePath)
            }
        })
        if (this.options.delete) {this.delFiles();
            this.delEmptyDir('./src', (err) => {if (err) {console.log(err)
                } else {console.log('删除空文件夹 DONE')
                }
            });
        }
    }
    getSrcFiles () {return new Promise((resolve, reject) => {
            glob('./src/**/*', {nodir: true}, (err, files) => {if (err) {reject(err)
                } else {
                    let out = files.map(file => {let tmpFilePath = path.resolve(file);
                        return tmpFilePath.slice(0, 1).toUpperCase() + tmpFilePath.slice(1);
                    });
                    resolve(out)
                }
            })
        })
    }
    matchExclude (pathname) {
        let matchResult = false;
        if (this.options.excludeRegex.length) {for (let i = 0; i < this.options.excludeRegex.length; i++) {if (matchResult = this.options.excludeRegex[i].test(pathname)) {return matchResult}
            }
        }
        return matchResult;
    }
    delEmptyDir (dir, cb) {fs.stat(dir, (err, stat) => {if (err) {cb(err)
                return;
            }
            if (stat.isDirectory()) {fs.readdir(dir, (err, objs) => {objs = objs.map(item=>path.join(dir,item));
                    if (err) {cb(err)
                        return
                    }
                    if (objs.length === 0) {return fs.rmdir(dir, cb)
                    } else {
                        let count = 0
                        function done(...rest) {
                            count++;
                            if (count === objs.length) {cb(...rest);
                            }
                        }
                        objs.forEach(obj => {this.delEmptyDir(obj, done)
                        })
                    }
                })
            }
        })
    }
    delFiles () {
        this.toDelFiles.forEach(item => {fs.unlink(item, (err) => {console.log(err)
            });
        })
        console.log('删除文件 DONE')
    }
}

module.exports = FileShaking;

以上代码已放到我的 github: https://github.com/Rockergmai…

后果

  1. 在删除路由文件之后,跑一遍,失去后果:删除 162 个文件

残余的需要点,人工调整即可。

Reference

https://segmentfault.com/a/11…
https://github.com/Viyozc/use…

退出移动版