javascript
js后退页面不重新加载_快应用:支持加载单独JS文件的规范思考
本文討論的主要是:webpack打包時(shí),能夠?qū)⒁蕾?lài)JS不合入到頁(yè)面JS中,而是在rpk的ZIP壓縮文件中單獨(dú)存在,然后運(yùn)行時(shí),頁(yè)面在需要的時(shí)候加載該JS文件;
當(dāng)前如果每個(gè)頁(yè)面都引入一個(gè)共同的JS文件,如:foo.js;那么toolkit工具打包時(shí),會(huì)將每個(gè)foo.js都打包到頁(yè)面JS代碼中;
這樣帶來(lái)以下幾個(gè)問(wèn)題,造成頁(yè)面渲染變慢:
1) 每個(gè)頁(yè)面JS都包含了foo.js,使得JS代碼重復(fù),導(dǎo)致rpk包體積增大,從而運(yùn)行時(shí)下載rpk時(shí)間變長(zhǎng);
2) 每個(gè)頁(yè)面JS都包含各自的foo.js,運(yùn)行時(shí)JS引擎編譯代碼字符串為AST和JS對(duì)象,用時(shí)變長(zhǎng);
3) 由于JS代碼存在多個(gè)地方,導(dǎo)致每個(gè)頁(yè)面引入的foo都是獨(dú)立的JS對(duì)象,而不是:同一個(gè)JS內(nèi)存對(duì)象;
面對(duì)上面的問(wèn)題,我們提供一種新的規(guī)范來(lái)解決:編譯時(shí)支持JS單獨(dú)構(gòu)建,并在運(yùn)行時(shí)調(diào)用加載;
2. 規(guī)范的方案拆解
針對(duì)上面所述,規(guī)范實(shí)現(xiàn)的工作主要拆解到兩大模塊中:
1) 編譯時(shí):頁(yè)面中用到的公共JS文件;在構(gòu)建時(shí),生成單獨(dú)的JS文件;保持rpk中只有一份該JS文件的代碼;
2) 運(yùn)行時(shí):提供動(dòng)態(tài)加載JS文件的API:頁(yè)面中首次加載時(shí),則從內(nèi)存/磁盤(pán)中同步讀取并執(zhí)行;第二次加載時(shí),選擇復(fù)用內(nèi)存中之前的JS對(duì)象或者重新執(zhí)行;
上面劃分看上去實(shí)現(xiàn)簡(jiǎn)單,但圍繞一些拆解出來(lái)的子任務(wù),其實(shí)思路很多,需要權(quán)衡利弊:
1) 編譯時(shí)的單獨(dú)JS構(gòu)建方案;
2) 當(dāng)前編譯時(shí)用的toolkit使用的是webpack4;如果使用其它工具(如:rollupjs),如何持續(xù)兼容;
3) 動(dòng)態(tài)加載JS文件時(shí),需要考慮:模塊化,緩存,頁(yè)面/APP上下文,時(shí)間成本,向后兼容,可擴(kuò)展能力等的問(wèn)題;
比如:模塊化是否要放在運(yùn)行時(shí);當(dāng)頁(yè)面銷(xiāo)毀后如果保證頁(yè)面里的接口調(diào)用也跟著銷(xiāo)毀;
接下來(lái),圍繞各項(xiàng)子任務(wù)分別進(jìn)行考慮;
3. 任務(wù)1:編譯時(shí)的單獨(dú)JS構(gòu)建方案
目前市面上主流的構(gòu)建方案分為兩類(lèi):語(yǔ)法型,配置型;其中webpack4中的splitChunkPlugin應(yīng)用就屬于配置型;
3.1 語(yǔ)法型
這種指的是,快應(yīng)用平臺(tái)運(yùn)行時(shí)直接給開(kāi)發(fā)者暴露一個(gè) $app_evaluate$ 的函數(shù),開(kāi)發(fā)者在自己的ux文件或者js文件中,通過(guò)`const foo = $app_evaluate$('./foo.js')`方式引入JS依賴(lài);
當(dāng)工具toolkit編譯時(shí),檢測(cè)到這樣的函數(shù)API的JS,就不再打包到頁(yè)面JS中;
優(yōu)點(diǎn):
1) 開(kāi)發(fā)者可以靈活改寫(xiě),讓某個(gè)JS既可以打包到某個(gè)頁(yè)面中,又可以獨(dú)立存在;
2) toolkit編譯時(shí)無(wú)需開(kāi)發(fā)者配置,簡(jiǎn)單場(chǎng)景下方便易用;
缺點(diǎn):
1) 這種方式對(duì)單個(gè)JS引用,很容易替換不完全,造成重復(fù)打包;
2) 編譯工具需要自行構(gòu)建依賴(lài)分析的能力,將相對(duì)路徑的引入轉(zhuǎn)換為絕對(duì)路徑;不僅無(wú)法復(fù)用webpack本身的依賴(lài)分析,而且需要增加依賴(lài)分析的邏輯和管理;實(shí)現(xiàn)起來(lái)增加工作量和難度;
3) 對(duì)開(kāi)發(fā)者來(lái)說(shuō),有認(rèn)知成本,與現(xiàn)有的require, import語(yǔ)法相比,不夠貼切自然;
3.2 配置型
這種指的是,開(kāi)發(fā)者通過(guò)給某個(gè)JS文件增加標(biāo)識(shí),標(biāo)識(shí)為獨(dú)立打包;接著在ux與js中,如果通過(guò)require或者import引入的,那么toolkit在編譯時(shí),根據(jù)標(biāo)識(shí),就不再打包到頁(yè)面JS的代碼中,而是單獨(dú)創(chuàng)建一個(gè)JS文件;
比如:page1,page2等都引用了 foo.js文件,foo.js中聲明:`/** quickapp:standalone */` 標(biāo)識(shí)為該文件不打包到頁(yè)面JS中;
優(yōu)點(diǎn):
1) 開(kāi)發(fā)者只需要在JS文件中聲明一下即可,或者采用其它配置形式(如:在manifest.json或者quickapp.config.js的配置文件中聲明);
缺點(diǎn):
1) 如果JS文件很多,可能每個(gè)希望獨(dú)立打包的JS都需要聲明;
2) 編譯時(shí)webpack分析出依賴(lài)關(guān)系后,需要替換代碼中的`require`關(guān)鍵字為`$app_evaluate$`,避免被webpack引入;
當(dāng)然,配置型的表現(xiàn)形式也有很多:
1) 與文件所在路徑放在一起,同時(shí)更新:
在JS文件中,使用標(biāo)記 `/** quickapp:standalone */` 類(lèi)似語(yǔ)法,配置每個(gè)JS文件單獨(dú)打包;
2) 在manifest.json中聲明:
在manifest.json文件中,配置JS文件單獨(dú)打包,通過(guò)resource屬性去聲明單獨(dú)打包的JS文件的路徑;
3) 在quickapp.config.js中聲明:
`quickapp.config.js`是快應(yīng)用項(xiàng)目下的一個(gè)配置文件,代表項(xiàng)目將如何構(gòu)建;通過(guò)在配置文件中聲明,以確定哪些文件單獨(dú)創(chuàng)建;
由于JS單獨(dú)打包的功能僅僅只是一個(gè)編譯時(shí)的工作,不應(yīng)該與manifest這種運(yùn)行時(shí)檢查的文件混合在一起,因此優(yōu)于上一條;
以上三種類(lèi)似Java中的Annotation與XML聲明,各有優(yōu)缺:
前者直接分散,和代碼一起,不必?fù)?dān)心JS文件路徑變化時(shí),引起后者配置路徑無(wú)效的問(wèn)題;
后者直觀方便,統(tǒng)一性強(qiáng),但是與JS文件代碼分離較遠(yuǎn),容易發(fā)生路徑找不到;
3.3 總結(jié)
其實(shí)對(duì)開(kāi)發(fā)者來(lái)說(shuō),他關(guān)注的是:如何以最小的認(rèn)知成本,來(lái)達(dá)到rpk最小的體積與最快的頁(yè)面加載;
所以,最佳的方式是:什么配置都不需要就能實(shí)現(xiàn),對(duì)開(kāi)發(fā)者無(wú)感知;
其次是簡(jiǎn)單的一條選擇項(xiàng)或一條配置就能完成;
語(yǔ)法型有較大的缺陷、難度、認(rèn)知,就不再考慮了;
上面說(shuō)的語(yǔ)法型、配置型是站在開(kāi)發(fā)者的角度上說(shuō)的;如果站在內(nèi)部實(shí)現(xiàn)的角度來(lái)說(shuō),可能部分屬于語(yǔ)法型了,因?yàn)樯婕暗綄?duì)`require('foo.js')`替換為`$app_evaluate$('foo.js')`的更改;4. 任務(wù)2:各頁(yè)面加載同一個(gè)JS文件時(shí),其JS對(duì)象是否應(yīng)該共用
下文的`evaluate`指的是:編譯JS文件的字符串代碼 轉(zhuǎn)換為 JS對(duì)象;(通過(guò):eval,new Function等形式)共用指的是:各頁(yè)面都依賴(lài)同一個(gè)JS文件時(shí),那么這個(gè)JS文件在evaluate后對(duì)應(yīng)的JS對(duì)象,是否應(yīng)該被各頁(yè)面共用?
如果共用,意味著:各頁(yè)面之間訪問(wèn)的是:同一個(gè)JS對(duì)象;頁(yè)面之間對(duì)該模塊可以一起更新并取值;
如果不共用,意味著:各頁(yè)面之間訪問(wèn)的是:不同的JS對(duì)象;頁(yè)面之間的該模塊操作互不干擾影響;
4.1 總結(jié)
經(jīng)過(guò)討論,我們認(rèn)為:各頁(yè)面之間不要共用,利大于弊;
因?yàn)榭鞈?yīng)用是一個(gè)APP的概念,頁(yè)面與頁(yè)面之間應(yīng)該保持很強(qiáng)的獨(dú)立性,頁(yè)面之間的數(shù)據(jù)與狀態(tài)更新應(yīng)該通過(guò)專(zhuān)有的固定通道來(lái)通信;
否則的話,開(kāi)發(fā)者容易濫用,帶來(lái)頁(yè)面之間公用模塊的互相操作影響,造成:耦合性強(qiáng),狀態(tài)不穩(wěn)定,通信機(jī)制泛濫,監(jiān)控不到位;
與瀏覽器中運(yùn)行的SPA模式、NodeJS中模塊共用的方式相比,快應(yīng)用選擇了不一樣的設(shè)計(jì)思路,最主要的目的就是:保證頁(yè)面的強(qiáng)獨(dú)立性;
5. 任務(wù)3:如果JS模塊不共用,那么頁(yè)面之間隔離的方式怎么實(shí)現(xiàn)?
5.1 方式1. JS文件只會(huì)evaluate一次;
雖然JS文件僅evaluate一次,但是利用編譯時(shí)webpack的能力,它將每個(gè)JS文件封裝為模塊化代碼(`function (module, exports, __webpack_require__) { ...code... }`);
當(dāng)每個(gè)頁(yè)面重新加載該JS時(shí),其實(shí)是填充到了各自頁(yè)面級(jí)別的module緩存`installedModules`中;
這樣,當(dāng)每次頁(yè)面引入該JS時(shí),得到的就是,該頁(yè)面下的該模塊的JS對(duì)象;
優(yōu)點(diǎn):JS的evaluate只會(huì)一次,但頁(yè)面對(duì)應(yīng)的該JS模塊會(huì)重新填充一次;
缺點(diǎn):JS中模塊化代碼之外的代碼只會(huì)執(zhí)行一次,但是模塊內(nèi)的代碼會(huì)執(zhí)行N次(N個(gè)頁(yè)面加載);
5.2 方式2. 每次頁(yè)面加載JS文件時(shí),都重新evaluate對(duì)應(yīng)的JS代碼,得到不同的JS對(duì)象;
優(yōu)點(diǎn):做到了運(yùn)行時(shí)的天然隔離每個(gè)JS模塊,即使以后更換為沒(méi)有模塊化的打包工具時(shí),仍然能夠無(wú)縫隔離;
缺點(diǎn):每次新頁(yè)面加載,都重新evaluate,引起同樣的JS代碼執(zhí)行多次;
5.3 總結(jié)
綜上所述,我們認(rèn)為:頁(yè)面中的JS模塊獨(dú)立性是一項(xiàng)運(yùn)行時(shí)的能力;
這種能力不應(yīng)該依賴(lài)于編譯時(shí)的保證,以后即使不使用webpack編譯工具時(shí),仍然能夠保證隔離,而且還提高了頁(yè)面的安全性;
所以,選擇方式2,頁(yè)面每次加載都重新evaluate對(duì)應(yīng)的JS代碼;
6. 任務(wù)4. 單獨(dú)JS文件的模塊化管理
既然要支持單獨(dú)的JS文件加載,那么就需要對(duì)所有的單獨(dú)JS文件做模塊化管理;
需要考慮幾個(gè)方面:實(shí)現(xiàn)位置,遞歸依賴(lài),路徑轉(zhuǎn)換;
實(shí)現(xiàn)位置指的是:模塊化的功能在運(yùn)行時(shí)實(shí)現(xiàn),還是編譯時(shí)實(shí)現(xiàn);遞歸依賴(lài)的問(wèn)題,可以參考NodeJS中的模塊化實(shí)現(xiàn),是在首次加載JS文件前,先定義模塊為空對(duì)象,然后執(zhí)行時(shí)包裹一段模塊化代碼:`function (exports, require, module, __filename, __dirname) { ...code... }`;當(dāng)發(fā)生遞歸式依賴(lài)時(shí),傳遞之前已經(jīng)定義的模塊對(duì)象;
由此來(lái)看,模塊化無(wú)論發(fā)生在編譯時(shí)還是運(yùn)行時(shí),都需要解決遞歸依賴(lài);
6.1 實(shí)現(xiàn)位置:編譯時(shí)
在webpack構(gòu)建中,如果是僅生成一個(gè)JS文件,是通過(guò)`installedModules`內(nèi)部緩存了各個(gè)模塊;如果要生成多個(gè)JS文件時(shí),是通過(guò)`webpackJsonp`完成的,因此只需要對(duì)這塊針對(duì)快應(yīng)用添加適配,提供同步讀取rpk中JS文件的能力,即可完成編譯時(shí)模塊化的能力;
編譯時(shí)同時(shí)也需要完成源代碼中相對(duì)路徑到絕對(duì)路徑的轉(zhuǎn)換,這塊都可以使用webpack現(xiàn)有的實(shí)現(xiàn)方式;
6.2 實(shí)現(xiàn)位置:運(yùn)行時(shí)
運(yùn)行時(shí)模塊化增加這塊機(jī)制的好處在于:可以擺脫對(duì)webpack編譯工具的模塊管理依賴(lài),以后如果有其它的編譯工具,那么也能夠無(wú)縫支持;
6.3 總結(jié)
考慮到規(guī)范發(fā)版,開(kāi)發(fā)的時(shí)間成本,本次先利用成熟的webpack工具,即:編譯時(shí)能力解決,待以后再增加對(duì)應(yīng)的運(yùn)行時(shí)實(shí)現(xiàn);
關(guān)于模塊化中的`動(dòng)態(tài)加載`模塊的能力(`const variable1 = 'test'; require(variable1);`),以及異步加載的能力,由于需求不太緊迫,因此本次規(guī)范暫不考慮;7. 任務(wù)5:流式加載
當(dāng)前快應(yīng)用項(xiàng)目構(gòu)建的RPK文件是一個(gè)ZIP文件,里面根據(jù)運(yùn)行時(shí)加載JS文件的順序,相應(yīng)的安排了每個(gè)文件在ZIP索引中的順序;比如根據(jù)順序,先后主要為:`manifest.json`,`app.js`,每個(gè)`page.js`;
這項(xiàng)任務(wù)指的是:如何安排獨(dú)立JS文件的位置(哪些在頁(yè)面JS之前與之后),讓頁(yè)面首屏渲染需要的文件能夠保證內(nèi)存級(jí)別的同步讀取耗時(shí)最少(避免磁盤(pán)讀引起的時(shí)間損耗);
也就是說(shuō),這項(xiàng)任務(wù)的核心是:是否有辦法確定哪些JS是屬于頁(yè)面首屏渲染必須的?
經(jīng)過(guò)分析之后,目前沒(méi)有直接的辦法確定,通過(guò)讓開(kāi)發(fā)者加標(biāo)記FLAG也并不是一件好辦法;
但可以換個(gè)思路考慮問(wèn)題:假設(shè)所有的獨(dú)立JS放在頁(yè)面JS之前,因?yàn)樗⒉粫?huì)被立即執(zhí)行為為JS對(duì)象,所以放在頁(yè)面之前也僅僅只是增加了RPK下載與ZIP解壓縮這些獨(dú)立JS的時(shí)間;目前看,這些時(shí)間相對(duì)較小;
所以實(shí)現(xiàn)思路上,可以將:單獨(dú)JS全部放在app.js與頁(yè)面JS之前,因?yàn)閍pp.js也可能會(huì)用到獨(dú)立JS;
8. 任務(wù)6:分包支持
當(dāng)前快應(yīng)用支持分包能力,非獨(dú)立包又分為:頁(yè)面模塊包與基礎(chǔ)包;
為了管理上的方便,建議:將所有的獨(dú)立JS放置在一個(gè)統(tǒng)一的固定目錄下,如:(`%PROJECT%/build/chunks/foo.js`);
定義一個(gè)`chunks`的文件夾,將獨(dú)立JS在里面,當(dāng)然這個(gè)目錄下可能還存在子目錄的路徑;
8.1 非獨(dú)立包
如果是非獨(dú)立包的話,可以將所有的獨(dú)立JS也就是對(duì)應(yīng)的目錄,移動(dòng)到:`基礎(chǔ)包`中;
8.2 獨(dú)立包
如果是獨(dú)立包的話,則將自己用到的獨(dú)立JS放在該模塊下;
9. 文章總結(jié)
經(jīng)過(guò)上面幾個(gè)任務(wù)的分析,為了使得v1070版本提供的規(guī)范盡快發(fā)版,將使用編譯時(shí)的模塊化能力,運(yùn)行時(shí)僅提供文件的同步讀取API,以支持:加載單獨(dú)JS文件的規(guī)范;
針對(duì)于運(yùn)行時(shí)模塊化(即:動(dòng)態(tài)加載)、異步加載的其它需求,將在后面收到開(kāi)發(fā)者需求后再制定快應(yīng)用聯(lián)盟規(guī)范。
總結(jié)
以上是生活随笔為你收集整理的js后退页面不重新加载_快应用:支持加载单独JS文件的规范思考的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 最高响应比优先算法(HRRF)及例题详解
- 下一篇: Spring Beans 初始化流程分析