LABjs、RequireJS、SeaJS 哪個最好用?為什麼?


哎尼瑪,被seajs 帶溝里了,來吐槽一下。

  1. 文檔很爛,官網只有一個很簡單的例子,玉伯開了用Github issues 做文檔的壞頭,導致有的文檔issue 里說的東西已經過時了,給人誤導。
  2. 打包痛苦,配置起來極其複雜,而且一變再變,早期的transport/concat組件簡直了,bug挺多。而且這個打包跟spm的關係到底是啥?!!
  3. sea.js文件路徑,模塊路徑,spm路徑,打包路徑,相對路徑,絕對路徑,html入口文件路徑,這些關係在sea.js初期的時候開發團隊沒有理清楚,現在我也不知道seajs3和spm3搞清楚沒。

  4. 每個小版本大版本都有很大變化,從seajs1到2到3,每次變化都挺大的,有些配置莫名其妙地就沒了,也沒有一個統一的文檔來記錄。

以上幾點,拔出蘿蔔帶出泥,在使用時你以為你解決了其中一點,但其實還會在其他幾點裡爆發。所以非常不推薦使用sea.js。

一兩年前有個項目不慎用了sea.js,現在改好bug想發布,打包出來js路徑總是不對。。


LABjs 的核心是 LAB(Loading and Blocking):Loading 指非同步並行載入,Blocking 是指同步等待執行。LABjs 通過優雅的語法(script 和 wait)實現了這兩大特性,核心價值是性能優化。LABjs 是一個文件載入器。

RequireJS 和 SeaJS 則是模塊載入器,倡導的是一種模塊化開發理念,核心價值是讓 JavaScript 的模塊化開發變得更簡單自然。

模塊載入器一般可降級為文件載入器用,因此使用 RequireJS 和 SeaJS,也可以達成 LABjs 的性能優化目的。

RequireJS 和 SeaJS 都是很不錯的模塊載入器,兩者區別如下:

1. 兩者定位有差異。RequireJS 想成為瀏覽器端的模塊載入器,同時也想成為 Rhino / Node 等環境的模塊載入器。SeaJS 則專註於 Web 瀏覽器端,同時通過 Node 擴展的方式可以很方便跑在 Node 伺服器端

2. 兩者遵循的標準有差異。RequireJS 遵循的是 AMD(非同步模塊定義)規範,SeaJS 遵循的是 CMD (通用模塊定義)規範。規範的不同,導致了兩者 API 的不同。SeaJS 更簡潔優雅,更貼近 CommonJS Modules/1.1 和 Node Modules 規範。

3. 兩者社區理念有差異。RequireJS 在嘗試讓第三方類庫修改自身來支持 RequireJS,目前只有少數社區採納。SeaJS 不強推,而採用自主封裝的方式來「海納百川」,目前已有較成熟的封裝策略。

4. 兩者代碼質量有差異。RequireJS 是沒有明顯的 bug,SeaJS 是明顯沒有 bug。

5. 兩者對調試等的支持有差異。SeaJS 通過插件,可以實現 Fiddler 中自動映射的功能,還可以實現自動 combo 等功能,非常方便便捷。RequireJS 無這方面的支持。

6. 兩者的插件機制有差異。RequireJS 採取的是在源碼中預留介面的形式,源碼中留有為插件而寫的代碼。SeaJS 採取的插件機制則與 Node 的方式一致:開放自身,讓插件開發者可直接訪問或修改,從而非常靈活,可以實現各種類型的插件。

還有不少細節差異就不多說了。

總之,SeaJS 從 API 到實現,都比 RequireJS 更簡潔優雅。如果說 RequireJS 是 Prototype 類庫的話,則 SeaJS 是 jQuery 類庫。

最後,向 RequireJS 致敬!RequireJS 和 SeaJS 是好兄弟,一起努力推廣模塊化開發思想,這才是最重要的。

玉伯


個人更加偏好RequireJS

1. RequireJS的非同步模塊載入迎合了瀏覽器端JS程序員固有的非同步思維,學習成本低

--------------------------------

Sea.js的主頁中寫到:

  • Sea.js 遵循 CMD 規範,可以像 Node.js 一般書寫模塊代碼

  • 依賴的自動載入、配置的簡潔清晰,可以讓我們更多地享受編碼的樂趣

兩年前,我看到Sea.js這樣的自我描述,第一感覺是:為什麼瀏覽器的JS編程和NodeJS相仿就是優雅呢?

那麼,來討論RequireJS和Sea.js的學習成本問題

如RequireJS中依賴模塊:

require(["jquery","創建了全局變數的module"],function($,b){
//既然我在開頭明確聲明依賴需求,那可以確定在執行這個回調函數時,依賴肯定是已經滿足了
//所以,放心地使用吧
})

而Sea.js中表現為:

define(function(require,exports,modules){
var $ = require("jquery")
$.get("http://www.zhihu.com")
//傳統JS程序員的思維:
//「咦,好神奇,JS載入不應該是非同步的么,怎麼我一說要依賴,jquery就自己跳出來了?」
})

所以,是「理所應當」容易理解,還是變魔術容易理解呢?

2. RequireJS的實現方式符合JS一般執行流程

--------------------------------

當我們看到RequireJS的介面,

require(["a","b"],function(){
//Do something
})

實際做的事情是:

  1. require函數檢查依賴的模塊,根據配置文件,獲取js文件的實際路徑
  2. 根據js文件實際路徑,在dom中插入script節點,並綁定onload事件來獲取該模塊載入完成的通知。
  3. 依賴script全部載入完成後,調用回調函數

以上步驟是容易想像的

而Sea.js在調用

define("a",function(require,exports,modules){
var b = require("b")
})

時,

  1. 通過回調函數的Function.toString函數,使用正則表達式來捕捉內部的require欄位,找到require("jquery")內部依賴的模塊jquery
  2. 根據配置文件,找到jquery的js文件的實際路徑
  3. 在dom中插入script標籤,載入模塊指定的js,綁定載入完成的事件,使得載入完成後將js文件綁定到require模塊指定的id(這裡就是jquery這個字元串)上
  4. 回調函數內部依賴的js全部載入(暫不調用)完後,調用回調函數
  5. 當回調函數調用require("jquery"),即執行綁定在"jquery"這個id上的js文件,即刻執行,並將返回值傳給var b

這種用正則表達式捕捉require內部依賴模塊的方式,使得無法利用尚未執行的回調函數中的js運行環境,導致require函數的內部只能將依賴的模塊名稱硬編碼,就不能寫下面這樣的代碼了

define("a",function(require,exports,modules){
var b = require("Us"+"er")
})

而只能寫成

define("a",function(require,exports,modules){
var b = require("User")
})

而以上Sea.js最根本的原理在Sea.js的文檔中居然毫無蹤影!

「你變了精彩的魔術,我們會為你喝彩。但你想讓我們信任你,你得主動解釋魔術的奧秘。否則我會覺得自己被耍了。」

所以就引來了下一條:

3. Sea.js的文檔立足高度高,未提及重點細節

--------------------------------

其實如果像上一段一樣,將Sea.js的核心原理進行簡單解釋,有基本知識的JS程序員大概是可以「五分鐘上手Sea.js」的,但在官方文檔中,這些都隻字不提。

Sea.js - 官方文檔 中開始在講要解決的問題和問題解決後的價值,OK,能開始有意識嘗試使用Sea.js的JS程序員想必是遇到模塊錯綜複雜的問題需要解決了。

然後給了看起來同步載入的,顯得很魔術的示例代碼(「哇,好厲害的趕腳!大牛,所以這是怎麼做到的呢?」)

// 所有模塊都通過 define 來定義
define(function(require, exports, module) {

// 通過 require 引入依賴
var $ = require("jquery");
var Spinning = require("./spinning");

// 通過 exports 對外提供介面
exports.doSomething = ...

// 或者通過 module.exports 提供整個介面
module.exports = ...

});

接著就直接開講API和約定了。(「誰來照顧下我的好奇心呢!說好的我今天對你愛理不理,明天你讓我高攀不起呢!」)

這種感覺好像是數學老師剛剛還在講微積分的歷史沿革及其重要性,低頭看了眼手機,抬起頭就已經在講三重積分了呢!(捂臉)

這也是我通篇充滿了對作者的怨恨的原因。當時初聞Sea.js,發現是國人的作品耶,恩,網上評價不錯,立馬學習,沒有理由地想從RequireJS遷移到Sea.js(RequireJS在一邊問:「我做錯什麼了嗎?做錯了我改嘛」)。但看文檔嘗試閱讀了好幾次(沒有看代碼,罵我吧),還是覺得很魔術,覺得是霧裡看花。

今天不知從哪裡瞟了一眼,說是Sea.js通過正則表達式來找回調函數內容中的require來確定載入的函數,然後一看代碼果然是,頓時火從心燃,不寫篇文章吐槽絕不能滅火(想像拋妻棄子追尋真愛,最終發現真愛原是博愛的感覺)。

技術文檔應當切中要害,節約讀者時間,而不是故作深沉。將簡單的複雜化怎麼也不應該是程序員的作風。

「為世界和平穩定,家庭的幸福快樂」不能停留在嘴上說說,最終要落到實處。

4. Sea.js部署優化工具尚未完善

--------------------------------

好吧,這是聽大家在上面說的,我沒怎麼用Sea.js,還沒遇到這樣的問題

最後想對 @李翌 的回答中,關於懶載入的部分表達下我的理解

LazyLoad的優勢體現在:僅當資源需要被使用前載入資源。在RequireJS和Sea.js中表現為,在回調函數調用前載入js腳本資源。

RequireJS和Sea.js在資源載入的時間點都是一樣的,所以論「懶」的程度都是一樣的。差別僅僅在於載入的腳本什麼時候執行。RequireJS的依賴模塊在回調函數執行前執行完畢,而Sea.js的依賴模塊在回調函數執行require時執行。

而回調函數希望實現的目標就好比「事務的原子性」,僅當整個回調函數結束才算完成了目標。那麼既然整個回調函數的執行時間是恆定的,那麼個人認為腳本載入的時間到底耗費在回調前還是回調時,並沒有本質的差別。如果不認同是否本質相同,那麼至少也沒有誰更懶的區別。

關於循環引用的補充

================

發現評論區不支持代碼格式,貼在正文區更容易看清 @xs yin

一般來說,產生循環依賴的地方可能設計有問題,首先需要考慮是否要調整設計。如果實在沒辦法,就像第三個評論說的,requirejs是兼容CommonJS的依賴寫法的。如下例:

a.js

------

define(["require", "b"], function (require, b) {
console.log("in a, print b", b);
//這裡的 b 由於 b.js 實際尚未執行,所以是 undefined 的
return {
x: function () {
console.log("a.x");
b = require("b");
//此時,b.js 已經執行,可以通過 require("b") 直接獲取 b 模塊對象
b.z();
}
};
});

b.js

--------

define(["require", "a"], function (require, a) {
console.log("in b, print a", a);
return {
y: function () {
console.log("b.y");
a.x();
},
z: function () {
console.log("b.z");
}
};
});

然後在使用的地方

---------

require(["b"], function (b) {
b.y();
})

就會輸出期望的結果了

------------

a.js:2 in a, print b undefined

b.js:2 in b, print a Object {}

b.js:5 b.y

a.js:5 a.x

b.js:9 b.z


AMD明顯比CMD靠譜多了。

推薦用requireJS或者其他AMD載入器吧。


------------------------------更新於2014.6.21------------------------------

因為最近在給InfoQ寫一篇投稿,關於瀏覽器資源載入優化的文章,

又因為文章經過玉伯的審核,於是和他有了一些交流,

發現原帖的部分內容是不夠準確甚至是錯誤的(很遺憾為什麼沒有人在這裡給我指出來……)。

關於requirejs和seajs的正確的比較都融入在我發表在InfoQ的這篇文章上:讓我們再聊聊瀏覽器資源載入優化 或者移步到我的個人博客(這裡的排版會比較好看- -)讓我們再聊聊瀏覽器資源載入優化

原帖就不刪除了,大家可以對比一下錯誤的原帖和正確的解讀應該是怎樣的

-------------------------------以下是原帖-------------------------------------

個人認為玉伯聊的比較抽象和宏觀,比如標準,概念,機制,社區等。最近一直在做性能優化方面的工作,就自己的經驗談談require.js和sea.js的異同。這兩個載入器和標準沒有優劣之分,這裡指出的只是差別。具體還是要根據實際情況進行選擇

在開始之前我已經假設你對requirejs與seajs語法已經基本熟悉了,如果還沒有,請移步這裡:

- CMD標準:https://github.com/cmdjs/specification/blob/master/draft/module.md

- AMD標準:https://github.com/amdjs/amdjs-api/blob/master/AMD.md

對比require.js與sea.js,某種意義上說就是對比AMD標準與CMD標準,個人認為兩個類庫在模塊和factory的書寫上其實無太大差異,差異在於

  • 模塊的載入

  • factory函數的執行。

這裡提前說一句,如果你的網站再上線前習慣把所有的模塊打包壓縮,其實requirejs和seajs並無太大差別。

## 載入差異

這一小節請允許我照搬一個帖子

玉伯轉過的一個SeaJS與RequireJS最大的區別,這個帖子原始(不包括後記)的結論是

RequireJS你坑的我一滾啊, 這也就是為什麼我不喜歡RequireJS的原因, 坑隱藏得太深了.

SeaJS是非同步載入模塊的沒錯, 但執行模塊的順序也是嚴格按照模塊在代碼中出現(require)的順序, 這樣才更符合邏輯吧! 你說呢, RequireJS?

而RequireJS會先儘早地執行(依賴)模塊, 相當於所有的require都被提前了, 而且模塊執行的順序也不一定100%就是先mod1再mod2

因此你看到執行順序和你預想的完全不一樣! 顫抖吧~ RequireJS!

因為他認為他的測試代碼

define(function(require, exports, module) {
console.log("require module: main");

var mod1 = require("./mod1");
mod1.hello();
var mod2 = require("./mod2");
mod2.hello();

return {
hello: function() {
console.log("hello main");
}
};
});

運行結果應該是順序的(sea.js下的結果):

require module: main
require module: mod1
hello mod1
require module: mod2
hello mod2
helo main

而不應該是非同步的require.js下:

require module: mod2
require module: mod1
require module: main
hello mod1
hello mod2
helo main

但問題是,為什麼"執行模塊的順序"應該是"嚴格按照模塊在代碼中出現(require)的順序"? 並且"這樣才更符合邏輯吧"?

如果他以seajs的運行結果來要求requirejs,那requirejs肯定吃虧了。AMD標準從來都沒有規定模塊的載入順序,它只是需要保證:

The dependencies must be resolved prior to the execution of the module factory function, and the resolved values should be passed as arguments to the factory function with argument positions corresponding to indexes in the dependencies array.

評論下方有人(jockchou)的回復更切中要害:

我個人感覺requirejs更科學,所有依賴的模塊要先執行好。如果A模塊依賴B。當執行A中的某個操doSomething()後,再去依賴執行B模塊require("B");如果B模塊出錯了,doSomething的操作如何回滾?

很多語言中的import, include, useing都是先將導入的類或者模塊執行好。如果被導入的模塊都有問題,有錯誤,執行當前模塊有何意義?

樓主說requirejs是坑,是因為你還不太理解AMD「非同步模塊」的定義,被依賴的模塊必須先於當前模塊執行,而沒有依賴關係的模塊,可以沒有先後。

想像一下factory是個模塊工廠吧,而依賴dependencies是工廠的原材料,在工廠進行生產的時候,是先把原材料一次性都在它自己的工廠里加工好,還是把原材料的工廠搬到當前的factory來什麼時候需要,什麼時候加工,哪個整體時間效率更高?顯然是requirejs,requirejs是載入即可用的。為了響應用戶的某個操作,當前工廠正在進行生產,當發現需要某種原材料的時候,突然要停止生產,去啟動原材料加工,這不是讓當前工廠非常焦燥嗎?

這樣看來其實兩者並無太大差別。

但考慮這樣一種業務情況,考慮某一個功能只對登陸用戶開放,這樣的話requirejs提前把模塊載入是否有必要?(因為來到你頁面的用戶到離開也不會登陸)。

這是非常實際的問題,一個頁面可以有非常多的功能,比如登陸、分享、留言、收藏……但不一定每一個來到頁面的用戶都會使用這些功能,如果都作為頁面模塊的依賴提前載入的話,對頁面一定是一個不小的負擔。

但seajs可以即用即載入,比如代碼可以這麼寫

define(function () {

if (user_login) {
require(login_feature_module)
}

document.body.onclick = function () {
require(show_module)
}
})

我同意這句話:

很多語言中的import, include, useing都是先將導入的類或者模塊執行好。如果被導入的模塊都有問題,有錯誤,執行當前模塊有何意義?

但個人覺得考慮到頁面的性能,可以考慮將要導入模塊的懶載入(Lazy load)。

你會不會覺得我上面說的懶載入是一種天方夜譚?

但然不是,你去看看現在的人人網個人主頁看看

圖中標註的「與我相關」、「相冊」、「分享」都是在點擊之後才載入對應的模塊

Facebook的情況更為嚴重,不僅要考慮內部不同團隊的功能模塊,還要考慮第三方的功能模塊。

早在09年,他們就是用了一套靜態資源管理方案(Static Resource Management),用於管理一個功能所需要的js/css靜態文件:

簡單來說,就是由頁面上有沒有功能所需的html片段,來決定是否載入和打包功能所需的js與style。也就是說靜態文件需要通過html「聲明」之後才可用。

具體可以參考這裡:Frontend Performance Engineering in Facebook : Velocity 2009

## 執行差異

為了增強對比,我們在定義依賴模塊的時候,故意讓它的factory函數要執行相當長的時間,比如1秒:

// dep_A.js定義如下,dep_B/dep_C定義同理

define(function(require, exports, module) {

(function(second) {
var start = +new Date();
while (start + second * 1000 &> +new Date()) {}
})(window.EXE_TIME);

// window.EXE_TIME = 1;此處會連續執行1s

exports.foo = function() {
console.log("A");
}
})

// main中同時載入三個相同模塊

//require.js:
require(["dep_A", "dep_B", "dep_C"], function(A, B, C) {

});

//sea.js:
define(function(require, exports, module) {

var mod_A = require("dep_A");
var mod_B = require("dep_B");
var mod_C = require("dep_C");
});

requirejs載入的瀑布圖:

seajs載入的瀑布圖:

如果把一個模塊的執行拆分為執行define和執行factory函數的話(對requirejs和seajs都適用),從上圖可以看出:

  • requirejs:一個模塊的factory函數執行是緊跟隨在define(也就是Evaluate Script腳本模塊文件)之後
  • seajs: 執行一個模塊的factory函數需要等待所有模塊define完畢。

重點不是這些,我想說的是我在seajs中看到一個閃光點。

在上面一節中我提到了懶載入模塊,在載入模塊的時候需要1. 臨時請求模塊文件 ; 2. 執行factory函數。

但如果我們在載入頁面時僅僅是執行把懶載入的模塊的define(從上面兩個圖可以看出define的代價是非常小的),而設法不執行factory函數。

那麼在真正需要懶載入的時候只要執行factory函數即可。這樣不是能夠讓模塊響應更及時,更靠譜?

這是可以實現的。但技術細節就不贅述了。這可以作為優化模塊載入的一種方案。


用了5年多的RequireJs,和一段時間的SeaJs,補充一下區別吧,

1、對於第3點,其實,RequireJS早有了Shim等支持,不需要修改第三方類庫就可以完全支持.如Ember,JQuery等引用,都直接可以非同步載入為一個模塊.

2、對於調試的支持,RequireJS2.1版本也支持了.

3、RequireJs的打包功能很強大,很適合在CI下做各種配置下的打包發布,非常靈活方便,SeaJs這方面略力不從心.

4、對於模塊的語法,RequireJs比較靈活,支持類似於SeaJs的語法

5、總體來說兩個框架都很不錯.SeaJs借鑒了前人框架的很多優點,發展勢頭不錯.

RequireJs支持更全面一些,如Txt等文本文件的依賴,多版本的Js依賴,多語言支持,打包比SeaJs要簡單些,在Jenkins等CI環境中,不受系統環境的限制.文檔和範例比SeaJs全多了.缺點是:官方更新比較快,有好多次新版本都有些不兼容,如果你英文比較吃力,看文檔可能就比較痛苦了.

SeaJs很簡潔,很多功能也在慢慢完善,但底層細節暴露比較多,文檔需要再補充些.希望在以後能再接再厲,加以改進.


- -像我這種每頁最多不超過5行js引入的表示,RequireJS、SeaJS對我都沒用(大實話)。

1.並沒有顯著提升頁面載入性能

2.並沒有使得代碼更優雅


* requirejs 一樣支持 CMD/seajs 的寫法,文檔中也清楚的說了不推薦,因為一要做源碼解析,二是由此帶來只能依賴靜態路徑

* requirejs 的配套 r.js (r我猜可能是resource的意思吧),可以將資源打包,就像webpack那樣,專門用於生產環境部署

* requirejs 模塊一樣可以用在 nodejs 模塊,如果模塊需要同時在瀏覽器端和Nodejs使用的話


天貓都用 requirejs, 你說呢?


seajs有的requirejs一樣有 反之未必 果斷requirejs


用requirejs遇到問題可以google,用seajs你就慢慢百度吧。


一年前,項目還有使用sea.js和require.js,當我現在看到這個問題的時候,都已經不用了,感慨變化真的快啊


Just use webpack actually.

--------------------------------

Just use browserify.


LABjs跟後兩者作用不一樣

後兩者相比我更喜歡後者,不過前者在國外影響很大,Jquery比較新的版本支持RequireJS載入


seajs打包真心讓人有點想吐的感覺!果斷拋


就我個人使用來看,requirejs上手就比seajs容易的多。

1.seajs打包spm實在是太難了,這跟r.js比起來簡直不是一個檔次。

2.requirejs支持css @import依賴的打包,對於不喜歡使用less的童鞋,就方便多了。

3.對於非AMD規範的js插件,require js提供了shim支持,非常方便。

4.requirejs目前支持了sourcemap,配合grunt,簡直爽爆了。

出於從上手和配置上的方便性來看,我最終拋棄了seajs。


評價一個技術好不好用,我想應該主要根據以下幾點進行比較

1、文檔完整度

2、穩定性(bug多少)

3、上手速度、培訓成本

4、再說其他的


現在的js工具真是尼瑪多啊,眼花繚亂


seajs是國人做的這一點就已經讓我覺得這個項目基本上不能存活很久了,鑒於幾點:

1. 國內能幫忙github貢獻優化代碼的人有限

2. 國內平時工作都那麼忙了,玉伯能長期維護更新優化這個項目?我覺得很多時候由不得他控制。

3. 優秀第三方文檔、教程視頻推廣有限,導致seajs無法廣泛受到認同與推廣發展,特別是需要和國外開發者一同團隊工作配合的話,你讓老外去看中文文檔?(我知道有e文版的,但中文版文檔都有限了,更何必說e文的,沒有多少老外用seajs的)

而且後起之秀源源不斷,seajs, requirejs 都有點落後了。


在用SEAJS,除了打包非常痛苦外,其他的還好


推薦閱讀:

編寫獨有閉源瀏覽器可以提高安全性嗎?
為什麼有些遊戲公司的前端職位和其他的研發職位工資差距那麼大?
前端的MVC框架和後端的SSH框架怎麼結合?
為什麼Bootstrap不設計得像Semantic UI那樣簡潔易懂?

TAG:前端開發 | JavaScript | 前端開發框架和庫 | SeaJS | RequireJS |