django 與 vue 的完美結合 以及NodeJS與Django協同應用開發


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) —— 原型搭建

 


系列目錄


前文我們介紹了node.js還有socket.io的基礎知識,這篇文章我們來說一下如何將node.js與Django一起使用,並且搭建一個簡單的原型出來。

原本我們的項目全部都基於Django框架,並且也能夠滿足基本需求了,但是后來新增了實時需求,在Django框架下比較難做,為了少挖點坑,多省點時間,我們選擇使用node.js。

基本框架

在沒有node.js之前,我們的結構是這樣的:

初始結構.png

增加的node.js系統應該是與原本的Django系統平行的,而我們使用node.js的初衷是將它作為實時需求的服務器,不承擔或者只承擔一小部分的業務邏輯,且完全不需要和數據庫有交互。所以之后的結構就是這樣的:

nodejs+django結構.png

數據庫依然只有Django負責連接,這和一般的系統並沒有什么區別,所以文章里就不涉及具體讀寫數據庫的實現了。
於是問題的關鍵就在於django和node.js該如何交互。
Django和node.js幾乎是兩種風格的網絡框架,語言也不同,所以我們需要一個通信手段。而系統間通信不外乎就是靠網絡請求(部署在本機的不同系統不在此列,也不值得討論),或是另一個可以用作通信的系統。通常來說對於node.js和django之間交互的話,一般有3種手段可選:

  1. HTTP Request
  2. Redis publish/subscribe
  3. 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端的架構設計。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM