前端工程化-使用自定义的console loader 增强控制台打印功能

需求背景

在前端开发中,开发环境下很常见的调试方法就是使用console.log,但是随着项目规模的扩大和复杂度的上升,console.log的内容需要写的比较详细才能定位到触发的调用点。为了减少不必要的心智负担和源码和控制台,页面之间的无效切换,希望能增强console的功能,让开发人员只需要书写console.log()就能在控制台中定位到具体文件中的具体行的代码。

技术选型

目前常用的前端工程化打包工具有webpack,vite等,本文基于webpack实现开发demo。

前置知识

编译原理和babel

编译器工作的原理是这样的:

  1. 通过词法分析,生成token序列,在这一步会去掉无效的缩进,空格和注释。
  2. 通过语法分析,生成AST抽象语法书
  3. 通过语义分析,验证AST是否符合预期。
  4. 中间代码生成:生成一种中间表示形式,能够简化目标代码的生成和优化过程,JS生成的是字节码。
  5. 中间代码优化:对中间代码进行一系列的转换和优化,以提高代码的执行效率。
  6. 目标代码生成:将中间代码转换为目标平台上的机器代码,包括汇编代码和 ELF 文件等,比如JS会生成对应的机器码。

而babel是一个js的编译器,用于将ES6+的代码经过编译生成大多数浏览器可以识别的ES5代码。 比较重要的有以下的库:

  1. babel/types:用来生成或者判断节点的AST语法树的节点
  2. babel/core: babel核心模块

也就是说,我们的工作需要在拿到老的代码,老代码生成AST后,对AST进行操作,生成新的代码,而对AST的具体操作也就是去增强console的功能。

必备工具

  • AST explorer 使用该工具我们可以输入代码,查看生成的AST,鼠标选中某一块代码,会定位到AST书的具体节点
  • 使用fs.appendFileSync()输出打包过程的细节 工程中的调试是必不可少的,但是在webpack成功编译后,会把打包过程中控制台的内容清除,只保留了编译成功及细节。

手很快才截到的,打印转换后的code的图:

最终稳定的控制台输出:

而且代码文件一般内容不少,直接打印也不是一个好方法,所以需要把输出的内容进行存储。 有以下两个方案:

  1. 输出到浏览器的缓存中,localstorage或者indexDB,但是打包过程的工具没有涉及到浏览器,无法在webpack打包工作过程中向浏览器缓存写入内容。有知道这个方案怎么解到大佬么?
  2. 使用fs模块,把内容输出到文件系统中,在这里输出到code.txt文件。

工作细节

无论做什么事情,我习惯于先完成顶层设计,再倒推实现细节。顶层设计的步骤是,书写loader,在webpack的配置中使用该loader。 而书写loader又分为定义输入,进行转换,定义输出。使用loader重点在决定loader的位置。

书写loader

定义输入

我们知道loader的输入是上一个loader的输出,这里从上帝视角看我们知道这个loader的位置是需要放在babel-loader的左边的(在babel-loader执行后才会执行,将一系列不同类型的代码(jsx 高版本js等)都转化为ES5的js,这样我们在使用AST explorer验证AST的时候只需要使用纯ES5即可。所以在这里的输入是ES5的JS代码。

进行转换

转换这里使用babel-core提供的功能,核心代码如下:

js 复制代码
const targetSource = core.transform(sourceCode, {
    plugins: [logPlugin], //使用插件
    filename: fileName,
  });

其中fileName是文件名,而logPlugin是我们需要编写的插件

visitor用于在遍历树节点的时候,只要节点的 type 在 visitor 对象中出现,就会调用该方法。

观察树的结构可以知道,console表达式位于node.callee.object.name

而log函数内的内容位于argument这一层级

根据上述的分析写出如下代码:

js 复制代码
const logPlugin = {
  visitor: {
    CallExpression(path, state) {
      const { node } = path;
      if (types.isMemberExpression(node.callee)) {
        if (node.callee.object.name === "console") {
          //找到console
          if (
            ["log", "info", "warn", "error"].includes(node.callee.property.name)
          ) {
            //找到文件名
            const filename = state.file.opts.filename.split('"')[1];

            fs.appendFileSync(
              "/Users/zhengmingsheng/Code/gongchenghua1/src/loader/code.txt",
              "================================================\n" + filename
            );
            node.arguments.push(
              types.stringLiteral(`调用点位于${filename}文件`)
            ); //向右边添加我们的行和列信息

            const { line } = node.loc.start; //找到所处位置的行和列
            // 其实这个行数还是有问题的,编译后有一些额外的代码"var _jsxDevRuntime = require("react/jsx-dev-runtime");
            node.arguments.push(types.stringLiteral(`第${line}行`)); //向右边添加我们的行和列信息
          }
        }
      }
    },
  },
};

进行输出

js 复制代码
if (typeof targetSource.code === "string") {
    return targetSource.code;
  } else {
    return sourceCode;
  }

注意不能直接输出targetSource,它是一个object。

使用loader

和之前提到的一样,因为loader是从右到左执行的(从后到前)。

Console Loader完整代码

js 复制代码
const core = require("@babel/core"); //babel核心模块
let types = require("@babel/types"); //用来生成或者判断节点的AST语法树的节点
const fs = require("fs");

const logPlugin = {
  visitor: {
    CallExpression(path, state) {
      const { node } = path;
      if (types.isMemberExpression(node.callee)) {
        if (node.callee.object.name === "console") {
          //找到console
          if (
            ["log", "info", "warn", "error"].includes(node.callee.property.name)
          ) {
            //找到文件名
            const filename = state.file.opts.filename.split('"')[1];

            fs.appendFileSync(
              "/Users/zhengmingsheng/Code/gongchenghua1/src/loader/code.txt",
              "================================================\n" + filename
            );
            node.arguments.push(
              types.stringLiteral(`调用点位于${filename}文件`)
            ); //向右边添加我们的行和列信息

            const { line } = node.loc.start; //找到所处位置的行和列
            // 其实这个行数还是有问题的,编译后有一些额外的代码"var _jsxDevRuntime = require("react/jsx-dev-runtime");
            node.arguments.push(types.stringLiteral(`第${line}行`)); //向右边添加我们的行和列信息
          }
        }
      }
    },
  },
};

module.exports = function (sourceCode) {
  sourceCode = String(sourceCode);
  let fileName = null;
  if (sourceCode.indexOf("_jsxFileName") !== -1) {
    fileName = sourceCode.match(/\"\S+\"/)[0];
  } else {
    fileName = "default";
  }
  const targetSource = core.transform(sourceCode, {
    plugins: [logPlugin], //使用插件
    filename: fileName,
  });
  
  fs.appendFileSync(
    "/Users/zhengmingsheng/Code/gongchenghua1/src/loader/code.txt",
    "================================================\n" + targetSource.code
  );

  if (typeof targetSource.code === "string") {
    return targetSource.code;
  } else {
    return sourceCode;
  }
};

完整项目工程地址:826625219/demo-consoleLoader: Write a loader to enhance the function of console, including the location of the call point. (github.com)

希望能帮到你~

相关推荐
会发光的猪。10 分钟前
【 ElementUI 组件Steps 步骤条使用新手详细教程】
前端·javascript·vue.js·elementui·前端框架
我家媳妇儿萌哒哒10 分钟前
el-table合并单元格之后,再进行隔行换色的且覆盖表格行鼠标移入的背景色的实现
前端·javascript·elementui
baiduguoyun26 分钟前
react的import 导入语句中的特殊符号
前端·react.js
前端青山27 分钟前
webpack指南
开发语言·前端·javascript·webpack·前端框架
NiNg_1_23439 分钟前
ECharts实现数据可视化入门详解
前端·信息可视化·echarts
励志前端小黑哥1 小时前
有了Miniconda,再也不用担心nodejs、python、go的版本问题了
前端·python
喵叔哟1 小时前
重构代码之取消临时字段
java·前端·重构
还是大剑师兰特2 小时前
D3的竞品有哪些,D3的优势,D3和echarts的对比
前端·javascript·echarts
王解2 小时前
【深度解析】CSS工程化全攻略(1)
前端·css
一只小白菜~2 小时前
web浏览器环境下使用window.open()打开PDF文件不是预览,而是下载文件?
前端·javascript·pdf·windowopen预览pdf