ASP.NET MVC應用require.js實踐


這里有更好的閱讀體驗和及時的更新:http://pchou.info/javascript/asp.net/2013/11/10/527f6ec41d6ad.html

Require.js是一個支持javascript模塊化編程的類庫,不了解的讀者請移步至:Javascript模塊化編程(三):require.js的用法

require在單頁面應用中能夠如魚得水,然而對於傳統的多頁面應用,使用require多少會有些困惑和不方便。

多頁面應用的一個典型的例子是https://github.com/requirejs/example-multipage,讀者可以clone下來參考。本文參考這個例子在ASP.NET MVC的結構中應用require,並且給出了壓縮腳本,實現半自動化壓縮。

將js代碼分離

一般而言ASP.NET MVC的一個路由對應一個視圖,視圖的文件結構可能如下:

Views
 |--Shared
     |--_layout.cshtml
 |--Home
     |--Index.cshtml
 |--Blog
     |--Create.cshtml
     |--Edit.cshtml
     |--Detail.cshtml
     |--Index.cshtml

這里假設_layout.cshtml是所有頁面共享的。一般情況下,我們會在_layout中引用公共的js類庫,比如jQuerybootstrap等,這樣的話其他的頁面就不需要對這些類庫再引用一遍,提高了編碼的效率。然而,不同的頁面終究會依賴不同的js,尤其是實現頁面本身功能的自定義的js,這樣我們不得不在其他頁面中再引用特殊的js,甚至將js直接寫在頁面中,例如下面的代碼經常會出現在View中:

<script type="text/javascript">
   $(function(){...});
</script>

這樣會導致頁面比較混亂,而且頁面<script>標簽中代碼不能被瀏覽器緩存,增加了頁面代碼的長度。更為重要的缺陷是,諸如jQuery之類的類庫會在加載到頁面后執行匿名函數,這需要一些時間,而如果有些頁面根本不需要jQuery的話,只要頁面把_layout作為布局頁面,那么jQuery的初始化代碼將不可避免的執行,這是一種浪費。事實上,javascript的模塊化加載的思想就是為了解決這些問題的。

接下來我們來用require規划我們的js,構建諸如下面結構的js目錄

js
|--app
    |--home.index.js
    |--blog.create.js
    |--blog.edit.js
    |--blog.detail.js
    |--blog.index.js
|--jquery.js
|--bootstrap.js
|--underscore.js
|--jquery.ui.js
|--jquery.customplugin.js
|--config.js
|--require.js

把公共的類庫級別的js模塊直接放在js目錄下,而把頁面級別的js放在一個app的子目錄下。注意,在app中,每個頁面一個js文件,這意味着我們需要把頁面各自的js提取出來,雖然這樣增加了結構復雜度,但是避免了在頁面中隨手寫<script>標簽的陋習。另外,在js目錄下的公共庫,除了第三方的庫,還包括自己開發的庫,還有一個叫config.js的文件,這個文件很關鍵,稍后會說到。

然后,我們可以刪除_layout中所有的js引用,並使用@RenderSection的命令要求子頁面提供js引用:

_layout.cshtml

<head>
...
@RenderSection("require_js_module", false)
...
</head>

這樣對js的需求就下放到每個view頁面中了,根據require的用法,我們需要在各個子View中引用require.js,並指定主模塊,而這些主模塊就是上面app目錄下的一個個js

@section require_js_module{
    <script src="@Url.Content("~/js/require.js")" data-main="@Url.Content("~/js/app/home.index.js")" ></script>
}

所有的js代碼都將寫到app下的js中,這樣規范了js,使得頁面更干凈,更為重要的是這些js還可以經過壓縮,以及被瀏覽器緩存等,進一步提高執行效率

公共的config

我們知道主模塊除了使用require方法外,經常需要通過require.config來配置其他模塊的路徑,甚至需要shim,例如下面的代碼經常會出現在主模塊的開頭:

require.config({
	paths: {
      "jquery": "lib/jquery.min",
      "underscore": "lib/underscore.min",
      "backbone": "lib/backbone.min"
    },
    shim: {
      'underscore':{
        exports: '_'
      },
      'backbone': {
        deps: ['underscore', 'jquery'],
        exports: 'Backbone'
      }
    }
  });

對於單頁面應用來說,主模塊往往只有一個,所以上面的代碼寫一遍也就OK了。但是,在多頁面的情況下,主模塊有多個,每個主模塊都要包含這樣的代碼,豈不是很不科學?於是,希望有一個統一配置的地方,但是應該如何來寫呢?我們想到,將這些配置作為一個模塊config.js,讓其他的主模塊對這個模塊產生依賴就可以了,例如下面的config.js:

config.js

requirejs.config({
	paths: {
      "jquery": "/js/jquery.min",
	    "bootstrap": "/js/bootstrap"
    },
    shim: {
	    'bootstrap': {
                deps: ['jquery'],
                exports: "jQuery.fn.popover"
            }
    }
  });

config.js的寫法沒有什么特別的,接下來只要在home.index.js中引用

home.index.js

require(['../config','jquery', 'bootstrap'], function () {
    //main module code here

});

不過這樣寫還是不對的,因為,被主模塊依賴的模塊(這里的config,jquery,bootstrap),在加載的時候,加載順序是不確定的,但是又需要config模塊在其他模塊之前加載,怎么辦呢?一個折衷的方案是修改home.index.js,成為如下代碼:

home.index.js

require(['../config'], function () {
    require(['home.index2']);
})
, define("home.index2", ['jquery', 'bootstrap'], function () {
	//main module code here
})

使用一個命名的模塊home.index2作為過渡,在主模塊中手動require,這樣可以保證config在主模塊執行之前加載,也就使得home.index2在加載的時候已經加載了config了。

壓縮

require提供一個壓縮工具,用於壓縮和合並js,詳情請移步至http://requirejs.org/docs/optimization.html。簡單的說,require提供一個叫r.js的文件,通過本地的node程序(Node.js),執行這個r.js並傳入一些參數,即可自動分析模塊互相之間的依賴,以達到合並和壓縮的目的。同樣的,這對於單頁面應用來說是容易的,因為主模塊只有一個,但是對於多頁面又如何做呢?好在這個壓縮工具支持用一個配置文件來指導壓縮,這樣的話,我們可以編寫下面的配置腳本:

build.js

var build = {
    appDir: '../js',
    baseUrl: '.',
    dir: '../js-built',
    modules: [
        //First set up the common build layer.
        {
            //module names are relative to baseUrl
            name: 'config',
            //List common dependencies here. Only need to list
            //top level dependencies, "include" will find
            //nested dependencies.
            include: ["bootstrap", "config","jquery"]
        },
	//Now set up a build layer for each page, but exclude
        //the common one. "exclude" will exclude nested
        //the nested, built dependencies from "common". Any
        //"exclude" that includes built modules should be
        //listed before the build layer that wants to exclude it.
        //"include" the appropriate "app/main*" module since by default
        //it will not get added to the build since it is loaded by a nested
        //require in the page*.js files.
	{
	    name:"app/home.index",
	    exclude:["config"]
	},
	{
	    name:"app/blog.create",
	    exclude:["config"]
	},
	...
    ]

}

通過這個命令來執行壓縮,壓縮的結果將被保存到js-build目錄:

node.exe r.js -o build.js

build.js腳本實際上是一個js對象,我們將config加入公共模塊,而在各個主模塊中將其排除。這樣,所有的公共庫包括config將壓縮成一個js,而主模塊又不會包含多余的config。這樣可想而知,每個頁面在加載時最多只會下載兩個js,而且公共模塊的代碼會“按需執行”。

執行上面的腳本壓縮,需要安裝有node。可以在從這里下載

自動腳本

但是,隨着主模塊的增加,需要隨時跟蹤和修改這個build文件,這也是很麻煩的。於是,筆者基於node.js開發了一個叫build-build.js的腳本,用來根據目錄結構自動生成build.js:

build-build.js

fs = require('fs');
var target_build = process.argv[2];
//console.log(__filename);
var pwd = __dirname;
var js_path = pwd.substring(0,pwd.lastIndexOf('\\')) + '\\js';
console.log('js path : ' + js_path);
var app_path = js_path + '\\app';
console.log('js app path : ' +app_path);

var app_modules = [];
var global_modules = [];

//build json object
var build = {
	appDir: '../js',
    baseUrl: '.',
    dir: '../js-built',
    modules: [
        //First set up the common build layer.
        {
            //module names are relative to baseUrl
            name: 'config',
            //List common dependencies here. Only need to list
            //top level dependencies, "include" will find
            //nested dependencies.
            include: []
        }
    ]
}

fs.readdir(app_path,function (err,files) {
	// body...
	if (err) throw err;
	for(var i in files){
		//put module in app_modules
		var dotindex = files[i].lastIndexOf('.');
		if(dotindex >= 0){
			var extension = files[i].substring(dotindex+1,files[i].length);
			if(extension == 'js'){
				app_modules.push({
					name: 'app/' + files[i].substring(0,dotindex),
            		exclude: ['config']
				});
			}
		}
	}

	for(var j in app_modules){
		build.modules.push(app_modules[j]);
	}
	
	fs.readdir(js_path,function (err,files){
		if (err) throw err;
		for(var i in files){
			//put module in app_modules
			var dotindex = files[i].lastIndexOf('.');
			if(dotindex >= 0){
				var extension = files[i].substring(dotindex+1,files[i].length);
				if(extension == 'js'){
					global_modules.push(files[i].substring(0,dotindex));
				}
			}	
		}

		build.modules[0].include = global_modules;
		//console.log(build);
		var t = pwd + '\\' + target_build;
		console.log(t);
		var fd = fs.openSync(t, 'w');
		fs.closeSync(fd);
		var json = JSON.stringify(build);
		fs.writeFileSync(t, json);
	});
});

這里的代碼並不復雜,主要是遍歷目錄,生成對象,最后將對象序列化為build.js。讀者可以自行閱讀並修改。最后,編寫一個bat,完成一鍵壓縮功能:

build.bat

@echo off
set PWD=%~p0
set PWD=%PWD:\=/%
cd "D:\node"
node.exe %PWD%build-build.js build.js
node.exe %PWD%r.js -o %PWD%build.js
cd %~dp0

這樣,我們就簡單實現了一個方便的多頁面require方案,最后項目目錄可能是這樣的:

Views
 |--Shared
     |--_layout.cshtml
 |--Home
     |--Index.cshtml
 |--Blog
     |--Create.cshtml
     |--Edit.cshtml
     |--Detail.cshtml
     |--Index.cshtml

build
|--build.js
|--r.js
|--build-build.js
|--build.bat

js
|--app
    |--home.index.js
    |--blog.create.js
    |--blog.edit.js
    |--blog.detail.js
    |--blog.index.js
|--jquery.js
|--bootstrap.js
|--underscore.js
|--jquery.ui.js
|--jquery.customplugin.js
|--config.js
|--require.js

可以從這里fork示例程序


免責聲明!

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



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