django 与 vue 的完美结合 实现前后端的分离开发之后在整合
用django后端,前端用vue,做一个普通的简单系统,我就是一搞后端的,听到vue也是比较震惊,之前压根没接触过vue.
看了vue的一些文档,还有一些项目,先说一下django与vue的完美结合吧!
首先是创建一个django项目
django-admin startproject mysite # 创建mysite项目
django-admin startapp blog # 创建blog应用
一、接下来就是安装关于vue 的东西了
1、首先安装node.js,官网地址:https://nodejs.org/zh-cn/download/
2、使用npm淘宝镜像,避免npm下载速度过慢的问题 npm install -g cnpm --registry=https://registry.npm.taobao.org
3、使用cnpm 下载vue-cli cnmp install -g cue-cli
二、在django项目中创建vue项目
首先,进去django项目的项目目录中,执行:
vue-init webpack firstvue## firstvue为前端项目的名称,可自定义。创建的项目会跟django中的app一样的目录登记。类似一个前端app一样。
mysite 文件夹 blog 文件夹 和 firstvue文件夹 要在同一目录级别
在创建时,会弹出很多选择项,根据自己需求自定义修改。也可以全部回车,使用默认的。这里我就直接全部回车。
三、编写vue前端项目,直接编写就是,调试则可以执行。也可先不编写,跳过这一步
cd firstvue## 进入到上一部创建的firstvue项目中
cnpm install ## 安装需要的依赖模块
cnpm run dev ## 运行调式的服务,会启动一个web服务,访问localhost:8080 即可调式
四、vue项目写完后,打包vue项目,然后修改django配置,将vue集成到django中
cnpm run build ## 打包vue项目,会将所有东西打包成一个dist文件夹
五、接下来就是配置django中的setting文件了:
六、修改django的主目录的urls文件:
七、启动django服务,访问localhost:8000 则可以出现vue的首页。
python manage.py runserver
NodeJS与Django协同应用开发(1) —— 原型搭建
系列目录
- NodeJS与Django协同应用开发(0) node.js基础知识
- NodeJS与Django协同应用开发(1)原型搭建
- NodeJS与Django协同应用开发(2)业务框架
- NodeJS与Django协同应用开发(3)测试与优化
- NodeJS与Django协同应用开发(4)部署
前文我们介绍了node.js还有socket.io的基础知识,这篇文章我们来说一下如何将node.js与Django一起使用,并且搭建一个简单的原型出来。
原本我们的项目全部都基于Django框架,并且也能够满足基本需求了,但是后来新增了实时需求,在Django框架下比较难做,为了少挖点坑,多省点时间,我们选择使用node.js。
基本框架
在没有node.js之前,我们的结构是这样的:

增加的node.js系统应该是与原本的Django系统平行的,而我们使用node.js的初衷是将它作为实时需求的服务器,不承担或者只承担一小部分的业务逻辑,且完全不需要和数据库有交互。所以之后的结构就是这样的:

数据库依然只有Django负责连接,这和一般的系统并没有什么区别,所以文章里就不涉及具体读写数据库的实现了。
于是问题的关键就在于django和node.js该如何交互。
Django和node.js几乎是两种风格的网络框架,语言也不同,所以我们需要一个通信手段。而系统间通信不外乎就是靠网络请求(部署在本机的不同系统不在此列,也不值得讨论),或是另一个可以用作通信的系统。通常来说对于node.js和django之间交互的话,一般有3种手段可选:
- HTTP Request
- Redis publish/subscribe
- RPC
三种都是可行的方案,但是也有各自的应用场景。
原型实现(1) HTTP Request
首先是http request。先来看一下django代码:
[urls.py] from django.conf.urls import url urlpatterns = [ url(r'^get_data/$', 'backend.views.get_data'), ]
[backend.views.py] from django.http import JsonResponse from django.views.decorators.http import require_http_methods @require_http_methods(["GET"]) def get_data(request): data = { 'data1': 123, 'data2': 'abc', } return JsonResponse(data, safe=False)
这里我们定义了一个叫get_data
的api,方便起见我们使用JSON格式作为返回类型,返回一个整型一个字符串。
然后再来看一下node.js代码:
[django_request.js] var http = require('http'); var default_protocol = 'http://' var default_host = 'localhost'; var default_port = 8000; exports.get = function get(path, on_data_callback, on_err_callback) { var url = default_protocol + default_host + ':' + default_port + path; var req = http.get(url, function onDjangoRequestGet(res) { res.setEncoding('utf-8'); res.on('data', function onDjangoRequestGetData(data) { on_data_callback(JSON.parse(data)); }); res.resume(); }).on('error', function onDjangoRequestGetError(e) { if (on_err_callback) on_err_callback(e); else throw "error get " + url + ", " + e; }); }
[app.js] var django_request = require('./django_request'); django_request.get('/get_data/', function(data){ console.log('get_data response: %j',data); }, function(err) { console.log('error get_data: '+e); });
在django_request.js里面我们写了一个通用的get方法,可以用来向django发起http get请求。运行app.js以后我们就看到结果了。
alfred@workstation:~/Documents/node_django/nodeapp$ node app.js get_data response: {"data1":123,"data2":"abc"}
非常简单,但是别急,还有post请求。
普通的post请求和get类似,非常简单,用过http库的同学都应该会写,但是这年头已经没有普通的post了,大家的安全意识越来越高,没有哪个网站会不防跨域请求了,所以我们的post还需要解决跨域的问题。
默认配置下django的中间件是包含CsrfViewMiddleware的,也就是会在用户访问网页时向cookie中添加csrf_token。所以我们就写一个简单的页面,顺便把socket.io也使用起来。
在django的views中添加名为post_data
的api,以及为页面准备的view函数。
[backend.views.py] import json def index(request): return render_to_response('index.html', RequestContext(request, {})) def get_post_args(request, *args): try: args_info = json.loads(request.body) except Exception, e: args_info = {} return [request.POST.get(item, None) or args_info.get(item, None) for item in args] @require_http_methods(["POST"]) def post_data(request): data1, data2 = get_post_args(request, 'data1', 'data2') response = { 'status': 'success', 'data1': data1, 'data2': data2, } return JsonResponse(response, safe=False)
[urls.py] urlpatterns = [ url(r'^$', 'backend.views.index'), url(r'^get_data/$', 'backend.views.get_data'), url(r'^post_data/$', 'backend.views.post_data'), ]
socket.io监听9000端口。
[app.js] var http = require('http'); var sio = require('socket.io'); var chatroom = require('./chatroom'); var server = http.createServer(); var io = sio.listen(server, { log: true, }); chatroom.init(io); var port = 9000; server.listen(9000, function startapp() { console.log('Nodejs app listening on ' + port); });
定义通用的post方法。
[django_request.js] var cookie = require('cookie'); exports.post = function post(user_cookie, path, values, on_data_callback, on_err_callback) { var cookies = cookie.parse(user_cookie); var values = querystring.stringify(values); var options = { hostname: default_host, port: default_port, path: path, method: 'POST', headers: { 'Cookie': user_cookie, 'Content-Type': 'application/x-www-form-urlencoded', 'Content-Length': values.length, 'X-CSRFToken': cookies['csrftoken'], } }; var post_req = http.request(options, function onDjangoRequestPost(res) { res.setEncoding('utf-8'); res.on('data', function onDjangoRequestPostData(data) { on_data_callback(data); }); }).on('error', function onDjangoRequestPostError(e) { console.log(e); if (on_err_callback) on_err_callback(e); else throw "error get " + url + ", " + e; }); post_req.write(values); post_req.end(); }
为get和post事件设定handler。
[chatroom.js] var cookie_reader = require('cookie'); var django_request = require('./django_request'); function initSocketEvent(socket) { socket.on('get', function() { console.log('event: get'); django_request.get('/get_data/', function(res){ console.log('get_data response: %j',res); }, function(err) { //经指正这里应该是err而不是e,保留BUG以此为鉴 console.log('error get_data: '+e); }); }); socket.on('post', function(data) { console.log('event: post'); django_request.post(socket.handshake.headers.cookie, '/post_data/', {'data1':123, 'data2':'abc', function(res){ console.log('post_data response: %j', res); }, function(err){ console.log('error post_data: '+e); }); }); }; exports.init = function(io) { io.on('connection', function onSocketConnection(socket) { console.log('new connection'); initSocketEvent(socket); }); };
简单的html页面。
[index.html]
...
<div> <button id="btn" style="width:200px;height:150px;">hit me</button> </div> <div id="content"></div> <script type="text/javascript" src="/static/backend/js/jquery-1.9.1.min.js"></script> <script type="text/javascript" src="/static/backend/js/socket.io.min.js"></script> <script type="text/javascript"> (function() { socket = io.connect('http://localhost:9000/'); socket.on('connect', function() { console.log('connected'); }); $('#btn').click(function() { socket.emit('get'); socket.emit('post'); }); })(); </script>
实现post的重点在于cookie的设置。socket.io在客户端连接的时候默认就会带上浏览器的cookie,这帮我们省去了不少功夫,也省去了显示传递csrftoken的烦恼。但是在node.js中向django发起post请求时不能只设定X-CSRFToken,也不能只设定cookie。看一下django的源码(django.middleware.csrf)就能够了解到是同时获取cookie和HTTP_X_CSRFTOKEN的。所以我们必须把cookie传给post函数,这样才能成功发起请求。
顺便一提,这同时也解决了sessionid的问题,如果是登录用户,django是能够获取到user信息的。
以上是node.js端向django端发起请求,但是这仅仅只是由node.sj主动而已,还缺少django向node.js发起HTTP请求的部分。
所以我们在app.js中添加如下代码
[app.js] function onGetData(request, response){ if (request.method == 'GET'){ response.writeHead(200, {"Content-Type": "application/json"}); jsonobj = { 'data1': 123, 'data2': 'abc' } response.end(JSON.stringify(jsonobj)); } else { response.writeHead(403); response.end(); } } function onPostData(request, response){ if (request.method == 'POST'){ var body = ''; request.on('data', function (data) { body += data; if (body.length > 1e6) request.connection.destroy(); }); request.on('end', function () { var post = qs.parse(body); response.writeHead(200, {'Content-Type': 'application/json'}); jsonobj = { 'data1': 123, 'data2': 'abc', 'post_data': post, } response.end(JSON.stringify(jsonobj)); }); } else { response.writeHead(403); response.end(); } }
然后我们写一小段python代码来测试一下
[http_test.py] import urllib import urllib2 httpClient = None try: headers = {"Content-type": "application/x-www-form-urlencoded", "Accept": "text/plain"} data = urllib.urlencode({'post_arg1': 'def', 'post_arg2': 456}) get_request = urllib2.Request('http://localhost:9000/node_get_data/', headers=headers) get_response = urllib2.urlopen(get_request) get_plainRes = get_response.read().decode('utf-8') print(get_plainRes) post_request = urllib2.Request('http://localhost:9000/node_post_data/', data, headers) post_response = urllib2.urlopen(post_request) post_plainRes = post_response.read().decode('utf-8') print(post_plainRes) except Exception, e: print e
然后就能看到成功的输出:
[nodejs] Nodejs app listening on 9000 url: /node_get_data/, method: GET url: /node_post_data/, method: POST [python] {"data1":123,"data2":"abc"} {"data1":123,"data2":"abc","post_data":{"post_arg1":"def","post_arg2":"456"}}
到此双向的HTTP Request就建立起来了。只不过node.js端并没有csrf认证。而在我们的django端,csrf认证和api都是已经部署了的线上模块,所以不需要在这方面花精力。
然而如果最终决定采用双向HTTP Reqeust的话,那node.js端的csrf认证必须要做好,因为HTTP API都是向外暴露的,这是这种方式最大的缺点。并不是所有的系统间调用都需要向公网露接口,一旦被他人知道了一些非公开的api路径,那很有可能引发安全问题。
并且HTTP是要走外网的,这还带来了一些额外的开销。
原型实现(2) Redis Publish/Subscribe
相比HTTP Request,这种方式的代码量要少的多。(关于Redis Pub/Sub,请移步相关文档)
要实现双向通信,无非是两边同时建立pub与sub channel。而subscribe需要持续监听,关于这一点,我们先看代码再说。
首先是node.js端,npm安装redis库,库里已经包含了所有我们需要的了。
[app.js] var redis = require('redis'); // subscribe var sub = redis.createClient(); sub.subscribe('test_channel'); sub.on('message', function onSubNewMessage(channel, data) { console.log(channel, data); }); // publish var pub = redis.createClient(); pub.publish('test_channel', 'nodejs data published to test_channel');
node.js是事件驱动的异步非阻塞框架,pub/sub这种方式的实现和它本身的代码风格非常相近,所以8行代码就实现了sub与pub的功能。
再来看python代码
[redis_test.py] import redis r = redis.StrictRedis(host='localhost', port=6379) # publish r.publish('test_channel', 'python data published to test_channel'); # subscribe sub = r.pubsub() sub.subscribe('test_channel') for item in sub.listen(): if item['type'] == 'message': print(item['data'])
代码中的channel名是可以自定义的。实际应用中可以按照不同的需求管理不同的channel,这样就不会造成消息的混乱。
多看几眼代码,细心的同学会发现,python的sub代码只会执行一次,也就是说如果需要持续监听的话,至少要新开一个线程。也就是说对于django,我们还需要额外做线程间通信的工作。这种做法并不是说不可以,只是与django原本的风格不太吻合,并不是非常推荐。
(顺便一提,不要将开启线程的工作放在views函数中,因为views的执行是多线程的,线程数量会随着访问压力增大而增加,放在views中会导致重复开心线程,这个坑我爬过。)
原型实现(3) RPC
在我的另一篇文章(ZeroRPC应用)中提到过项目所使用的RPC系统。这个系统的建立是在node.js应用之前的,非常庆幸当时选用的是zerorpc,正好可以无缝接合node.js。。
类似于HTTP Request,如果要实现双向通信那就需要在两端同时建立server。
python端的代码可以看我的那篇文章里所写的内容,这边我们就来说一下node.js端的调用和建立server。
[app.js] var zerorpc = require("zerorpc"); var client = new zerorpc.Client(); client.connect("tcp://127.0.0.1:4242"); client.invoke("test_connection", "arg1", "arg2", function(error, res, more) { if (!error){ console.log(res, more); } else { console.log(error); } }); var rpcserver = new zerorpc.Server({ test_connection: function(arg1, arg2, reply) { reply(null, True, arg1, arg2); } }); rpcserver.bind("tcp://0.0.0.0:5353");
和python一样,在node.js里写zerorpc也可以返回多个值,这就是invoke的回调函数里的more参数的作用。res表示返回的第一个值,而more包含了其他的返回值。
rpc方式的概念和HTTP Request的方式一样,不过比HTTP Request好在不需要暴露API,因为完全可以在内网下部署,并把外网端口禁封。但是他们又有一个共同的缺点,那就是对于node.js来说,我们需要一个额外的消息分发机制。为什么呢?因为我们接受消息的入口是统一的。
考虑这个情况:
在node.js里我们有2个子系统,子系统A和子系统B,他们分别为功能I和功能II服务,各自也都有需要和django交互的地方。如果此时功能I和功能II分别有一条消息到来,那我们就必须要区分消息的送达对象。这里就又是额外的工作量了。
这个情况在使用redis时就不会出现。redis下我们可以只subscribe自己关心的channel,也就是说只会收到与自身系统相关的消息。
总结
对于三种方式的优缺点,我们总结如下:
实现方式 | 优点 | 缺点 |
---|---|---|
HTTP Request | 方便和现有系统集成 | 暴露外网API,流量走外网,需要额外安全工作 |
Redis | 切合node.js风格,容易按channel名管理 | django端subscribe需要额外工作量 |
RPC | 流量走内网,不暴露API | node.js端分发消息需要额外工作量 |
工作中我们可以按照实际需求来组合使用,我的项目里原本是使用HTTP Request实现的原型,后来也是因为其暴露API的缺点以及node.js端需要csrf认证才放弃用django向node.js发起HTTP请求。
目前我们项目中django向node.js发消息使用的是redis,node.js向django请求数据或发送消息使用的是rpc。这么做没有什么额外的工作量,可以让我专注于业务逻辑。
业务逻辑涉及到node.js端的架构设计。