在上篇中,咱們分別用 Django 和 Nuxt 實現了後端和前端的雛形。在這一部分,咱們將實現先後端之間的通訊,使得前端能夠從後端獲取數據,而且將進一步豐富網站的功能。css
本文所涉及的源代碼都放在了 Github 上,若是您以爲咱們寫得還不錯,但願您能給❤️ 這篇文章點贊+Github倉庫加星❤️哦~ 本文代碼改編自 Scotch。
在這一部分,咱們將真正實現一個全棧應用——讓前端可以向後端發起請求,從而獲取想要的數據。html
首先咱們要配置一下 Django 服務器,使前端可以訪問其靜態文件。調整 api/api/urls.py 文件以下:前端
# ... from django.conf import settings from django.conf.urls.static import static urlpatterns = [ path('admin/', admin.site.urls), path('api/', include('core.urls')), ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
注意這樣配置靜態文件路由的方式僅應當在開發環境下使用。在生產環境下(settings.py 中的
DEBUG
設爲False
時),靜態文件路由將自動失效(由於 Django 並不適合做爲靜態文件服務器,應該選用相似 Nginx 之類的服務器,在後續教程中咱們將更深刻地討論)。vue
在客戶端,咱們先要對 Nuxt 進行全局配置。Nuxt 包括 axios 包,這是一個很是出色的基於 Promise 的 HTTP 請求庫。在 nuxt.config.js 中的 axios
一項中添加 Django 服務器的 URL:python
export default { // ... /* ** Axios module configuration ** See https://axios.nuxtjs.org/options */ axios: { baseURL: 'http://localhost:8000/api', }, // ... }
將食譜列表頁面中暫時填充的假數據刪去,經過 asyncData
方法獲取數據。因爲咱們以前配置好了 axios,因此 asyncData
函數能夠獲取到 $axios
對象用於發起 HTTP 請求。咱們實現頁面加載的數據獲取以及 deleteRecipe
事件,代碼以下:ios
<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"; export default { head() { return { title: "食譜列表" }; }, components: { RecipeCard }, async asyncData({ $axios, params }) { try { let recipes = await $axios.$get(`/recipes/`); return { recipes }; } catch (e) { return { recipes: [] }; } }, data() { return { recipes: [] }; }, methods: { async deleteRecipe(recipe_id) { try { if (confirm('確認要刪除嗎?')) { await this.$axios.$delete(`/recipes/${recipe_id}/`); let newRecipes = await this.$axios.$get("/recipes/"); this.recipes = newRecipes; } } catch (e) { console.log(e); } } } }; </script> <style scoped> </style>
咱們進一步實現食譜詳情頁面。在 pages/recipes 目錄中建立 _id 目錄,在其中添加 index.vue 文件,代碼以下:git
<template> <main class="container my-5"> <div class="row"> <div class="col-12 text-center my-3"> <h2 class="mb-3 display-4 text-uppercase">{{ recipe.name }}</h2> </div> <div class="col-md-6 mb-4"> <img class="img-fluid" style="width: 400px; border-radius: 10px; box-shadow: 0 1rem 1rem rgba(0,0,0,.7);" :src="recipe.picture" alt > </div> <div class="col-md-6"> <div class="recipe-details"> <h4>食材</h4> <p>{{ recipe.ingredients }}</p> <h4>準備時間 ⏱</h4> <p>{{ recipe.prep_time }} mins</p> <h4>製做難度</h4> <p>{{ recipe.difficulty }}</p> <h4>製做指南</h4> <textarea class="form-control" rows="10" v-html="recipe.prep_guide" disabled/> </div> </div> </div> </main> </template> <script> export default { head() { return { title: "食譜詳情" }; }, async asyncData({ $axios, params }) { try { let recipe = await $axios.$get(`/recipes/${params.id}`); return { recipe }; } catch (e) { return { recipe: [] }; } }, data() { return { recipe: { name: "", picture: "", ingredients: "", difficulty: "", prep_time: null, prep_guide: "" } }; } }; </script> <style scoped> </style>
爲了測試前端頁面可否真正從後端獲取數據,咱們先要在後端數據庫中添加一些數據,而這對 Django 來講就很是方便了。進入 api 目錄,運行 python manage.py runserver
打開服務器,而後進入後臺管理頁面(http://localhost:8000/admin),添加一些數據:github
再運行前端頁面,能夠看到咱們剛剛在 Django 後臺管理中添加的項目:數據庫
有了前面的鋪墊,實現食譜的添加和刪除也基本上是循序漸進了。咱們在 pages/recipes/_id 中實現 edit.vue
(食譜編輯頁面),代碼以下:django
<template> <main class="container my-5"> <div class="row"> <div class="col-12 text-center my-3"> <h2 class="mb-3 display-4 text-uppercase">{{ recipe.name }}</h2> </div> <div class="col-md-6 mb-4"> <img v-if="!preview" class="img-fluid" style="width: 400px; border-radius: 10px; box-shadow: 0 1rem 1rem rgba(0,0,0,.7);" :src="recipe.picture"> <img v-else class="img-fluid" style="width: 400px; border-radius: 10px; box-shadow: 0 1rem 1rem rgba(0,0,0,.7);" :src="preview"> </div> <div class="col-md-4"> <form @submit.prevent="submitRecipe"> <div class="form-group"> <label for>Recipe Name</label> <input type="text" class="form-control" v-model="recipe.name" > </div> <div class="form-group"> <label for>Ingredients</label> <input type="text" v-model="recipe.ingredients" class="form-control" name="Ingredients" > </div> <div class="form-group"> <label for>Food picture</label> <input type="file" @change="onFileChange"> </div> <div class="row"> <div class="col-md-6"> <div class="form-group"> <label for>Difficulty</label> <select v-model="recipe.difficulty" class="form-control" > <option value="Easy">Easy</option> <option value="Medium">Medium</option> <option value="Hard">Hard</option> </select> </div> </div> <div class="col-md-6"> <div class="form-group"> <label for> Prep time <small>(minutes)</small> </label> <input type="text" v-model="recipe.prep_time" class="form-control" name="Ingredients" > </div> </div> </div> <div class="form-group mb-3"> <label for>Preparation guide</label> <textarea v-model="recipe.prep_guide" class="form-control" rows="8"></textarea> </div> <button type="submit" class="btn btn-success">Save</button> </form> </div> </div> </main> </template> <script> export default { head(){ return { title: "編輯食譜" } }, async asyncData({ $axios, params }) { try { let recipe = await $axios.$get(`/recipes/${params.id}`); return { recipe }; } catch (e) { return { recipe: [] }; } }, data() { return { recipe: { name: "", picture: "", ingredients: "", difficulty: "", prep_time: null, prep_guide: "" }, preview: "" }; }, methods: { onFileChange(e) { let files = e.target.files || e.dataTransfer.files; if (!files.length) { return; } this.recipe.picture = files[0] this.createImage(files[0]); }, createImage(file) { let reader = new FileReader(); let vm = this; reader.onload = e => { vm.preview = e.target.result; }; reader.readAsDataURL(file); }, async submitRecipe() { let editedRecipe = this.recipe if (editedRecipe.picture.indexOf("http://") != -1){ delete editedRecipe["picture"] } const config = { headers: { "content-type": "multipart/form-data" } }; let formData = new FormData(); for (let data in editedRecipe) { formData.append(data, editedRecipe[data]); } try { let response = await this.$axios.$patch(`/recipes/${editedRecipe.id}/`, formData, config); this.$router.push("/recipes/"); } catch (e) { console.log(e); } } } }; </script> <style> </style>
實現以後的頁面以下:
繼續在 pages/recipes/_id 中實現 add.vue
(建立食譜頁面)以下:
<template> <main class="container my-5"> <div class="row"> <div class="col-12 text-center my-3"> <h2 class="mb-3 display-4 text-uppercase">{{ recipe.name }}</h2> </div> <div class="col-md-6 mb-4"> <img v-if="preview" class="img-fluid" style="width: 400px; border-radius: 10px; box-shadow: 0 1rem 1rem rgba(0,0,0,.7);" :src="preview" alt > <img v-else class="img-fluid" style="width: 400px; border-radius: 10px; box-shadow: 0 1rem 1rem rgba(0,0,0,.7);" src="@/static/images/placeholder.png" > </div> <div class="col-md-4"> <form @submit.prevent="submitRecipe"> <div class="form-group"> <label for>食譜名稱</label> <input type="text" class="form-control" v-model="recipe.name"> </div> <div class="form-group"> <label for>食材</label> <input v-model="recipe.ingredients" type="text" class="form-control"> </div> <div class="form-group"> <label for>圖片</label> <input type="file" name="file" @change="onFileChange"> </div> <div class="row"> <div class="col-md-6"> <div class="form-group"> <label for>難度</label> <select v-model="recipe.difficulty" class="form-control"> <option value="Easy">容易</option> <option value="Medium">中等</option> <option value="Hard">困難</option> </select> </div> </div> <div class="col-md-6"> <div class="form-group"> <label for> 製做時間 <small>(分鐘)</small> </label> <input v-model="recipe.prep_time" type="number" class="form-control"> </div> </div> </div> <div class="form-group mb-3"> <label for>製做指南</label> <textarea v-model="recipe.prep_guide" class="form-control" rows="8"></textarea> </div> <button type="submit" class="btn btn-primary">提交</button> </form> </div> </div> </main> </template> <script> export default { head() { return { title: "Add Recipe" }; }, data() { return { recipe: { name: "", picture: "", ingredients: "", difficulty: "", prep_time: null, prep_guide: "" }, preview: "" }; }, methods: { onFileChange(e) { let files = e.target.files || e.dataTransfer.files; if (!files.length) { return; } this.recipe.picture = files[0]; this.createImage(files[0]); }, createImage(file) { let reader = new FileReader(); let vm = this; reader.onload = e => { vm.preview = e.target.result; }; reader.readAsDataURL(file); }, async submitRecipe() { const config = { headers: { "content-type": "multipart/form-data" } }; let formData = new FormData(); for (let data in this.recipe) { formData.append(data, this.recipe[data]); } try { let response = await this.$axios.$post("/recipes/", formData, config); this.$router.push("/recipes/"); } catch (e) { console.log(e); } } } }; </script> <style scoped> </style>
實現的頁面以下:
在這一節中,咱們將演示如何在 Nuxt 中添加全局樣式文件,來實現前端頁面之間的跳轉效果。
首先在 assets 目錄中建立 css 目錄,並在其中添加 transition.css 文件,代碼以下:
.page-enter-active, .page-leave-active { transition: opacity .3s ease; } .page-enter, .page-leave-to { opacity: 0; }
在 Nuxt 配置文件中將剛纔寫的 transition.css 中添加到全局 CSS 中:
export default { // ... /* ** Global CSS */ css: [ '~/assets/css/transition.css', ], // ... }
歐耶,一個具備完整增刪改查功能、實現了先後端分離的美食分享網站就完成了!
想要學習更多精彩的實戰技術教程?來 圖雀社區逛逛吧。本文所涉及的源代碼都放在了 Github 上,若是您以爲咱們寫得還不錯,但願您能給❤️這篇文章點贊+Github倉庫加星❤️哦~ 本文代碼改編自 Scotch。