如何利用 console 输出有助于调试问题的日志?

3,290 阅读6分钟

前言

在我设计 structured-react-hook 的时候我就想要实现一款具有实际使用价值的 logger 插件, 这个想法最早来自于 redux-logger.

但 redux-logger 只能输出通过 action 触发的一些状态变更, 在实际开发中, 状态是一个应用的核心没错, 但组成应用的最终要元素是函数, 而不是状态, 状态只是起到了控制作用, 大量的控制逻辑其实都是在函数中完成的. 为此在使用了一段 redux-logger, 我一直在思考如何能写出一个更贴合实际场景的日志输出插件.

正文

一个贴合真实开发场景的日志调试插件要具备哪些功能?

在我的设想中, 一个能够在实际开发中起到作用的日志调试插件, 应该具备这些能力

  • 输出类似函数调用堆栈的结构化信息
  • 能够区分不同函数的功能和类型
  • 能够通过某种方式直接定位到函数源码进行调试
  • 信息友好的. 不杂乱无章
  • 业务代码无侵入, 日志自动化

例如在防御性编程中会提到, 对于一些异常一定要 catch 住, 并 throw 出去, 在 Java 中对于 IO 异常规定是必须处理的.

通过抛出异常可以实现上述部分的效果, 比如函数堆栈信息和定位到源码.

不上图了, 写过 try catch 的应该都知道. 不知道自己试试

但这种方式没有达成其余的要求, 手动 try catch 会侵入业务代码, 并且 catch 块会影响作用域, 通常业务逻辑上的问题未必是 JavaScript Error, 这两者并不等价.

其次非自动化的, 堆栈信息中包含大量无用的函数信息.

为此, 需要重新构思一种新的方式.

在阐述这个思考过程之前, 我先展示下成品吧.

srhLogger 演示.gif

附上 Demo 的代码

import "./styles.css";
import { createStore } from "structured-react-hook";
import { srhLogger } from "srh-plugins-logger";

const storeConfig = {
  initState: {
    h1Text: "Hello CodeSandbox",
    h2Text: "Start editing to see some magic happen!"
  },
  service: {
    transform(text) {
      this.rc.setH1Text(text + ` 修改时间: ${new Date(new Date().getTime())}`);
    }
  },
  controller: {
    onH1TextChange() {
      this.service.transform("标题发生了变动");
    }
  }
};
const useStore = createStore(storeConfig, [srhLogger]);
export default function App() {
  const store = useStore();
  return (
    <div className="App">
      <h1>{store.state.h1Text}</h1>
      <h2>{store.state.h2Text}</h2>
      <button onClick={store.controller.onH1TextChange}>
        {" "}
        修改标题并加上时间戳{" "}
      </button>
    </div>
  );
}

codeSandBox 地址: codesandbox.io/s/admiring-…

掘金啥时候支持 playground 就好了

接下来回到本文的主题, 如果单纯自己实现这样一款插件要如何做到?

日志分析的起点 → 函数调用入口

如果你曾经写过类似 C++ 这样的编译语言, 应该对 main 函数都会有印象, 在程序启动之前, 都要求必须实现一个 main 函数作为程序执行的入口.

但 JavaScript 运行环境的一部分, 并不强调 JavaScript 程序必须从某个入口开始, 通常默认就是 JavaScript 脚本加载的第一行.

这在过去没啥大问题, 那时候的 JavaScript 主要完成一些辅助编程, 脚本通常都很短小, 也没有复杂的模块依赖关系, 更不要提类似 webpack babel 这样的工具介入了.

这导致单纯的通过执行引擎提供的堆栈信息很难快速定位到想要调试的函数位置.

加上并发和异步逻辑的存在, 很多函数调用的入口都变得模糊起来. 即便是自己写的代码, 可能过几个月也已经分不清哪到哪了.

但这还不是最糟糕的, 更糟糕的还在后面

函数调用关系无法预测

自从 redux 演示了时间旅行, flux 单向数据流的概念就已经深入人心, 但实际开发中, 使用 React 开发的代码在逻辑上几乎很难完全实现单向, 并且边界模糊导致即便数据流是单向的, 但是函调调用却未必.

对于 React 来说, setState 和 props 都可以导致视图发生变更, 对于一个复杂的 SPA 应用, 我们是无法预测, 视图变更就行是来自与 setState 还是 props 又或者是某个 event bus 传递过来的强制更新?

如果把 视图 控制视图的逻辑 都看成函数, 在实际开发中存在大量类似这样的关系

  • 视图 → 视图
  • 控制函数 → 视图
  • 接口调用 → 视图

如果我们单纯的把所有的函数调用都打印出来, 光从信息上我们是无法推测他们之间的调用关系. 也不能凭借顺序来判断是谁调用了谁.

函数类型不明确

在 classComponent 时代, 一个 classComponent 存在大量的方法, 这些方法的命名五花八门, 其职责也一言难尽, 例如这样的代码


class App extends Component{
  getElement(){
      return (<div></div>)
  }
  transfrom(){}
  click(){}
  render(){
      
  }
}

这些方法都属于 App, 除掉 React 自己的生命周期, 唯一能识别的, 可能就是 render 方法.

但随着时间推移, 很快你就会发现一个 class 里存在大量的无法理解的方法. 命名和逻辑很容易产生偏差.

想要输出结构化的日志, 必须让函数们结构化起来

关于函数结构化我在之前的文章里有提到, 简单讲就是将函数分门别类的管理起来, 例如示例代码中的 controller service, 同时为了连接到 react 的状态, 我设计了 structured-react-hook.

日志工具本质上是一个信息输出端, 输出信息的质量取决于信息的结构化是否合理.

利用 srh 我将 react 中的函数进行了结构化分类, 然后借鉴了 redux 中 中间件实现方式, 利用 reduce 函数将 redux 的中间件转换成了 srh 中的插件.

简单来讲, 我给所有的结构化函数都包裹上了一层函数, 就像高阶函数那样, 在函数调用前输出日志, 并且根据结构化标签给每个函数加上一个标志,用来区分.

同时利用 srh 内部的限制, 将函数的执行过程限制成可预测的单向过程

view → controller → service → reducer

这样在日志输出的时候就形成了顺序的输出链条. 加上 chrome 可以通过函数字符串定位到源码的功能就补上了最后一个环节.

一些尚未实现的难点

目前日志输出是同步的, 所以无法准确获取异步函数返回的结果, 不过好在 dispatch 本身是同步的, 倒也不会丢失最终结果, 不过我想如果能够在日志上进一步体现出并行和串行的函数调用链就更完美了, 如果你有更好的想法可以联系我, 也可以直接给我的插件的 git 仓库提 PR.

附上 srhLogger 的 Git 地址: github.com/kinop112365…

代码很简单, 欢迎交流👏🏻👏🏻

这篇文章是被诅咒了么?