Django 做为 Python 社区最受欢迎的 Web 框架之一,凭借其高度抽象的组件和强大方便的脚手架,将快速且流畅的开发体验演绎到了极致。而 Nuxt 做为从 Vue.js 进化而来的前端框架,可以轻松胜任复杂的 SPA(单页应用)开发。二者相遇,可以擦出怎样的火花?这篇教程将用 Django + Nuxt 实现带有完整的增删改查(CRUD)功能的全栈应用。最后郑重警告:不要在深夜阅读此教程!!!html
本文所涉及的源代码都放在了 Github 上,若是您以为咱们写得还不错,但愿您能给❤️ 这篇文章点赞+Github仓库加星❤️哦~ 本文代码改编自 Scotch。
在这一系列教程中,咱们将会实现一个全栈美食分享网站,后端用 Django 实现,前端则是 Nuxt 框架,下面是最终完成后的项目效果:前端
本教程假定你已经知道了vue
学完这篇教程后,你将: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
安装命令以下: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,能够看到后台管理的登陆页面。输入刚才建立的超级用户的用户名和密码,就进入了后台管理系统,以下所示:
熟悉的界面,可是——没什么东西,并且全是英文!别担忧,后面咱们会一个个搞定。
接下来咱们将实现本项目所须要用的全部 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。