Javascript
模塊化?模塊化是一種處理複雜系統分解成爲更好的可管理模塊的方式,它能夠把系統代碼劃分爲一系列職責單一,高度解耦且可替換的模塊,系統中某一部分的變化將如何影響其它部分就會變得顯而易見,系統的可維護性更加簡單易得。javascript
一個模塊就是實現特定功能的文件, 邏輯上相關的代碼組織到同一個包內,包內是一個相對獨立的王國,不用擔憂命名衝突什麼的,那麼外部使用的話直接引入對應的package
便可.css
就好像做家會把他的書分章節和段落;程序員會把他的代碼分紅模塊。html
就好像書籍的一章,模塊僅僅是一坨代碼而已。前端
好的代碼模塊分割的內容必定是很合理的,便於你增長減小或者修改功能,同時又不會影響整個系統。java
Javascript
模塊化?早期前端只是爲了實現簡單的頁面交互邏輯,隨着Ajax
技術的普遍應用,前端庫的層出不窮,前端代碼日益膨脹,JavaScript
卻沒有爲組織代碼提供任何明顯幫助,甚至沒有類的概念,更不用說模塊(module
)了,這時候JavaScript
極其簡單的代碼組織規範不足以駕馭如此龐大規模的代碼.node
模塊化可使你的代碼低耦合,功能模塊直接不相互影響。jquery
可維護性:根據定義,每一個模塊都是獨立的。良好設計的模塊會盡可能與外部的代碼撇清關係,以便於獨立對其進行改進和維護。維護一個獨立的模塊比起一團凌亂的代碼來講要輕鬆不少。webpack
命名空間:在JavaScript中,最高級別的函數外定義的變量都是全局變量(這意味着全部人均可以訪問到它們)。也正因如此,當一些無關的代碼碰巧使用到同名變量的時候,咱們就會遇到「命名空間污染」的問題。git
可複用性:現實來說,在平常工做中咱們常常會複製本身以前寫過的代碼到新項目中, 有了模塊, 想複用的時候直接引用進來就行。程序員
前端的先驅在刀耕火種的階段開始,作了不少努力,在現有的運行環境中,實現"模塊"的效果。
模塊就是實現特定功能的一組方法。在JavaScript中,函數是建立做用域的惟一方式, 因此把函數做爲模塊化的第一步是很天然的事情.
function foo(){
//...
}
function bar(){
//...
}
複製代碼
上面的,組成一個模塊。使用的時候,直接調用就好了。
這種作法的缺點很明顯:全局變量被污染,很容易命名衝突, 並且模塊成員之間看不出直接關係。
爲了解決上面的缺點,能夠把模塊寫成一個對象,全部的模塊成員都放到這個對象裏面。
var MYAPP = {
count: 0,
foo: function(){},
bar: function(){}
}
MYAPP.foo();
複製代碼
上面的代碼中,函數foo
和bar
, 都封裝在MYAPP對象裏。使用的時候,就是調用這個對象的屬性。 可是,這樣的寫法會暴露全部模塊成員,內部狀態能夠被外部改寫.
使用當即執行函數(Immediately-Invoked Function Expression,IIFE),能夠達到不暴露私有成員的目的。
var Module = (function(){
var _private = "safe now";
var foo = function(){
console.log(_private)
}
return {
foo: foo
}
})()
Module.foo();
Module._private; // undefined
複製代碼
這種方法的好處在於,你能夠在函數內部使用局部變量,而不會意外覆蓋同名全局變量,但仍然可以訪問到全局變量, 在模塊外部沒法修改咱們沒有暴露出來的變量、函數.
將全局變量當成一個參數傳入到匿名函數而後使用
var Module = (function($){
var _$body = $("body"); // we can use jQuery now!
var foo = function(){
console.log(_$body); // 特權方法
}
// Revelation Pattern
return {
foo: foo
}
})(jQuery)
Module.foo();
複製代碼
jQuery
的封裝風格曾經被不少框架模仿,經過匿名函數包裝代碼,所依賴的外部變量傳給這個函數,在函數內部可使用這些依賴,而後在函數的最後把模塊自身暴漏給window
。
若是須要添加擴展,則能夠做爲jQuery
的插件,把它掛載到$上。 這種風格雖然靈活了些,但並未解決根本問題:所需依賴仍是得外部提早提供、仍是增長了全局變量。
從以上的嘗試中,能夠概括出js模塊化須要解決那些問題:
圍繞着這些問題,js模塊化開始了一段艱苦而曲折的征途。
上述的全部解決方案都有一個共同點:使用單個全局變量來把全部的代碼包含在一個函數內,由此來建立私有的命名空間和閉包做用域。
你必須清楚地瞭解引入依賴文件的正確順序。就拿Backbone.js
來舉個例子,想要使用Backbone
就必須在你的頁面裏引入Backbone
的源文件。
然而Backbone
又依賴 Underscore.js
,因此Backbone
的引入必須在其以後。
而在工做中,這些依賴管理常常會成爲讓人頭疼的問題。
另一點,這些方法也有可能引發命名空間衝突。舉個例子,要是你碰巧寫了倆重名的模塊怎麼辦?或者你同時須要一個模塊的兩個版本時該怎麼辦?
還有就是協同開發的時候, 你們編寫模塊的方式各不相同,你有你的寫法,我有個人寫法, 那就亂了套.
接下來介紹幾種廣受歡迎的解決方案
2009年,美國程序員Ryan Dahl
創造了node.js
項目,將javascript
語言用於服務器端編程。
這標誌Javascript
模塊化編程正式誕生。由於老實說,在瀏覽器環境下,沒有模塊也不是特別大的問題,畢竟網頁程序的複雜性有限;可是在服務器端,必定要有模塊,與操做系統和其餘應用程序互動,不然根本無法編程。
node.js
的模塊系統,就是參照CommonJS
規範實現的。
CommonJS
定義的模塊分爲:
定義模塊: 根據CommonJS
規範,一個單獨的文件就是一個模塊。每個模塊都是一個單獨的做用域,也就是說,在該模塊內部定義的變量,沒法被其餘模塊讀取,除非定義爲global
對象的屬性。
模塊輸出: 模塊只有一個出口,module.exports
對象,咱們須要把模塊但願輸出的內容放入該對象。module
對象就表明模塊自己。
加載模塊: 加載模塊使用require
方法,該方法讀取一個文件並執行,返回文件內部的module.exports
對象。
// math.js
exports.add = function(a, b){
return a + b;
}
複製代碼
// main.js
var math = require('math') // ./math in node
console.log(math.add(1, 2)); // 3
複製代碼
這種實現模式有兩點好處:
可是, 因爲一個重大的侷限,使得CommonJS
規範不適用於瀏覽器環境。
看上面的main.js
代碼, 第二行的math.add(1, 2)
,在第一行require('math')以後運行,所以必須等math.js
加載完成。也就是說,若是加載的依賴不少, 時間很長,整個應用就會停在那裏等。
咱們分析一下瀏覽器端的js和服務器端js都主要作了哪些事,有什麼不一樣:
服務器端JS | 瀏覽器端JS |
---|---|
相同的代碼須要屢次執行 | 代碼須要從一個服務器端分發到多個客戶端執行 |
CPU和內存資源是瓶頸 | 帶寬是瓶頸 |
加載時從磁盤中加載 | 加載時須要經過網絡加載 |
這對服務器端不是一個問題,由於全部的模塊都存放在本地硬盤,能夠同步加載完成,等待時間就是硬盤的讀取時間。可是,對於瀏覽器,這倒是一個大問題,由於模塊都放在服務器端,等待時間取決於網速的快慢,可能要等很長時間,瀏覽器處於"假死"狀態。
所以,瀏覽器端的模塊,不能採用"同步加載"(synchronous
),只能採用"異步加載"(asynchronous
)。這就是AMD
規範誕生的背景。
AMD 即
Asynchronous Module Definition
,中文名是異步模塊定義的意思。它採用異步方式加載模塊,模塊的加載不影響它後面語句的運行。全部依賴這個模塊的語句,都定義在一個回調函數中,等到加載完成以後,這個回調函數纔會運行。
// main.js
  require(['moduleA', 'moduleB', 'moduleC'], function (moduleA, moduleB, moduleC){
    // some code here
  });
複製代碼
define
來定義模塊,用法爲:define(id?, dependencies?, factory)
;id
爲模塊標識,聽從CommonJS Module Identifiers
規範dependencies
爲依賴的模塊數組,在factory
中需傳入形參與之一一對應dependencies
的值中有"require"、"exports"
或"module"
,則與commonjs
中的實現保持一致dependencies
省略不寫,則默認爲["require", "exports", "module"]
,factory
中也會默認傳入require,exports,module
.factory
爲函數,模塊對外暴漏API
的方法有三種:return
任意類型的數據、exports.xxx=xxx、module.exports=xxx
.factory
爲對象,則該對象即爲模塊的返回值大名鼎鼎的require.js
就是AMD規範的實現.
require.js
要求,每一個模塊是一個單獨的js
文件。這樣的話,若是加載多個模塊,就會發出屢次HTTP
請求,會影響網頁的加載速度。所以,require.js
提供了一個優化工具(Optimizer
)r.js
,當模塊部署完畢之後,能夠用這個工具將多個模塊合併在一個文件中,實現前端文件的壓縮與合併, 減小HTTP請求數。
咱們來看一個require.js
的例子
//a.js
define(function(){
console.log('a.js執行');
return {
hello: function(){
console.log('hello, a.js');
}
}
});
複製代碼
//b.js
define(function(){
console.log('b.js執行');
return {
hello: function(){
console.log('hello, b.js');
}
}
});
複製代碼
//main.js
require.config({
paths: {
"jquery": "../js/jquery.min"
},
});
require(['jquery','a', 'b'], function($, a, b){
console.log('main.js執行');
a.hello();
$('#btn').click(function(){
b.hello();
});
})
複製代碼
上面的main.js被執行的時候,會有以下的輸出: a.js執行 b.js執行 main.js執行 hello, a.js
在點擊按鈕後,會輸出: hello, b.js
可是若是細細來看,b.js
被預先加載而且預先執行了,(第二行輸出),b.hello
這個方法是在點擊了按鈕以後纔會執行,若是用戶壓根就沒點,那麼b.js
中的代碼應不該該執行呢?
這其實也是AMD/RequireJs
被吐槽的一點,因爲瀏覽器的環境特色,被依賴的模塊確定要預先下載的。問題在於,是否須要預先執行?若是一個模塊依賴了十個其餘模塊,那麼在本模塊的代碼執行以前,要先把其餘十個模塊的代碼都執行一遍,無論這些模塊是否是立刻會被用到。這個性能消耗是不容忽視的。
另外一點被吐槽的是,在定義模塊的時候,要把全部依賴模塊都羅列一遍,並且還要在factory
中做爲形參傳進去,要寫兩遍很大一串模塊名稱,像這樣:
define(['a', 'b', 'c', 'd', 'e', 'f', 'g'], function(a, b, c, d, e, f, g){ ..... })
複製代碼
CMD 即
Common Module Definition
, CMD是sea.js
的做者在推廣sea.js
時提出的一種規範.
在 CMD
規範中,一個模塊就是一個文件。代碼的書寫格式以下:
define(function(require, exports, module) {
// 模塊代碼
// 使用require獲取依賴模塊的接口
// 使用exports或者module或者return來暴露該模塊的對外接口
})
複製代碼
define
函數定義模塊, 無需羅列依賴數組,在factory
函數中需傳入形參require,exports,module
.require
用來加載一個 js
文件模塊,require
用來獲取指定模塊的接口對象 module.exports
。//a.js
define(function(require, exports, module){
console.log('a.js執行');
return {
hello: function(){
console.log('hello, a.js');
}
}
});
複製代碼
//b.js
define(function(require, exports, module){
console.log('b.js執行');
return {
hello: function(){
console.log('hello, b.js');
}
}
});
複製代碼
//main.js
define(function(require, exports, module){
console.log('main.js執行');
var a = require('a');
a.hello();
$('#b').click(function(){
var b = require('b');
b.hello();
});
});
複製代碼
上面的main.js執行會輸出以下: main.js執行 a.js執行 hello, a.js
a.js和b.js都會預先下載,可是b.js中的代碼卻沒有執行,由於尚未點擊按鈕。當點擊按鈕的時候,會輸出以下: b.js執行 hello, b.js
Sea.js
加載依賴的方式
AMD vs CMD
一樣都是異步加載模塊,AMD在加載模塊完成後就會執行改模塊,全部模塊都加載執行完後會進入require的回調函數,執行主邏輯.
CMD加載完某個依賴模塊後並不執行,只是下載而已,在全部依賴模塊加載完成後進入主邏輯,遇到require
語句的時候才執行對應的模塊,這樣模塊的執行順序和書寫順序是徹底一致的。
這也是不少人說AMD用戶體驗好,由於沒有延遲,依賴模塊提早執行了,CMD性能好,由於只有用戶須要的時候才執行的緣由。
上述的這幾種方法都不是JS原生支持的, 在
ECMAScript 6 (ES6)
中,引入了模塊功能, ES6 的模塊功能汲取了CommonJS 和 AMD 的優勢,擁有簡潔的語法並支持異步加載,而且還有其餘諸多更好的支持。
簡單來講,ES6 模塊的設計思想就是:一個 JS 文件就表明一個 JS 模塊。在模塊中你可使用 import 和 export 關鍵字來導入或導出模塊中的東西。
ES6 模塊主要具有如下幾個基本特色:
通常來說,組織聲明文件的方式取決於庫是如何被使用的。 在JavaScript中一個庫有不少使用方式,這就須要你書寫聲明文件去匹配它們.
經過庫的使用方法及其源碼來識別庫的類型。
全局庫是指能在全局命名空間下訪問的,許多庫都是簡單的暴露出一個或多個全局變量。 好比jQuery.
當你查看全局庫的源代碼時,你一般會看到:
一些庫只能工做在模塊加載器的環境下。 好比,像 express
只能在Node.js
裏工做因此必須使用CommonJS
的require
函數加載。
模塊庫至少會包含下列具備表明性的條目之一:
require
或define
import * as a from 'b'; or export c
;這樣的聲明exports
或module.exports
UMD (Universal Module Definition)
庫
UMD創造了一種同時使用兩種規範的方法,而且也支持全局變量定義。因此UMD的模塊能夠同時在客戶端和服務端使用。
本質上,UMD 是一套用來識別當前環境支持的模塊風格的 if/else 語句。下面是一個解釋其功能的例子:
(function (root, factory) {
if (typeof define === "function" && define.amd) {
define(["libName"], factory);
} else if (typeof module === "object" && module.exports) {
module.exports = factory(require("libName"));
} else {
root.returnExports = factory(root.libName);
}
}(this, function (b) {})
複製代碼
簡單的說,Grunt / Gulp 和 browserify / webpack
不是一回事。
Gulp / Grunt
Gulp / Grunt 是一種工具,可以優化前端工做流程。好比自動刷新頁面、combo、壓縮css、js、編譯less等等。簡單來講,就是使用Gulp/Grunt,而後配置你須要的插件,就能夠把之前須要手工作的事情讓它幫你作了。
說到 browserify / webpack
,那還要說到 seajs / requirejs
。這四個都是JS模塊化的方案。其中seajs / require
是一種類型,browserify / webpack
是另外一種類型。seajs / require
: 是一種在線"編譯" 模塊的方案,至關於在頁面上加載一個 CMD/AMD
解釋器。這樣瀏覽器就認識了 define、exports、module
這些東西。也就實現了模塊化。
browserify / webpack
: 是一個預編譯模塊的方案,相比於上面 ,這個方案更加智能, 首先,它是預編譯的,不須要在瀏覽器中加載解釋器。另外,你在本地直接寫JS,無論是 AMD / CMD / ES6
風格的模塊化,它都能認識,而且編譯成瀏覽器認識的JS。這樣就知道,Gulp
是一個工具,而webpack
等等是模塊化方案。Gulp
也能夠配置seajs、requirejs
甚至webpack
的插件。
每次運行grunt
時,他就利用node
提供的require()
系統查找本地安裝的 Grunt
。
若是找到一份本地安裝的 Grunt
,grunt-CLI
就將其加載,並傳遞Gruntfile
中的配置信息,而後執行你所指定的任務。
npm install -g grunt-cli
複製代碼
gruntfile.js
文件module.exports = function (grunt) {
// 項目配置.
grunt.initConfig({
// 定義Grunt任務
});
// 加載可以提供"uglify"任務的插件。
grunt.loadNpmTasks('grunt插件');
// Default task(s).
grunt.registerTask('default', ['任務名']);
}
複製代碼
gulp
是基於Nodejs的自動化任務運行器,它能自動化地完成javascript/sass/less/html/image/css
等文件的的測試、檢查、合併、壓縮、格式化、瀏覽器自動刷新、部署文件生成,並監聽文件在改動後重復指定的這些步驟。
使用Gulp
的優點就是利用流的方式進行文件的處理,使用管道(pipe
)思想,前一級的輸出,直接變成後一級的輸入,經過管道將多個任務和操做鏈接起來,所以只有一次I/O
的過程,流程更清晰,更純粹。Gulp
去除了中間文件,只將最後的輸出寫入磁盤,整個過程所以變得更快。
使用Gulp
,能夠避免瀏覽器緩存機制,性能優化(文件合併,減小http請求;文件壓縮)以及效率提高(自動添加CSS3前綴;代碼分析檢查)
Browserify
是一個模塊打包器,它遍歷代碼的依賴樹,將依賴樹中的全部模塊打包成一個文件。有了 Browserify
,咱們就能夠在瀏覽器應用程序中使用 CommonJS
模塊。
browserify模塊化的用法和node是同樣的,因此npm上那些本來僅僅用於node環境的包,在瀏覽器環境裏也同樣能用.
webpack官網有對兩者的使用方法進行對比,能夠看一下:webpack for browserify users
browserify main.js -o bundle.js
複製代碼
Compare Webpack vs Browserify vs RequireJS
官網對webpack
的定義是MODULE BUNDLER
(模塊打包器),他的目的就是把有依賴關係的各類文件打包成一系列的靜態資源。 請看下圖
Webpack
的工做方式是:把你的項目當作一個總體,經過一個給定的主文件(如:main.js
),Webpack
將從這個文件開始找到你的項目的全部依賴文件,使用loaders
處理它們,最後打包爲一個(或多個)瀏覽器可識別的JavaScript
文件。
webpack
將建立全部應用程序的依賴關係圖表(dependency graph
)。
entry配置項告訴Webpack應用的根模塊或起始點在哪裏, 入口起點告訴 webpack
從哪裏開始,並遵循着依賴關係圖表知道要打包什麼。能夠將應用程序的入口起點認爲是根上下文或 app
第一個啓動文件。它的值能夠是字符串、數組或對象.
//webpack.config.js
const config = {
entry: {
app: './src/app.js',
vendors: './src/vendors.js'
}
};
複製代碼
將全部的資源(assets
)合併在一塊兒後,咱們還須要告訴 webpack
在哪裏打包咱們的應用程序。output
選項控制 webpack
如何向硬盤寫入編譯文件。注意,即便能夠存在多個入口起點,但只指定一個輸出配置。
output: {
path: helpers.root('dist/nonghe'),
publicPath: '/',
filename: 'js/[name].[chunkhash].bundle.js',
chunkFilename: 'js/[name].[chunkhash].bundle.js'
}
複製代碼
在webpack的世界裏, 一切皆模塊, 經過
loader
的轉換,任何形式的資源均可以視做模塊,好比CommonJs 模塊、 AMD 模塊、 ES6 模塊、CSS、圖片、 JSON、Coffeescript、 LESS
等。並且 webpack 只理解 JavaScript。
對比 Node.js 模塊,webpack 模塊可以以各類方式表達它們的依賴關係:
url(...
))或 HTML 文件(<img src=...>
)中的圖片連接webpack compiler
在碰到上面那些語句的時候, 經過與其相對應的loader
將這些文件進行轉換,而轉換後的文件會被添加到依賴圖表中。
module: {
loaders: [{
test: /\.scss$/,
loaders: 'style!css!sass'
}, {
test: /\.(png|jpg|svg)$/,
loader: 'url?limit=20480' //20k
}]
}}
複製代碼
plugin
插件,用於擴展webpack
的功能,在webpack
構建生命週期的節點上加入擴展hook
爲webpack
加入功能。
Loaders
和Plugins
經常被弄混,可是他們實際上是徹底不一樣的東西,能夠這麼來講,loaders
是在打包構建過程當中用來處理源文件的(js,ts, Scss,Less..),一次處理一個,一般做用於包生成以前或生成的過程當中。
插件並不直接操做單個文件,它直接對整個構建過程其做用。
幾款經常使用的插件
HtmlWebpackPlugin : 這個插件的做用是依據一個簡單的html
模板,生成一個自動引用打包後的JS文件的新index.html
。
Hot Module Replacement: 它容許你在修改組件代碼後,自動刷新實時預覽修改後的效果。
CommonsChunkPlugin: 對於有多個入口文件的, 能夠抽取公共的模塊,最終合成的文件可以在最開始的時候加載一次,便存起來到緩存中供後續使用。
DefinePlugin: 容許你建立一個在編譯時能夠配置的全局常量。這可能會對開發模式和發佈模式的構建容許不一樣的行爲很是有用。
ExtractTextWebpackPlugin: 它會將打包在js
代碼中的樣式文件抽離出來, 放到一個單獨的 css
包文件 (styles.css)當中, 這樣js
代碼就能夠和css
並行加載.
UglifyjsWebpackPlugin: 這個插件使用 UglifyJS 去壓縮你的JavaScript代碼。
從啓動webpack構建到輸出結果經歷了一系列過程,它們是:
webpack
配置參數,合併從shell
傳入和webpack.config.js
文件裏配置的參數,生產最後的配置結果。entry
入口文件開始解析文件構建依賴圖譜,找出每一個文件所依賴的文件,遞歸下去。loader
配置找出合適的loader
用來對文件進行轉換。entry
配置生成代碼塊chunk
。chunk
到文件系統。代碼拆分是 webpack
中最引人注目的特性之一。你能夠把代碼分離到不一樣的 bundle
中,而後就能夠去按需加載這些文件.
CommonsChunkPlugin
module bundlers
)最終將全部的模塊編譯生成一個龐大的bundle.js
文件。所以Webpack使用許多特性來分割代碼而後生成多個「bundle」文件,並且異步加載部分代碼以實現按需加載使用 require.ensure()
按需分離代碼
require.ensure(dependencies: String[], callback: function(require), chunkName: String)
複製代碼
模塊熱替換功能會在應用程序運行過程當中替換、添加或刪除模塊,而無需從新加載頁面。這使得你能夠在獨立模塊變動後,無需刷新整個頁面,就能夠更新這些模塊.
webpack-dev-server
支持熱模式,在試圖從新加載整個頁面以前,熱模式會嘗試使用 HMR 來更新。
webpack-dev-server 主要是啓動了一個使用 express 的 Http服務器 。它的做用 主要是用來伺服資源文件 。此外這個 Http服務器 和 client 使用了 websocket 通信協議,原始文件做出改動後, webpack-dev-server 會實時的編譯,可是最後的編譯的文件並無輸出到目標文件夾, 實時編譯後的文件都保存到了內存當中。
"server": "webpack-dev-server --inline --progress --hot",
複製代碼
webpack-dev-server 支持2種自動刷新的方式:
Iframe mode
Iframe mode 是在網頁中嵌入了一個 iframe ,將咱們本身的應用注入到這個 iframe 當中去,所以每次你修改的文件後,都是這個 iframe 進行了 reload 。
inline mode
而 Inline-mode ,是 webpack-dev-server 會在你的 webpack.config.js 的入口配置文件中再添加一個入口,
module.exports = {
entry: {
app: [
'webpack-dev-server/client?http://localhost:8080/',
'./src/js/index.js'
]
},
output: {
path: './dist/js',
filename: 'bundle.js'
}
}
複製代碼
這樣就完成了將 inlinedJS
打包進 bundle.js
裏的功能,同時 inlinedJS
裏面也包含了 socket.io
的 client
代碼,能夠和 webpack-dev-server
進行 websocket
通信。
其餘配置選項
Hot Module Replacement
功能