发布于 2016-03-05 11:27:22 | 168 次阅读 | 评论: 0 | 来源: 分享
Grunt JavaScript构建工具
Grunt 是一个基于任务的JavaScript工程命令行构建工具
前后端分离这个词一点都不新鲜,完全的前后端分离在岗位协作方面,前端不写任何后台,后台不写任何页面,双方通过接口传递数据完成软件的各个功能实现。此种情况下,前后端的项目都独立开发和独立部署,在开发期间有2个问题不可避免:第一是前端调用后台接口时的跨域问题(因为前后端分开部署);第二是前端脱离后台服务后无法独立运行。本文总结最近一个项目的工作经验,介绍利用grunt-contrib-connect和grunt-connect-proxy搭建前后端分离的开发环境的实践过程,希望能对你有所帮助。
注:
(1)本文的相关内容需对前端构建工具grunt有所了解:
分别代表前后端独立运行的两个项目,client表示前端,server表示服务端。在实际运行client和server里面的服务之前,请确保已经安装好了grunt-cli,如果没有安装,请按照grunt的文档先安装好grunt-cli这个npm的包。如果你已经安装好了grunt-cli,那么进入到client或者server文件夹下,就能直接使用grunt的命令来启动服务了,不需要再运行npm install 来安装依赖了,因为client和server文件夹下已经包含进了下载好的依赖。在实际的前后端项目中,server端可以是任何架构类型的项目,java web ,php, asp.net等等都可以,demo里面为了简单模拟一个后台服务,于是就利用grunt启动一个静态服务来充当server端,不过它实际上的作用跟java web等传统后端项目是一样的。
为了看到请求被代理转发的效果,请先在server文件夹下启动服务,命令是:grunt staticServer:
只要看到跟截图运行类似的结果,就表示server端的服务启动成功。从截图中还能看到server端的服务的访问地址是:
只要看到跟截图运行类似的结果,就表示client端的服务启动成功。从截图中能看到client端服务的访问地址是:
这段运行结果说明,client端里面以/provider开头的请求都会被代理转发,并且会被代理到
在client的首页里面,我发起了一个ajax请求,请求地址为
如果不通过代理,在localhost:9001/的服务里,请求localhost:9002/的数据是肯定会有跨域问题的,而代理可以完美的解决这个问题。
前面这一小部分演示了demo里面如何通过代理来解决跨域问题,下面一部分演示如何在脱离后端服务的情况下如何正常运行前端项目,首先请关闭之前打开的client服务和server端服务以及浏览器打开的client页面,然后打开client/Gruntfile.js文件,找到以下部分:
把provider改成api,把false改成true;
接着在client文件夹,运行非代理的静态服务,这个服务不会配置代理,启动命令是:grunt staticServer:
打开浏览器的开发者工具,在控制台应该可以看到如下消息:
这个过程是:原来通过代理请求地址是:
也就是说我会把跟服务端所有接口的返回的数据都按相同的路径,在本地以json文件的形式存在api文件夹下,在没有代理的时候,只要请求这些json文件,就能保证我所有的操作都能正确请求到数据,前端的项目也就能脱离代理运行起来了,当然这个模式下的数据都是静态的了。
接下来我会介绍如何前面这些内容的实现细节,只介绍client里面的要点,server里面的内容很简单,只要搞清楚了client,server一看就懂:)
在了解配置之前,先要熟悉项目的文件夹结构:
仅仅是为了完成demo,所以项目的文件夹结构和Grunt配置都做了最大程度的简化,目的就是为了方便理解,本文提供的不是一个解决方案,而是一个思路,在你有需要的时候可以参考改进应用到自己的项目当中,在前端工程化这一块,要用到的插件比demo里面要用到的多的多,你得按需配置。就demo而言,最核心的插件当然是grunt-contrib-connect和grunt-connect-proxy,但是要完成demo,也离不开一些其它的插件:
load-grunt-tasks:我用它一次性加载package.json里面的所有插件:
grunt-contrib-copy:我用它来复制src里面的内容,粘贴到dist目录下:
只要运行grunt copy任务,就会看到项目结构了多了一个dist文件夹:
grunt-contrib-watch: 我用它监听文件的改变,并自动执行定义的grunt任务,同时还可以通过livereload自动刷新浏览器页面:
grunt-replace:我用它来替换文件中某些特殊字符串,这样就能够在不手动更改源代码的情况下改变代码。非代理模式之所以能请求到本地的静态json数据,并不是因为我手动改变了请求地址,而是改变了请求地址处理函数中的处理规则,这个规则的改变实际上就是通过grunt-replace来做的:
替换的规则通过getReplaceOptions这个函数来配置:
注意注释部分的说明,所谓的本地模式,其实就是运行grunt staticServer的时候,代理模式就是运行grunt proxyServer的时候,这段注释要求在运行grunt staticServer之前必须先把API_NAME改成api,把DEVELOP_MODE改成true,只有这样那些需要代理的请求才会请求本地的json文件,在运行grunt proxyServer之前必须先把API_NAME改成provider,把DEVELOP_MODE改成false,只有这样才能正确地将需要代理的请求进行转发。
在grunt任务配置中,通常每个插件都会配置成一个任务,但是grunt-connect-proxy不是这样,它是与grunt-contrib-connect一起配置的:
connect: {
options: {
port: '9001',
hostname: 'localhost',
protocol: 'http',
open: true,
base: {
path: './',
options: {
index: 'html/index.html'
}
},
livereload: true
},
proxies: [
{
context: '/' + API_NAME,
host: 'localhost',
port: '9002',
https: false,
changeOrigin: true,
rewrite: proxyRewrite
}
],
default: {},
proxy: {
options: {
middleware: function (connect, options) {
if (!Array.isArray(options.base)) {
options.base = [options.base];
}
// Setup the proxy
var middlewares = [require('grunt-connect-proxy/lib/utils').proxyRequest];
// Serve static files.
options.base.forEach(function (base) {
middlewares.push(serveStatic(base.path, base.options));
});
// Make directory browse-able.
/*var directory = options.directory || options.base[options.base.length - 1];
middlewares.push(connect.directory(directory));
*/
return middlewares;
}
}
}
}
在以上配置中:
options节是通用的配置,用来配置要启动的静态服务器信息,port表示端口,hostname表示主机地址,protocol表示协议比如http,https,open表示静态服务启动之后是否以默认浏览器打开首页base.options.index指定的页面,base.path用来配置站点的根目录,demo中把根目录配置成了当前的项目文件夹(./);
以上配置都在配置grunt-contrib-connect任务里面,但是上面配置中的proxies节其实是grunt-connect-proxy需要的,用来配置代理信息:context配置需要被代理的请求前缀,通常配置成/开头的一段字符串,比如/provider,这样相对站点根目录的并以provider开头的请求都会被代理到;host,port,https用来配置要代理到的服务地址,端口以及所使用的协议;changeOrigin配置成true即可;rewrite用来配置代理规则,proxyRewrite这个变量在配置文件的前面有定义:
意思就是把client端里provider开头的部分,替换成代理服务的/provider/目录来处理,注意/provider/这个字符串最后的斜杠不能省略!比如client里有一个请求
这个是grunt启动静态服务必须的,照着用就行了。
最后看下静态服务和代理服务的相关任务定义:
grunt.registerTask('staticServer', '启动静态服务......', function () {
grunt.task.run([
'copy',
'replace',
'connect:default',
'watch'
]);
});
grunt.registerTask('proxyServer', '启动代理服务......', function () {
grunt.task.run([
'copy',
'replace',
'configureProxies:proxy',
'connect:proxy',
'watch'
]);
});
在配置代理服务的时候,’configureProxies:proxy’一定要加,而且要加在connect:proxy之前,否则代理配置还没有注册成功,静态服务就启动完毕了,configureProxies这个任务并不是在配置文件中配置的,而是grunt-connect-proxy插件里面定义的,只要grunt-connect-proxy被加载进来,这个任务就能用。
这部分看看如何发送请求,打开首页,会看到底部引用了4个js文件:
其中util.js封装了处理请求地址的功能:
var DEVELOP_MODE = '@@DEVELOP_MODE';
var Util = (function(){
var BASE_URL = location.protocol + '//' + location.hostname +
(location.port == '' ? '' : (':' + location.port)) + '/' + '@@CONTEXT_PATH';
return {
api: function (requestPath) {
var pathParts = requestPath.split('?');
pathParts[0] = pathParts[0] + (DEVELOP_MODE == 'true' ? '.json' : '');
return BASE_URL + '@@API_NAME/' + pathParts.join('?');
}
}
})();
这是源代码,还记得那个replace的任务吗,它的替换规则是
replace任务会把文件中以@@开头,按照patterns里面的配置,将匹配到的字符串替换成对应的串。在本地模式下,API_NAME是api,DEVELOP_MODE是true,CONTEXT_PATH始终是空,经过replace任务处理之后,util.js的代码会变成:
var DEVELOP_MODE = 'true';
var Util = (function(){
var BASE_URL = location.protocol + '//' + location.hostname +
(location.port == '' ? '' : (':' + location.port)) + '/' + '';
return {
api: function (requestPath) {
var pathParts = requestPath.split('?');
pathParts[0] = pathParts[0] + (DEVELOP_MODE == 'true' ? '.json' : '');
return BASE_URL + 'api/' + pathParts.join('?');
}
}
})();
在代理模式下,API_NAME是provider,DEVELOP_MODE是false,util.js经过replace之后就会变成:
var DEVELOP_MODE = 'false';
var Util = (function(){
var BASE_URL = location.protocol + '//' + location.hostname +
(location.port == '' ? '' : (':' + location.port)) + '/' + '';
return {
api: function (requestPath) {
var pathParts = requestPath.split('?');
pathParts[0] = pathParts[0] + (DEVELOP_MODE == 'true' ? '.json' : '');
return BASE_URL + 'provider/' + pathParts.join('?');
}
}
})();
这样同一个请求地址,比如query/json/language/list,经过Util.api处理之后:
Util.api(‘query/json/language/list’)
在本地模式下就会返回:
第二是,grunt-contrib-connect所启动的静态服务,只能发送get请求,不能发送post请求,所以如果在代码中有写$.post的调用就无法脱离后端服务运行起来,会报405 Method not Allowed的错误,而这个封装可以把Ajax.post这样的请求,在本地模式的时候全部替换成get方式来处理:
这其实还是replace任务的功劳!
index.js就是首页发请求的js了,可以看看:
Ajax.get(Util.api('query/json/language/list')).done(function(response){
console.log(response.data);
}).fail(function(){
console.log('请求失败');
});
结合util.js和ajax.js,相信你很快就能明白这个过程了。
答案还是代理。开发期间,前端通过grunt-connect-proxy把某个命名空间下的请求全部代理到了后端服务来处理,线上部署的时候后端把项目部署到tomcat这种web服务器里,前端把项目部署到Nginx服务器,然后请运维人员按照开发期间的代理规则,在Nginx服务器上加反向代理的配置,把浏览器请求前端的那些需要后端支持的请求,全部代理到tomcat服务器下的后端服务来处理。也就是说线上部署跟开发期间的交互原理是一样的,只不过代理的提供者变成Nginx而已。
本文总结自己这段时间做一个前后端分离的项目的一些环境准备方面的经验,文中提到的方法帮助我们解决了跨域和前端独立运行的两大问题,现在项目开发的情况非常顺利,所以从我自身的实践来说,本文的内容是比较有参考价值的,希望能够帮助到有需要的人,谢谢阅读:)