全棧“食”代:用 Django + Nuxt 實現美食分享網站(上)

duanhao發表於2021-09-09

圖片描述
Django 作為 Python 社群最受歡迎的 Web 框架之一,憑藉其高度抽象的元件和強大方便的腳手架,將快速且流暢的開發體驗演繹到了極致。而 Nuxt 作為從 Vue.js 進化而來的前端框架,能夠輕鬆勝任複雜的 SPA(單頁應用)開發。兩者相遇,能夠擦出怎樣的火花?這篇教程將用 Django + Nuxt 實現帶有完整的增刪改查(CRUD)功能的全棧應用。最後鄭重警告:不要在深夜閱讀此教程!!!

本文所涉及的原始碼都放在了 上,如果您覺得我們寫得還不錯,希望您能給❤️這篇文章點贊+Github倉庫加星❤️哦~ 本文程式碼改編自

專案初始化

在這一系列教程中,我們將會實現一個全棧美食分享網站,後端用 Django 實現,前端則是 Nuxt 框架,下面是最終完成後的專案效果:

圖片描述

預備知識

本教程假定你已經知道了

  • 基本的 Python 3 語言知識,包括使用 pip 安裝包
  • Django 框架的基礎概念(MTV 架構),可參考這篇進行學習
  • Vue 的基礎概念,以及用 npm 工具鏈的使用,可參考
  • 前後端分離的基本概念,包括前端如何透過發起 HTTP(S) 請求從後端獲取資料

學習目標

學完這篇教程後,你將:

  • 瞭解用 pipenv 工具管理 Python 依賴
  • 學會用 Django REST Framework 快速開發 REST API
  • 學會用 Nuxt 框架快速開發 SPA(單頁應用),能夠從後端獲取資料並渲染

用 pipenv 初始化 Python 環境

首先建立專案目錄,並進入:

$ mkdir recipes_app && cd recipes_app

在這個專案中,我們用 來管理 Python 專案的環境依賴。Pipenv 是 Python 社群偶像級大師 Kenneth Reitz 牽頭開發的開發流程最佳化工具,立志集所有專案管理工具(Node 的 npm、Ruby 的 bundler、PHP 的 composer 等等)的優勢為一體。我們透過下面的命令安裝 pipenv,並建立專案的依賴環境:

$ pip install pipenv
$ pipenv shell

如果看到命令提示符前面出現 (recipes_app-nV3wuGJ1) 的提示(後面那串隨機字串可能不一樣),就表明我們已經成功地建立了專案獨有的虛擬環境!我們接著安裝 Django “三件套”:

  • Django: Django 框架本身,提供了豐富且強大的伺服器開發元件;
  • DRF (Django Rest Framework):Django 框架的超級搭檔,大大方便了 REST API 的開發;
  • Django CORS Headers:用於實現跨域資源請求(CORS)的 Django 中介軟體(如果你不瞭解 CORS,可以參考阮一峰的)。

安裝命令如下:

(recipes_app-nV3wuGJ1) $ pipenv install django django-rest-framework django-cors-headers

這時 pipenv 便產生了 Pipfile 檔案,它的作用就類似 Node 專案中的 package.json 檔案:

[[source]]
url = ""
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

訪問 ,可以看到後臺管理的登入頁面。輸入剛才建立的超級使用者的使用者名稱和密碼,就進入了後臺管理系統,如下所示:

圖片描述

熟悉的介面,但是——沒什麼東西,而且全是英文!別擔心,後面我們會一個個搞定。

用 Django 實現 REST API

接下來我們將實現本專案所需要用的所有 API。對,你沒有聽錯,我們會在這一步實現所有後端介面,大概只 10 分鐘左右可以敲完!這就是 Django 的宣言:

The web framework for perfectionists with deadlines.

“為趕時間的完美主義者而生!”

全域性配置

首先,在全域性配置檔案 settings.py 中做如下改動:

  1. INSTALLED_APPS 中新增 rest_frameworkcorsheaderscore,前兩個分別是 Django Rest Framework 和 Django CORS Headers 的應用,最後一個是我們網站的應用;
  2. MIDDLEWARE 中新增 corsheaders.middleware.CorsMiddleware,註冊跨域請求中介軟體(注意一定要放在最前面!);
  3. 設定 CORS_ORIGIN_WHITELIST,新增跨域請求白名單,這裡我們先寫上 ,後面開發前端時將用到;
  4. 設定 LANGUAGE_CODEzh-hans,可以將後臺管理設定為中文,非常方便;
  5. 設定 MEDIA_URLMEDIA_ROOT,用於在開發中提供圖片資原始檔的訪問。

具體程式碼如下:

# ...

INSTALLED_APPS = [
    # 預設的 App ...

    'rest_framework',
    'corsheaders',
    'core',
]

MIDDLEWARE = [
    'corsheaders.middleware.CorsMiddleware',
    # 預設的中介軟體 ...
]

CORS_ORIGIN_WHITELIST = (
    '',
)

# ...

LANGUAGE_CODE = 'zh-hans'

MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')

實現 core 應用

接下來就是實現 core 這個 Django 應用。實現一個 Django 應用大致都是按照這樣的流程:

  1. 定義資料模型(models.py),用於實現和資料庫之間的繫結;
  2. 定義後臺管理配置(admin.py),用於在後臺管理系統中進行操作;
  3. 定義序列化器(serializers.py),僅當實現 REST API 時需要,用於提供資料模型的 JSON 序列化(或其他資料交換格式);
  4. 定義檢視(views.py),用於實現具體的業務邏輯;
  5. 定義路由(urls.py),用於定義路由規則,將其對映到相應的檢視;
  6. 將應用路由接入全域性路由檔案(api/urls.py)中。

我們從第一步開始,完成菜譜 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 等工具進行測試了。用瀏覽器訪問 ,就進入瞭如下所示的 API 測試頁面:

圖片描述

這個頁面的下方還有新增資料(發起 POST 請求)的表單,我們填一些資料,然後點選 POST 按鈕:

圖片描述

然後再次訪問食譜列表頁面,就有我們剛剛新增的食譜了!此外,你還可以嘗試訪問單個食譜的詳情頁面(例如 ),並且可以透過 Web 頁面直接修改或刪除哦!

用 Nuxt.js 實現網站首頁

Django 的 MTV 架構固然優秀,但是隨著現在的業務邏輯越來越多地向前端傾斜(也就是現在流行的富前端應用),其中的 T(Template)需要更強大的武器來解決,這裡就是我們的第二位主角 Nuxt。

用腳手架初始化 Nuxt 專案

我們將把所有的前端程式碼放到 client 目錄中,不過無需自己建立,我們呼叫 nuxt 的腳手架來建立前端應用:

$ npx create-nuxt-app client

之後腳手架應用會詢問一系列問題,按下面的截圖進行選擇(當然作者名填自己):

圖片描述

我們對 Nuxt 腳手架生成的目錄結構稍作講解。可以看到 client 目錄下有以下子目錄:

  • assets:存放圖片、CSS、JS 等原始資原始檔
  • components:存放 Vue 元件
  • layouts:存放應用佈局檔案,佈局可在多個頁面中使用
  • middleware:存放應用的中介軟體。Nuxt 中的中介軟體指頁面渲染前執行的自定義函式(本教程中不需要)
  • pages:應用的檢視和路由。Nuxt 會根據此目錄中的 .vue 檔案自動建立應用的路由
  • plugins: 存放 JavaScript 外掛,用於在應用啟動前載入(本教程中不需要)
  • static:存放通常不會改變的靜態檔案,並且將直接對映到路由(即可透過 /static/picture.png 訪問)
  • store:存放 Vuex Store 檔案(本教程中不需要)

本專案所用到的圖片資源請訪問我們的 ,並下載到對應的目錄中。

編寫前端首頁

我們在 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">&rarr;</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 元件

首先,實現將會在多個頁面中反覆使用的食譜卡片元件 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 的路由功能

在實現第二個頁面之前,我們有必要先了解一下 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倉庫加星❤️哦~ 本文程式碼改編自

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/1343/viewspace-2824880/,如需轉載,請註明出處,否則將追究法律責任。

相關文章