高德JS依赖分析工程及关键原理

一、背景

高德 App 进行 Bundle 化后,由于业务的复杂性,Bundle 的数量非常多。而这带来了一个新的问题——Bundle 之间的依赖关系错综复杂,需要进行管控,使 Bundle 之间的依赖保持在架构设计之下。

并且,为了保证 Bundle 能实现独立运转,在业务持续迭代的过程中,需要逆向的依赖关系来迅速确定迭代的影响范围。同时,对于切面 API(即对容器提供的系统 API,类似浏览器中的 BOM API),也需要确定每个切面 API 的影响范围以及使用趋势,来作为修改或下线某个 API 的依据。

以组件库为例,由于组件会被若干业务项目所使用,我们对组件的修改会影响这些业务项目。在计划修改前,需要根据正向的依赖关系(业务依赖组件)来算出逆向的依赖关系——该组件被哪些地方所依赖,从而确定这个组件修改的影响范围。

比文件更高的维度,是 Bundle 间的依赖。我们有业务 Bundle,也有公共 Bundle。公共 Bundle 也分为不同层级的 Bundle。

对于公用 Bundle,业务 Bundle 可以依赖它,但公用 Bundle 不能反过来依赖业务 Bundle;同样的,底层的 Bundle 也禁止依赖上层封装的 Bundle。我们需要通过依赖分析,来确保这些依赖按照上述规则进行设计。

二、实现关键步骤

实现 JS 依赖分析,整个实现过程大致如下图所示:

下面挑一些关键步骤来展开介绍。

使用 AST 提取依赖路径

要做文件级别的依赖分析,就需要提取每个文件中的依赖路径,提取依赖路径有 2 个方法:

  • 使用正则表达式,优点是方便实现,缺点是难以剔除注释,灵活度也受限;
  • 先进行词法分析和语法分析,得到 AST(抽象语法树)后,遍历每个语法树节点,此方案的优点是分析精确,缺点是实现起来要比纯正则麻烦,如果对应语言没有提供 parser API(如 Less),那就不好实现。

一般为了保证准确性,能用第 2 个方案的都会用第 2 个方案。

以类 JS(.js、.jsx、.ts、.tsx)文件为例,我们可以通过 TypeScript 提供的 API ts.createSourceFile 来对类 JS 文件进行词法分析和语法分析,得到 AST:

const ast = ts.createSourceFile(abPath,content,ts.ScriptTarget.Latest,false,SCRIPT_KIND[path.extname(abPath)]
);

得到 AST 后,就可以开始遍历 AST 找到所有我们需要的依赖路径了。遍历时,可以通过使用 typeScript 模块提供的 ts.forEachChild 来遍历一个语法树节点的所有子节点,从而实现一个遍历函数 walk:

function walk (node: ts.Node) {ts.forEachChild(node, walk); // 深度优先遍历// 根据不同类型的语法树节点,进行不同的处理// 目的是找到 import、require 和 require.resolve 中的路径// 上面 3 种写法分为两类——import 声明和函数调用表达式// 其中函数调用表达式又分为直接调用(require)和属性调用(require.resolve)switch (node.kind) {// import 声明处理case ts.SyntaxKind.ImportDeclaration:// 省略细节……break;// 函数调用表达式处理case ts.SyntaxKind.CallExpression:// 省略细节break;}
}

通过这种方式,我们就可以精确地找到类 JS 文件中所有直接引用的依赖文件了。

当然了,在 case 具体实现中,除了用户显式地写依赖路径的情况,用户还有可能通过变量的方式动态地进行依赖加载,这种情况就需要进行基于上下文的语义分析,使得一些常量可以替换成字符串。

但并不是所有的动态依赖都有办法提取到,比如如果这个动态依赖路径是 Ajax 返回的,那就没有办法了。不过无需过度考虑这些情况,直接写字符串字面量的方式已经能满足绝大多数场景了,之后计划通过流程管控+编译器检验对这类写法进行限制,同时在运行时进行收集报警,要求必需显式引用,以 100% 确保对切面 API 的引用是可以被静态分析的。

建立文件地图进行寻路

我们对于依赖路径的写法,有一套自己的规则:

  • 引用类 JS 文件支持不写扩展名;
  • 引用本 Bundle 文件,可直接只写文件名;
  • 使用相对路径;
  • 引用公用 Bundle 文件,通过 @${bundleName}/${fileName} 的方式引用,fileName 同样是直接只写该 Bundle 内的文件名。

这些方式要比 CommonJS 或 ECMAScript Module 的规划要稍复杂一些,尤其是「直接只写文件名」这个规则。对于我们来说,需要找到这个文件对应的真实路径,才能继续进行依赖分析。

要实现这个,做法是先构建一个文件地图,其数据结构为 { [fileName]: ‘relative/path/to/file’ } 。我使用了 glob 来得到整个 Bundle 目录下的所有文件树节点,筛选出所有文件节点,将文件名作为 key,相对于 Bundle 根目录的路径作为 value,生成文件地图。在使用时,「直接只写文件名」的情况就可以直接根据文件名以 O(1) 的时间复杂度找到对应的相对路径。

此外,对于「引用类 JS 文件支持不写扩展名」这个规则,需要遍历每个可能的扩展名,对路径进行补充后查找对应路径,复杂度会高一些。

依赖是图的关系,需先建节点后建关系

在最开始实现依赖关系时,由于作为前端的惯性思维,会认为「一个文件依赖另一些文件」是一个树的关系,在数据结构上就会自然地使用类似文件树中 children: Node[] 的方式——链式树结构。而实际上,依赖是会出现这种情况的:

如果使用树的方式来维护,那么 utils.js 节点就会分别出现在 page.jsx 和 comp.jsx 的 children 中,出现冗余数据,在实际项目中这种情况会非常多。

但如果仅仅是体积的问题,可能还没那么严重,顶多费点空间成本。但我们又会发现,文件依赖还会出现这种循环依赖情况:

写 TypeScript 时在进行类型声明的时候,就经常会有这样循环依赖的情况。甚至两个文件之间也会循环依赖。这是合理的写法。

但是,这种写法对于直接使用链式树结构来说,如果创建链式树的算法是「在创建节点时,先创建子节点,待子节点创建返回后再完成自身的创建」的话,就不可能实现了,因为我们会发现,假如这样写就会出现无限依赖:


const fooTs = new Node({name: 'foo.ts',children: [new Node({ name: 'bar.ts', children: [new Node({name: 'baz.ts',children: [new Node({name: 'foo.ts', // 和最顶的 foo.ts 是同一个children: [...] // 无限循环……})]})]})]
})

此问题的根本原因是,这个关系是图的关系,而不是树的关系,所以在创建这个数据结构时,不能使用「在创建节点时,先创建子节点,待子节点创建返回后再完成自身的创建」算法,必须把思路切换回图的思路——先创建节点,再创建关系。

采用这种做法后,就相当于使用的是图的邻接链表结构了。我们来看看换成「先创建节点,再创建关系」后的写法:

// 先创建各节点,并且将 children 置为空数组
const fooTs = new Node({name: 'foo.ts',children: []
});const barTs = new Node({name: 'bar.ts',children: []
});const bazTs = new Node({name: 'baz.ts',children: []
});// 然后再创建关系
fooTs.children.push(barTs);
barTs.children.push(bazTs);
bazTs.children.push(fooTs);

使用这种写法,就可以完成图的创建了。

但是,这种数据结构只能存在于内存当中,无法进行序列化,因为它是循环引用的。而无法进行序列化就意味着无法进行储存或传输,只能在自己进程里玩这样子,这显然是不行的。

所以还需要对数据结构进行改造,将邻接链表中的引用换成子指针表,也就是为每个节点添加一个索引,在 children 里使用索引来进行对应:


const graph = {nodes: [{ id: 0, name: 'foo.ts', children: [1] },{ id: 1, name: 'bar.ts', children: [2] },{ id: 2, name: 'baz.ts', children: [0] }]
}

这里会有同学问:为什么我们不直接用 nodes 的下标,而要再添加一个跟下标数字一样的 id 字段?原因很简单,因为下标是依赖数组本身的顺序的,如果一旦打乱了这个顺序——比如使用 filter 过滤出一部分节点出来,那这些下标就会发生变化。而添加一个 id 字段看起来有点冗余,但却为后面的算法降低了很多复杂度,更加具备可扩展性。

用栈来解决循环引用(有环有向图)的问题

当我们需要使用上面生成的这个依赖关系数据时,如果需要进行 DFS(深度遍历)或 BFS(广度遍历)算法进行遍历,就会发现由于这个依赖关系是循环依赖的,所以这些递归遍历算法是会死循环的。要解决这个问题很简单,有三个办法:

  • 在已有图上添加一个字段来进行标记
    每次进入遍历一个新节点时,先检查之前是否遍历过。但这种做法会污染这个图。
  • 创建一个新的同样依赖关系的图,在这个新图中进行标记
    这种做法虽然能实现,但比较麻烦,也浪费空间。
  • 使用栈来记录遍历路径
    我们创建一个数组作为栈,用以下规则执行:

每遍历一个节点,就往栈里压入新节点的索引(push);

每从一个节点中返回,则移除栈中的顶部索引(pop);

每次进入新节点前,先检测这个索引值是否已经在栈中存在(使用 includes),若存在则回退。

这种方式适用于 DFS 算法。

三、总结

依赖关系是源代码的另一种表达方式,也是把控巨型项目质量极为有利的工具。我们可以利用依赖关系挖掘出无数的想象空间,比如无用文件查找、版本间变动测试范围精确化等场景。若结合 Android、iOS、C++ 等底层依赖关系,就可以计算出更多的分析结果。

目前,依赖关系扫描工程是迭代式进行的,我们采用敏捷开发模式,从一些简单、粗略的 Bundle 级依赖关系,逐渐精确化到文件级甚至标识符级,在落地的过程中根据不同的精确度来逐渐满足对精度要求不同的需求,使得整个过程都可获得不同程度的收益和反馈,驱使我们不断持续迭代和优化。


原文链接
本文为云栖社区原创内容,未经允许不得转载。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/517284.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

为了这个技术,操作系统把 CPU 害惨了!

来源 | 编程技术宇宙责编 | Carol封图 | CSDN 下载自视觉中国内存访问瓶颈我是CPU一号车间的阿Q,前一阵子我们厂里发生了一件大喜事,老板拉到了一笔投资,准备扩大生产规模。不过老板挺抠门的,拉到了投资也不给我们涨点工资&#x…

一个周内上线50个增长策略,竟然能这么高效!

导读:年初的一个晨会上,用户增长负责人湘翁问我说:一个周内上线50个增长策略,技术兄弟们能做到么? 在闲鱼用户增长业务上的实验 闲鱼的用户增长业务具有如下现状: 闲鱼的卖家都是普通小卖家,而…

cannot convert value of type ‘org.codehaus.xfire.spring.editors.ServiceFactoryEditor

文章目录一、分析定位1.异常现象2. 分析定位二、解决方案2.1.找坐标2.3. 找jar包2.3. 打开jar包2.4. 找目录2.5. 编辑customEditors.xml一、分析定位 1.异常现象 cannot convert value of type ‘org.codehaus.xfire.spring.editors.ServiceFactoryEditor 2. 分析定位 原因…

网商银行首席架构师余锋:网商银行的每一笔贷款都是AI贷款

11月28日,在2019年度观察家金融峰会上,网商银行首席架构师余锋表示,网商银行在人工智能领域一直在探索新的应用方向,目前网商银行的每一笔贷款都是AI贷款。 网商银行是国内首家运行在云上的银行,没有线下网点&#xf…

解密 云HBase时序引擎OpenTSDB 优化技术

逝者如斯夫,不舍昼夜。 —— 孔子 时间如流水,一去不复返。自古不乏对时间流逝的感慨,而现代已经有很多技术记录流逝的过去。我们可以拍照,可以录像,当然还可…

官宣!CSDN 重磅发布「AI开源贡献奖Top5」「AI新锐公司奖Top10」「AI优秀案例奖Top30」三大榜单...

2020 年无疑是特殊的一年,AI 在开年的这场“战疫”中表现出惊人的力量。站在“新十年”的起点上,CSDN发起【百万人学AI】评选活动。我们继续聚焦AI的技术落地,关注开源和新生的力量。作为CSDN第三届AI评选活动,本次活动受到数百家…

SpringBoot2 整合 XFIRE 服务端和客户端

文章目录一、集成XFIRE1. 版本选型2. 导入依赖3. 注入XFireSpringServlet4. 创建一个xml文件5. 使用ImportResource注入xml6. 创建WebService接口6. 创建实现类7. 添加配置类8. 工具类二、XFIRE 发布服务2.1. 运行项目2.2. 异常解决2.3. 测试验证三、XFIRE客户端开源源码.一、集…

css3动画过渡按钮

css(css代码是网上找的)和html代码: .mui-switch {width: 52px;height: 31px;position: relative;border: 1px solid #dfdfdf;background-color: #fdfdfd;box-shadow: #dfdfdf 0 0 0 0 inset;border-radius: 20px;border-top-left-radius: 20px;border-top-right-ra…

阿里云上万个 Kubernetes 集群大规模管理实践

内容简介: 阿里云容器服务从2015年上线后,一路伴随并支撑双十一发展。在2019年的双十一中,容器服务ACK除了支撑集团内部核心系统容器化上云和阿里云的云产品本身,也将阿里多年的大规模容器技术以产品化的能力输出给众多围绕双十一…

SpringBoot2 整合 AXIS 服务端和客户端

文章目录一、服务端1. 版本选型2.导入依赖3. SERVLET4. 接口5.实现类6. 配置工厂7.启动类8. WEB-INF目录18. WEB-INF目录29. /目录19. /目录210. wsdd11. 测试验证二、客户端开源源码.一、服务端 1. 版本选型 阿健/框架版本spring-boot2.5.4axis1.4axis-jaxrpc1.4commons-dis…

地理文本处理技术在高德的演进(下)

在【上篇】里,我们介绍了地理文本处理技术在高德的整体演进,选取了几个通用query分析的点进行了介绍。下篇中,我们会选取几个地图搜索文本处理中特有的文本分析技术做出分析,包括城市分析,wherewhat分析,路…

真正拿大厂offer的人,都赢在这一点

学好一门技术最有价值的体现就是“面试”,对于大部分人来说 “面试”是涨薪的主要途径之一,因此我们需要认真的准备面试,因为它直接决定着你今后几年内的薪资水平,所以在面试这件事上花费再多的时间和精力都是值得的。你会发现有…

今天的这个小成绩,需要向阿里云的朋友报告一下!

今天,想向大家报告一个最新的小成绩: 在数据库领域的权威评选——Gartner全球数据库魔力象限评比中,阿里云成功进入“挑战者”象限,连续两年作为唯一的中国企业入选。 最新评选表明,阿里云过去一年在产品技术领域进展迅…

90%的人会遇到性能问题,如何用1行代码快速定位?

阿里妹导读:在《如何回答性能优化的问题,才能打动阿里面试官?》中,主要是介绍了应用常见性能瓶颈点的分布,及如何初判若干指标是否出现了异常。 今天,齐光将会基于之前列举的众多指标,给出一些常…

SpringBoot2 整合 CXF 服务端和客户端

文章目录一、CXF服务端1. 导入依赖2. 创建service接口3. 接口实现类4. cxf配置类5. 查看wsdl结果二、CXF客户端2.1. 客户端2.2. 断点调试2.3. 发起调用服务开源源码.一、CXF服务端 1. 导入依赖 <properties><cxf.version>3.3.1</cxf.version></propertie…

如果张东升是个程序员,你还有机会吗?

来源 | 编程技术宇宙责编 | Carol封图 | CSDN 下载自视觉中国张东升是一家互联网公司的程序员&#xff0c;一直以来都勤勤恳恳老实工作。可最近一段时间&#xff0c;老板接了几个项目回来&#xff0c;不但开启了996的工作模式&#xff0c;更要命的是频频更改需求&#xff0c;弄…

蚂蚁金服资深总监韩鸿源:企业级数据库平台的持续与创新

2019年11月19日&#xff0c;蚂蚁金服在北京举办“巅峰洞见聚焦金融新技术”发布会&#xff0c;介绍2019双11支付宝背后的技术&#xff0c;并重磅发布全新OceanBase 2.2版本。欢迎持续关注&#xff5e; 蚂蚁金服研究员韩鸿源在发布会分享了《企业级数据库平台的持续与创新》&…

jquery标题左右移动动画

标题会在红框范围内来回移动 html和css代码 <div class"menu-notice" click"check_cart"><div class"menu-notice-logo"></div><div class"menu-notice-title" ref"noticeTitle">{{storeinfo[0] ?…

解密 云HBase 冷热分离技术原理

前言 HBase是当下流行的一款海量数据存储的分布式数据库。往往海量数据存储会涉及到一个成本问题&#xff0c;如何降低成本。常见的方案就是通过冷热分离来治理数据。冷数据可以用更高的压缩比算法&#xff08;ZSTD&#xff09;&#xff0c;更低副本数算法&#xff08;Erasure…

再见,工资!2020年6月程序员工资统计,平均14404元,网友:又跌了!

见了鬼&#xff01;工资竟然又跌了2020 年 6 月全国招收程序员 313739 人。2020 年 6 月全国程序员平均工资 14404 元&#xff0c;工资中位数 12500 元&#xff0c;其中 95% 的人的工资介于 5250 元到 35000 元。怪不得小陈发现最近猎头的“骚扰”电话越来越少了&#xff0c;这…