深圳电子商务网站有哪些,江苏工程招标网,厦门建设网站首页,建设微信营销网站当迁移框架的代价大到无法承受时#xff0c;不妨换个思路------不迁移代码#xff0c;迁移开发体验。 一、背景#xff1a;一个不敢动的老项目 我们团队维护着一个庞大的 AngularJS 1.x 前端项目#xff0c;而且业务还在持续增长#xff0c;新功能不断在加。…当迁移框架的代价大到无法承受时不妨换个思路------不迁移代码迁移开发体验。一、背景一个不敢动的老项目我们团队维护着一个庞大的 AngularJS 1.x 前端项目而且业务还在持续增长新功能不断在加。升级框架我们认真评估过------迁移到 React 或 Angular 2无论哪条路代价都大到难以接受几十万行的模板代码要重写、业务逻辑要重新梳理、回归测试的工作量巨大更关键的是业务不会因为你要重构就停下来等你。所以我们选择了另一条路不迁移框架而是让在老框架上的开发体验尽可能现代化。第一步是引入 TypeScript。这一步效果立竿见影------类型系统带来的重构信心和代码可维护性提升是巨大的。但还有一个痛点没有解决HTML 模板里的开发体验依然原始。在.html文件中写 AngularJS 表达式没有自动补全、没有类型提示、没有跳转定义写错了属性名只能等运行时才发现。相比之下现代框架React JSX、Vue SFC、Angular 2 模板的 IDE 支持已经非常成熟了。于是我决定写一个 VS Code 插件ng-helper为 AngularJS 1.x 补上这块缺失的拼图。二、它能做什么先看效果再聊技术。这个插件目前已经覆盖了日常开发中最高频的场景数据绑定的智能提示这是最核心的能力。在 HTML 模板的{{ }}表达式和指令属性中你可以获得自动补全输入ctrl.后自动列出控制器上的所有属性和方法带有完整的类型信息悬停类型提示鼠标悬停在表达式上显示其 TypeScript 类型跳转到定义CtrlClick 直接跳到 TypeScript 中对应的属性或方法定义函数签名提示调用方法时显示参数列表和类型组件与指令自定义 component 和 directive 的标签名、属性名都有自动补全和悬停提示点击可以跳转到定义处。补全时还会自动插入必填属性。模板表达式诊断在 HTML 中写错了 AngularJS 表达式不用等到运行时了------插件会实时标红并给出错误信息。更多实用功能Filter 的补全、提示和跳转ng-*内置指令补全templateUrl一键跳转到 HTML 文件通过 Controller/Service 名称跳转到实现文件搜索 component/directive 在哪些地方被使用依赖注入匹配校验inline-html 语法高亮三、技术挑战在无类型的 HTML 和有类型的 TypeScript 之间架桥功能看起来很自然但背后的技术问题并不简单。核心挑战是AngularJS 的 HTML 模板是纯字符串没有任何类型信息而我们的业务逻辑已经用 TypeScript 写了。如何把两者连接起来我并不是一开始就想好了所有方案而是一步步被真实需求推着往前走的。下面按实际开发历程来讲。第一步先解决类型从哪来的问题最初的目标很简单在 HTML 模板的{{ ctrl.userName }}上提供自动补全和类型提示。表达式本身就是合法的 JS 属性访问不需要特殊解析------但关键问题是TypeScript 的类型信息怎么拿到这里有一个方案选型的思考过程。方案一自己启动一个 TypeScript 编译器最直觉的想法是在扩展中直接调用ts.createProgram()自己建一个 TypeScript 程序实例来做类型分析。但很快就会发现几个严重问题内存翻倍VS Code 已经通过内置的 tsserver 为项目维护了一整套 AST 和类型信息。再起一个等于把整个项目的类型图在内存里复制一份对于大型项目这是不可接受的。编辑不同步用户在编辑器里改了代码还没保存createProgram()只能读磁盘上的旧文件。tsserver 维护了内存中的编辑缓冲区能实时反映用户的修改。重复造轮子tsconfig.json的解析、项目引用的处理、文件监听和增量编译…这些 tsserver 都已经做好了自己搞一套成本很高。方案二利用 VS Code 已有的 APIVS Code 提供了vscode.executeCompletionItemProvider、vscode.executeHoverProvider等命令可以请求内置 TypeScript 扩展返回补全和悬停信息。但问题是这些 API 只对.ts/.js文件生效对.html文件不会返回 TypeScript 的类型信息。即使通过虚拟文档等技巧绕过文件类型限制返回的也是展示层的数据字符串形式的标签、文档而不是结构化的类型对象。我需要的是ts.Type需要能调用.getApparentProperties()获取所有属性、.getCallSignatures()获取函数签名、.getNumberIndexType()获取数组元素类型等方法来逐层深入。方案三最终选择写一个 TypeScript Server Plugin“住进” tsserver 里面TypeScript 提供了一个官方机制TypeScript Server Plugin。通过这个机制我可以把自己的代码注入到 tsserver 进程中运行直接访问它内部的Program和TypeChecker对象------不需要额外的内存天然与用户编辑同步。// TypeScript Server Plugin 的入口functioninit(modules:{typescript:typeofimport(typescript)}){return{create(info:ts.server.PluginCreateInfo){// info.project[program] 就是 tsserver 当前维护的 Program// 通过它可以拿到 TypeChecker进行任意类型查询}};}这是三个方案中唯一能同时满足零额外内存、“实时同步编辑”、完整类型 API 访问的方案。代价是------它运行在 tsserver 进程中而我的 VS Code 扩展运行在扩展宿主进程中两者之间没有直接的调用接口。第二步搭建跨进程通信的桥梁选择了 TypeScript Server Plugin 方案后新的难题来了扩展和插件运行在两个完全隔离的进程中怎么让它们对话这个问题困扰了我相当长时间。我研究了各种可能的方案也看了其他插件是怎么做的方案一走 tsserver 的标准协议行不通。VS Code 严格管控了与 tsserver 之间的通信------标准协议中没有为插件预留自定义请求/响应的通道多余的数据无法添加和返回。官方提供了一个configurePluginAPI但它是严格单向的只能从扩展向插件传配置插件无法返回任何数据。方案二参考 VolarVue 语言工具的 Request ForwardingVolar 借助 VS Code 内部的typescript.tsserverRequest命令通过 Language Server 作为中间层转发请求。方案很优雅但它依赖一个独立的 LSP 服务器层对我的场景来说太重了。方案三在 TS Plugin 中启动一个 HTTP Server。研究了日本开发者做的 ts-type-expand 等项目后我发现这条路最简单、限制也最少------插件在 tsserver 进程内启动一个 HTTP 服务扩展通过configurePlugin把端口号传过去然后作为 HTTP Client 发请求获取类型数据。早期版本就是用的这个方案TS Plugin 是 ServerVS Code 扩展是 Client。简单直接很快就跑起来了。但随着使用一个问题开始频繁出现tsserver 会因为各种原因重启tsconfig.json变更、TypeScript 版本切换、内存压力等每次重启都意味着插件进程被销毁HTTP Server 随之消失扩展侧的连接断掉。虽然可以加重试逻辑但扩展侧并不知道 tsserver 何时重启完成轮询检测既浪费又不可靠。于是我想能不能把 Server 和 Client 的角色反过来如果扩展侧是 Server它的生命周期是稳定的只要 VS Code 窗口在就不会消失。TS Plugin 重启后主动作为 Client 重连上来------这个方向的重连逻辑就简单多了因为 Plugin 加载时一定会触发onConfigurationChanged在这个回调里发起连接就行。同时角色反转后HTTP 的请求-响应模式就不太合适了Server 在扩展侧但发起请求的也是扩展侧。WebSocket 的全双工通信天然适合这种场景------连接建立后双方可以自由收发消息不再受谁是请求方的约束。最终的架构演化成了这样VS Code 扩展 │ Node.js IPC (process.send) ▼ RPC 服务进程扩展 fork 出的子进程WebSocket Server │ WebSocket ▼ TypeScript 插件运行在 tsserver 中WebSocket Client中间多出一个 fork 的子进程是因为 WebSocket Server 需要一个稳定运行的宿主。扩展通过configurePlugin把端口号传给插件插件加载后主动连上来。tsserver 重启没关系插件重新加载后会自动重连整个过程对用户无感。到这一步通信通道打通了。但具体怎么从 TypeScript 的类型系统里挖出类型信息呢以ctrl.user.name这个表达式为例。Plugin 收到请求后做了两件事第一件找到ctrl对应的控制器类型。Plugin 启动时会扫描项目中所有源文件找到.component(myComp, { controller: MyController, controllerAs: ctrl })这样的注册语句并缓存起来。当请求到来时通过controllerAs的值ctrl匹配到对应组件再用 TypeChecker 从controller: MyController这个 AST 节点上取出类型。这里有个小坑getTypeAtLocation()返回的是typeof MyController构造函数类型而不是实例类型需要再通过getDeclaredTypeOfSymbol()转换成实例类型。第二件沿着属性链逐层查询类型。把表达式字符串ctrl.user.name用ts.createSourceFile()解析成一棵 AST纯内存操作不涉及磁盘然后按 AST 结构逐层向下ctrl → 直接使用控制器实例类型rootType ctrl.user → rootType.getProperty(user) → 得到 User 类型 ctrl.user.name → User类型.getProperty(name) → 得到 string 类型每一步都调用 TypeChecker 的getTypeOfSymbolAtLocation()获取真实类型支持泛型推导、联合类型、函数返回值等所有 TypeScript 类型系统能力。这个临时 AST 定结构真实类型图查类型的两步法就是整个插件类型解析的核心。早期版本的补全、悬停、跳转定义就是这样实现的。第三步被迫手写解析器------当 AngularJS 语法超出 JavaScript 的边界随着功能往深处走我开始遇到麻烦。前面那套把表达式直接交给 TypeScript 解析的流程能跑起来是因为ctrl.userName这类表达式本身就是合法的 JavaScript。但当我尝试支持ng-repeat和 Filter 时这条路走不通了------AngularJS 模板中的表达式不是标准 JavaScript而是 AngularJS 自己的表达式语言。它长得像 JS但有关键差异|是 Filter管道操作符不是位或items | orderBy:nameng-repeatitem in items track by item.id有自己独特的语法没有var、let、function等声明语句某些 JS 关键字如for、return在这里是合法的标识符这些语法直接丢给ts.createSourceFile()会解析出错。而我又不能放弃已有的类型查询流程------那是整个插件的核心能力。所以解析器的定位很明确它不是要替代 TypeScript 做类型分析而是作为一个预处理层把 AngularJS 的特殊语法解析理解后从中提取出合法的 JavaScript 表达式再交给已有的流程去查类型。比如items | orderBy:name经过解析器处理后插件知道这是一个 Filter 表达式真正需要查类型的部分是itemsng-repeatitem in ctrl.users track by item.id被解析后插件能提取出ctrl.users作为集合表达式item作为迭代变量。参考 AngularJS 源码中$parse的实现我手写了一个完整的词法分析器和语法分析器ng-parser有一些有趣的技术细节完整实现了 AngularJS 的表达式文法定义在一份 CFG 语法规则中支持 Filter 表达式、ng-repeat、ng-controller 三种不同的解析入口产出带有完整位置信息的类型化 AST支持 Visitor 模式遍历对ng-repeat中的as和track by子句使用了正则预扫描 扫描范围收缩的技巧来避免歧义------先用正则找到as或track by的位置然后把扫描器的结束位置设置在那里这样表达式解析器就不会越界第四步巧妙的变量替换------让 TypeScript 理解 ng-repeat 的作用域有了解析器之后还有一个问题。考虑这样一段模板divng-controllerUserCtrl as ctrldivng-repeatitem in ctrl.users{{ item.name }}/div/div当用户在item.name上悬停时我们需要知道item的类型。但item是ng-repeat在运行时创建的作用域变量TypeScript 并不认识它。解决方案是一层变量替换把item替换成ctrl.users[0]。这样item.name就变成了ctrl.users[0].name------一个 TypeScript 完全可以解析和推导类型的合法表达式。类似地ng-repeat的特殊变量也有对应的处理$index→ 直接标记为number类型$first、$last、$even、$odd→ 直接标记为boolean类型当用户在 HTML 中触发补全时完整的流程是扩展侧用 ng-parser 解析表达式识别出光标位置的语义上下文对 ng-repeat 等作用域变量进行替换生成合法的 TypeScript 表达式通过 RPC 发送给 tsserver 中的 TypeScript PluginPlugin 用 TypeChecker 逐层解析类型属性访问 → 函数调用 → 数组索引…结果原路返回展示给用户这就是整个插件的核心链路解析 → 替换 → 查询 → 展示。每一层解决一个特定的问题最终让 HTML 模板中的 AngularJS 表达式获得了与.ts文件中同等质量的类型信息。四、从 0 到 1.0一年的迭代回顾这个项目的 Changelog从 2024 年 7 月的 v0.0.5 到 2025 年 7 月的 v1.0.0经历了 18 个版本。功能是逐步叠加的阶段版本核心里程碑起步v0.0.5 ~ v0.1.0组件名补全、数据绑定的补全和类型提示成长v0.2.0 ~ v0.5.0跳转到定义、directive 支持、语法高亮成熟v0.6.0 ~ v0.8.0依赖注入校验、ng-repeat 支持、Filter 支持完善v0.9.0 ~ v1.0.0表达式诊断、函数签名提示每个版本都是由真实的开发痛点驱动的。比如ng-repeat的支持是因为团队日常大量使用列表渲染没有item的类型提示严重影响效率。又比如依赖注入校验是因为 AngularJS 的 DI 是基于字符串匹配的参数顺序写错是非常隐蔽的 Bug。五、AI 视角如果今天重新来过这个插件的开发始于 2024 年中当时 AI 编程助手还没有今天这么强大。如果今天重新来过有一些环节 AI 可以显著加速解析器开发手写词法分析器和语法分析器是这个项目中最耗时的部分之一。如果有 AI 辅助可以先描述语法规则让 AI 生成初版解析器代码再手动调优和补全边界情况开发周期能大幅缩短。测试用例生成解析器需要大量的测试用例覆盖各种边界情况。AI 非常擅长根据语法规则生成多样化的测试输入包括各种合法和非法的表达式组合。HTML 模板分析光标位置分析判断光标在标签名上、属性名上、属性值上、模板表达式里…涉及大量的条件分支和边界判断这类规则明确但情况繁多的代码正是 AI 的强项。但有些地方 AI 帮不了太多整体架构设计三层 RPC、变量替换策略、TypeScript 内部 API 的摸索很多没有文档需要读源码、以及那些只有在真实大型项目中才会暴露的 Edge Case。这些仍然需要开发者对问题域的深入理解。我的体会是AI 是优秀的代码执行者但架构和设计上的创造性思考仍然是人的核心价值。六、写在最后面对遗留系统开发者常常陷入两个极端要么忍着用要么推倒重来。但其实还有第三条路------用工具化的思维让当下变得更好。一个 VS Code 插件不会让 AngularJS 变成 React但它能让每天在这个代码库中工作的开发者少一些心智负担、多一些开发效率。有时候解决问题的最好方式不是消灭问题本身而是改善与问题共处的方式。如果你也在维护类似的老项目希望这篇文章能给你一些启发------不一定是去写一个 VS Code 插件而是面对不可改变的约束时找到自己能改变的那个切入点。ng-helper 是一个开源项目欢迎体验和反馈GitHub | VS Code Marketplace