在上篇中,咱们分别用 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。