js后退页面不重新加载_快应用:支持加载单独JS文件的规范思考

当前快应用的项目中,支持加载其它JS文件(通过:require('./foo.js')),然后通过webpack工具处理依赖,最终完成页面JS的构建,其中页面JS包含了引入的所有JS内容;
本文讨论的主要是:webpack打包时,能够将依赖JS不合入到页面JS中,而是在rpk的ZIP压缩文件中单独存在,然后运行时,页面在需要的时候加载该JS文件;
  1. 背景描述

当前如果每个页面都引入一个共同的JS文件,如:foo.js;那么toolkit工具打包时,会将每个foo.js都打包到页面JS代码中;

这样带来以下几个问题,造成页面渲染变慢:

1) 每个页面JS都包含了foo.js,使得JS代码重复,导致rpk包体积增大,从而运行时下载rpk时间变长;

2) 每个页面JS都包含各自的foo.js,运行时JS引擎编译代码字符串为AST和JS对象,用时变长;

3) 由于JS代码存在多个地方,导致每个页面引入的foo都是独立的JS对象,而不是:同一个JS内存对象;

面对上面的问题,我们提供一种新的规范来解决:编译时支持JS单独构建,并在运行时调用加载

2. 规范的方案拆解

针对上面所述,规范实现的工作主要拆解到两大模块中:

1) 编译时:页面中用到的公共JS文件;在构建时,生成单独的JS文件;保持rpk中只有一份该JS文件的代码;

2) 运行时:提供动态加载JS文件的API:页面中首次加载时,则从内存/磁盘中同步读取并执行;第二次加载时,选择复用内存中之前的JS对象或者重新执行;

上面划分看上去实现简单,但围绕一些拆解出来的子任务,其实思路很多,需要权衡利弊:

1) 编译时的单独JS构建方案;

2) 当前编译时用的toolkit使用的是webpack4;如果使用其它工具(如:rollupjs),如何持续兼容;

3) 动态加载JS文件时,需要考虑:模块化,缓存,页面/APP上下文,时间成本,向后兼容,可扩展能力等的问题;

比如:模块化是否要放在运行时;当页面销毁后如果保证页面里的接口调用也跟着销毁;

接下来,围绕各项子任务分别进行考虑;

3. 任务1:编译时的单独JS构建方案

目前市面上主流的构建方案分为两类:语法型,配置型;其中webpack4中的splitChunkPlugin应用就属于配置型;

3.1 语法型

这种指的是,快应用平台运行时直接给开发者暴露一个 $app_evaluate$ 的函数,开发者在自己的ux文件或者js文件中,通过`const foo = $app_evaluate$('./foo.js')`方式引入JS依赖;

当工具toolkit编译时,检测到这样的函数API的JS,就不再打包到页面JS中;

优点:

1) 开发者可以灵活改写,让某个JS既可以打包到某个页面中,又可以独立存在;

2) toolkit编译时无需开发者配置,简单场景下方便易用;

缺点:

1) 这种方式对单个JS引用,很容易替换不完全,造成重复打包;

2) 编译工具需要自行构建依赖分析的能力,将相对路径的引入转换为绝对路径;不仅无法复用webpack本身的依赖分析,而且需要增加依赖分析的逻辑和管理;实现起来增加工作量和难度;

3) 对开发者来说,有认知成本,与现有的require, import语法相比,不够贴切自然;

3.2 配置型

这种指的是,开发者通过给某个JS文件增加标识,标识为独立打包;接着在ux与js中,如果通过require或者import引入的,那么toolkit在编译时,根据标识,就不再打包到页面JS的代码中,而是单独创建一个JS文件;

比如:page1,page2等都引用了 foo.js文件,foo.js中声明:`/** quickapp:standalone */` 标识为该文件不打包到页面JS中;

优点:

1) 开发者只需要在JS文件中声明一下即可,或者采用其它配置形式(如:在manifest.json或者quickapp.config.js的配置文件中声明);

缺点:

1) 如果JS文件很多,可能每个希望独立打包的JS都需要声明;

2) 编译时webpack分析出依赖关系后,需要替换代码中的`require`关键字为`$app_evaluate$`,避免被webpack引入;

当然,配置型的表现形式也有很多:

1) 与文件所在路径放在一起,同时更新:

在JS文件中,使用标记 `/** quickapp:standalone */` 类似语法,配置每个JS文件单独打包;

2) 在manifest.json中声明:

在manifest.json文件中,配置JS文件单独打包,通过resource属性去声明单独打包的JS文件的路径;

3) 在quickapp.config.js中声明:

`quickapp.config.js`是快应用项目下的一个配置文件,代表项目将如何构建;通过在配置文件中声明,以确定哪些文件单独创建;

由于JS单独打包的功能仅仅只是一个编译时的工作,不应该与manifest这种运行时检查的文件混合在一起,因此优于上一条;

以上三种类似Java中的Annotation与XML声明,各有优缺:

前者直接分散,和代码一起,不必担心JS文件路径变化时,引起后者配置路径无效的问题;

后者直观方便,统一性强,但是与JS文件代码分离较远,容易发生路径找不到;

3.3 总结

其实对开发者来说,他关注的是:如何以最小的认知成本,来达到rpk最小的体积与最快的页面加载;

所以,最佳的方式是:什么配置都不需要就能实现,对开发者无感知;

其次是简单的一条选择项或一条配置就能完成;

语法型有较大的缺陷、难度、认知,就不再考虑了;

上面说的语法型、配置型是站在 开发者的角度上说的;如果站在内部实现的角度来说,可能部分属于语法型了,因为涉及到对`require('foo.js')`替换为`$app_evaluate$('foo.js')`的更改;

4. 任务2:各页面加载同一个JS文件时,其JS对象是否应该共用

下文的`evaluate`指的是:编译JS文件的字符串代码 转换为 JS对象;(通过:eval,new Function等形式)

共用指的是:各页面都依赖同一个JS文件时,那么这个JS文件在evaluate后对应的JS对象,是否应该被各页面共用?

如果共用,意味着:各页面之间访问的是:同一个JS对象;页面之间对该模块可以一起更新并取值;

如果不共用,意味着:各页面之间访问的是:不同的JS对象;页面之间的该模块操作互不干扰影响;

4.1 总结

经过讨论,我们认为:各页面之间不要共用,利大于弊;

因为快应用是一个APP的概念,页面与页面之间应该保持很强的独立性,页面之间的数据与状态更新应该通过专有的固定通道来通信;

否则的话,开发者容易滥用,带来页面之间公用模块的互相操作影响,造成:耦合性强,状态不稳定,通信机制泛滥,监控不到位;

与浏览器中运行的SPA模式、NodeJS中模块共用的方式相比,快应用选择了不一样的设计思路,最主要的目的就是:保证页面的强独立性;

5. 任务3:如果JS模块不共用,那么页面之间隔离的方式怎么实现?

5.1 方式1. JS文件只会evaluate一次;

虽然JS文件仅evaluate一次,但是利用编译时webpack的能力,它将每个JS文件封装为模块化代码(`function (module, exports, __webpack_require__) { ...code... }`);

当每个页面重新加载该JS时,其实是填充到了各自页面级别的module缓存`installedModules`中;

这样,当每次页面引入该JS时,得到的就是,该页面下的该模块的JS对象;

优点:JS的evaluate只会一次,但页面对应的该JS模块会重新填充一次;

缺点:JS中模块化代码之外的代码只会执行一次,但是模块内的代码会执行N次(N个页面加载);

5.2 方式2. 每次页面加载JS文件时,都重新evaluate对应的JS代码,得到不同的JS对象;

优点:做到了运行时的天然隔离每个JS模块,即使以后更换为没有模块化的打包工具时,仍然能够无缝隔离;

缺点:每次新页面加载,都重新evaluate,引起同样的JS代码执行多次;

5.3 总结

综上所述,我们认为:页面中的JS模块独立性是一项运行时的能力;

这种能力不应该依赖于编译时的保证,以后即使不使用webpack编译工具时,仍然能够保证隔离,而且还提高了页面的安全性;

所以,选择方式2,页面每次加载都重新evaluate对应的JS代码;

6. 任务4. 单独JS文件的模块化管理

既然要支持单独的JS文件加载,那么就需要对所有的单独JS文件做模块化管理;

需要考虑几个方面:实现位置,递归依赖,路径转换;

实现位置指的是:模块化的功能在运行时实现,还是编译时实现;

递归依赖的问题,可以参考NodeJS中的模块化实现,是在首次加载JS文件前,先定义模块为空对象,然后执行时包裹一段模块化代码:`function (exports, require, module, __filename, __dirname) { ...code... }`;当发生递归式依赖时,传递之前已经定义的模块对象;

由此来看,模块化无论发生在编译时还是运行时,都需要解决递归依赖;

6.1 实现位置:编译时

在webpack构建中,如果是仅生成一个JS文件,是通过`installedModules`内部缓存了各个模块;如果要生成多个JS文件时,是通过`webpackJsonp`完成的,因此只需要对这块针对快应用添加适配,提供同步读取rpk中JS文件的能力,即可完成编译时模块化的能力;

编译时同时也需要完成源代码中相对路径到绝对路径的转换,这块都可以使用webpack现有的实现方式;

6.2 实现位置:运行时

运行时模块化增加这块机制的好处在于:可以摆脱对webpack编译工具的模块管理依赖,以后如果有其它的编译工具,那么也能够无缝支持;

6.3 总结

考虑到规范发版,开发的时间成本,本次先利用成熟的webpack工具,即:编译时能力解决,待以后再增加对应的运行时实现;

关于模块化中的`动态加载`模块的能力(`const variable1 = 'test'; require(variable1);`),以及异步加载的能力,由于需求不太紧迫,因此本次规范暂不考虑;

7. 任务5:流式加载

当前快应用项目构建的RPK文件是一个ZIP文件,里面根据运行时加载JS文件的顺序,相应的安排了每个文件在ZIP索引中的顺序;比如根据顺序,先后主要为:`manifest.json`,`app.js`,每个`page.js`;

这项任务指的是:如何安排独立JS文件的位置(哪些在页面JS之前与之后),让页面首屏渲染需要的文件能够保证内存级别的同步读取耗时最少(避免磁盘读引起的时间损耗);

也就是说,这项任务的核心是:是否有办法确定哪些JS是属于页面首屏渲染必须的?

经过分析之后,目前没有直接的办法确定,通过让开发者加标记FLAG也并不是一件好办法;

但可以换个思路考虑问题:假设所有的独立JS放在页面JS之前,因为它并不会被立即执行为为JS对象,所以放在页面之前也仅仅只是增加了RPK下载与ZIP解压缩这些独立JS的时间;目前看,这些时间相对较小;

所以实现思路上,可以将:单独JS全部放在app.js与页面JS之前,因为app.js也可能会用到独立JS;

8. 任务6:分包支持

当前快应用支持分包能力,非独立包又分为:页面模块包与基础包;

为了管理上的方便,建议:将所有的独立JS放置在一个统一的固定目录下,如:(`%PROJECT%/build/chunks/foo.js`);

定义一个`chunks`的文件夹,将独立JS在里面,当然这个目录下可能还存在子目录的路径;

8.1 非独立包

如果是非独立包的话,可以将所有的独立JS也就是对应的目录,移动到:`基础包`中;

8.2 独立包

如果是独立包的话,则将自己用到的独立JS放在该模块下;

9. 文章总结

经过上面几个任务的分析,为了使得v1070版本提供的规范尽快发版,将使用编译时的模块化能力,运行时仅提供文件的同步读取API,以支持:加载单独JS文件的规范;

针对于运行时模块化(即:动态加载)、异步加载的其它需求,将在后面收到开发者需求后再制定快应用联盟规范。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值