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

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

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

本文所涉及的源代碼都放在了 Github 上,若是您以爲咱們寫得還不錯,但願您能給❤️ 這篇文章點贊+Github倉庫加星❤️哦~ 本文代碼改編自 Scotch

項目初始化

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

預備知識

本教程假定你已經知道了vue

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

學習目標

學完這篇教程後,你將:python

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

用 pipenv 初始化 Python 環境

首先建立項目目錄,並進入:git

$ mkdir recipes_app && cd recipes_app

在這個項目中,咱們用 pipenv 來管理 Python 項目的環境依賴。Pipenv 是 Python 社區偶像級大師 Kenneth Reitz 牽頭開發的開發流程優化工具,立志集全部項目管理工具(Node 的 npm、Ruby 的 bundler、PHP 的 composer 等等)的優點爲一體。咱們經過下面的命令安裝 pipenv,並建立項目的依賴環境:github

$ pip install pipenv
$ pipenv shell

若是看到命令提示符前面出現 (recipes_app-nV3wuGJ1) 的提示(後面那串隨機字符串可能不同),就代表咱們已經成功地建立了項目獨有的虛擬環境!咱們接着安裝 Django 「三件套」:web

  • Django: Django 框架自己,提供了豐富且強大的服務器開發組件;
  • DRF (Django Rest Framework):Django 框架的超級搭檔,大大方便了 REST API 的開發;
  • Django CORS Headers:用於實現跨域資源請求(CORS)的 Django 中間件(若是你不瞭解 CORS,能夠參考阮一峯的日誌)。

安裝命令以下:vue-router

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

這時 pipenv 便產生了 Pipfile 文件,它的做用就相似 Node 項目中的 package.json 文件:shell

[[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,能夠看到後臺管理的登陸頁面。輸入剛纔建立的超級用戶的用戶名和密碼,就進入了後臺管理系統,以下所示:

熟悉的界面,可是——沒什麼東西,並且全是英文!別擔憂,後面咱們會一個個搞定。

用 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,添加跨域請求白名單,這裏咱們先寫上 http://localhost:3000,後面開發前端時將用到;
  4. 設置 LANGUAGE_CODEzh-hans,能夠將後臺管理設置爲中文,很是方便;
  5. 設置 MEDIA_URLMEDIA_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 應用

接下來就是實現 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 等工具進行測試了。用瀏覽器訪問 localhost:8000/api/recipes,就進入了以下所示的 API 測試頁面:

這個頁面的下方還有添加數據(發起 POST 請求)的表單,咱們填一些數據,而後點擊 POST 按鈕:

而後再次訪問食譜列表頁面,就有咱們剛剛添加的食譜了!此外,你還能夠嘗試訪問單個食譜的詳情頁面(例如 localhost:8000/api/recipes/1),而且能夠經過 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 文件(本教程中不須要)

本項目所用到的圖片資源請訪問咱們的 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">&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 上,若是您以爲咱們寫得還不錯,但願您能給❤️這篇文章點贊+Github倉庫加星❤️哦~ 本文代碼改編自 Scotch

相關文章
相關標籤/搜索