压缩JavaScript代码时修改语法树

一、背景和意义

前端JavaScript发布时,一般要对代码进行压缩。在做代码压缩时,可以考虑增加一些额外的操作,比如移除测试与调试使用的代码,补充必要的日志信息等。这些操作可以通过修改js语法树实现,文本以uglify-js为示例实现JavaScript语法树修改操作。

二、压缩代码时移除debug日志

console.debug打印的日志一般仅在本地调试时使用,发布之后可以考虑移除,以避免日志太多。这里给出一个示例代码:

JavaScript 复制代码
const uglifyjs = require("uglify-js");

// 将某个字符串从startPos到endPos之间的内容替换为replacement参数的内容
function spliceString(str, startPos, endPos, replacement) {
    return str.substr(0, startPos) + replacement + str.substr(endPos);
}

// 替换代码,做移除调试代码等操作
function replaceCode(code) {
    const nodes = [];
    uglifyjs.parse(code).walk(new uglifyjs.TreeWalker(node => {
        if (node instanceof uglifyjs.AST_Call
            && node.expression.print_to_string().endsWith('console.debug')) {
            nodes.push(node);
        }
    }));
    for (let i = nodes.length; --i >= 0;) {
        const node = nodes[i];
        code = spliceString(code, node.start.pos, node.end.endpos, "");
    }
    return code;
}


// 被用于测试的JS代码
const func = `
function helloWorld() {
    console.debug("helloWorld arguments: ", arguments);
    console.log("helloWorld");
}
helloWorld();
`;

const revisedCode = replaceCode(func);
console.log("修改语法树之后的代码:\n", revisedCode);
console.log("压缩之后的代码:\n", uglifyjs.minify(revisedCode).code);

上述代码保存为文件compress.js,然后运行如下命令安装uglify-js依赖(需要先安装nodejs):

bash 复制代码
npm install uglify-js

再运行compress.js文件:

bash 复制代码
node compress.js

运行结果如下:

可以看到,console.debug调用被删掉了。

三、压缩代码时给日志操作增加时间输出

console.log等日志操作不打印时间,如果想修改console.log等方法的调用代码,增加时间输出以方便了解各个日志的触发时间,这也可以通过uglify-js实现。将前面的compress.js代码修改为:

JavaScript 复制代码
const uglifyjs = require("uglify-js");

// 将某个字符串从startPos到endPos之间的内容替换为replacement参数的内容
function spliceString(str, startPos, endPos, replacement) {
    return str.substr(0, startPos) + replacement + str.substr(endPos);
}

// 解析一段指定的JavaScript代码,并遍历其中的语法树节点
function parseAndTraverseNode(codeStr, handler) {
    uglifyjs.parse(codeStr).walk(new uglifyjs.TreeWalker(handler));
}

// 替换代码,做移除调试代码等操作
function replaceCode(code) {
    // 将"new Date()"这一段代码转换成语法树对象,后面要用到
    let newDateNode = null;
    parseAndTraverseNode("`${new Date()}: `", n => {
        if (!(n instanceof uglifyjs.AST_Toplevel || n instanceof uglifyjs.AST_SimpleStatement)
            && newDateNode === null) {
            newDateNode = n;
        }
    });

    const nodes = [];
    parseAndTraverseNode(code, node => {
        if (node instanceof uglifyjs.AST_Call) {
            const nodeStr = node.expression.print_to_string();
            const startPos = node.start.pos;
            const endPos = node.end.endpos;
            if (nodeStr.endsWith('console.debug')) {
                nodes.push({replacement: "", startPos, endPos});
            } else if (nodeStr.match(/console\.(log|info|warn|error)+$/)) {
                // 在console.log、console.warn等方法调用增加一个new Date()入参
                node.args.unshift(newDateNode);
                replacement = node.print_to_string({ beautify: true });
                nodes.push({replacement, startPos, endPos});
            }
        }
    });
    for (let i = nodes.length; --i >= 0;) {
        const node = nodes[i];
        code = spliceString(code, node.startPos, node.endPos, node.replacement);
    }
    return code;
}


// 被用于测试的JS代码
const func = `
function helloWorld() {
    console.debug("helloWorld arguments: ", arguments);
    console.log("helloWorld");
}
helloWorld();
`;

const revisedCode = replaceCode(func);
console.log("原始代码:\n", func);
console.log("修改语法树之后的代码:\n", revisedCode);
console.log("压缩之后的代码:\n", uglifyjs.minify(revisedCode).code);

运行结果如下:

可以看到,console.log("helloWorld")调用在最前面插入了一个new Date(),其他参数往后移。

四、移除单元测试代码

假设约定所有单元测试函数名以UnitTest结尾,在压缩代码时,可以移除这些单元测试函数以及对这些函数的调用,将前面的compress.js代码修改为:

JavaScript 复制代码
const uglifyjs = require("uglify-js");

// 将某个字符串从startPos到endPos之间的内容替换为replacement参数的内容
function spliceString(str, startPos, endPos, replacement) {
    return str.substr(0, startPos) + replacement + str.substr(endPos);
}

// 解析一段指定的JavaScript代码,并遍历其中的语法树节点
function parseAndTraverseNode(codeStr, handler) {
    uglifyjs.parse(codeStr).walk(new uglifyjs.TreeWalker(handler));
}

// 替换代码,做移除调试代码等操作
function replaceCode(code) {
    // 将"new Date()"这一段代码转换成语法树对象,后面要用到
    let newDateNode = null;
    parseAndTraverseNode("`${new Date()}: `", n => {
        if (!(n instanceof uglifyjs.AST_Toplevel || n instanceof uglifyjs.AST_SimpleStatement)
            && newDateNode === null) {
            newDateNode = n;
        }
    });

    const nodes = [];
    parseAndTraverseNode(code, node => {
        const startPos = node.start.pos;
        const endPos = node.end.endpos;
        if (nodes.filter(i => i.startPos <= startPos && i.endPos >= endPos).length > 0) {
            return;   // 如果前面的某一个节点的替换操作已经覆盖了当前节点,那么当前节点就不需要再处理
        }
        if (node instanceof uglifyjs.AST_Call) {
            const nodeStr = node.expression.print_to_string();
            if (nodeStr.endsWith('console.debug') || nodeStr.endsWith("UnitTest")) {
                nodes.push({replacement: "", startPos, endPos});
            } else if (nodeStr.match(/console\.(log|info|warn|error)+$/)) {
                // 在console.log、console.warn等方法调用增加一个new Date()入参
                node.args.unshift(newDateNode);
                replacement = node.print_to_string({ beautify: true });
                nodes.push({replacement, startPos, endPos});
            }
        } else if (node instanceof uglifyjs.AST_Defun) {
            if (node.name?.name?.endsWith("UnitTest")) {
                nodes.push({replacement: "", startPos, endPos});
            }
        }
    });
    for (let i = nodes.length; --i >= 0;) {
        const node = nodes[i];
        code = spliceString(code, node.startPos, node.endPos, node.replacement);
    }
    return code;
}


// 被用于测试的JS代码
const func = `
function helloWorld() {
    console.debug("helloWorld arguments: ", arguments);
    console.log("helloWorld");
}
function round2UnitTest(n) {  /* 对一个数进行四舍五入操作,保留两位小数 */
    const ret = Math.round(n * 100) / 100;
    console.log("round2UnitTest input:", n, ", result:", ret);
}

helloWorld();
round2UnitTest(1.5379);
`;

const revisedCode = replaceCode(func);
console.log("原始代码:\n", func);
console.log("修改语法树之后的代码:\n", revisedCode);
console.log("压缩之后的代码:\n", uglifyjs.minify(revisedCode).code);

运行结果如下:

可以看到,round2UnitTest函数及其调用都被移除掉了。

相关推荐
大模型码小白31 分钟前
数据可视化:AI 生成 HTML5 动态交互式数据图表
前端·数据库·人工智能·深度学习·机器学习·信息可视化·html5
八荒启·交互动画32 分钟前
Web特效01—什么是渲染
前端·javascript·网页特效·八荒启-交互动画
小肥君35 分钟前
前端测试websocket
前端·websocket·状态模式
南京兴帝文化传媒有限公司1 小时前
基于地图平台的本地商户信息优化:药店夜间服务标注与客户转化实操
前端·javascript·数据库·人工智能·geo 优化·geo优化避坑·ai搜索获客
八荒启·交互动画1 小时前
Web特效04——GPUvs CPU,为什么图形计算要交给GPU,什么是“并行计算”
前端
呃呃呃呃ex1 小时前
2026年古法编程的末法时代,如何评估自己完成迅速转行
前端·后端
南京兴帝文化传媒有限公司1 小时前
地图SEO与AI搜索优化结合实践:宁国摄影工作室本地获客案例分析
大数据·前端·人工智能·geo 优化·geo优化避坑
用户921080262861 小时前
前端 Vue 专栏 07:模板编译、虚拟 DOM、Diff 与 key
前端
光影少年1 小时前
react navite process.nextTick 和 Promise.then 的执行顺序
前端·react native·react.js
雪芽蓝域zzs1 小时前
第四十二节:全局字典封装(后端字典,下拉选择复用)
开发语言·前端·javascript