全国免费咨询:

13245491521

VR图标白色 VR图标黑色
X

中高端软件定制开发服务商

与我们取得联系

13245491521     13245491521

2024-03-16_万物皆可转:前端框架编译原理内参

您的位置:首页 >> 新闻 >> 行业资讯

万物皆可转:前端框架编译原理内参 点击关注公众号,“技术干货”及时达! 「Rax2Taro」:点击查看Github地址(https://github.com/Trade-Offf/Rax2Taro?tab=readme-ov-file) 一、前置背景笔者日常使用 Rax 框架开发前端需求。但随着业务扩展,我面临一个头痛的需求:将现有的 Rax 组件适配为 Taro 组件,以实现一些特定商业场景的跨平台功能。 这一需求可以概括为: 「新功能开发」 - 在 Taro 框架中实现,确保多端兼容性。「旧功能复用」 - 将现有 Rax 组件转换为 Taro,避免重写。重写组件成本高昂,特别是对于缺乏文档和原开发者不在的旧组件。因此我需要一种自动化工具,能够轻松地「一键式」将 Rax 组件转化为 Taro 组件,减少工作量,加快开发进程。 本篇博客内容,将探讨构建一个从 Rax 转换到 Taro 的编译器,从零开始实现组件级转换。 ?「 恐惧通常源自未知,你恐惧的不是造轮子,你恐惧的是你从来没造过轮子 」 ?作为前端同学,如果遇到这种工作可能会汗流浃背。 但不要担心,只要我们把目标拆解到足够清晰、足够细化,一切困难都是纸老虎。 二、编译器编译器是个宽泛的概念,最初是指将「高级语言」转换为计算机能识别的「汇编/机器语言」的工具。 个人理解:编译器本质是个从 A 转换的 B 的翻译工具 (如有不妥,还原评论区指正 ??) 但是编译器的翻译过程不是简单的翻译,通常涉及到多个步骤(词法、语法、语义分析、中间代码生成等)。详细知识点不赘述了,感兴趣的朋友请翻阅《编译原理》(https://book.douban.com/subject/3296317/)。 01 | Babel:JavaScript 编译器我们以日常开发中,接触最多的 JavaScript 编译器 Babel 为例,了解编译器工作逻辑,这里只介绍 Babel 基本流程。更多详细内容可以看以下Github官方文档:Babel 插件手册(https://github.com/jamiebuilds/babel-handbook/blob/master/translations/zh-Hans/plugin-handbook.md) Babel 工作流程 02 | 基本用法这里以将 const a = 1 转换成 var a = 1 为例,看下 Babel 是如何工作的 i. 解析(parse)成抽象语法树 AST?「抽象语法树(Abstract Syntax Tree)」:本质是一种数据结构,它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构。 ?Babel 提供了 @babel/parser 将代码解析成 AST。这一步主要做两件事: 「词法分析」:把代码转换为令牌流(tokens flow:解析的中间产物,不用管)「语法分析」:再把每个 token 转换为 AST 结构constparse=require('@babel/parser').parse; constast=parse('consta=1'); ii. 转换(transform)ASTBabel 提供了 @babel/traverse 对解析后的 AST 进行处理。 转换接收 AST 并对其遍历,在此过程中对节点进行增删改查,是 Babel 编译器最核心的过程。 traverse()能够接收 ast 以及 visitor 两个参数: 「ast」 是上一步解析得到的抽象语法树。「visitor」 提供访问不同节点的能力,当遍历到一个匹配的节点时,能够调用具体方法对节点进行处理。constt=require('@babel/types'); consttraverse=require('@babel/traverse').default; traverse(ast,{ VariableDeclaration:function(path){ if(path.node.kind==='const'){ path.replaceWith( t.variableDeclaration('var',path.node.declarations)//替换成var } path.skip(); } }); Babel 提供了@babel/types 用于定义 AST 节点,在 visitor 处理节点的时候用于新增/替换等操作。 这个例子中,我们遍历上一步得到的 AST,在匹配到变量声明 VariableDeclaration 时,判断值是否为 const,并操作替换成 var。 iii. 生成(generate)代码Babel 提供了 @babel/generator 把转换后的最终 AST 还原为字符串形式的代码,同时创建源码映射。 constgenerate=require('@babel/generator').default; letcode=generate(ast).code; 以上就是 Babel 在编译时的流程,这里涉及到了几个关键的包: 「@babel/types」 :用于构建、验证和修改 AST 节点「@babel/parser」:提供默认的 parse 方法用于解析「@babel/traverse」: 封装了对 AST 树的遍历和节点的增删改查操作「@babel/generator」: 提供给默认的 generate 方法用于代码生成我们接下来写的编译器,就是基于上述介绍的 Babel 包来实现。 三、Rax2Taro除了转换工具,我们还需要了解被转换和生产的对象。 因此需要了解 Rax 和 Taro 框架,比较两者的差异和相似之处,注意转换过程中需要抹平的部分: 「Rax」 是阿里巴巴的的跨端解决方案,它的设计理念与 React 类似,提供了类似的组件化开发体验,能够运行在 Web、Node.js、阿里小程序、Weex 等多个平台。 「Taro」 是京东的跨端跨框架解决方案,支持使用 React 语法开发一次,然后将代码编译成不同平台的小程序(微信/百度/支付宝/字节跳动/京东小程序等)和 H5 应用,甚至可以编译成 React Native 应用。 通过阅读对比二者官方文档,寻找到接下来开发的关键破局点:「Rax 和 Taro 均支持组件化开发」 由于Rax 和 Taro 都受到 React 的影响,并且都使用 JSX 语法,导致它们的许多基础组件在概念上是类似的,这意味着有一些组件和属性是可以在两者之间直接映射的。 01 | 本期目标从组件化下手,本期编译器能力计划将左侧 Rax 文档中(除 Link 外)的 7 个组件一键转换 Taro 组件 在 Taro 中优先寻找平替组件,若能找到则增加转换逻辑抹平差异,实现转换器。如果找不到对应组件就标红,等后续对特例地统一处理。 02 | 编译器设计 编译器结构设计 /Rax2Taro |--/node_modules#项目依赖库安装文件夹 |--/src ||--index.js#主入口文件,协调整个转换过程 ||--parser.js#用于解析源代码生成AST ||--generator.js#用于从修改后的AST生成新的源代码 |--/Transformers#存放转换逻辑的模块文件夹 ||--index.js#整合各种转换规则的主要转换器 ||--FunctionTransformer.js#函数转换逻辑 ||--/JSXElementsTransformer#存放JSX元素的特定转换器 ||--index.js#整合JSX元素转换规则 ||--...#其他JSX元素转换模块 ||--...#其他转换逻辑模块 |--/Input#存放待转化的Rax.js的文件夹 |--/Output#存放转化后的Taro.js的文件夹 |--package.json#定义项目依赖和脚本 |--README.md#项目说明文档 编译器代码结构设计 03 | 转换 View 组件?「 写一个编译器可能很难,但是转换一个小组件很简单 」 ?我们的目标是转换 N 个基本组件。 一旦我们知道了如何转换 View 组件,我们只需重复相同的步骤六次即可完成目标。因此,我们将以 View 组件为案例,探讨如何制定单个组件的转换规则。 「转换,本质就是找不同,并让不同变相同」 通过对比二者代码之间的差异,我们发现需要做这三件事: Rax 需要引入 createElement 不然会报错,Taro 除了组件外没其他引入行为;Import 引入写法不同,Rax 用单文件引入单组件,Taro 是在 @tarojs/components 集中引入;同样都是View /组件,两个框架间的组件 API 属性可能不同,或者属性名相同功能不同。需要抹平差异,或者特异处理;接下来让我们一步步实现,至于读取文件和转换 SourceCode 得到 AST 部分不讲,具体细节可以在 Github 里看,就两行代码。「下述一切操作均默认为转换器获取到 AST 之后」。 i. 删除 createElement//主入口文件 consttraverse=require("@babel/traverse").default; constimportsTransformer=require("./ImportsTransformer"); functiontransform(ast){ traverse(ast,{ ImportDeclaration(path){ importsTransformer.transformImportDeclaration(path); }, //...添加其他节点类型的转换规则 } module.exports={ transform, }; 开始之前,首先了解一下转换器主入口结构。 主入口文件引入了@babel/traverse,还引入我们新增用来删除 createElement 的方法。 其中使用了traverse()函数,它用于遍历抽象语法树(AST),访问树中的每个节点,并对这些节点进行修改、添加或删除。 //功能函数 functiontransformImportDeclaration(path){ constimportSource=path.node.source.value; //删除"rax"模块里的createElement if(importSource==="rax"){ //过滤createElement引入 path.node.specifiers=path.node.specifiers.filter( (specifier)= !( t.isImportSpecifier(specifier)&& specifier.imported.name==="createElement" ) //删除空引用 if(path.node.specifiers.length===0){ path.remove(); } } } 接下来看功能函数,因为会遍历节点,所以 我们首先通过 path.node.source.value=== "rax" 定位,找到我们要增删改的目标节点; 然后过滤这个语句中的所有导入说明符 specifiers ,检查值是否为 createElement path.node.specifiers 是 AST 中的一个部分,表示一个模块导入语句中的所有导入说明符。t.isImportSpecifier 是 Babel 类型检查器的一部分。如果是,就被过滤掉。 最后加一步空引用清除,删除 import { } from 'rax' ii. 改变组件引入写法引入写法的修改方式,类似上面的处理方法: 先定位找到 import View from "rax-view",删除这句引入;声明一个对象,存 Taro 引入组件,把上面删掉的组件再以 import { View } from "@tarojs/components"的形式声明;但是考虑到可扩展性,之后还会遇到 Text、Image 等组件,所以这里我写了一张映射表,批量重复上面操作。 constcomponentImportMap={ "rax-view":{ source:"@tarojs/components", importName:"View", }, "rax-text":{ source:"@tarojs/components", importName:"Text", } //...添加更多组件及其转换规则 }; consttaroComponentsToImport=newSet();//声明去重Taro对象 //基础组件映射转换 functiontransformImportDeclaration(path){ constimportSource=path.node.source.value; //...createElement删除逻辑 constnewImportInfo=componentImportMap[importSource]; if(newImportInfo){ //如果映射表里有,就存这个值到Taro对象中 taroComponentsToImport.add(newImportInfo.importName); path.remove();//并移除原rax-xxx导入声明 } } 由于功能类似,所以这两个功能(删除 createElement、改变组件引入写法)我都写在 transformImportDeclaration 函数里。 iii. 转换 View 组件转换View组件听上去很复杂,其实拆解下,本质就是把两套属性差异抹平,用的也是上面对 AST 节点的增删改操作。 从二者官方文档里展示的组件 API 可以明显看到 Rax 比 Taro 的 View 少了很多属性,但由于我们实现的是 「Rax - Taro」 的单向转换。 所以「一切编译行为以 Rax API 为转换基准」。 至于 Taro 多出来的 API ,编译器不用管,如果后续开发需要用到Taro属性,则开发者根据 Taro 官方文档自行配置使用即可(反正 Rax 没有 ??) 对比 View API 差异,我整理出下面的表格内容: 可以看到有 5 条 API 属性有不同,其中 3 条是使用方法不同,2 条是需要编译器处理的属性。 onClick - onTaponLongpress - onLongTapView 转换器的逻辑如下: constt=require("@babel/types"); functiontransformViewElement(path){ //确保我们只处理具有name属性的JSXElement if(path.node.openingElementpath.node.openingElement.name){ constopeningElementName=path.node.openingElement.name; if( t.isJSXIdentifier(openingElementName)&& openingElementName.name==="View" ){ path.node.openingElement.attributes.forEach((attribute)={ if(t.isJSXAttribute(attribute)attribute.name){ constattributeName=attribute.name.name; switch(attributeName){ case"onClick": attribute.name.name="onTap"; break; case"onLongpress": attribute.name.name="onLongTap"; break; } } } } } module.exports={ transformViewElement, }; 跟之前操作差不多,还是遍历找节点,找到要处理的 API 属性,将属性重命名。 至此,我们实现了 Rax - Taro View 组件的编译转换。接下来需要对剩下的 6 个基础组件做同样操作,即可完成本期目标: 重复流程如下: 列表格,整理 API 差异,标明处理方式遍历 AST 找对应节点、找到需要处理的 API 属性执行重命名 or 删除动作机械性动作 * n ... 为了保证转换器的拓展性,这里新增了一个主入口 index.js 文件用来批量管理各个独立组建的小转换器。 其他组件的对应表请在语雀文档查看: https://www.yuque.com/cascading/bwnowo/uwp3s510g0im9ue9?singleDoc# 《基础组件 API 差异》 四、自动化测试由于在本地转换各个 API 需要反复调试,并且需要实时查看组件编译后的情况。为了开发提效,我本地还需要运行 Rax 和 Taro 两个用脚手架生成的项目,加一个小自动化测试脚本进行一键编译调试。 具体步骤如下,在e2e.test.js 中: 设定 Rax 项目路径:本项目从 Rax 应用的源文件夹rax-test-demo/src/index.js读取待转换组件代码。设定 Taro 项目路径:将转换后的 Taro 组件代码写入 Taro 应用的目标文件夹 TaroTestDemo/src/app.js转换代码:命令行执行:npm run test:e2e 转换器将源代码解析为 AST,并进行转换。监测转换结果:在 Taro 测试环境中检查转换后的代码,确保没有报错且符合预期。constfs=require("fs"); constpath=require("path"); const{transform}=require("../src/Transformers"); constparser=require("@babel/parser"); constgenerator=require("@babel/generator").default; //基于你Rax源文件和Taro输出的路径 constraxSourcePath=path.join(__dirname,"../../rax-test-demo/src/index.js"); consttaroOutputPath=path.join( __dirname, "../../TaroTestDemo/src/pages/index/index.jsx" ); describe("End-to-EndTransformation",()={ it("从Rax组件中读取源码,转换为Taro组件",()={ constraxSourceCode=fs.readFileSync(raxSourcePath,"utf8"); //1.解析Rax源代码为AST constraxAst=parser.parse(raxSourceCode,{ sourceType:"module", plugins:["jsx"], //2.转换AST transform(raxAst); //3.生成转换后的Taro源代码 consttaroOutput=generator(raxAst, fs.writeFileSync(taroOutputPath,taroOutput.code); }); 01 | 准备 Rax 测试环境npminstall-grax-cli#安装Rax脚手架(如果尚未安装) cdDesktop#进入桌面 raxinitRaxTestDemo#初始化Rax项目 cdrax-test-demo#进入文件夹 npminstall#安装依赖 npmstart#运行 脚手架选项如下: 02 | 准备 Taro 测试环境npminstall-g@tarojs/cli#安装Taro脚手架(如果尚未安装) cdDesktop#进入桌面 taroinitTaroTestDemo#初始化Taro项目 cdTaroTestDemo#进入文件夹 npminstall#安装依赖 npmrundev:h5#运行 脚手架选项如下: 确保遵循上述步骤来准备 Rax 和 Taro 的测试环境。在双方都构建完成后,您可以执行 Jest 测试来验证转换过程。 03 | 执行自动化测试安装 Jest 命令行执行:npm install --save-dev jestpackage.json 配置:"test:e2e": "jest tests/e2e.test.js"命令行执行:npm run test:e2e此时,你就可以在本地同时运行 Rax 与 Taro 项目,一边写 Rax 一边可实时通过此条命令进行编译转换 Taro。 五、总结本篇文章从零开始构造了一个略具复杂度的 Rax 转 Taro 编译器。 初始目标挺吓人,但经过合理拆解发现大目标也不过只是走通 MVP(最小可行产品)后的重复累加。工作如此,生活亦如此。专注你的目标,不要被纷繁的信息流影响,脚踏实地一步步完成你的小Step,一切总能完成的。 后续对这个编译器,我计划如下内容,欢迎持续关注: 新增自定义脚手架功能抹平转换过程中的 CSS 样式差异新增 README_EN 完善中英文使用文档写作不易,如果觉得本文对你有启发有帮助的话,请在 GitHub 帮我点个 Star ? 交个朋友,愿我们更高处相见?,比心感谢 ? ~~~ 点击关注公众号,“技术干货”及时达! 阅读原文

上一篇:2024-03-19_「转」为什么做了这么多传播 , 业务就是不增长 下一篇:2018-02-02_我喜欢iPhone X这部最新广告电影,陈可辛牛逼

TAG标签:

17
网站开发网络凭借多年的网站建设经验,坚持以“帮助中小企业实现网络营销化”为宗旨,累计为4000多家客户提供品质建站服务,得到了客户的一致好评。如果您有网站建设网站改版域名注册主机空间手机网站建设网站备案等方面的需求...
请立即点击咨询我们或拨打咨询热线:13245491521 13245491521 ,我们会详细为你一一解答你心中的疑难。
项目经理在线

相关阅读 更多>>

猜您喜欢更多>>

我们已经准备好了,你呢?
2022我们与您携手共赢,为您的企业营销保驾护航!

不达标就退款

高性价比建站

免费网站代备案

1对1原创设计服务

7×24小时售后支持

 

全国免费咨询:

13245491521

业务咨询:13245491521 / 13245491521

节假值班:13245491521()

联系地址:

Copyright © 2019-2025      ICP备案:沪ICP备19027192号-6 法律顾问:律师XXX支持

在线
客服

技术在线服务时间:9:00-20:00

在网站开发,您对接的直接是技术员,而非客服传话!

电话
咨询

13245491521
7*24小时客服热线

13245491521
项目经理手机

微信
咨询

加微信获取报价