grunt任務之seajs模塊打包


grunt與seajs

         grunt是前端流行的自定義任務的腳手架工具,我們可以使用grunt來為我們做一些重復度很高的事情,如壓縮,合並,js語法檢查等。通過定義grunt的配置文件Gruntfile.js,配置並注冊grunt的任務,最終我們可以通過命令行來執行任務。

         seajs主要用於模塊化,通過define定義一個模塊,可以通過require加載模塊,exports導出模塊。具體的seajs實現可通過本博客的系列博文--Seajs源碼解析系列來進一步了解。

         在實際生產中,如果緊緊定義一系列seajs模塊而並不進行合並壓縮的話,加載性能很低,原因大家都懂的,seajs在瀏覽器端處理依賴模塊,並進行異步加載,這個過程中會有多個http請求,大大降低頁面的加載速度。所以結合grunt構建工具,我們可以將模塊的依賴處理放到服務端進行,並將所有模塊合並壓縮,完成生產所需的最終文件。

         在seajs社區中,已經提供了一款npm模塊,即grunt-cmd-transport。我們通過該模塊給seajs模塊命名,並處理各模塊之間的依賴。這項工作聽起來很簡單,但是在筆者的實踐過程中出現的問題卻不少,因此本文着重講解transport任務的相關配置。

grunt的相關文件

         grunt相關文件包括了2個,首先是Gruntfile.js,另一個是package.json文件。Gruntfile進行grunt任務的配置及注冊,package.json用於向Gruntfile提供參數,並設置依賴的npm模塊。

         在下面package.json中,定義spm鍵,設置模塊的別名,在Gruntfile中,通過pkg = grunt.file.readJSON()來讀取package配置文件,並通過<%= pkg.spm.alias %>獲取模塊別名。

package.json

{
     "name" : "HelloSeaJS",
     "version" : "1.0.0",
     "author" : "yang li",
     "spm": {
        "alias": {
            "jquery": "jquery"
        }
     },
     "devDependencies" : {
          "grunt" : "0.4.1",
          "grunt-cmd-transport": "~0.2.0",
          "grunt-cmd-concat": "~0.2.0",
          "grunt-contrib-uglify" : "0.2.0",
          "grunt-contrib-clean" : "0.4.0"
     }
}

         接下來我們進行設置grunt。Gruntfile.js其實就是一個node模塊,依然使用閉包將所有的邏輯進行包裹,並提供了grunt參數,通過grunt.initConfig進行任務的配置。

         對於seajs模塊而言,首先需要處理各模塊之間的依賴,我們通過設置transport任務來完成。seajs遵循的是CMD規范,在定義模塊時不需要制定模塊名和模塊的依賴組,只需設置工廠函數即可。其實在未使用grunt進行合並seajs時(即在瀏覽器端處理模塊依賴),seajs設置模塊id和uri相同,為絕對路徑。在這個過程中有些小技巧,在Seajs源碼解析系列中並未提到,現在在這里着重分析下:

<script src="../sea-debug.js"></script>
    <script>
        seajs.config({
            base: "../gallery/",
            alias:{
                jquery: 'jquery/jquery-1.11.1'
            }
        })
        seajs.use("../application.js")
    </script>

         對於上述代碼,application.js並沒有合並seajs模塊,我們通過seajs.use創建了一個匿名use模塊,通過

var mod = Module.get(uri, isArray(ids) ? ids : [ids])

來實現,並設置依賴。在此處,依賴為[‘../application.js’];然后設置use模塊的callback,並調用load函數加載依賴模塊。在load函數中,use模塊調用resolve函數解析出依賴的絕對路徑,即[‘http://localhost:63342/mywork/js/application.js’],並創建一個新的Module表示該模塊,這里用appMod表示,並以uri為key保存到modCache中。調用appMod.fetch加載對應的文件並設置回調函數onRequest,在application.js中定義了一個匿名模塊define(function(){return {};}),此時模塊的配置信息

meta = {
    id: id,
    uri: Module.resolve(id), // 絕對url
    deps: deps,
    factory: factory
  }

中id=‘undefined’,url=’’,

meta.uri ? Module.save(meta.uri, meta) :
      // Save information for "saving" work in the script onload event
      anonymousMeta = meta

由於此時meta.uri為空,因此meta信息保存在全局變量anonymousMeta中,用於后續處理。

        紅色字體強調了在調用fetch時設置的回調onRequest函數,當文件加載完畢,執行onRequest,

function onRequest() {
    delete fetchingList[requestUri]
    fetchedList[requestUri] = true

    // Save meta data of anonymous module
    if (anonymousMeta) {
      Module.save(uri, anonymousMeta)
      anonymousMeta = null
    }

    // Call callbacks
    var m, mods = callbackList[requestUri]
    delete callbackList[requestUri]
    while ((m = mods.shift())) m.load()
  }

此時anonymousMeta已在模塊define時設置,因此將該meta配置文件配置到uri對應的Module對象上,

Module.save = function(uri, meta) {
  var mod = Module.get(uri)

  // Do NOT override already saved modules
  if (mod.status < STATUS.SAVED) {
    mod.id = meta.id || uri
    mod.dependencies = meta.deps || []
    mod.factory = meta.factory
    mod.status = STATUS.SAVED
  }
}

由於meta.id=undefined,因此最終mod.id=uri。

       對於通過define(id,deps,function(){})設置了id的具名模塊,是根據id生成uri。在meta中,通過Module.resolve(id)完成,

Module.resolve = function(id, refUri) {
  if (!id) return ""

  id = parseAlias(id)
  id = parsePaths(id)
  id = parseVars(id)
  id = normalize(id)

  var uri = addBase(id, refUri)
  uri = parseMap(uri)

  return uri
}

通過對id的一系列設置(別名解析,路徑修正,變量解析以及添加擴展名,最終添加協議等)生成uri。

transport任務

        transport任務是打包seajs模塊的難點,上節提到了seajs模塊的id和uri之間的關系,它們是由seajs來維護的。但是如果通過grunt對seajs進行打包,則模塊之間的關系由transport來維護。通過transport生產的seajs模塊,有一個顯著的變化,即匿名模塊變為了具名模塊,並且設置了依賴模塊。

        下面通過配置項來講解transport任務:

grunt.initConfig({
        pkg: grunt.file.readJSON('package.json'),
        transport : {
            options : {
                path: ['.'],
                alias: '<%= pkg.spm.alias %>' // 注意在package.json中jquery的alias設置
            },
            utils: { // 存放引用的模塊,設定模塊名和依賴,模塊的idleading要與在application中引用的路徑一致
                options: {
                    idleading: '../dist/src/'
                },
                files: [{
                    expand: true,
                    cwd: 'lib/src',
                    src: '*',
                    filter: 'isFile',
                    dest: '.build/lib/src'
                }]
            },
            application : {
                options: {
                    idleading: '../dist/'
                },
                files:  [
                    {
                        expand: true,
                        cwd : 'lib',
                        src : 'application.js',
                        filter : 'isFile',
                        dest : '.build/lib'
                    }
                ]
            }
        }
}

       在options中,設定了路徑為‘.’,即相對於Gruntfile文件的當前路徑,alias為package.json中定義的alias;在utils任務中,設置了idleading選項,最終模塊的id = idleading + 文件名。值得注意的是idleading路徑的設置,這里需要小心設置,它是根據引用最終打包后文件的html的位置決定的。最后,將lib/src下的所有文件設置完id和依賴后放到.build/lib/src下。application任務和utils任務類似,只是單獨設置application.js文件的id和依賴。

        着重講解idleading的設置。我們計划將生成的文件(處理完依賴且合並壓縮后的文件)放到dist文件夾下面,最終通過view/hello.html引用,

圖像 1

設置transport:util任務的idleading = ‘../dist/src/’,文件經過transport之后,lib/src/name.js文件會被設置並且保存到.build/lib/src中,此時name.js的模塊名為’../dist/src/name’,依賴為[]。同理,lib/application.js保存到.build/lib中,並且模塊名為’../dist/application’,依賴為[‘./src/util’,’jquery’,’./src/test’,’./src/name’]。然后經過合並壓縮之后,生產最終的application.js文件,在view/hello.html中引用(開篇提到)。

        在hello.html引用的application文件包含了5個模塊,並且每個模塊都有id和依賴,因此根據上節具名模塊的id與uri的關系,可知道模塊id影響到文件的加載。之所以在設置idleading = “../dist/”是根據hello.html的位置決定的。在Module.resolve(id)中,有一步驟為addBase,即有當前相對路徑轉換為絕對路徑,而當前路徑是相對於html的位置定義的,具體原因是html引入了seajs,seajs判斷當前html的位置,設置為當前路徑。這也正是idleading的設置為”../dist/”的原因。

       下圖可以印證了上文所述:

圖像 2

當然如果html的路徑有變化,相應的idleading也要改變:

圖像 3

如果在view/layout/hello.html中引用文件,那么需要改變transport:util的idleading = ‘../../dist/lib/src’,transport:application的idleading = ‘../../dist/lib’,只有保證每個模塊的uri正確,才能fetch文件並且執行相應的回調函數。

        因此,對於transport任務而言,idleading的設置需要十分注意。

concat、uglify、clean任務

           這兩個任務很容易定義,而且grunt官網上就是以uglify為例講解Gruntfile的配置,因此,這兩個任務的配置我們有很多資料可以參考。我們使用通配符來匹配文件,使用expand來批量處理,也可以自定義過濾函數。廢話不多說,呈上這兩個任務的配置:

        concat : {
            options : {
                separator: '/*-------每個文件的分割-------*/',
                banner:  '/*! <%= pkg.name %> - v<%= pkg.version %> - ' +
                    '<%= grunt.template.today("yyyy-mm-dd") %> */',
                footer: '/*-------合並文件的footer-------*/'
            },
            utils: {
                src: ['.build/lib/src/*.js','!.build/lib/src/*-debug.js'],
                dest: '.build/util.js'
            },
            application : {
                src: ['.build/lib/application.js','.build/util.js'],
                dest: 'dist/application.js'
            }
        },
        uglify : {
            main : {
                files : {
                    'dist/application.min.js' : ['dist/application.js'] //對dist/application.js進行壓縮,之后存入dist/application.js文件
                }
            }
        },
        clean : {
            build : ['.build'] //清除.build文件
        }

最終的Gruntfile文件定義如下:

module.exports = function(grunt){

    grunt.initConfig({
        pkg: grunt.file.readJSON('package.json'),
        transport : {
            options : {
                alias: '<%= pkg.spm.alias %>' // 注意在package.json中jquery的alias設置
            },
            utils: { // 存放引用的模塊,設定模塊名和依賴,模塊的idleading要與在application中引用的路徑一致
                options: {
                    idleading: '../../dist/src/'
                },
                files: [{
                    expand: true,
                    cwd: 'lib/src',
                    src: '*',
                    filter: 'isFile',
                    dest: '.build/lib/src'
                }]
            },
            application : {
                options: {
                    idleading: '../../dist/'
                },
                files:  [
                    {
                        expand: true,
                        cwd : 'lib',
                        src : 'application.js',
                        filter : 'isFile',
                        dest : '.build/lib'
                    }
                ]
            }
        },
        concat : {
            options : {
                separator: '/*-------每個文件的分割-------*/',
                banner:  '/*! <%= pkg.name %> - v<%= pkg.version %> - ' +
                    '<%= grunt.template.today("yyyy-mm-dd") %> */',
                footer: '/*-------合並文件的footer-------*/'
            },
            utils: {
                src: ['.build/lib/src/*.js','!.build/lib/src/*-debug.js'],
                dest: '.build/util.js'
            },
            application : {
                src: ['.build/lib/application.js','.build/util.js'],
                dest: 'dist/application.js'
            }
        },
        uglify : {
            main : {
                files : {
                    'dist/application.min.js' : ['dist/application.js'] //對dist/application.js進行壓縮,之后存入dist/application.js文件
                }
            }
        },
        clean : {
            build : ['.build'] //清除.build文件
        }
    });

    grunt.loadNpmTasks('grunt-cmd-transport');
    grunt.loadNpmTasks('grunt-cmd-concat');
    grunt.loadNpmTasks('grunt-contrib-uglify');
    grunt.loadNpmTasks('grunt-contrib-clean');
    // 默認被執行的任務列表。
    grunt.registerTask('build',['transport','concat','uglify'])
};

總結

          通過對seajs的源碼分析可以了解模塊id與uri的關系,進而方便對grunt的transport調試。其實之所以用grunt對seajs模塊進行打包會出現各種各樣的問題,歸根結底是路徑錯誤。路徑錯誤包括很多,比如模塊依賴的路徑錯誤,模塊名錯誤,以及package.json的alias設置錯誤,最后,需要十分注意html文件的位置,因為seajs定義的cmd依賴於html文件當前位置。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM