前端工程化-之构建工具和AST

38 阅读7分钟

构建工具篇

1.构建工具在工程化中的定位 【联想vue中高级语法糖转换】

构建工具在工程化中扮演着至关重要的角色,它能够自动化处理项目开发、测试、部署等多个环节中的重复性任务,极大地提升开发效率和代码质量。以下从多个方面介绍构建工具的作用:

代码转换与编译

  • 语法兼容:不同浏览器和运行环境对 JavaScript、CSS 等语言的支持程度有所差异。构建工具可以将使用新语法编写的代码转换为兼容性更好的旧版本语法。例如,Babel 可以把 ES6+ 代码编译成能在旧浏览器中运行的 ES5 代码;
  • 代码优化:构建工具能够对代码进行压缩和混淆。在 JavaScript 中,可以去除代码中的多余空格、注释,缩短变量名,从而减小文件体积,加快代码加载速度。

资源合并与压缩

  • 文件合并:在项目中,通常会有多个 JavaScript 文件和 CSS 文件。构建工具可以将这些分散的文件合并成一个或少数几个文件,减少浏览器的 HTTP 请求次数,从而提升页面加载性能。例如,在大型 Web 应用中,将多个 JavaScript 模块合并成一个 main.js 文件,将多个 CSS 文件合并成一个 style.css 文件。

自动化任务执行

  • 实时监听与自动刷新:构建工具可以实时监听文件的变化,当文件发生修改时,自动触发相应的任务,如重新编译代码、刷新浏览器等。例如,使用 vite Webpack 的热更新功能,在开发过程中修改代码后,浏览器会自动刷新页面,展示最新的修改结果,无需手动刷新。

依赖管理

  • 包管理:构建工具通常与包管理工具(如 npm、yarn,pnpm)集成,帮助管理项目的依赖项。它可以自动下载、安装和更新项目所需的各种库和框架,并确保依赖项的版本兼容性。
  • 模块打包:在现代前端开发中,项目通常采用模块化开发的方式。构建工具可以将各个模块打包成一个或多个文件,解决模块之间的依赖关系。例如,Webpack 可以分析项目中的模块依赖关系,将所有模块打包成一个或多个 bundle 文件,方便在浏览器中加载。

2.打包工具方案大乱斗: vite,webpack,tsup,rollup,esbuild...

目前使用最广泛的打包工具还是基于vite,社区非常活跃,打包构建非常快,包产物丰富。 houbb.github.io/2024/05/07/…

特性ViteWebpackParcelesbuildSnowpackRollupTurbo
启动速度极快(基于原生 ES 模块,开发时无需打包)较慢(需要打包过程)快(无需配置,快速启动)极快(专注于性能)快(原生模块加载,快速启动)快(特别适合构建类库)快(增量构建、缓存优化)
开发体验快速热更新,模块热替换(HMR)热更新支持,配置复杂零配置,快速热更新快速编译和构建,支持 HMR快速、零配置,使用 ES 模块支持现代模块和树摇适合大规模 monorepo 项目,增量构建和缓存
配置复杂度低(开箱即用,易于使用)高(灵活配置,功能强大但较复杂)低(零配置,自动化)低(使用简便,快速配置)低(无配置)低(主要用于库构建,配置简单)低(专注于 monorepo 和增量构建)
打包模式按需加载,开发时不打包,生产时使用 Rollup 打包静态打包,所有文件统一打包打包时自动优化,零配置打包时使用高效的优化策略无打包,直接使用原生模块加载,生产时需要构建打包,优化输出文件体积通过缓存和增量构建优化打包过程
性能优化极高(热更新、按需加载、快速构建)树摇、代码分割、各种优化插件快速编译和构建,但性能略逊于 Vite非常快速,且支持高效的压缩和树摇快速(原生模块,减少构建过程)小巧、优化的输出文件,支持树摇(Tree-shaking)高效的增量构建和缓存机制
生态支持强大(插件丰富,逐步发展中)最强(广泛的插件和工具支持)中等(插件支持较少,但增长迅速)中等(主要针对性能优化,插件生态较小)中等(支持许多现代 Web 工具)强(专注于库和小型项目,支持 ES 模块)强(专注于 monorepo 和企业级应用)
支持的语言/格式支持现代 JavaScript、TypeScript、Vue、React 等支持几乎所有语言和框架(通过 loader 插件)支持 JavaScript、TypeScript、CSS、HTML 等支持 JavaScript、TypeScript、JSX、CSS 等支持 JavaScript、TypeScript、CSS、HTML 等支持 JavaScript、TypeScript、JSX、CSS 等支持 JavaScript、TypeScript、JSON、YAML 等
适用场景现代 Web 应用、前端框架(Vue、React 等)大型复杂应用,需高度定制化中小型项目,快速开发快速构建和编译,尤其适合 TypeScript 和 JSX开发时希望尽可能简化配置和构建过程的项目构建 JavaScript 库或小型 Web 应用大型项目、monorepo 或分布式架构
增量构建支持支持(但配置复杂)支持支持支持支持强(优化了增量构建和缓存)
支持热更新(HMR)支持支持支持支持支持支持支持
社区和文档支持非常活跃,文档清晰非常活跃,文档全面快速增长,文档相对简洁社区较小,但文档简洁易懂较小,但文档简单明了非常活跃,文档良好非常活跃,特别是针对 monorepo 和 CI/CD 设计

Vite: 提供极快的启动和开发体验,适用于现代 Web 应用,尤其是 Vue 和 React 项目,配置简单,热更新迅速。

Webpack: 最强大的构建工具,适用于复杂的项目,支持高度定制,但配置较为复杂。

Parcel: 零配置工具,适用于小型到中型项目,快速开发,但性能略逊于 Vite。

esbuild: 以极快的速度进行编译和打包,适合需要快速构建的 TypeScript 项目。

Snowpack: 基于原生 ES 模块,适合快速构建并减少打包过程,适合小型项目。

Rollup: 主要用于 JavaScript 库的构建,生成优化的小体积包,适合类库开发。

Turbo: 专注于大型 monorepo 项目,增量构建和缓存优化是其特色,适合企业级应用。

选择合适的构建工具通常取决于项目的需求和复杂性

3.编译打包代码转换的核心AST-以webpack中babel为例 代码演示

//ast 转换过程 source=> parser => transform => generate =>target
console.log('Happy developing ✨');
const fs = require('fs');
const babel = require('@babel/core');
const t = require('@babel/types');
const parser = require('@babel/parser');

// 定义一个简单的 JavaScript 代码字符串
let sourceCode = `const x = 5 * 6; 
    const getinfo=(a, b,... args) => {
    console.log("执行行号",args);
    return a + b;
    }
    
    getinfo(1, 2);
    `;

// 定义一个 Babel 插件,用于转换 AST
const addToMultiplyPlugin = {
    visitor: {
        BinaryExpression(path) {
            console.log("进入 BinaryExpression 访问器");
            console.log("当前 BinaryExpression 节点信息:", path.node);
            // 检查是否为乘法操作符
            if (path.node.operator === '*') {
                console.log("检测到乘法操作符 '*',准备将其替换为 '+'");
                // 将操作符从乘法替换为加法
                path.node.operator = '+';
                console.log("操作符已替换为 '+',当前节点信息:", path.node);
            } else {
                console.log(`当前操作符是 '${path.node.operator}',不进行替换`);
            }
        },
        VariableDeclaration(path) {
            console.log("进入 VariableDeclaration 访问器");
            console.log("当前 VariableDeclaration 节点信息:", path.node);
            // 检查变量声明的类型是否为 const
            if (path.node.kind === 'const') {
                console.log("检测到 'const' 声明,准备将其替换为 'let'");
                // 将变量声明类型从 const 替换为 let
                path.node.kind = 'let';
                console.log("声明类型已替换为 'let',当前节点信息:", path.node);
            } else {
                console.log(`当前声明类型是 '${path.node.kind}',不进行替换`);
            }
        },
        FunctionDeclaration(path) {
            console.log("进入 FunctionDeclaration 访问器");
            console.log("当前 FunctionDeclaration 节点信息:", path.node);
        },
        ArrowFunctionExpression(path) {
            console.log("进入 ArrowFunctionExpression 访问器");
            console.log("当前 ArrowFunctionExpression 节点信息:", path.node);
        },
        CallExpression(path) {
            console.log("当前 CallExpression 节点信息:", path.node);
            console.log("开始检查是否为 getinfo 函数调用");

            if (isGetInfoFunctionCall(path)) {
                console.log("检测到 getinfo 函数调用");
                // replaceGetInfoFunctionBody(path);
                replaceGetInfoCallArguments(path);
            }

            console.log("getinfo 函数调用检查结束");
        }
    }
};

// 检查是否为 getinfo 函数调用
function isGetInfoFunctionCall(path) {
    return t.isIdentifier(path.node.callee) && path.node.callee.name === 'getinfo';
}

// 替换 getinfo 函数的函数体为 return 1;
function replaceGetInfoFunctionBody(path) {
    const scope = path.scope;
    const binding = scope.getBinding('getinfo');

    if (!binding) return;

    const defPath = binding.path;
    if (!isValidGetInfoDefinition(defPath)) return;

    const returnStatement = t.returnStatement(t.numericLiteral(1));
    defPath.node.init.body = t.blockStatement([returnStatement]);
}

// 检查是否是有效的 getinfo 函数定义
function isValidGetInfoDefinition(defPath) {
    return t.isVariableDeclarator(defPath) && t.isArrowFunctionExpression(defPath.node.init);
}

// 替换 getinfo 函数调用的参数
function replaceGetInfoCallArguments(path) {
    // 这里可以根据需求修改替换的参数,这里简单替换为 10 和 20
    // const [line,column]=path.node.loc.start;
    const line=path.node.loc.start.line;
    const column=path.node.loc.start.column;

    // console.log("column,line",column,line)
    const newArgs = [t.numericLiteral(10), t.numericLiteral(20), t.stringLiteral(`${line}:${column}`)];
    path.node.arguments = newArgs;
    console.log("getinfo 函数调用参数已替换为", newArgs);
}

const ast = parser.parse(sourceCode);

console.log("ast----", ast);

// 使用 Babel 转换代码
const { code } = babel.transformSync(sourceCode, {
    plugins: [addToMultiplyPlugin]
});

console.log('原始代码:', sourceCode);
console.log('修改后的代码:', code);

// 将转换后的代码写入 target.js 文件
fs.writeFile('target.js', code, 'utf8', (err) => {
    if (err) {
        console.error('写入文件时出错:', err);
    } else {
        console.log('转换后的代码已成功写入 target.js 文件');
    }
});