通过分析 AST 自动重构 three.js 的老旧代码

2019-03-19 10:37:03 +08:00
 hujiulong

本文同步于我的 github 博客

前言

先简单介绍一些背景: three.js是一个非常流行的 JS 三维渲染库,通常是做 web 端三维效果的第一选择。但是同时 three.js 已经有了将近 9 年的历史,所有它很多代码仍然是使用非常老旧的模式。

three.js 曾经所有的文件都是使用全局变量THREE的方式来组织,比如欧拉角Euler.js

// three.js/src/math/Euler.js
THREE.Euler = function ( x, y, z, order ) {
  this._x = x || 0;
  this._y = y || 0;
  this._z = z || 0;
  this._order = order || THREE.Euler.DefaultOrder;
};

在经历几次重构以后,three.js 的核心代码已经完全迁移成用 ES6 Module 来组织了,直接通过export { Euler }来输出变量。

但是在核心代码以外,仍然有大量非常常用的代码使用这种老旧方式来组织,比如所有的模型加载器loaders,以及控制器controls。如果想直接import它们,需要自己手动去改成 ES6 Module 的形式,在我以前的一个项目vue-3d-model中,所有的 loaders 就是我手动修改的。

为什么要用 AST 来做

粗略看来这些老旧代码大多遵循一些特定的模式,例如很多都是以THREE.XX = xx的形式来输出变量,很容易想到用正则去处理它。 但是用正则匹配会遇到非常多的问题:

1.正则要求很严格,每一个字符都要写规则来匹配它 如果代码风格不统一,例如想匹配THREE.XX = xx这种代码,你写的正则必须要同时兼容THREE.XX=xx这种等号两边没有空格的情况。实践中还要处理各种特殊情况,非常麻烦。

2.很难避开注释中的代码 注释中也可能会出现你要匹配的字符串,会导致很多错误。

但是绕过代码本身,直接分析代码的抽象语法树(AST),这些问题就都迎刃而解了。 AST 是源代码语法结构的一种抽象表示,代码对应的 AST 和代码风格无关,多写一个空格少写一个分号都没关系,通过 AST 来查找代码节点也更加可靠,不必担心错误匹配到别的代码,像 eslint,webpack 之类的工具都是通过分析 AST 来处理代码的。

JS 的 AST 已经形成了一套规范,具体可以看这个文档

生成 AST 的工具也有很多,我选择的是acorn

找出输出语句

输出语句大多是直接给全局变量 THREE 赋值的,例如这样前言中说的 Euler.js ,我们期望将这样的代码:

THREE.Euler = function() { /* ... */ };

转换成:

const Euler = function() { /* ... */ };
export { Euler };

可以看到输出语句大都是THREE.XX = xx的形式,后面的xx可能是一个类、变量、函数或别的什么东西,总的来说它是一个赋值语句。 先抛开要处理的代码,我们来看一个简单的给属性赋值语句代码对应的 AST 是什么样的。

THREE.A = 1;

通过acorn.parse(code)可以得到 AST:

{
  "type": "AssignmentExpression",
  "start": 1,
  "end": 12,
  "operator": "=",
  "left": {
    "type": "MemberExpression",
    "start": 1,
    "end": 8,
    "object": {
      "type": "Identifier",
      "start": 1,
      "end": 6,
      "name": "THREE"
    },
    "property": {
      "type": "Identifier",
      "start": 7,
      "end": 8,
      "name": "A"
    },
    "computed": false
  },
  "right": {
    "type": "Literal",
    "start": 11,
    "end": 12,
    "value": 1,
    "raw": "1"
  }
}

简单分析一下: 首先整个节点的type"AssignmentExpression",表示它是一个赋值表达式,里面的startend是源代码中对应的位置,leftright即表达式左边和右边的值,也就是被赋值的变量和赋值的值。 lefttype"MemberExpression",即成员表达式,也就是A.B的形式的代码,也可以看到它所属的object的名称为THREE。 而righttype"Literal",即字面量,其实我们并不关心right,它可能是字面量,也可能是函数、对象或别的东西。

到这里我们的目标就变得明确了,我们只需要找到所有的"AssignmentExpression",并且它的left"MemberExpression",且nameTHREE

接下来就可以处理所有代码了,遍历每个文件并得到它们的 AST,然后使用acorn/walk遍历 AST 所有的节点,就可以知道每个文件都输出了什么。

walk.simple( ast, {
  AssignmentExpression: ( node ) => {
    if (node.left.type === 'MemberExpression' &&
      node.left.object.name === 'THREE') {
      const { start, end, property } = node.left;
      code.overwrite( start, end, `const ${property.name}` );  // 将 THREE.XX = xx 替换为 const XX = xx
      exportVars.push(property.name);  // 将输出的变量保存,最后 export 它们
    }
  }
})

这样最后我们得到了所有的输出变量,就可以在文件末尾 export 它们。

处理依赖

除了找到输出的变量,我们还需要处理文件的依赖。值得高兴的是 THREE 所有文件都没有任何外部依赖,所有的依赖情况只有两种: 1.依赖 three.js 的核心库 2.依赖别的需要转化的文件

比如文件中有这样一段代码

const v = new THREE.Vector3();
const loader = new THREE.OBJLoader();

我们期望的转化后的文件应该是这样:

import { Vector3 } from 'three';
import { OBJLoader } from '../loader/OBJLoader.js';
const v = new Vector3();
const loader = new OBJLoader();

我们先找出代码中所有有依赖的地方,这两种依赖情况都是获取 THREE 中的一个值,所以只要像处理输出语句那样找到所有nameTHREEMemberExpression节点就可以了。

walk.simple( ast, {
  MemberExpression: node => {
    const { object, property } = node;
    if ( object.name === 'THREE' && property.type === 'Identifier' ) {
      code.overwrite(object.start, object.end + 1, ''); // 将代码中的 THREE.XX 替换为 XX
      dependences.push( property.name );  // 得到依赖
    }
  }
})

得到所有依赖的名称后,通过判断 three 的核心库中是否包含这个值,就可以知道它是位于 three 中还是别的文件中,然后通过计算文件之间的相对位置,可以得到依赖文件的地址。

后话

转换实际情况要更加复杂一点,但是基本都可以通过 AST 来做正确的替换,通过这种方式我处理了将近 300 个文件,只有很少的一部分需要再手动修改一下。 另外 three.js 目前实现类的方式都还是 ES5 时代的 function 的方式,后面会通过各种方式来将它们批量转换成 ES6 的 class,这中间肯定也需要用到 AST。

相关代码:

本文同步于我的 github 博客,欢迎订阅

2400 次点击
所在节点    前端开发
2 条回复
dandycheung
2019-03-19 10:41:15 +08:00
赞。
fy
2019-03-19 10:43:03 +08:00
哇 大佬

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/546066

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX