用spm2構建seajs專案的過程

桃子夭夭發表於2015-10-22

前言

Javascript模組化規範有CommonJs規範,和主要適用於瀏覽器環境的AMD規範,以及國內的CMD規範,它是SeaJs遵循的模組化規範。因為以前專案中用SeaJs做過前端的模組管理工具,所以這裡總結一下自己的使用心得。

在試用SeaJs和官方推薦的CMD包管理工具——Spm2.x的過程中,遇到了很多高低版本不相容和配置引數沒弄明白的問題,後來在網上各處找資料才大概弄懂。這裡我強調一下版本,是因為可能有的同學專案開始較早,用了以前版本的Seajs,再去看Seajs官網的API有些地方會不適用!下面各節對框架的描述中也都會帶上版本。

文中可能會有一些理解有誤或者沒講清楚的地方,有大神路過懇請指導...

一個簡單的DEMO

Seajs可以實現前端Js檔案的按需載入和前端模組管理的功能,不熟悉Seajs API (v2.3.0)的同學可以看看這裡,非常簡單。

首先上一個使用Seajs的demo。目錄結構如下:

 

html:

 1 <!DOCTYPE html>
 2 <html>
 3 <head lang="en">
 4     <meta charset="UTF-8">
 5     <title></title>
 6     <script src="../js/lib/seajs1.2.0.js"></script>
 7 </head>
 8 <body>
 9 <h1>spm前端構建測試</h1>
10 <script>
11     seajs.config({
12         base: '../js'
13         // paths:{'pathtest':'src'}, //生效版本:seajs2.3.0
14     });
15     seajs.use("src/eat.js",function(eat){
16         if(eat) eat.start();
17     });
18 </script>
19 </body>
20 </html>
View Code

eat.js

 1 "use strict"
 2 define(function(require,exports,module){
 3     var rice=require("./rice"),
 4         water=require("./water"),
 5         i=require("./i");
 6     exports.start=function(){
 7         i.eat(rice.rice);
 8         i.drink(water.water);
 9     }
10 })
View Code

rice.js

1 "use strict"
2 define({rice:"i'm rice"})
View Code

i.js

 1 "use strict"
 2 define(function () {
 3     return {
 4         name:"tzyy",
 5         eat:function(rice){
 6             alert(rice+",Delicious!");
 7         },
 8         drink: function (water) {
 9             alert(water+",爽!");
10         }
11     }
12 })
View Code

water.js

1 "use strict"
2 define({water:"i'm water"});
View Code

專案結構:

這樣就實現的前端程式碼的模組化,和模組的按需載入、執行。

但是,這樣還不夠,我們開啟Chrome控制檯,network標籤,重新整理網頁發現:

程式碼確實是模組化了,同時程式碼也分散到了各個檔案中。當專案中的檔案非常多,由於http請求是無狀態的,每次都要建立、斷開連線,另外瀏覽器下載靜態檔案的併發數量也是有上限的,會導致頁面載入的速度比所有程式碼在同一個檔案中要慢。

所以我們要對js檔案進行壓縮、合併。那麼問題來了:

1.define函式是seajs提供的,require函式也是seajs給注入的,程式碼壓縮,函式名字發生變化後seajs還認得嗎?

2.現在的程式碼中require接收的引數都是模組檔案的路徑,檔案要是都合併了,這些程式碼還上哪找去?

幸好有spm,它提供了構建CMD模組程式碼的功能,可以對模組檔案進行壓縮、合併的操作。

用spm 2.2.5進行模組程式碼的壓縮&合併

瞭解關於CMD模組的壓縮和合並之前最好先了解一下模組標識和構建過程相關的知識,以免掉入萬人坑。

這兩個點官方文件講得很好,我就不再綴述了。

seajs模組標識

https://github.com/seajs/seajs/issues/258

spm2.x構建過程詳解

 http://docs.spmjs.org/doc/build-task

package.json配置

使用spm2構建cmd模組需要在模組根目錄建立一個package.json檔案。 繼續以上面的專案為例:

{
    "family":"tzyy",
    "name":"test",
    "version": "1.0.0",
    "spm": {
        "source": "src",
        "idleading":"pack/","output": ["eat.js"]
    }
}

spm檢視像java專案裡的maven那樣建立一個依賴管理的機制,family、name、version,這三個屬性用來定位一個專案,是必須填寫的。但是我們這裡只是用spm來做構建工具,所以這幾個屬性隨便填寫就可以了。

spm屬性的值是一個json,其屬性

src——指定原始碼目錄,spm將使用src配置地址中的檔案來構建專案

idleading——指定模組id字首,spm在生成transport檔案的過程中將這個屬性作為模組id的字首

output——一個陣列,指定應該輸出哪些檔案的構建結果

壓縮&合併

首先安裝好nodejs、npm、spm2.2.5,安裝的教程網上有很多。

進入/js目錄,shift+右鍵——在此處開啟命令視窗,然後執行命令:

這個命令在/js下建立了一個mypack目錄來存放壓縮後的檔案

兩個檔案的內容為:

eat.js:

1 "use strict";define("pack/eat",["./rice","./water","./i"],function(a,b){var c=a("./rice"),d=a("./water"),e=a("./i");b.start=function(){e.eat(c.rice),e.drink(d.water)}}),define("pack/rice",[],{rice:"i'm rice"}),define("pack/water",[],{water:"i'm water"}),define("pack/i",[],function(){return{name:"tzyy",eat:function(a){alert(a+",Delicious!")},drink:function(a){alert(a+",爽!")}}});
View Code

eat-debug.js

 1 "use strict";
 2 
 3 define("pack/eat-debug", [ "./rice-debug", "./water-debug", "./i-debug" ], function(require, exports, module) {
 4     var rice = require("./rice-debug"), water = require("./water-debug"), i = require("./i-debug");
 5     exports.start = function() {
 6         i.eat(rice.rice);
 7         i.drink(water.water);
 8     };
 9 });
10 
11 /**
12  * Created by zouchengzhuo on 2015/10/21.
13  */
14 "use strict";
15 
16 define("pack/rice-debug", [], {
17     rice: "i'm rice"
18 });
19 
20 /**
21  * Created by zouchengzhuo on 2015/10/21.
22  */
23 "use strict";
24 
25 define("pack/water-debug", [], {
26     water: "i'm water"
27 });
28 
29 /**
30  * Created by zouchengzhuo on 2015/10/21.
31  */
32 "use strict";
33 
34 define("pack/i-debug", [], function() {
35     return {
36         name: "tzyy",
37         eat: function(rice) {
38             alert(rice + ",Delicious!");
39         },
40         drink: function(water) {
41             alert(water + ",爽!");
42         }
43     };
44 });
View Code

可以看到eat.js已經被壓縮,並且其依賴模組也被壓縮後合併在了同一個檔案中。

將html中啟動模組的程式碼修改一下:

1     seajs.use("mypack/eat.js",function(eat){
2         if(eat) eat.start();
3     });
View Code

執行正常,看看網路請求:

剛剛那些檔案只產生了一個網路請求,3ms遠遠小於 3ms+4ms+12ms+14ms吧。

seajs config中使用了別名的壓縮&合併

但是,當我們的seajs專案中使用了別名的配置時,情況又不同了!修改專案結構如下:

 

為water.js配置別名如下:

1     seajs.config({
2         base: '../js',
3         alias:{
4             "water":"src/alias/water.js"
5         }
6     });
View Code

從src目錄啟動,可以正常執行。然後構建一下,發現:

得到的eat-debug.js:

 1 /**
 2  * Created by zouchengzhuo on 2015/10/21.
 3  */
 4 "use strict";
 5 
 6 define("pack/eat-debug", [ "./rice-debug", "water-debug", "./i-debug" ], function(require, exports, module) {
 7     var rice = require("./rice-debug"), water = require("water-debug"), i = require("./i-debug");
 8     exports.start = function() {
 9         i.eat(rice.rice);
10         i.drink(water.water);
11     };
12 });
13 
14 /**
15  * Created by zouchengzhuo on 2015/10/21.
16  */
17 "use strict";
18 
19 define("pack/rice-debug", [], {
20     rice: "i'm rice"
21 });
22 
23 /**
24  * Created by zouchengzhuo on 2015/10/21.
25  */
26 "use strict";
27 
28 define("pack/i-debug", [], function() {
29     return {
30         name: "tzyy",
31         eat: function(rice) {
32             alert(rice + ",Delicious!");
33         },
34         drink: function(water) {
35             alert(water + ",爽!");
36         }
37     };
38 });
View Code

報錯,找不到water模組,最終得到的輸出檔案中也沒有包含water模組。

原因是如果seajs配置了別名,在package.json的spm屬性裡邊應該也配置一下,這樣spm會首先將別名替換成全名,然後再構建。

修改package.json,把alias配置貼過去,如下:

{
    "family":"tzyy",
    "name":"test",
    "version": "1.0.0",
    "spm": {
        "source": "src",
        "idleading":"pack/",
        "alias":{
            "water":"src/alias/water.js"
        },
        "output": ["eat.js"]
    }
}
View Code

再次執行,結果依然不對!這種現象官網上並沒有解釋,經過試驗和網上查資料,

我的理解:

這裡要注意一下,由於seajs的config中指定了base路徑 ../js,別名攜程 src/alias/water.js是可以的,但是spm構建的時候,不會這麼想,它似乎將src/alias/water.js作為一個頂級標識來處理,而頂級標識一般被認為是其他的公用模組,不會被transport,也不會被合併到輸出檔案中。 這裡不對的話請指正:)

在package.json中將alias裡邊water的別名配置為 ./alias/water.js 這種相對路徑,輸出檔案表現就正常了:

 1 /**
 2  * Created by zouchengzhuo on 2015/10/21.
 3  */
 4 "use strict";
 5 
 6 define("pack/eat-debug", [ "./rice-debug", "./alias/water-debug.js", "./i-debug" ], function(require, exports, module) {
 7     var rice = require("./rice-debug"), water = require("./alias/water-debug.js"), i = require("./i-debug");
 8     exports.start = function() {
 9         i.eat(rice.rice);
10         i.drink(water.water);
11     };
12 });
13 
14 /**
15  * Created by zouchengzhuo on 2015/10/21.
16  */
17 "use strict";
18 
19 define("pack/rice-debug", [], {
20     rice: "i'm rice"
21 });
22 
23 /**
24  * Created by zouchengzhuo on 2015/10/21.
25  */
26 "use strict";
27 
28 define("pack/alias/water-debug", [], {
29     water: "i'm water"
30 });
31 
32 /**
33  * Created by zouchengzhuo on 2015/10/21.
34  */
35 "use strict";
36 
37 define("pack/i-debug", [], function() {
38     return {
39         name: "tzyy",
40         eat: function(rice) {
41             alert(rice + ",Delicious!");
42         },
43         drink: function(water) {
44             alert(water + ",爽!");
45         }
46     };
47 });
View Code

但是此時cmd中依然會報錯

不知道這是spm2.2.5的bug,還是我上面的理解有問題,官方文件和網上也沒找到資料,求指導~

啟動模組合併

 從上一小節看來,壓縮之後不是會自動合併模組嗎? 為什麼還要合併呢?

下面我們來變一下專案內容。增加一個啟動模組drink.js:

drink.js內容:

1 define(function(require,exports,module){
2     var water=require("water"),
3         i=require("./i.js");
4     return function(){
5         i.drink(water.water);
6     }
7 });
View Code

在package中增加一個輸出模組 drink.js

 1 {
 2     "family":"tzyy",
 3     "name":"test",
 4     "version": "1.0.0",
 5     "spm": {
 6         "source": "src",
 7         "idleading":"pack/",
 8         "alias":{
 9             "water":"./alias/water.js"
10         },
11         "output": ["eat.js","drink.js"]
12     }
13 }
View Code

執行

可以看到生成了兩個模組

html中增加一個啟動模組

 1 <!DOCTYPE html>
 2 <html>
 3 <head lang="en">
 4     <meta charset="UTF-8">
 5     <title></title>
 6     <script src="../js/lib/seajs1.2.0.js"></script>
 7 </head>
 8 <body>
 9 <h1>spm前端構建測試</h1>
10 <script>
11     seajs.config({
12         base: '../js',
13         alias:{
14             "water":"src/alias/water.js"
15         }
16     });
17     seajs.use("mypack/eat.js",function(eat){
18         if(eat) eat.start();
19     });
20     seajs.use("mypack/drink.js",function(drink){
21         if(drink) drink();
22     });
23 </script>
24 </body>
25 </html>
View Code

在瀏覽器中執行,正常,開啟控制檯檢視網路請求

可以看到請求了兩個模組啟動檔案,eat.js 和 drink.js ,並沒有被合併為一個檔案。

而且,開啟drink-debug.js,可以看到

 1 define("pack/drink-debug", [ "./alias/water-debug.js", "./i-debug" ], function(require, exports, module) {
 2     var water = require("./alias/water-debug.js"), i = require("./i-debug");
 3     return function() {
 4         i.drink(water.water);
 5     };
 6 });
 7 
 8 /**
 9  * Created by zouchengzhuo on 2015/10/21.
10  */
11 "use strict";
12 
13 define("pack/alias/water-debug", [], {
14     water: "i'm water"
15 });
16 
17 /**
18  * Created by zouchengzhuo on 2015/10/21.
19  */
20 "use strict";
21 
22 define("pack/i-debug", [], function() {
23     return {
24         name: "tzyy",
25         eat: function(rice) {
26             alert(rice + ",Delicious!");
27         },
28         drink: function(water) {
29             alert(water + ",爽!");
30         }
31     };
32 });
View Code

而drink.js中,water和i 兩個模組在eat.js中也是存在的,這也是對資源的浪費。

此時我們可以再新建一個模組,將上面那兩個模組放入該模組中啟動:

main.js

1 define(function (require,exports,module) {
2     var eat=require("./eat"),
3         drink=require("./drink");
4     eat.start();
5     drink();
6 })
View Code

package.json輸出模組改為main.js

 1 {
 2     "family":"tzyy",
 3     "name":"test",
 4     "version": "1.0.0",
 5     "spm": {
 6         "source": "src",
 7         "idleading":"pack/",
 8         "alias":{
 9             "water":"./alias/water.js"
10         },
11         "output": ["./main.js"]
12     }
13 }
View Code

html中啟動程式碼修改為:

seajs.use("mypack/main.js");

再次執行可以看到所有檔案都被壓縮、合併了

 當啟動模組越來越多時,我們就可以這樣幹。

css的壓縮、合併

spm2.x對css的壓縮合並是通過seajs的seajs.importStyle函式來完成的。

我們在src目錄下建一個css資料夾(之前把src目錄放到js目錄裡邊了,不想改了,大家記得調換一下順序)

然後在main.js中require :

1 define(function (require,exports,module) {
2     var eat=require("./eat"),
3         drink=require("./drink");
4     require("./css/style.css"),
5     require("./css/style2.css");
6     eat.start();
7     drink();
8 })
View Code

執行spm build,最後得到的main-debug.js中可以看到如下程式碼片段:

這樣css檔案也被壓縮到了main.js檔案中。 

 1 define("pack/main-debug", [ "./eat-debug", "./rice-debug", "./alias/water-debug.js", "./i-debug", "./drink-debug", "./css/style-debug.css", "./css/style2-debug.css" ], function(require, exports, module) {
 2     var eat = require("./eat-debug"), drink = require("./drink-debug");
 3     require("./css/style-debug.css"), require("./css/style2-debug.css");
 4     eat.start();
 5     drink();
 6 });
 7 
 8 以及最下方的
 9 define("pack/css/style-debug.css", [], function() {
10     seajs.importStyle("body{color:red}");
11 });
12 
13 define("pack/css/style2-debug.css", [], function() {
14     seajs.importStyle("h1{font-size:100px}");
15 });
View Code

 這裡有一些關於seajs版本的坑

1.seajs2.2.1中,可以直接require css檔案,但是seajs2.3.0不行,需要引入一個seajs-css外掛

2.seajs2.3.0中,通過seajs.use啟動一個transport檔案中的模組時(也就是從合併過的程式碼中啟動模組時),use中寫的模組id必須和define函式中寫的模組id相同,但是在seajs2.2.1中卻是必須和檔案路徑相同。 seajs2.3.0如此做的原因是,可以在合併過的檔案中可以通過模組id準確獲取模組exports的內容,好傳回use的回撥函式中。

這些坑在seajs官網文件裡面都不太容易找到。

開發、生產環境自動切換

定義一個變數,來設定模組啟動路徑,可以很方便的切換開發、生產環境。

將html中seajs配置、啟動模組程式碼做如下修改:

1    var environment=location.hostname=="localhost"?"src":"mypack";
2     seajs.config({
3         base: '../js',
4         alias:{
5             "water":"src/alias/water.js"
6         }
7     });
8     seajs.use(environment+"/main.js");
View Code

這樣通過localhost訪問時,自動啟用src目錄下的程式碼,生產環境時,自動啟用打包目錄下的程式碼。

最後

前端領域的技術正在飛速發展,日新月異,各種技術層出不窮! 這篇文章主要在講我使用spm2.2.5作為seajs的構建工具的使用經歷,spm3.x和spm2.x的差別都非常大。和CommonJs、AMD規範相關的還有很多優秀的模組管理工具,希望能在學習使用這些工具的過程中能不斷深入理解模組化的精髓!

這篇文章是學習過程中的產物,可能有很多沒講清楚或者理解有誤的地方,如果您路過發現,還望在評論裡指出,我會在文中更正!

相關文章