通过分析 AST 自动重构 three.js 的老旧代码 - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
hujiulong
V2EX    前端开发

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

  •  
  •   hujiulong 2019-03-19 10:37:03 +08:00 2812 次点击
    这是一个创建于 2482 天前的主题,其中的信息可能已经有所发展或是发生改变。

    本文同步于我的 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 博客,欢迎订阅

    2 条回复    2019-03-19 10:43:03 +08:00
    dandycheung
        1
    dandycheung  
       2019-03-19 10:41:15 +08:00 via Android
    赞。
    fy
        2
    fy  
       2019-03-19 10:43:03 +08:00
    哇 大佬
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     5572 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 26ms UTC 01:44 PVG 09:44 LAX 17:44 JFK 20:44
    Do have faith in what you're doing.
    ubao msn snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86