在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 相关文章推荐
javaScript NameSpace 简单说明介绍
Jul 18 Javascript
javascript获取当前鼠标坐标的方法
Jan 10 Javascript
JavaScript合并两个数组并去除重复项的方法
Jun 13 Javascript
利用CSS3在Angular中实现动画
Jan 15 Javascript
微信开发之调起摄像头、本地展示图片、上传下载图片实例
Dec 08 Javascript
微信小程序上滑加载下拉刷新(onscrollLower)分批加载数据(二)
May 11 Javascript
Bootstrap popover 实现鼠标移入移除显示隐藏功能方法
Jan 24 Javascript
解决eclipse中没有js代码提示的问题
Oct 10 Javascript
webpack4与babel配合使es6代码可运行于低版本浏览器的方法
Oct 12 Javascript
深入浅析Vue 中 ref 的使用
Apr 29 Javascript
react quill中图片上传由默认转成base64改成上传到服务器的方法
Oct 30 Javascript
JavaScript实现英语单词题库
Dec 24 Javascript
一道常被人轻视的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
Discuz! 5.0.0论坛程序中加入一段js代码,让会员点击下载附件前自动弹出提示窗口
2007/04/18 PHP
PHP Smarty生成EXCEL文档的代码
2008/08/23 PHP
thinkphp备份数据库的方法分享
2015/01/04 PHP
PHP快速生成各种信息提示框的方法
2016/02/03 PHP
PHP实现的折半查询算法示例
2017/10/09 PHP
PHP排序算法之归并排序(Merging Sort)实例详解
2018/04/21 PHP
比较不错的JS/JQuery显示或隐藏文本的方法
2014/02/13 Javascript
js 触发select onchange事件代码
2014/03/20 Javascript
原生javascript模仿win8等待提示圆圈进度条
2014/04/24 Javascript
用循环或if语句从json中取数据示例
2014/08/18 Javascript
JQuery判断radio(单选框)是否选中和获取选中值方法总结
2015/04/15 Javascript
Ionic实现仿通讯录点击滑动及$ionicscrolldelegate使用分析
2016/01/18 Javascript
拥有一个属于自己的javascript表单验证插件
2016/03/24 Javascript
BootStrap 智能表单实战系列(十)自动完成组件的支持
2016/06/13 Javascript
微信小程序 实例应用(记账)详解
2016/09/28 Javascript
微信小程序 http请求详细介绍
2016/10/09 Javascript
JavaScript使用delete删除数组元素用法示例【数组长度不变】
2017/01/17 Javascript
jquery实现图片轮播器
2017/05/23 jQuery
vue基于element-ui的三级CheckBox复选框功能的实现代码
2018/10/15 Javascript
Vue Autocomplete 自动完成功能简单示例
2019/05/25 Javascript
js中火星坐标、百度坐标、WGS84坐标转换实现方法示例
2020/03/02 Javascript
用Python编写简单的微博爬虫
2016/03/04 Python
python+matplotlib实现礼盒柱状图实例代码
2018/01/16 Python
opencv设置采集视频分辨率方式
2019/12/10 Python
pyftplib中文乱码问题解决方案
2020/01/11 Python
Python eval函数原理及用法解析
2020/11/14 Python
使用CSS3 制作一个material-design 风格登录界面实例
2016/12/12 HTML / CSS
德国隐形眼镜店:LuckyLens
2018/07/29 全球购物
FitFlop美国官网:英国符合人体工学的鞋类品牌
2018/10/05 全球购物
泰国第一在线超市:Tops
2021/02/13 全球购物
个人主要事迹材料
2014/08/26 职场文书
学生会干部自我鉴定2014
2014/09/18 职场文书
习近平在党的群众路线教育实践活动总结大会上的讲话
2014/10/21 职场文书
综合管理员岗位职责
2015/02/11 职场文书
用JS实现飞机大战小游戏
2021/06/09 Javascript
Springboot使用Spring Data JPA实现数据库操作
2021/06/30 Java/Android