javascript模組化以及載入打包

世有因果知因求果發表於2016-04-17

https://addyosmani.com/writing-modular-js/

一些術語:

模組:可以理解為一個js檔案,就像你以前需要import的那個檔案一樣:module不一定非要是一個外部檔案,你可以動態建立一個module。

loaded into global context,module中的變數或者函式都是封裝起來的,不會被暴露到

global,只有export出來的才會暴露;

你不能使用<script>來直接引用,只能通過systemjs來載入;

每個模組只有一個例項signleton;

模組就是一個黑盒子。

Package: cards package可以包含card.js module, deck.js module, 而index.js卻是一個入口,將多個模組打包在一起。

app也可以看做一個package

module loader:

什麼是模組化

當我們說一個應用是modular的,我們通常意思是:這個應用是由一系列高度解耦的具有不同功能的分佈在不同module裡面的功能模組組成的。正如你可能知道的:輕量級解耦往往具有下面的好處:你可以輕鬆地刪除部分依賴而不會影響到其他模組的功能。

不像其他的程式語言,javascript並不原生具備模組化的能力。正因為如此,程式設計師只能使用第三方開發的模組依賴處理庫來實現模組化開發模式:AMD, COMMONJS,ES6

script loader

在探討AMD, COMMONJS之前我們必須來談談script loader這個概念。

script loading is a means to a goal, that goal being modular JavaScript that can be used in applications today - for this, use of a compatible script loader is unfortunately necessary.

有幾種廣泛應用的Loaders來處理AMD,COMMJS格式的模組載入,比如requireJS, curl.js,dojo,甚至system.js

從產品角度來看,使用優化工具,比如RequireJS Optimizer來拼接所有的指令碼檔案也是很有必要的。但是另外一個可行的應用場景是:應用程式可以在頁面page load後動態載入需要的指令碼,而RequireJS就能很好地滿足到這一點。

AMD, COMMONJS, ES6三種模組化方案簡單對比:

AMD: define + require

CMD: exports + require

ES6: export + import

AMD

AMD(Asynchronous Module Defination)format的目標是提供一個可供js開發人員當下使用的模組化js方案。

AMD module的格式本身建議定義定義的模組以及該模組的依賴dependencies可以被非同步地載入。

AMD模組定義和使用

在AMD模組哲學中,有兩個重要的概念: define 方法用於定義一個模組,而require 方法則用於處理dependency loading. 

define(
    module_id /*optional:如果該引數不存在,我們就成為匿名模組*/, 
    [dependencies] /*optional*/, 
    definition function /*function for instantiating the module or object*/
);

例項:

// A module_id (myModule) is used here for demonstration purposes only
 
define('myModule', 
    ['foo', 'bar'], 
    // module definition function
    // dependencies (foo and bar) are mapped to function parameters
    function ( foo, bar ) {
        // return a value that defines the module export
        // (i.e the functionality we want to expose for consumption)
    
        // create your module here
        var myModule = {
            doStuff:function(){
                console.log('Yay! Stuff');
            }
        }
 
        return myModule;
});
 
// An alternative example could be..
define('myModule', 
    ['math', 'graph'], 
    function ( math, graph ) {
 
        // Note that this is a slightly different pattern
        // With AMD, it's possible to define modules in a few
        // different ways due as it's relatively flexible with
        // certain aspects of the syntax
        return {
            plot: function(x, y){
                return graph.drawPie(math.randomGrid(x,y));
            }
        }
    };
});

理解AMD:require()

// Consider 'foo' and 'bar' are two external modules
// In this example, the 'exports' from the two modules loaded are passed as
// function arguments to the callback (foo and bar)
// so that they can similarly be accessed
 
require(['foo', 'bar'], function ( foo, bar ) {
        // rest of your code here
        foo.doSomething();
});

動態載入dependecies

define(function ( require ) {
    var isReady = false, foobar;
 
    // note the inline require within our module definition
    require(['foo', 'bar'], function (foo, bar) {
        isReady = true;
        foobar = foo() + bar();
    });
 
    // we can still return a module
    return {
        isReady: isReady,
        foobar: foobar
    };
});

理解AMD Plugin

// With AMD, it's possible to load in assets of almost any kind
// including text-files and HTML. This enables us to have template
// dependencies which can be used to skin components either on
// page-load or dynamically.
 
define(['./templates', 'text!./template.md','css!./template.css'],
    function( templates, template ){
        console.log(templates);
        // do some fun template stuff here.
    }
});

通過兩種方式來載入AMD模組:

1. require.js

require(['app/myModule'], 
    function( myModule ){
        // start the main module which in-turn
        // loads other modules
        var module = new myModule();
        module.doStuff();
});

 

2.curl.js

curl(['app/myModule.js'], 
    function( myModule ){
        // start the main module which in-turn
        // loads other modules
        var module = new myModule();
        module.doStuff();
});

為什麼說AMD是javascript模組化的一個比較好的選擇?

我們來看看他解決的問題和優勢:

1. 定義了一個清晰的建議去如何實現靈活的模組;

2. 相比於當前通過全域性引入一個物件,和通過<script>標籤載入檔案的方案要清晰地多。清晰地定義一個模組以及這個模組的所有依賴;

3. 模組定義是封裝好的,這樣我們就不會汙染global namespace。(比如常用的jquery,實際上我們就汙染全域性空間,引入了$這個變數)

4. 比一些替代方案可能更好用(比如commonjs),沒有cross-domain的問題,沒有local/debugging問題,也沒有對server side工具有任何依賴。大部分AMD Loaders支援並不需要任何build process的AMD modules

5. 提供了一種在單個檔案中包含多個模組的'transport'方案。而比如commonjs卻要求必須統一一個transport format

6. 可以輕鬆實現lazy load script

AMD Module Design patterns

傳統的js設計模式也可以方便地以AMD方式傳承和使用:

 

AMD modules with jQuery

使用Jquery(注意這種AMD模組引入jquery的方式來使用沒有在全域性名稱空間中引入$或者jquery!):

define(['js/jquery.js','js/jquery.color.js','js/underscore.js'],
    function($, colorPlugin, _){
        // Here we've passed in jQuery, the color plugin and Underscore
        // None of these will be accessible in the global scope, but we
        // can easily reference them below.
 
        // Pseudo-randomize an array of colors, selecting the first
        // item in the shuffled array
        var shuffleColor = _.first(_.shuffle(['#666','#333','#111']));
 
        // Animate the background-color of any elements with the class
        // 'item' on the page using the shuffled color
        $('.item').animate({'backgroundColor': shuffleColor });
        
        return {};
        // What we return can be used by other modules
    });

上面是jquery以模組方式來使用,我們還得看看jquery為了可以被以AMD方式載入應用,我們應該做什麼來改造jquery:

CommonJS

require()和exports

這一點和AMD類似的, exports指示本模組需要暴露給其他模組使用的物件;require則表明本模組的依賴模組

// package/lib is a dependency we require
var lib = require('package/lib');
 
// some behaviour for our module
function foo(){
    lib.log('hello world!');
}
 
// export (expose) foo to other modules
exports.foo = foo;

更復雜一點的例子:

// define more behaviour we would like to expose
function foobar(){
        this.foo = function(){
                console.log('Hello foo');
        }
 
        this.bar = function(){
                console.log('Hello bar');
        }
}
 
// expose foobar to other modules
exports.foobar = foobar;
 
 
// an application consuming 'foobar'
 
// access the module relative to the path
// where both usage and module files exist
// in the same directory
 
var foobar = require('./foobar').foobar,
    test   = new foobar();
 
test.bar(); // 'Hello bar'

同時使用多個依賴:

var modA = require('./foo');
var modB = require('./bar');
 
exports.app = function(){
    console.log('Im an application!');
}
 
exports.foo = function(){
    return modA.helloWorld();
}

注意commonJS module export時的以下情況:

// CMD 模式下的合法exports
exports.someFunc = someFunc;
module.exports.someFunc = someFunc;

module.exports = {};
module.exports = function(){};

//相反下面就是非法的!
exports = {};
exports = function(){};

 

ES6

由於ES6本身是原生語言支援實現的模組化,但是現代瀏覽器大多都還未支援,因此必須使用相應的transpiler工具轉換成ES5的AMD,CMD模組,再借助於systemjs/requirejs等模組載入工具才能使用。

上圖中直到生成AMD, CommonJS的ES5模組都屬於dev/build流程,而從ES5 CMD,AMD程式碼到SystemJS/RequireJS載入則屬於runtime過程 

module 'math' {
// named exports export default
function sum(x, y) { //注意default export是當import return x + y; } export var pi = 3.141593; }
//或者使用下面的literal export方法
export { sum as sumFunc, pi }
// we can import in script code, not just inside a module import {sum, pi} from 'math';
alert("2π = " + sum(pi, pi));
// 或者這樣import和呼叫
import *as mathmodule from 'math';
alert("2π = " + mathmodule.sum(mathmodule.pi, mathmodule.pi));

// 下面的語法則同時import了default export和自選export: sum是上面export default定義的
// 而pi則沒有export default關鍵字!
import sum, { pi } from 'math'

 

關於babel

babel是一個transpiler,程式碼轉換器,它起到了es6和es5的橋樑的作用,作為build step而存在於工具鏈的, 支援所有的模組格式:module formats. Typescript和babel是類似的,它支援es6並且transpile到es5

module bundler

在上面ES6模組章節提到要讓build step將es6模組轉化出來的es5 cmd模組能在瀏覽器中執行,有一個解決方案就是:在瀏覽器中載入一個module loader(requireJS/SystemJS),由載入器來動態載入相應的js檔案。和這個方案對應的有另外一個方案就是使用module bundler(browserify/webpack),這個bundler工具作為build step的延伸。實際上如果是ES6 module,webpack將呼叫babel來transpile成CMD模組,並且最後打包成一個或者多個大的chunk直接載入到瀏覽器執行(注意:這時無需systemjs/requirejs載入器,因為webpack本身已經能夠認識CMD的require/exports,AMD的define!!)

模組化工具鏈

我們已經知道js語言本身並不支援模組化,同時瀏覽器中js和服務端nodejs中的js執行環境是不同的,如何實現瀏覽器中js模組化,不是一個很簡單的事情,主流有兩種方案:

1. requirejs/seajs:他們是一種線上“編譯”模組的方案,相當於在頁面上載入一個CommonJS/AMD模組格式直譯器。這樣瀏覽器就認識了上面講到的define, exports,module這些東西,也就實現了模組化。

2.browserify/webpack:是一個預編譯模組打包的方案,相比於第一種方案,這個方案更加智慧。由於是預編譯的,不需要在瀏覽器中載入直譯器。你在本地直接寫JS,不管是AMD/CMD/ES6風格的模組化,它都能認識,並且編譯成瀏覽器認識的JS

注意: browerify打包器本身只支援Commonjs模組,如果要打包AMD模組,則需要另外的plugin來實現AMD到CMD的轉換!!https://github.com/jaredhanson/deamdify

gulp一般作為task runner,在前端構建流程中起到非常重要的作用,它在軟體工程界和C語言的make類似。

Browserify: It provides a way to bundle CommonJS modules together, adheres to the Unix philosophy, is in fact a good alternative to Webpack.

http://www.zhihu.com/question/37020798

相關文章