百度高级研发工程师梅旭光分享:TypeScript转PHP工具ts2php的开发与应用
前言

想到有这样的开发场景真是太酷了。今天晨读文章由百度@梅旭光提交分享。
@美旭光,现任百度搜索前端团队高级研发工程师,参与搜索组件渲染框架、多终端统一开发框架的研发。毕业于天津大学,拥有丰富的前端框架和前端组件的设计和实现经验,致力于改善Web产品的用户体验。摩托车爱好者。个人博客:meixg.com
正文从这里开始~~
我们实现了一个工具 ts2php,它可以将 TypeScript 文件转换为 PHP 文件,并支持大多数 TypeScript 语法。它的意义在于,同样的逻辑可以在前端(通过JS)和后端(通过PHP)执行。
另一个 X 安装项目?
为什么将 TypeScript 转换为 PHP?当然,因为 PHP 是世界上最好的语言。
抛开笑话不谈,技术必须为业务服务。之所以这样做,是因为业务需要,对业务有帮助。
我们目前的项目有以下特点:
针对这种情况,我们目前的解决方案就是将一段代码编译成两个文件,JS和PHP,分别运行在前端和后端。
在这种场景下,不可避免地需要在前后端同时执行一些逻辑。除了将DSL转换为PHP之外,业务逻辑还需要转换为PHP在后端执行。 ts2php 尝试将 TS 转换为 PHP 文件,作为后端执行业务逻辑的实践。
很多人可能会觉得直接重写成Nodejs不是更好吗?但这需要重构大量的代码。我相信每个程序员都有重构代码的冲动,但重构并不一定是一个好的项目决策。它会阻碍整个项目的迭代速度。重复 bug 之前已解决的内容。 Joel大师的博客里有这样一篇关于重构的文章,我觉得写得非常好:Things You Should Never Do, Part I[1]。
很多时候增量修改是更有效的方法。使用ts2php,我们可以逐步升级现有的架构。同时,由于项目代码可以用ts编写,后续节点迁移的成本也会小很多。
另外,我们还通过实践证明,虽然这个想法听起来有点标新立异,但确实是可行的。
为什么不是JS?
转换编程语言的困难之一是消除不同语言之间操作语法的不一致。不同语言之间的运算不能完全匹配,这就需要我们根据变量类型将相同的源语言运算符转换为不同的目标语言运算符。
我没有选择将JS转换为PHP,因为JS是弱类型语言,在编译阶段无法确定变量类型,因此在转换时无法选择运算符。例如JS中的+符号,当运算符两边都是数字时,执行加法;当两边都是字符串时,进行字符串连接。
console.log(123 + 456); // 579
console.log('123' + '456'): // '123456'
但在 PHP 中,+ 符号只执行加法:
echo 123 + 456; // 579
echo '123' + '456'; // 579
我们需要判断+两边何时都有字符串,并将其转换为.:
echo '123' . '456'; // '123456'
打字稿
为了在编译阶段确定变量类型,我们需要给JS添加一个类型。目前,在 JS 社区,有两种强类型解决方案,Typescript 和 Flow,但选择 TypeScript 其实并不难。 TypeScript的更新速度、生态、用户体验都相对较好。 Vue 在 3.0 中也将使用 TypeScript 进行重写 [2]。
越来越多的项目使用 Typescript 进行开发。我相信TypeScript不需要过多介绍。对于复杂项目的维护和开发非常有帮助。
我之前的文章:TypeScript编译流程和内部类介绍,希望对大家了解TypeScript的编译和内部类有所帮助。
进展如何?
ts2php 支持大部分 TS 语法,并且是开源的。使用方法和支持的语法特性列表可以在Github项目主页上找到:
目前TS转PHP的方案已经上线应用。我们的目标是实现真正的TS子集,以满足业务发展。
如何?
接下来介绍一下ts2php是如何实现的。
语言之间的转换是一个编译过程:将源语言转换为AST,然后根据AST生成目标语言。
所以首先我们需要获取 TypeScript 的 AST。
TypeScript 编译器 API
要在语言之间进行转换,首先要做的是获取源语言的 AST。你可以在 GitHub 上找到很多 JS 的解析器,但 TypeScript 目前基本上只能依赖官方 API。
目前,TypeScript 的 Compiler API[3] 还不是很稳定,但已经可以提供强大的功能,比如:
ts.createProgram
createProgram 方法可以根据多个输入文件创建一个 Program。程序是指一个编译单元,它由多个SourceFiles和一些编译选项组成。程序是类型系统和代码生成的一般入口点。将多个文件合并在一起是为了方便依赖性分析。
let program = ts.createProgram(fileNames, options);
源文件
上面我们说了Program是由多个SourceFile和一些编译选项组成的,那么SourceFile是什么? SourceFile本身是基本抽象语法树结构Node的扩展。它在抽象语法树的基础上增加了额外的接口,可用于获取源文件代码、源文件地址、标识符列表等功能。
我们可以使用program.getSourceFiles来获取Program中的所有SourceFiles并遍历它们。
for (const sourceFile of program.getSourceFiles()) {
}
类型检查器
我们选择 TypeScript 的主要原因之一是类型系统。 TypeScript 提供了 TypeChecker 来帮助我们在编译阶段确定变量的类型。我们可以从Program中获取对应的TypeChecker:
let checker = program.getTypeChecker();
TypeChecker是ts类型系统的核心。它负责计算不同文件中Symbol之间的关系,判断Symbol的类型,并生成语法诊断(即语法错误)。
TypeChecker 可以回答以下问题:
转换过程
使用TypeScript的Compiler API,ts2php的执行路径可以抽象为以下步骤:
生成程序。
获取类型检查器。
获取并迭代 sourceFiles。
使用 Transformer 来转换 sourceFile 的 AST。
遍历AST并使用emitter获取PHP代码。
欢迎感兴趣的同学看一下ts2php的代码。希望对TypeScript编译方法有一定的帮助。
API 对齐
对于Core JavaScript API,很多方法在PHP中无法找到直接对应,比如String.prototype.replace等。
ts2php 添加了一个新的 PHP 类库来解决这个问题。类库实现了一些不对应的方法,使用时需要在PHP中引入:
require_once("/path/to/ts2php/dist/runtime/Ts2Php_Helper.php");
模块化的
PHP 的模块机制与 TypeScript 有很大不同。 PHP的require和include相当于直接将代码插入到文件中执行,会造成命名冲突。
例如,有两个 PHP 文件 file1.php 和 file2.php,它们都定义了一个名为 abc 的函数。如果file2.php引用file1.php,会报错:
php
function abc() {
echo 'file1';
}
php
require('./file1.php'); // Fatal error: Cannot redeclare abc()
function abc() {
echo 'file2';
}
因此,import不能直接转换为require。 ts2php 使用命名空间来转换 TypeScript 中的导入和导出。
导出转换为定义命名空间:
export default function run(a: string) {
var b = '111';
console.log(a, b);
}
php
namespace test\export;
function run($a) {
$b = "111";
echo $a, $b;
}
并且 import 会转换为 use namespace 来使用命名空间:
import {Other_Utils as Util} from '../some-utils';
import {Some_Utils, func} from '../some-utils';
type TplData = {
src?: string,
title?: string
};
const tplData: TplData = {};
tplData.src = Some_Utils.makeTcLink('url');
tplData.title = Some_Utils.highlight('title');
tplData.title = Util.sample;
tplData.title = func() + 'aa';
php
namespace test\import;
require_once(realpath(dirname(__FILE__) . '/' . "../some-utils.php"));
use \Other_Utils as Util;
use \Some_Utils;
use \func;
$tplData = array();
$tplData["src"] = Some_Utils::makeTcLink("url");
$tplData["title"] = Some_Utils::highlight("title");
$tplData["title"] = Util::$sample;
$tplData["title"] = func() . "aa";
注意事项
当然,将 TypeScript 转换为 PHP 肯定会存在陷阱。语言转换后,以下几种写法执行结果不同,目前尚未解决:
以上就是我们做ts2php的原因、进展以及一些实现方法。项目地址:
虽然它是一个不太主流的解决方案,但我们相信能够解决业务问题的技术才是好技术。