在ASP.NET MVC项目中使用RequireJS库的用法示例


Posted in Javascript onFebruary 15, 2016

RequireJS 是一个前端模块化开发的流行工具,本身是一个Javascript的库文件,即require.js 。
RequireJs的主要功能:

(1)实现js文件的异步加载,避免网页失去响应;

(2)管理模块之间的依赖性,便于代码的编写和维护。

前端模块化开发现在有好多的工具,大体上分为两类,一类是像dojo之类的高大全,dojo v1.8之后已经内置了模块化开发组件;另一类是像require.js,sea.js 这种专心做模块化开发的工具。

从模块化划分的规则来区分,主要分为AMD、CMD两类,dojo、require.js 遵从前者,而sea.js 依循CMD规范。

require在单页面应用中能够如鱼得水,然而对于传统的多页面应用,使用require多少会有些困惑和不方便。

本文讲解如何在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类库,比如jQuery,bootstrap等,这样的话其他的页面就不需要对这些类库再引用一遍,提高了编码的效率。然而,不同的页面终究会依赖不同的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:
requirejs.config({
 paths: {
 "jquery": "/js/jquery.min",
 "bootstrap": "/js/bootstrap"
 },
 shim: {
 'bootstrap': {
  deps: ['jquery'],
  exports: "jQuery.fn.popover"
 }
 }
});

config.js的写法没有什么特别的,接下来只要在home.index.js中引用

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

});

不过这样写还是不对的,因为,被主模块依赖的模块(这里的config,jquery,bootstrap),在加载的时候,加载顺序是不确定的,但是又需要config模块在其他模块之前加载,怎么办呢?一个折衷的方案是修改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',
 mainConfigFile: '../js/config.js',
 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:

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

Javascript 相关文章推荐
利用ASP发送和接收XML数据的处理方法与代码
Nov 13 Javascript
修改jquery里的dialog对话框插件为框架页(iframe) 的方法
Sep 14 Javascript
jQuery EasyUI API 中文文档 - Tabs标签页/选项卡
Oct 01 Javascript
公共js在页面底部加载的注意事项介绍
Jul 18 Javascript
JavaScript在for循环中绑定事件解决事件参数不同的情况
Jan 20 Javascript
如何判断Javascript对象是否存在的简单实例
May 18 Javascript
学习vue.js计算属性
Dec 03 Javascript
如何清除IE10+ input X 文本框的叉叉和密码输入框的眼睛图标
Dec 21 Javascript
JS基于正则截取替换特定字符之间字符串操作示例
Feb 03 Javascript
Node.js 多线程完全指南总结
Mar 27 Javascript
layUI实现三级导航菜单效果
Jul 26 Javascript
jquery插件实现搜索历史
Apr 24 jQuery
一道常被人轻视的web前端常见面试题(JS)
Feb 15 #Javascript
获取阴历(农历)和当前日期的js代码
Feb 15 #Javascript
极易被忽视的javascript面试题七问七答
Feb 15 #Javascript
在JavaScript中使用JSON数据
Feb 15 #Javascript
三分钟带你玩转jQuery.noConflict()
Feb 15 #Javascript
轻松搞定jQuery.noConflict()
Feb 15 #Javascript
JavaScript的Backbone.js框架的一些使用建议整理
Feb 14 #Javascript
You might like
PHP警告Cannot use a scalar value as an array的解决方法
2012/01/11 PHP
PHP下用Swoole实现Actor并发模型的方法
2019/06/12 PHP
Javascript 键盘keyCode键码值表
2009/12/24 Javascript
JS实现简单的顶部定时关闭层效果
2014/06/15 Javascript
JavaScript的arguments对象应用示例
2014/09/15 Javascript
JQuery勾选指定name的复选框集合并显示的方法
2015/05/18 Javascript
javascript自动恢复文本框点击清除后的默认文本
2016/01/12 Javascript
JS中使用正则表达式g模式和非g模式的区别
2017/04/01 Javascript
JS判断两个对象内容是否相等的方法示例
2017/04/10 Javascript
详解利用 Vue.js 实现前后端分离的RBAC角色权限管理
2017/09/15 Javascript
vue-cli 3.x 修改dist路径的方法
2018/09/19 Javascript
微信小程序列表时间戳转换实现过程解析
2019/10/12 Javascript
vue中的.$mount('#app')手动挂载操作
2020/09/02 Javascript
Javascript实现打鼓效果
2021/01/29 Javascript
python解析xml文件实例分享
2013/12/04 Python
python处理文本文件实现生成指定格式文件的方法
2014/07/31 Python
朴素贝叶斯算法的python实现方法
2014/11/18 Python
Python中应该使用%还是format来格式化字符串
2018/09/25 Python
python paramiko利用sftp上传目录到远程的实例
2019/01/03 Python
Python基础学习之基本数据结构详解【数字、字符串、列表、元组、集合、字典】
2019/06/18 Python
Pytorch加载部分预训练模型的参数实例
2019/08/18 Python
python @classmethod 的使用场合详解
2019/08/23 Python
Python 中的 import 机制之实现远程导入模块
2019/10/29 Python
基于Django实现日志记录报错信息
2019/12/17 Python
TensorFlow实现自定义Op方式
2020/02/04 Python
Python类和实例的属性机制原理详解
2020/03/21 Python
快速解决Django关闭Debug模式无法加载media图片与static静态文件
2020/04/07 Python
Python GUI库Tkiner使用方法代码示例
2020/11/27 Python
详解使用scrapy进行模拟登陆三种方式
2021/02/21 Python
土木工程应届生自荐信
2013/09/24 职场文书
酒店员工检讨书
2014/02/18 职场文书
《小山羊和小灰兔》教学反思
2014/02/19 职场文书
《日月潭》教学反思
2014/02/28 职场文书
大学毕业生个人总结
2015/02/28 职场文书
2016年党员创先争优公开承诺书
2016/03/25 职场文书
如何用六步教会你使用python爬虫爬取数据
2022/04/06 Python