bundleԴ?????
本系列深入探讨SPA单页应用技术栈,首篇聚焦于React动态加载机制,分析解析当前流行方案的源码实现原理。
随着项目复杂度的分析提升和代码量的激增,如企业微信文档融合项目,源码代码量翻倍,分析小游戏大全源码性能和用户体验面临挑战。源码SPA的分析特性使得代码分割成为优化代码体积的关键策略。
code-splitting原理在于将大型bundle拆分为多个,源码实现按需加载和缓存,分析显著降低前端应用的源码加载体积。ES标准的分析import()函数提供动态加载支持,babel编译后,源码import将模块内容转换为ESM数据结构,分析通过promise返回,源码加载后在then中注册回调。
webpack检测到import()时,自动进行code-splitting,动态import的模块被打包到新bundle中。通过注释可自定义命名,如指定bar为动态加载bundle。
实现简易版动态加载方案,利用code-splitting和import,组件在渲染前加载,渲染完成前展示Loading状态,优化用户体验。然而,复杂场景如加载失败、未完成等需要额外处理。
引入React-loadable,动态加载任意模块的高阶组件,封装动态加载逻辑,支持多资源加载。通过传入参数如模块加载函数、Loading状态组件,统一处理动态加载成功与异常。
通过react-loadable改造组件,实现加载前渲染Loading状态,加载完成后更新组件。支持单资源或多资源Map动态加载,兼容多种场景。
Loadable核心是createLoadableComponent函数,采用策略模式,根据不同场景(单资源或多资源Map)加载模块。load方法封装加载状态与结果,loadMap方法加载多个loader,返回对象。minio源码编译
LoadableComponent高阶组件实现逻辑简单,通过注册加载完成与失败的回调,更新组件状态。默认渲染方法为React.createElement(),使用Loadable.Map时需显式传入渲染函数。
在服务端渲染(SSR)场景下,动态加载组件无法准确获取DOM结构,react-loadable提供解决方案,将异步加载转化为同步,支持SSR。
React loadable原始仓库不再维护,局限性体现在适用的webpack与babel版本、兼容性问题以及不支持现代React项目。针对此问题,@react-loadable/revised包提供基于Hooks与ts重构的解决方案。
React-loadable的实现原理与思路较为直观,下文将深入探讨React.lazy + Suspense的原生解决方案,理解Fiber架构中的动态加载,有助于掌握更深层次的知识。
Vscode-nls源码解析-NLS国际化实现
探究vscode-nls源码解析,深入了解其实现细节。
NLS,自然语言字符串,vscode插件使用NLS进行国际化处理。
初始化时,通过initializeSettings函数根据vscode配置初始化options与resolvedBundles,此过程涉及languagePackSupport、messageFormat等。
调用nls.config,源码位于src/node/main.ts,此过程中重点在于处理opts.messageFormat与opts.bundleFormat。messageFormat类型有三种,bundleFormat类型有两种,若需进行bundle处理,则应调整nls.config输入形式。
在需要国际化的文件中,调用nls.loadMessageBundle,将当前文件路径作为参数传递,若messageFormat设置为both或bundle,将执行特定代码。
tryFindMetaDataHeaderFile成功返回,需在打包脚本中调用vscode-nls-dev中的nls.bundleMetaDataFiles函数生成nls.metadata.header.json文件。
初次获取bundle时,resolvedBundles为空数组,首次获取必定为undefined。通过loadNlsBundle内部的stl copy源码findInTheBoxBundle方法,读取nls.bundle.zh-cn.json文件,此文件需由vscode-nls-dev的bundleLanguageFiles函数打包所有in文件,读取数据为vscode当前语言信息。
如何获取文件的国际化信息?在插件中使用返回的createScopedLocalizeFunction,传入key、message、args,key转为number类型作为messages的索引,返回messages[key]。理解过程在vscode-nls插件使用场景中,文件国际化信息即通过此函数调用获取。
若设置messageFormat为.file,resolveLanguage则负责查找对应文件的in.json文件,通过readJsonFileSync读取,之后createScopedLocalizeFunction与bundle方式相同。
初次import vscode-nls,bundle将所有文件的国际化信息存储在resolvedBundles数组中,后续文件读取信息直接从数组中获取。而file方式则每次读取对应.in.json文件来获取信息。
Android App Bundle解析
Android App Bundle 是一种官方发布的格式,通过使用它可以减少应用的包大小,从而提升安装成功率并减少卸载量。文件格式包含Base Module和我们拆分的Feature Module文件夹,签名文件和其他的配置文件。每个Moudle文件夹内包含dex,manifest,res,和一个resources.pb文件,与APK的文件结构基本保持一致。base module和每个Dynamic Feature Module都包含各自的代码和资源,它们共同组成了apk文件的内容。
Google Play基于对aab文件处理,将App Bundle在多个维度进行拆分,在资源维度,ABI维度和Language维度进行了拆分。当用户下载apk时,gogle play会获取手机的信息,然后根据App Bundle拼装好一个apk,这个apk的资源只有手机所需的,而且so库只有与手机兼容的,其他无关的都会剔除,从而减少了apk的大小。
通过Android App bundle可以基于维度的选择减少apk大小,另外Google Play还提供了动态交付功能。Android App Bundle 支持模块化,通过Dynamic Delivery with split APKs,kungfu量化源码将一个apk拆分成多个apk,按需加载(包括加载C/C++ libraries)。split APK的类型包括:按需加载模块和基本模块。
启用按需加载功能需要我们在base module中集成Play Core Library。用户下载应用时,只会下载base module对应的apk文件,Dynamic Feature Module对应的apk文件会在运行时按需下载。Play Core Library用来在App运行时请求下载Dynamic Feature Module对应的apk。可以查看 Play Core API 使用。
Google Play的下载与更新APK的逻辑很简单,每次下载时都会只下载非按需加载模块,下载了基本模块之后,就可以按需加载其他模块。如果之后我升级了APK,按需在加载模块会同时被更新。这样我们无需为按需加载模块做版本兼容处理。Base Moudle与 Dynamic Moudle版本永远都会是保存一直的。
我们通过android studio 的build bundle功能生成aab格式文件,必须测试 Google Play 使用该 Android App Bundle 生成 APK 的情形。验证方式包括:使用bundletool 命令行工具进行测试和通过 Google Play 将您的 app bundle 上传到 Play 管理中心并使用测试轨道进行测试。通过bundletool build-apks 命令从 app bundle 生成一组 APK,splits目录对各个moudle在资源维度,ABI维度和Language维度进行拆分。standalones目录生成一个APK,适用于小于的android手机。通过install-apks命令将 APK 部署到连接的设备,根据手机的设备信息安装对应的apk。
Android App bundles项目依赖结构发生变化,有base和feature模块,base中无法直接引用feature模块的类,feature模块可以直接依赖base模块。应用模块化带来好处包括:每个feature moudle都会生成自己独立的arsc文件,资源id的头两位有差异,使用时需要注意资源id的引用。动态模块上传到maven不可行,需要源码依赖工程,保持API使用与Google Play一致。
App Bundles 方案在减少APK大小方面有优势,但依赖Google Play才能做到业务模块的按需加载。爱奇艺开源的 Qigsaw框架实现了一套类似Google Play的方案,实现国内与国外场景的自由切换。
webpack打包原理 ? 看完这篇你就懂了 !
深入浅出 webpack
webpack 是一个现代 JavaScript 应用程序的静态模块打包器,其本质是对模块进行递归构建依赖关系图,然后打包成一个或多个 bundle。
理解 webpack 的工作流程,像是粮油网店源码理解一条生产线,每一步都有其职责,依赖于前一步骤的结果。插件则像是生产线中的功能模块,根据特定时机对资源进行处理。
webpack 通过 Tapable 组织整个生产流程,它在运行中广播事件,插件只需监听感兴趣的事件,即可加入流程,改变整个系统。事件流机制确保了插件的有序性,提高了系统的扩展性。
webpack 的核心概念包括:入口起点(Entry),定义了构建开始的模块;输出(Output),指定生成的 bundle 存储位置;模块(Module),一切皆模块,所有资源被转换为模块;代码块(Chunk),多个模块组合用于代码优化;loader,处理非 JavaScript 文件,将所有类型转换为可引用的模块;插件,执行更广泛任务。
理解 webpack 的构建流程,从入口文件开始,遍历依赖关系,转换为浏览器可执行代码,并生成最终的 bundle。在实践过程中,定义 Compiler 类,使用 babel 解析源代码,遍历 AST 抽象语法树,找出依赖模块,转换为可执行代码,并构建依赖关系图,重写 require 函数以输出 bundle。
完成 webpack 的理解,需要通过实践,例如构建一个简易版本的 webpack。从定义 Compiler 类开始,解析入口文件,使用 babel 解析内部语法,找出依赖模块,将 AST 转换为代码,递归解析所有依赖项,构建依赖关系图,重写 require 函数以输出 bundle。
通过实际操作,可以深入理解 webpack 的 bundle 实现过程,从入口文件执行开始,利用 eval 执行代码,处理依赖引用,生成最终的 bundle。通过这个实践过程,可以全面掌握 webpack 的工作原理和使用方法。
Hermes源码分析(二)——解析字节码
前面一节 讲到字节码序列化为二进制是有固定的格式的,这里我们分析一下源码里面是怎么处理的这里可以看到首先写入的是魔数,他的值为
对应的二进制见下图,注意是小端字节序
第二项是字节码的版本,笔者的版本是,也即 上图中的4a
第三项是源码的hash,这里采用的是SHA1算法,生成的哈希值是位,因此占用了个字节
第四项是文件长度,这个字段是位的,也就是下图中的为0aa,转换成十进制就是,实际文件大小也是这么多
后面的字段类似,就不一一分析了,头部所有字段的类型都可以在BytecodeFileHeader.h中看到,Hermes按照既定的内存布局把字段写入后再序列化,就得到了我们看到的字节码文件。
这里写入的数据很多,以函数头的写入为例,我们调用了visitFunctionHeader方法,并通过byteCodeModule拿到函数的签名,将其写入函数表(存疑,在实际的文件中并没有看到这一部分)。注意这些数据必须按顺序写入,因为读出的时候也是按对应顺序来的。
我们知道react-native 在加载字节码的时候需要调用hermes的prepareJavaScript方法, 那这个方法做了些什么事呢?
这里做了两件事情:
1. 判断是否是字节码,如果是则调用createBCProviderFromBuffer,否则调用createBCProviderFromSrc,我们这里只关注createBCProviderFromBuffer
2.通过BCProviderFromBuffer的构造方法得到文件头和函数头的信息(populateFromBuffer方法),下面是这个方法的实现。
BytecodeFileFields的populateFromBuffer方法也是一个模版方法,注意这里调用populateFromBuffer方法的是一个 ConstBytecodeFileFields对象,他代表的是不可变的字节码字段。
细心的读者会发现这里也有visitFunctionHeaders方法, 这里主要为了复用visitBytecodeSegmentsInOrder的逻辑,把populator当作一个visitor来按顺序读取buffer的内容,并提前加载到BytecodeFileFields里面,以减少后面执行字节码时解析的时间。
Hermes引擎在读取了字节码之后会通过解析BytecodeFileHeader这个结构体中的字段来获取一些关键信息,例如bundle是否是字节码格式,是否包含了函数,字节码的版本是否匹配等。注意这里我们只是解析了头部,没有解析整个字节码,后面执行字节码时才会解析剩余的部分。
evaluatePreparedJavaScript这个方法,主要是调用了HermesRuntime的 runBytecode方法,这里hermesPrep时上一步解析头部时获取的BCProviderFromBuffer实例。
runBytecode这个方法比较长,主要做了几件事情:
这里说明一下,Domain是用于垃圾回收的运行时模块的代理, Domain被创建时是空的,并跟随着运行时模块进行传播, 在运行时模块的整个生命周期内都一直存在。在某个Domain下创建的所有函数都会保持着对这个Domain的强引用。当Domain被回收的时候,这个Domain下的所有函数都不能使用。
未完待续。。。
代码拆分-使用SplitChunks
前言
探索代码优化的世界,最近开始接触项目优化工作,其中涉及三方组件的拆分。在未进行拆分前,可能存在两个场景:单一js文件过大,影响缓存效率;无法有效管理第三方库。利用`splitChunks`工具,可以将模块进行分割,并提取重复代码,解决上述问题。
概念区分 - module、bundle、chunk
深入理解`splitChunks`之前,先梳理几个概念。module:模块,在webpack中,任何文件都可视为模块,需要配置loader将其转换为支持打包的文件。chunk:编译完成待输出时,webpack将module按特定规则组合成一个个chunk。bundle:webpack处理完chunk文件后,生成供浏览器运行的代码。
chunk与bundle的关系
探析chunk的构成与bundle之间的关联。chunk有两种形式:初始化(initial)chunk,即入口起点的主chunk,包含入口起点及其依赖的所有模块;非初始化(non-initial)chunk,用于延迟加载,可能在使用动态导入或`SplitChunksPlugin`时出现。
通过入口产生的chunk
假设目录结构如下:index.js, another-module.js, webpack.config.js, package.json添加script配置,运行webpack并使用ndb追踪代码执行。通过命令启动浏览器,点击播放按钮执行build命令,追踪chunk到bundle的流转。
chunk处理步骤概览
从`Compilation`类的`seal`方法出发,首先搜集chunks,然后调用`createChunkAssets`方法生成source,为输出文件做准备;通过`compilation.emitAssets`方法记录资源信息到`compilation.assets`对象;一系列回调最终调用`onCompiled`方法,将assets信息写入输出目录,生成bundle文件。
Demo2 - 动态导入
将`index.js`中的lodash通过`import`方式导入,动态导入返回promise,通过`then`获取导入信息。修改`webpack.config.js`入口为单个`index.js`。源码追踪显示,初始化文件新增一个名为`index`的chunk,但在模块分析中识别到`import`方式,为`index.js`模块增加了`AsyncDependenciesBlock`标记,经过处理生成一个名为`null`的chunk。
总结:`chunk`是源代码中的抽象,封装定义如何将模块组写入文件,而`bundle`则是输出目录的文件。
解决隐患 - `splitChunks`配置
在上述示例中,存在三方模块重复引用的问题。通过简单的`optimization.splitChunks`配置,实现了lodash的抽离,降低了单个入口文件的大小。总结使用心得,`splitChunks`主要用于代码优化,针对不同场景配置`chunks`选项,如`all`、`async`、`initial`以及自定义函数,以达到高效拆分效果。
比较`async`、`initial`、`all`的区别
在示例中增加`another.js`,静态导入lodash,对比`async`、`all`、`initial`的不同效果。默认情况下,`initial`影响HTML文件中的脚本标签,而`async`仅针对动态导入,`all`则考虑更多场景,适合存在复用模块的情况,但需权衡动态导入及其内部依赖的抽离。
splitChunks.cacheGroups
在使用`splitChunks`基础上,通过`cacheGroups`实现更细粒度的代码拆分,进一步优化项目结构。
总结
通过`splitChunks`配置,实现三方组件的高效管理与拆分,优化代码结构与加载效率。理解模块、bundle、chunk之间的关系,以及如何利用`splitChunks`与`cacheGroups`进行代码拆分与优化,是提升项目性能的关键步骤。
小游戏/H5 首包、分包、加载优化方案与项目示例
麒麟子最近将《Jare 大冒险》升级到了 Cocos Creator 3.8,并更新到了 Cocos Store。在优化过程中,他通过更精细的分包管理、资源加载拆分,并利用分析工具剔除了不必要的资源加载,最终几乎可以做到秒进游戏。这篇文章将分享他是如何进行分包加载优化的。
Cocos Creator 的 bundle(分包)机制允许游戏拆分为不同的包。麒麟子首先查看了内置的包,发现它们的优先级不同。通过分析,麒麟子得到了一个最粗略的分包方案。在这种机制下,首包仅包含最简单的资源,使得引擎在启动时快速加载首包,用户在进入首包后启动加载流程时,能看到画面和进度条,不会感到焦虑。不过,对于一些游戏,通常会有一个主菜单界面,供玩家选择玩法、自定义数据、选择关卡等,此时可以单独分一个包作为缓冲,以提高用户体验。
对于场景中大量面板的问题,麒麟子使用了最新的KylinsToolkit 中的 KFC(Kylin's Framework Core)框架优化了界面管理。只需编辑好Prefab,并写好 Controller,即可在任何地方通过一行代码显示所需界面。界面的分层、资源加载、分辨率适配等都由KFC自动管理。
为了进一步优化资源加载,麒麟子使用了微信开发者工具中的代码依赖分析功能。通过分析,他发现了资源中的问题,并优化了分包大小,最终从.MB降低到了7.MB,缩小了3.MB。麒麟子提到,虽然目前仅处理了一些较大的和移除了不必要的资源引用,但完全优化更多包体仍需使用如pngquat等压缩工具来处理3D模型纹理。
麒麟子重启并开源了KylinsToolkit,并将项目框架部分抽取为了 KFC。KylinsToolkit 是麒麟子多年项目经验的总结,虽然不是最优解,但在一定程度上使项目的起步、模块分割、多人协同和后期维护更加顺畅。KFC包含了基础功能,并计划逐步加入网络、2D游戏常用控件、3D游戏常用控件等。
麒麟子希望基于KFC和KylinsToolkit中的其他模块来制作更多项目模板和案例,并邀请使用KFC和KylinsToolkit制作项目的朋友们加入。麒麟子也提供了一个领取KFC的链接,并表示后续会考虑使用码云镜像,但暂时还不知道具体步骤,期待有懂的朋友指导。
关于如何体验Jare大冒险源码,读者可自行开始体验。
2024-11-30 20:06
2024-11-30 19:30
2024-11-30 19:20
2024-11-30 19:17
2024-11-30 18:55