利用grunt-contrib-connect和grunt-connect-proxy搭建先后端分离的开发环境

先后端分离这个词一点都不新鲜,彻底的先后端分离在岗位协做方面,前端不写任何后台,后台不写任何页面,双方经过接口传递数据完成软件的各个功能实现。此种状况下,先后端的项目都独立开发和独立部署,在开发期间有2个问题不可避免:第一是前端调用后台接口时的跨域问题(由于先后端分开部署);第二是前端脱离后台服务后没法独立运行。本文总结最近一个项目的工做经验,介绍利用grunt-contrib-connect和grunt-connect-proxy搭建先后端分离的开发环境的实践过程,但愿能对你有所帮助。php

注:css

(1)本文的相关内容需对前端构建工具grunt有所了解:http://www.gruntjs.net/getting-started,这个工具能够完成前端全部的工程化工做,包括代码和图片压缩,文件合并,静态资源替换,js混淆,less和sass编译成css等等,推荐没有用过相似工具的前端开发人员去了解。html

(2)grunt-contrib-connect和grunt-connect-proxy是grunt提供的两个插件,前者能够启动一个基于nodejs的静态服务器,这样前端就能脱离后端经过web服务的方式来访问本身开发的东西;后者能够把前端项目里面某些特殊的请求代理到其它服务器,哪些请求可以经过代理转发到别的服务器,这个规则都是可配置的,这样就能把一些跟后台交互的请求经过代理的方式,在开发期间,转发到后端的服务来处理,从而避免跨域问题。前端

代码下载java

1. 效果演示

在前面提供的代码中,里面有两个文件夹:node

image

分别表明先后端独立运行的两个项目,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等传统后端项目是同样的。jquery

为了看到请求被代理转发的效果,请先在server文件夹下启动服务,命令是:grunt staticServer:web

image

只要看到跟截图运行相似的结果,就表示server端的服务启动成功。从截图中还能看到server端的服务的访问地址是:http://localhost:9002/ajax

而后在client文件夹下启动配置了代理的服务,命令是:grunt proxyServer:npm

image

只要看到跟截图运行相似的结果,就表示client端的服务启动成功。从截图中能看到client端服务的访问地址是:http://localhost:9001/,同时还能够看到服务代理的配置:

image

这段运行结果说明,client端里面以/provider开头的请求都会被代理转发,而且会被代理到http://localhost:9002/provider 来处理。举例来讲,假如在client端里面发起一个请求,这个请求的URL是:http://localhost:9001/provider/query/json/language/list,那么最终处理这个请求的服务地址其实是:http://localhost:9002/provider/query/json/language/list

client端启动以后,应该会自动打开浏览器,访问http://localhost:9001/,显示的是client端的首页。打开首页以后,按F12打开开发者工具,若是在控制台看到以下相似的消息,就说明首页里的请求正确地经过代理请求到了服务端的数据:

image

在client的首页里面,我发起了一个ajax请求,请求地址为http://localhost:9001/provider/query/json/language/list,在client文件夹下根本不存在provider文件夹,因此若是没有代理的话,这个请求确定会报404的错误;它之因此可以正确的加载,彻底是由于经过代理,请求到了server文件夹下相应的文件:

image

若是不经过代理,在localhost:9001/的服务里,请求localhost:9002/的数据是确定会有跨域问题的,而代理能够完美的解决这个问题。

前面这一小部分演示了demo里面如何经过代理来解决跨域问题,下面一部分演示如何在脱离后端服务的状况下如何正常运行前端项目,首先请关闭以前打开的client服务和server端服务以及浏览器打开的client页面,而后打开client/Gruntfile.js文件,找到如下部分:

image

把provider改为api,把false改为true;

接着在client文件夹,运行非代理的静态服务,这个服务不会配置代理,启动命令是:grunt staticServer:

image

打开浏览器的开发者工具,在控制台应该能够看到以下消息:

image

这个过程是:原来经过代理请求地址是:http://localhost:9001/provider/query/json/language/list,在没有代理的时候,我会把http://localhost:9001/provider/query/json/language/list这个请求改为请求http://localhost:9001/api/query/json/language/list.json ,而在我client文件夹下存在这个json文件:

image

也就是说我会把跟服务端全部接口的返回的数据都按相同的路径,在本地以json文件的形式存在api文件夹下,在没有代理的时候,只要请求这些json文件,就能保证我全部的操做都能正确请求到数据,前端的项目也就能脱离代理运行起来了,固然这个模式下的数据都是静态的了。

接下来我会介绍如何前面这些内容的实现细节,只介绍client里面的要点,server里面的内容很简单,只要搞清楚了client,server一看就懂:)

2. Grunt配置

在了解配置以前,先要熟悉项目的文件夹结构:

image

仅仅是为了完成demo,因此项目的文件夹结构和Grunt配置都作了最大程度的简化,目的就是为了方便理解,本文提供的不是一个解决方案,而是一个思路,在你有须要的时候能够参考改进应用到本身的项目当中,在前端工程化这一块,要用到的插件比demo里面要用到的多的多,你得按需配置。就demo而言,最核心的插件固然是grunt-contrib-connect和grunt-connect-proxy,可是要完成demo,也离不开一些其它的插件:

load-grunt-tasks:我用它一次性加载package.json里面的全部插件:

image

grunt-contrib-copy:我用它来复制src里面的内容,粘贴到dist目录下:

image

只要运行grunt copy任务,就会看到项目结构了多了一个dist文件夹:

image

grunt-contrib-watch: 我用它监听文件的改变,并自动执行定义的grunt任务,同时还能够经过livereload自动刷新浏览器页面:

image

grunt-replace:我用它来替换文件中某些特殊字符串,这样就可以在不手动更改源代码的状况下改变代码。非代理模式之因此能请求到本地的静态json数据,并非由于我手动改变了请求地址,而是改变了请求地址处理函数中的处理规则,这个规则的改变实际上就是经过grunt-replace来作的:

image

替换的规则经过getReplaceOptions这个函数来配置:

image

注意注释部分的说明,所谓的本地模式,其实就是运行grunt staticServer的时候,代理模式就是运行grunt proxyServer的时候,这段注释要求在运行grunt staticServer以前必须先把API_NAME改为api,把DEVELOP_MODE改为true,只有这样那些须要代理的请求才会请求本地的json文件,在运行grunt proxyServer以前必须先把API_NAME改为provider,把DEVELOP_MODE改为false,只有这样才能正确地将须要代理的请求进行转发。

3. 重点:grunt-contrib-connect和grunt-connect-proxy的配置

在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这个变量在配置文件的前面有定义:

image

意思就是把client端里provider开头的部分,替换成代理服务的/provider/目录来处理,注意/provider/这个字符串最后的斜杠不能省略!好比client里有一个请求http://localhost:9001/provider/query/json/language/list,就会被代理到http://localhost:9002/provider/query/json/language/list来处理;

default是一个connect任务的目标,用它启动静态服务;

proxy也是一个connect任务的目标,用它启动代理服务,因为在demo里,watch任务和connect任务都启用了livereload,因此要在proxy任务里加上一个middleware中间件的配置,才能保证正确启动代理,这段代码是官网的提供的,直接使用便可。里面有一个serveStatic模块,在配置文件的前面已经引入过:

image

这个是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. 如何发送请求

这部分看看如何发送请求,打开首页,会看到底部引用了4个js文件:

image

其中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的任务吗,它的替换规则是

image

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')

在本地模式下就会返回:http://localhost:9001/api/query/json/language/list.json

在代理模式下返回:http://localhost:9001/provider/query/json/language/list

ajax.js对jquery的ajax进行了一下包装:

var Ajax = (function(){
    function create(_url, _method, _data, _async, _dataType) {
        //添加随机数
        if (_url.indexOf('?') > -1) {
            _url = _url + '&rnd=' + Math.random();
        } else {
            _url = _url + '?rnd=' + Math.random();
        }

        //为请求添加ajax标识,方便后台区分ajax和非ajax请求
        _url += '&_ajax=true';

        return $.ajax({
            url: _url,
            dataType: _dataType,
            async: _async,
            method: (DEVELOP_MODE == 'true' ? 'get' : _method),
            data: _data
        });
    }

    var ajax = {},
        methods = [
            {
                name: 'html',
                method: 'get',
                async: true,
                dataType: 'html'
            },
            {
                name: 'get',
                method: 'get',
                async: true,
                dataType: 'json'
            },
            {
                name: 'post',
                method: 'post',
                async: true,
                dataType: 'json'
            },
            {
                name: 'syncGet',
                method: 'get',
                async: false,
                dataType: 'json'
            },
            {
                name: 'syncPost',
                method: 'post',
                async: false,
                dataType: 'json'
            }
        ];

    for(var i = 0, l = methods.length; i < l; i++) {
        ajax[methods[i].name] = (function(i){
            return function(){
                var _url = arguments[0],
                    _data = arguments[1],
                    _dataType = arguments[2] || methods[i].dataType;

                return create(_url, methods[i].method, _data, methods[i].async, _dataType);
            }
        })(i);
    }

    //window.Ajax = ajax;
    return ajax;
})();

提供了Ajax.get,Ajax.post,Ajax.syncGet,Ajax.syncPost以及Ajax.html这五个方法,之因此要封装成这样缘由有2个:

第一是,统一加上随机数和ajax请求的标识:

image

第二是,grunt-contrib-connect所启动的静态服务,只能发送get请求,不能发送post请求,因此若是在代码中有写$.post的调用就没法脱离后端服务运行起来,会报405 Method not Allowed的错误,而这个封装能够把Ajax.post这样的请求,在本地模式的时候所有替换成get方式来处理:

image

这其实仍是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,相信你很快就能明白这个过程了。

5. 线上如何部署先后端的服务

答案仍是代理。开发期间,前端经过grunt-connect-proxy把某个命名空间下的请求所有代理到了后端服务来处理,线上部署的时候后端把项目部署到tomcat这种web服务器里,前端把项目部署到Nginx服务器,而后请运维人员按照开发期间的代理规则,在Nginx服务器上加反向代理的配置,把浏览器请求前端的那些须要后端支持的请求,所有代理到tomcat服务器下的后端服务来处理。也就是说线上部署跟开发期间的交互原理是同样的,只不过代理的提供者变成Nginx而已。

6. 小结

本文总结本身这段时间作一个先后端分离的项目的一些环境准备方面的经验,文中提到的方法帮助咱们解决了跨域和前端独立运行的两大问题,如今项目开发的状况很是顺利,因此从我自身的实践来讲,本文的内容是比较有参考价值的,但愿可以帮助到有须要的人,谢谢阅读:)

代码下载

相关文章
相关标签/搜索