隨着移動設備的升級、網絡速度的提高,用戶對於web應用的要求越來越高,web應用要提供的功能越來越。功能的增加導致的最直觀的后果就是資源文件越來越大。為了維護越來越龐大的客戶端代碼,提出了模塊化的概念來組織代碼。webpack作為一種模塊化打包工具,隨着react的流行也越來越流行。
webpack在官方文檔上解釋為什么又做一個模塊打包工具的時候,是這樣說的:
The most pressing reason for developing another module bundler was Code Splitting and that static assets should fit seamlessly together through modularization.
開發一個新的模塊打包工具最重要的原因就是Code Splitting,並且還要保證靜態資源也可以無縫集成到模塊化中。其中Code Splitting是webpack提供的一個重要功能,通過這個功能可以實現按需加載,減少首次加載時間。
Code Splitting
翻譯一下官方文檔對於Code Splitting的介紹:
對於大型的web 應用而言,把所有的代碼放到一個文件的做法效率很差,特別是在加載了一些只有在特定環境下才會使用到的阻塞的代碼的時候。Webpack有個功能會把你的代碼分離成Chunk,后者可以按需加載。這個功能就是Code Spliiting
Code Spliting的具體做法就是一個分離點,在分離點中依賴的模塊會被打包到一起,可以異步加載。一個分離點會產生一個打包文件。
例如下面使用CommonJS風格的require.ensure作為分離點的代碼:
1
2
3
4
5
6
7
8
|
// 第一個參數是依賴列表,webpack會加載模塊,但不會執行
// 第二個參數是一個回調,在其中可以使用require載入模塊
// 下面的代碼會把module-a,module-b,module-c打包一個文件中,雖然module-c沒有在依賴列表里,但是在回調里調用了,一樣會被打包進來
require.ensure(["module-a", "module-b"], function(require) {
var a = require("module-a");
var b = require("module-b");
var c = require('module-c');
});
|
除了這樣的寫法,還可以在配置文件中使用CommonChunkPlugin合並文件
問題
現在進入正題,本文不會針對React或者Vue做示例,因為這兩個框架有很成熟的按需加載方案。
下面這個例子用Backbone Router做路由,但是其中提到的按需加載方式可以用到大多數路由系統中。
假設應用有三個路由:
- 主頁
- 關於
- 支付
開始時的代碼(index.js):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
import Backbone from 'backbone'
import $ from 'jquery'
var Route = export default Backbone.Router.extend({
routes: {
'': 'home',
'about': 'about',
'pay': 'pay'
},
home() {
$('#app').html('it is home');
},
about() {
$('#app').html('it is about');
},
pay() {
$('#app').html('it is pay');
}
});
new Route();
Backbone.history.start();
|
這里有三個url路徑:index, about, pay,對應了三個很簡單的handler。這樣的代碼量的時候,這樣寫是沒問題的。
但是隨着功能的增加,handler里的內容會越來越多,所以要先把handler分離到不同的模塊里。
邏輯分離
把about的handler放在新的目錄下(about/index.js):
1
2
3
4
|
import $ from 'jquery'
export default () =>{
$('#app').html('it is about');
}
|
index,pay也按照同樣的辦法分離出去。
在index.js中修改一下代碼:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
import Backbone from 'backbone'
import home from './home/index'
import about from './about/index'
import pay from './pay/index'
var Route = Backbone.Router.extend({
routes: {
'': 'home',
'about': 'about',
'pay': 'pay'
},
home,
about,
pay
});
Backbone.history.start();
|
這樣分離之后對於開發而言減輕了痛苦,模塊化的好處顯而易見。但是分離出去的文件最終還是需要再引入的,最終生成的打包文件還是會非常大,用戶從而不得去花很長時間加載一整個大文件。
打開瀏覽器的主頁,可以看到請求了一個bundle.js文件,里面包含了這個應用的全部模塊。
也就是說這樣只是減少了開發的痛苦,對用戶而言不會有改善。
使用Code Splitting進行第一次優化
為了不讓用戶一次加載整個大文件,稍微好點的做法是讓用戶分開一次一次加載文件。
正好Code Splitting可以把在分離點中依賴的模塊會被打包到一起,然后異步加載。
修改一下index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
import Backbone from 'backbone'
import home from './home/index'
// 直接寫路由的原因是主頁訪問頻率高,直接打包會加快訪問速度
var HomeRoute = Backbone.Router.extend({
routes: {
'': 'home',
},
home
});
require.ensure([], function(require) {
var about = require('./about/index').default;
var AboutRoute = Backbone.Router.extend({
routes: {
'about': 'about'
},
about
});
new AboutRoute();
});
require.ensure([], function(require) {
var pay = require('./pay/index').default;
var PayRoute = Backbone.Router.extend({
routes: {
'pay': 'pay'
},
pay
});
new PayRoute();
});
Backbone.history.start();
|
因為require.ensure會生成一個小的打包文件,這樣可以保證用戶不一次加載全部文件,而是先加載bundle.js,再加載兩個小的js文件。
打開瀏覽器可以看到加載了三個js文件
現在瀏覽器要加載三個文件,增加了http請求數量。但是對於訪問頻率比較高的主頁而言,因為主頁的內容是直接打包的,會首先加載,用戶看到主頁的速度變快了。對於訪問about和pay的用戶而言,因為http請求數量變多,理論上會更慢的看到內容。是否分割代碼應該根據實際情況來分析,因為這篇文章主要說的是代碼分割,所以就先假設分離開之后對用戶訪問更有利。
然而類似about和pay這兩個頁面用戶不會每次都訪問,在打開主頁的時候就加載about和pay頁面的handler是一種浪費,應該等到用戶訪問about和pay鏈接的時候再加載對應的js文件。
第二次優化
想法很簡單:初始時只規定主頁的路由,而對於about和pay這種訪問頻率比較低的路由就動態加載。動態加載的方式:在處理未定義路由的handler中,通過匹配當前的路徑,增加router,然后重新解析頁面。
首先增加一個新路由:'*AllMissing': 'pathFinder'
pathFinder函數的思路是:先定義好about和pay頁面和路由和入口,然后把路由解析成正則表達式,通過正則表達式可以判斷出來當前的路徑符合哪條路由,然后增加新路由。
routes.js
1
2
3
4
5
|
// 路由:入口
export default {
'about': './about/index.js',
'pay': './pay/index.js'
}
|
router.js,具體的思路在代碼注釋中:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
|
import Backbone from 'backbone'
import $ from 'jquery'
import routes from './routes'
import home from './home/index'
var routesIndex = [];
export default Backbone.Router.extend({
routes: {
'': 'home',
'*allMissing': 'pathFinder'
},
home,
pathFinder
});
// 把路由解析成正則表達式,例如about會被解析成/^about(?:\?([\s\S]*))?$/,通過這個表達式可以確定當前路徑是那個路由
function routeToRegExp(route) {
return Backbone.Router.prototype._routeToRegExp.call(null, route);
}
// 把路由列表映射到新的數組
Object.keys(routes).forEach(key => {
routesIndex.push({
'entry': routes[key], // 入口
'regex': routeToRegExp(key), // 解析后的正則表達式
'route': key //路由
});
});
function pathFinder() {
// 循環遍歷所有路由索引,如果找到了對應的路由就加載新路由
for (var i = 0, l = routesIndex.length; i < l; i++) {
if (routesIndex[i].regex.test(path)) {
var route = {};
var entry = routesIndex[i].entry;
require.ensure([], function(require) {
var app = require(entry).default;
// 添加新的路由,重新解析當前url
route[routesIndex[i].route] = 'app';
var Router = Backbone.Router.extend({
routes: route,
app
});
new Router();
Backbone.history.loadUrl();
});
return;
}
}
$('#app').html('404');
}
|
然后在index.js引入router.js,路由就可以工作了
在我們看來,路由現在是動態解析,動態加載文件的。打開瀏覽器,再看一下網絡面板。
打開主頁,只請求了bundle.js,文件內容也是只包含了主頁的代碼。
再打開about頁面,請求了一個1.bundle.js,看一下1.bundle.js的內容就會發現,里面包含了about和pay兩個頁面的內容。這是webpack強大的地方,前文提到過,一個分離點會產生一個打包文件,而我們因為只有一個require.ensure,所以webpack通過自己的分析就只產生了一個打包文件,精准的包含了我們需要的內容。不得不說,webpack分析代碼的功能有點厲害。
直接使用require.ensure是不能保證完全按需加載了,好在有loader可以幫助解決這個問題:bundle-loader.
只用改變一點點就可以按需加載了:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
function pathFinder(path) {
for (var i = 0, l = routesIndex.length; i < l; i++) {
if (routesIndex[i].regex.test(path)) {
var route = {};
var entry = routesIndex[i].entry;
if (entry.startsWith('./')) {
entry = entry.substr(2, entry.length);
}
// 這里使用bundle-loader加載文件,依賴的文件不會全部打包到一個文件里。
var handler = require('bundle!./' + entry);
handler(bundle => {
var app = bundle.default;
route[routesIndex[i].route] = 'app';
var Router = Backbone.Router.extend({
routes: route,
app
});
new Router();
Backbone.history.loadUrl();
});
return;
}
}
$('#app').html('404');
}
|
bundle-loader是一個用來在運行時異步加載模塊的loader,使用了bundle-loader加載的文件可以動態的加載。
例如下面的官方示例:
1
2
3
4
5
6
7
8
|
// 在require bundle時,瀏覽器會加載它
var waitForChunk = require("bundle!./file.js");
// 等待加載,在回調中使用
waitForChunk(function(file) {
// 這里可以使用file,就像是用下面的代碼require進來一樣
// var file = require("./file.js");
});
|
因為webpack在編譯階段會遍歷到所有可能會使用到的文件,而bundle-loader就是在所有文件的外層加了一層wraper:
1
2
3
4
5
6
|
module.exports = function (cb) {
require.ensure([], function(require) {
var app = require('./file.js');
cb(app);
});
};
|
這樣,在require文件的時候只是引入了wraper,而且因為每個文件都會產生一個分離點,導致產生了多個打包文件,而打包文件的載入只有在條件命中的情況下才產生,也就可以按需加載。
經過這樣的修改,瀏覽器就可以在不同的路徑下加載不同的依賴文件了
總結
在單頁應用中使用這樣的方式按需加載文件,對於路由庫的要求也很簡單:
- 建立從路由到正則表達式的映射,如果沒有的話,自己寫也可以
- 能夠動態的添加路由
- 能夠加載指定的路由
大多數路由庫都可以做到上面三點,所以這篇文章提出的是比較普遍的辦法。當然,如果你用React或者Vue,他們配套的路由會比這個優化的更全面。
注:這篇文章的內容參考了https://medium.com/@somebody32/how-to-split-your-apps-by-routes-with-webpack-36b7a8a6231#.ncyca72ms,但是最后作者提出的方案也比較復雜,所以就自己寫了一篇,最后的辦法比較簡單。
原創文章轉載請注明:
轉載自AlloyTeam:http://www.alloyteam.com/2016/02/code-split-by-routes/