Django 做爲 Python 社區最受歡迎的 Web 框架之一,憑藉其高度抽象的組件和強大方便的腳手架,將快速且流暢的開發體驗演繹到了極致。而 Nuxt 做爲從 Vue.js 進化而來的前端框架,可以輕鬆勝任複雜的 SPA(單頁應用)開發。二者相遇,可以擦出怎樣的火花?這篇教程將用 Django + Nuxt 實現帶有完整的增刪改查(CRUD)功能的全棧應用。最後鄭重警告:不要在深夜閱讀此教程!!!javascript
本文所涉及的源代碼都放在了 Github 上,若是您以爲咱們寫得還不錯,但願您能給❤️這篇文章點贊+Github倉庫加星❤️哦~ 本文代碼改編自 Scotch。css
在這一系列教程中,咱們將會實現一個全棧美食分享網站,後端用 Django 實現,前端則是 Nuxt 框架,下面是最終完成後的項目效果:html
本教程假定你已經知道了前端
學完這篇教程後,你將:vue
首先建立項目目錄,並進入:java
$ mkdir recipes_app && cd recipes_app
複製代碼
在這個項目中,咱們用 pipenv 來管理 Python 項目的環境依賴。Pipenv 是 Python 社區偶像級大師 Kenneth Reitz 牽頭開發的開發流程優化工具,立志集全部項目管理工具(Node 的 npm、Ruby 的 bundler、PHP 的 composer 等等)的優點爲一體。咱們經過下面的命令安裝 pipenv,並建立項目的依賴環境:python
$ pip install pipenv
$ pipenv shell
複製代碼
若是看到命令提示符前面出現 (recipes_app-nV3wuGJ1)
的提示(後面那串隨機字符串可能不同),就代表咱們已經成功地建立了項目獨有的虛擬環境!咱們接着安裝 Django 「三件套」:git
安裝命令以下:github
(recipes_app-nV3wuGJ1) $ pipenv install django django-rest-framework django-cors-headers
複製代碼
這時 pipenv 便產生了 Pipfile 文件,它的做用就相似 Node 項目中的 package.json 文件:web
[[source]]
url = "https://mirrors.aliyun.com/pypi/simple/"
verify_ssl = true
name = "pypi"
[packages]
django = "*"
django-rest-framework = "*"
django-cors-headers = "*"
[dev-packages]
[requires]
python_version = "3.6"
複製代碼
而後用 Django 腳手架建立服務器項目 api
的基本結構,並進入到 api
建立一個子應用 core
:
(recipes_app-nV3wuGJ1) $ django-admin startproject api
(recipes_app-nV3wuGJ1) $ cd api
(recipes_app-nV3wuGJ1) $ python manage.py startapp core
複製代碼
接着進行數據庫遷移,並建立用於登陸後臺管理的超級用戶:
(recipes_app-nV3wuGJ1) $ python manage.py migrate
(recipes_app-nV3wuGJ1) $ python manage.py createsuperuser
複製代碼
按照問題輸入信息便可。要記住用戶名和密碼哦!而後運行開發服務器:
(recipes_app-nV3wuGJ1) $ python manage.py runserver
複製代碼
訪問 http://localhost:8000/admin,能夠看到後臺管理的登陸頁面。輸入剛纔建立的超級用戶的用戶名和密碼,就進入了後臺管理系統,以下所示:
熟悉的界面,可是——沒什麼東西,並且全是英文!別擔憂,後面咱們會一個個搞定。
接下來咱們將實現本項目所須要用的全部 API。對,你沒有聽錯,咱們會在這一步實現全部後端接口,大概只 10 分鐘左右能夠敲完!這就是 Django 的宣言:
The web framework for perfectionists with deadlines.
「爲趕時間的完美主義者而生!」
首先,在全局配置文件 settings.py 中作以下改動:
INSTALLED_APPS
中添加 rest_framework
、corsheaders
和 core
,前兩個分別是 Django Rest Framework 和 Django CORS Headers 的應用,最後一個是咱們網站的應用;MIDDLEWARE
中添加 corsheaders.middleware.CorsMiddleware
,註冊跨域請求中間件(注意必定要放在最前面!);CORS_ORIGIN_WHITELIST
,添加跨域請求白名單,這裏咱們先寫上 http://localhost:3000
,後面開發前端時將用到;LANGUAGE_CODE
爲 zh-hans
,能夠將後臺管理設置爲中文,很是方便;MEDIA_URL
和 MEDIA_ROOT
,用於在開發中提供圖片資源文件的訪問。具體代碼以下:
# ...
INSTALLED_APPS = [
# 默認的 App ...
'rest_framework',
'corsheaders',
'core',
]
MIDDLEWARE = [
'corsheaders.middleware.CorsMiddleware',
# 默認的中間件 ...
]
CORS_ORIGIN_WHITELIST = (
'http://localhost:3000',
)
# ...
LANGUAGE_CODE = 'zh-hans'
MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
複製代碼
接下來就是實現 core
這個 Django 應用。實現一個 Django 應用大體都是按照這樣的流程:
咱們從第一步開始,完成菜譜 Recipe
數據模型以下:
from django.db import models
class Recipe(models.Model):
DIFFICULTY_LEVELS = (
('Easy', '容易'),
('Medium', '中等'),
('Hard', '困難'),
)
name = models.CharField(max_length=120, verbose_name='名稱')
ingredients = models.CharField(max_length=400, verbose_name='食材')
picture = models.FileField(verbose_name='圖片')
difficulty = models.CharField(choices=DIFFICULTY_LEVELS, max_length=10,
verbose_name='製做難度')
prep_time = models.PositiveIntegerField(verbose_name='準備時間')
prep_guide = models.TextField(verbose_name='製做指南')
class Meta:
verbose_name = '食譜'
verbose_name_plural = '食譜'
def __str__(self):
return '{} 的食譜'.format(self.name)
複製代碼
其中,class Meta
定義了 Recipe
的元數據;__str__
方法定義了一個菜譜對象轉換爲字符串時應該怎樣顯示。這些設置的做用在打開後臺管理系統以後就會很清晰了。想要了解更多關於 Django 數據模型的知識,請參考相關中文文檔。
第二步,爲 core
子應用配置相應的後臺管理功能。很是簡單,只需註冊定義好的 Recipe
模型:
from django.contrib import admin
from .models import Recipe
# Register your models here.
admin.site.register(Recipe)
複製代碼
第三步,定義序列化器 serializers.py(腳手架並不會自動建立,須要手動建立)。序列化器是 Django Rest Framework 提供的功能,可以很是方便地將 Django 數據模型序列化成相應的 JSON 數據格式。在這裏,咱們定義一個 RecipeSerializer
,並在 class Meta
中指定對應的數據模型爲剛纔建立的 Recipe
,並選擇相應的字段展現:
from rest_framework import serializers
from .models import Recipe
class RecipeSerializer(serializers.ModelSerializer):
class Meta:
model = Recipe
fields = (
'id', 'name', 'ingredients', 'picture',
'difficulty', 'prep_time', 'prep_guide'
)
複製代碼
第四步,實現視圖。這裏咱們採用開掛模式,直接調用 Django Rest Framework 提供的模型視圖集(ModelViewset
)直接搞定數據模型的增刪改查邏輯:
from rest_framework import viewsets
from .serializers import RecipeSerializer
from .models import Recipe
class RecipeViewSet(viewsets.ModelViewSet):
serializer_class = RecipeSerializer
queryset = Recipe.objects.all()
複製代碼
只需指定 serializer_class
(序列器類)和 queryset
(模型查詢集),就自動定義好了模型的添加、刪除、查詢和修改!雖然視圖集很是強大,可是若是要實現更加靈活的業務邏輯,那麼仍是要爲每一個接口定義單獨的視圖類才行。
第五步,實現路由。因爲咱們上一步使用了視圖集,所以只需先調用 DefaultRouter
自動生成相關的路由,而後加入記錄路由映射的列表 urlpatterns
中:
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from .views import RecipeViewSet
router = DefaultRouter()
router.register(r'recipes', RecipeViewSet)
urlpatterns = [
path('', include(router.urls)),
]
複製代碼
router
爲咱們自動生成如下路由:
/recipes/
:建立食譜(POST 方法)或讀取食譜列表(GET方法);/recipes/{id}
:獲取單個食譜(GET)、更新單個食譜(PUT)或刪除食譜(DELETE)。注意
在 Django 路由定義中不包括 HTTP 方法,具體的 HTTP 方法能夠在視圖中讀取並判斷。
最後一步,咱們將 core
子應用中的路由接入全局路由:
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('api/', include('core.urls')),
]
複製代碼
沒錯,關於食譜的增刪改查的 API 咱們全都實現了!不信?先運行開發服務器:
(recipes_app-nV3wuGJ1) $ python manage.py runserver
複製代碼
因爲 Django REST Framework 爲咱們提供了測試 API 的 Web 界面,所以這裏就不用 Postman 等工具進行測試了。用瀏覽器訪問 localhost:8000/api/recipes,就進入了以下所示的 API 測試頁面:
這個頁面的下方還有添加數據(發起 POST 請求)的表單,咱們填一些數據,而後點擊 POST 按鈕:
而後再次訪問食譜列表頁面,就有咱們剛剛添加的食譜了!此外,你還能夠嘗試訪問單個食譜的詳情頁面(例如 localhost:8000/api/recipes/1),而且能夠經過 Web 頁面直接修改或刪除哦!
Django 的 MTV 架構當然優秀,可是隨着如今的業務邏輯愈來愈多地向前端傾斜(也就是如今流行的富前端應用),其中的 T(Template)須要更強大的武器來解決,這裏就是咱們的第二位主角 Nuxt。
咱們將把全部的前端代碼放到 client 目錄中,不過無需本身建立,咱們調用 nuxt 的腳手架來建立前端應用:
$ npx create-nuxt-app client
複製代碼
以後腳手架應用會詢問一系列問題,按下面的截圖進行選擇(固然做者名填本身):
咱們對 Nuxt 腳手架生成的目錄結構稍做講解。能夠看到 client 目錄下有如下子目錄:
.vue
文件自動建立應用的路由/static/picture.png
訪問)本項目所用到的圖片資源請訪問咱們的 GitHub 倉庫,並下載到對應的目錄中。
咱們在 client/pages 中建立 index.vue 文件,並在其中實現咱們的前端首頁:
<template>
<header>
<div class="text-box">
<h1>吃貨天堂 😋</h1>
<p class="mt-3">製做咱們喜好的美食 ❤️ ️</p>
<nuxt-link class="btn btn-outline btn-large btn-info" to="/recipes">
查看食譜
<span class="ml-2">→</span>
</nuxt-link>
</div>
</header>
</template>
<script> export default { head() { return { title: "首頁" }; } }; </script>
<style> header { min-height: 100vh; background-image: linear-gradient( to right, rgba(0, 0, 0, 0.9), rgba(12, 5, 5, 0.4) ), url("/images/banner.jpg"); background-position: center; background-size: cover; position: relative; } .text-box { position: absolute; top: 50%; left: 10%; transform: translateY(-50%); color: #fff; } .text-box h1 { font-family: cursive; font-size: 5rem; } .text-box p { font-size: 2rem; font-weight: lighter; } </style>
複製代碼
模板(Template)+ 腳本(Script)+ 樣式(Style),經典的 Vue.js 組件。
咱們剛剛建立了 pages 目錄下的 index.vue 文件,這意味着當訪問根路由 /
時,這個文件將被訪問到。經過 npm run dev
運行咱們的前端頁面(記得在 client 子目錄下運行!),能夠看到:
真是讓人食慾大開!
接下來咱們將演示如何展現數據,並實現食譜列表頁面。
首先,實現將會在多個頁面中反覆使用的食譜卡片組件 RecipeCard
以下:
<template>
<div class="card recipe-card">
<img :src="recipe.picture" class="card-img-top" />
<div class="card-body">
<h5 class="card-title">{{ recipe.name }}</h5>
<p class="card-text">
<strong>成分:</strong>
{{ recipe.ingredients }}
</p>
<div class="action-buttons">
<nuxt-link :to="`/recipes/${recipe.id}/`" class="btn btn-sm btn-success">查看</nuxt-link>
<nuxt-link :to="`/recipes/${recipe.id}/edit/`" class="btn btn-sm btn-primary">編輯</nuxt-link>
<button @click="onDelete(recipe.id)" class="btn btn-sm btn-danger">刪除</button>
</div>
</div>
</div>
</template>
<script> export default { props: ["recipe", "onDelete"] }; </script>
<style> .card-img-top { height: 12rem; width: 100%; } .recipe-card { border: none; box-shadow: 0 1rem 1.5rem rgba(0, 0, 0, 0.6); } </style>
複製代碼
在這個組件中,咱們定義了兩個 props
,分別是 recipe
(表明食譜對象)和 onDelete
(刪除時的回調函數),並在模板中使用這兩個成員。
在實現第二個頁面以前,咱們有必要先了解一下 Nuxt 的路由功能——經過 pages 目錄下的文檔結構,就能夠自動生成 vue-router 的路由器配置!
例如咱們這樣安排 pages 下面的目錄結構👇:
pages
├── README.md
├── index.vue
└── recipes
├── _id
│ ├── edit.vue
│ └── index.vue
├── add.vue
└── index.vue
複製代碼
_id
目錄(或者其餘以單下劃線開頭的目錄或 .vue 文件)被稱做是動態路由(Dynamic Routing),能夠接受參數做爲 URL 的一部分。上面的 pages 目錄自動生成下面的 router
:
router: {
routes: [
{
name: 'index',
path: '/',
component: 'pages/index.vue'
},
{
name: 'recipes',
path: '/recipes',
component: 'pages/recipes/index.vue'
},
{
name: 'recipes-add',
path: '/recipes/add',
component: 'pages/recipes/add.vue'
},
{
name: 'recipes-id',
path: '/recipes/:id?',
component: 'pages/recipes/_id/index.vue'
},
{
name: 'recipes-id-edit',
path: '/recipes/:id?/edit',
component: 'pages/recipes/_id/edit.vue'
}
]
}
複製代碼
提示
若是想要更深刻地瞭解 Nuxt 的路由功能,請參考官方文檔。
建立食譜列表頁面 pages/recipes/index.vue(先使用假數據填充),代碼以下:
<template>
<main class="container mt-5">
<div class="row">
<div class="col-12 text-right mb-4">
<div class="d-flex justify-content-between">
<h3>吃貨天堂</h3>
<nuxt-link to="/recipes/add" class="btn btn-info">添加食譜</nuxt-link>
</div>
</div>
<template v-for="recipe in recipes">
<div :key="recipe.id" class="col-lg-3 col-md-4 col-sm-6 mb-4">
<recipe-card :onDelete="deleteRecipe" :recipe="recipe"></recipe-card>
</div>
</template>
</div>
</main>
</template>
<script> import RecipeCard from "~/components/RecipeCard.vue"; const sampleData = [ { id: 1, name: "通心粉", picture: "/images/food-1.jpeg", ingredients: "牛肉, 豬肉, 羊肉", difficulty: "easy", prep_time: 15, prep_guide: "Lorem ipsum dolor sit amet consectetur adipisicing elit. Omnis, porro. Dignissimos ducimus ratione totam fugit officiis blanditiis exercitationem, nisi vero architecto quibusdam impedit, earum " }, { id: 2, name: "羊肉串", picture: "/images/food-2.jpeg", ingredients: "牛肉, 豬肉, 羊肉", difficulty: "easy", prep_time: 15, prep_guide: "Lorem ipsum dolor sit amet consectetur adipisicing elit. Omnis, porro. Dignissimos ducimus ratione totam fugit officiis blanditiis exercitationem, nisi vero architecto quibusdam impedit, earum " }, { id: 3, name: "炒飯", picture: "/images/banner.jpg", ingredients: "牛肉, 豬肉, 羊肉", difficulty: "easy", prep_time: 15, prep_guide: "Lorem ipsum dolor sit amet consectetur adipisicing elit. Omnis, porro. Dignissimos ducimus ratione totam fugit officiis blanditiis exercitationem, nisi vero architecto quibusdam impedit, earum " } ]; export default { head() { return { title: "食譜列表" }; }, components: { RecipeCard }, asyncData(context) { let data = sampleData; return { recipes: data }; }, data() { return { recipes: [] }; }, methods: { deleteRecipe(recipe_id) { console.log(deleted`${recipe.id}`); } } }; </script>
<style scoped> </style>
複製代碼
打開前端網站,能夠看到咱們剛纔實現的食譜列表頁面:
到這兒,咱們分別實現了這個全棧食譜網站的前端和後端應用,這篇教程的第一部分也就結束了。在接下來的教程中,咱們將實現先後端之間的通訊,並進一步實現食譜的詳情及添加頁面,不見不散!
想要學習更多精彩的實戰技術教程?來圖雀社區逛逛吧。
本文所涉及的源代碼都放在了 Github 上,若是您以爲咱們寫得還不錯,但願您能給❤️這篇文章點贊+Github倉庫加星❤️哦~ 本文代碼改編自 Scotch。