后端开发环境为:
在虚拟环境下,输入命令 pip install django==3.1.3
:
(venv) E:\drf> pip install django==3.1.3
Collecting django==3.1.3
Using cached
...
...
Successfully installed django-3.1.3
系统打印出以上文字表示 Django 安装成功了。(提示符以 (venv)
开头)
由于国内复杂的网络环境, Pip 的下载可能非常缓慢甚至失败。国内用户请更换国内的镜像下载源。
还是在虚拟环境下,在drf
文件夹中创建一个叫 drf_vue_blog
的Django项目:
(venv) E:\drf> django-admin startproject drf_vue_blog
查看drf
文件夹,发现多了drf_vue_blog
文件夹,其结构应该是这样:
drf_vue_blog
│ manage.py
│
└─drf_vue_blog
│ settings.py
│ urls.py
│ wsgi.py
└─ __init__.py
这就是我们刚创建出来的项目了。
Django 自带一个轻量的 Web 开发服务器,被叫做 runserver。
开发服务器是为了让你快速开发Web程序,通过它可以避开配置生产环境的服务器的繁琐环节。
开发服务器会自动的检测代码的改变,并且自动加载它,因此在修改代码后不需要手动去重启服务器,非常的方便。
要运行这个服务器,首先要进入drf_vue_blog
文件夹,即含有manage.py
文件的那个:
(venv) E:\drf> cd drf_vue_blog
(venv) E:\drf\drf_vue_blog>
输入命令python manage.py runserver
:
(venv) E:\drf\drf_vue_blog> python manage.py runserver
Performing system checks...
...
Django version 3.1.3, using settings 'drf_vue_blog.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CTRL-BREAK.
系统打印出这些信息,说明服务器启动成功了。
打开 Chrome 浏览器,输入http://127.0.0.1:8000/ ,网页中看到一个绿色的小火箭,恭喜你,项目已经正常运行了。
DATABASES = {
‘default’: {
‘ENGINE’: ‘django.db.backends.mysql’,
‘NAME’: ‘dbname’,
‘USER’:‘root’,
‘PASSWORD’:‘123456’,
‘HOST’:‘127.0.0.1’,
‘PORT’:‘3306’,
}
}
pip install PyMySQL
# 网络原因安装不上的话用这个命令
pip install pymysql -i http://pypi.douban.com/simple/ --trusted-host pypi.douban.com
import pymysql
pymysql.install_as_MySQLdb()pymysql.install_as_MySQLdb()
python3 manage.py makemigrations
python3 manage.py migrate
首先在命令行创建博客文章的 App:
(venv) > python manage.py startapp article
创建一个简单的博客文章模型:
# article/models.py
from django.db import models
from django.utils import timezone
# 博客文章 model
class Article(models.Model):
# 标题
title = models.CharField(max_length=100)
# 正文
body = models.TextField()
# 创建时间
created = models.DateTimeField(default=timezone.now)
# 更新时间
updated = models.DateTimeField(auto_now=True)
def __str__(self):
return self.title
前后端分离中涉及到一个重要的概念:序列化(后面讲解)。Django 有一个非常优秀的库 djangorestframework(后称 DRF)。它可以帮我们封装好序列化的底层实现,让开发者专注于业务本身。
安装 DRF 及其他依赖库:
pip install djangorestframework==3.12.2
pip install markdown==3.3.3
pip install django-filter==2.4.0
然后将 App 注册列表:
# drf_vue_blog/settings.py
INSTALLED_APPS = [
...
'rest_framework',
'article',
]
接着还需要添加 DRF 的登录视图,以便 DRF 自动为你的可视化接口页面生成一个用户登录的入口:
后续开发出接口页面后试着把这行代码删掉,看看会有什么不同。
# drf_vue_blog/urls.py
...
from django.urls import include
urlpatterns = [
...
path('api-auth/', include('rest_framework.urls')),
]
最后记得数据迁移:
(venv) > python manage.py makemigrations
(venv) > python manage.py migrate
准备工作就做好了。
前后端分离的核心思想之一,就是两端交互不通过模板语言,而只传输需要的数据。因此问题就来了。
在 Django 程序的运行过程中,变量都是存储在服务器的内存中;更要命的是,后端 Django 程序中存储的是 Python 变量,而前端的浏览器中是 Javascript 变量,这两者是无法直接通过你家的网线进行传递和交流的。因此需要规定一个“标准格式”,前后端都根据这个标准格式,对资源进行保存、读取、传输等操作。
JSON
就是这种标准格式之一。它很轻量,表示出来就是个字符串,可以直接被几乎所有的语言读取,非常方便。
举个例子,把 Python 对象转换为 JSON ,这被称为序列化(serialization):
>>> import json
>>> person = dict(name='Trump', age=82)
>>> json.dumps(person)
# 这是个字符串
'{"age": 82, "name": "Trump"}'
把 JSON 转换为 Javascript 对象,被称为反序列化:
>>> json_str = '{"age": 82, "name": "Trump"}'
>>> json.loads(json_str)
# 这是个 js 对象
{'age': 82, 'name': 'Trump'}
总之,把变量从内存中变成可存储或传输的过程称之为序列化,反过来把变量内容从序列化的对象重新读到内存里称之为反序列化。
回顾 Django 传统流程对一个网络请求的处理:
def a_list(request):
articles = Article.objects.all()
return render(..., context={'articles': articles})
视图函数将数据作为上下文返回,通过模板引擎将上下文渲染为页面中的数据。
Restful 的处理流程仅增加了一步,即对数据序列化的处理:
def a_list(request):
articles = Article.objects.all()
# 序列化数据
serializer = Serializer(articles, many=True)
return JsonResponse(serializer.data, safe=False)
数据被序列化为 Json 字符串,直接交由前端处理。
这就是前后端分离的雏形:
这里又出现了与前后端分离联系很紧密的新概念:Rest(表现层状态转化) 和 Restful。Restful 架构是指客户端和服务器之间的交互、操作符合 Rest 规范,即:每一个URI代表一种资源;客户端和服务器之间,传递资源的表现层;客户端通过四个HTTP动词,对服务器端资源进行操作,实现"表现层状态转化"。有点难理解,推荐读物阮一峰的博客和知乎文章。
按照这个思路,我们来写一个文章列表接口吧。
article
模型在前面已经写好了,接下来写视图:
# article/views.py
from django.http import JsonResponse
from article.models import Article
# 这个 ArticleListSerializer 暂时还没有
from article.serializers import ArticleListSerializer
def article_list(request):
articles = Article.objects.all()
serializer = ArticleListSerializer(articles, many=True)
return JsonResponse(serializer.data, safe=False)
代码一共就 3 行:
QuerySet
;QuerySet
数据,创建一个序列化器;跟说好的一样,返回的东西不再是传统的模板了,而是 Json 数据。
代码里的序列化器 ArticleListSerializer
我们还没写,接下来就完成它。
新建一个 article/serializers.py
的文件,写入下面的代码:
# article/serializers.py
from rest_framework import serializers
class ArticleListSerializer(serializers.Serializer):
id = serializers.IntegerField(read_only=True)
title = serializers.CharField(allow_blank=True, max_length=100)
body = serializers.CharField(allow_blank=True)
created = serializers.DateTimeField()
updated = serializers.DateTimeField()
序列化类看起来与 Django 的 Form
表单类非常的类似。它指定了接口数据中各个字段的具体类型,自动对请求和响应中的数据进行序列化和反序列化的转换。其底层实现逻辑已经由 DRF 框架封装好了,在入门阶段通常不需要你操心。
最后将各级 urls.py
配置好:
# drf_vue_blog/urls.py
...
urlpatterns = [
...
path('api/article/', include('article.urls', namespace='article')),
]
以及:
# article/urls.py
from django.urls import path
from article import views
app_name = 'article'
urlpatterns = [
path('', views.article_list, name='list'),
]
代码部分就完成了。
接下来创建一个管理员用户,并在后台中随意给 article
添加几个测试数据,启动服务器并在浏览器中访问 http://127.0.0.1:8000/api/article/
,可以看到页面中返回的 Json 字符串如下(稍作了排版):
[
{
"body": "Maybe say somthing here...",
"created": "2020-06-15T09:24:18Z",
"id": 1,
"title": "My first post",
"updated": "2020-06-15T09:24:38.622789Z"
},
{
"body": "Second test..",
"created": "2020-06-15T09:24:38Z",
"id": 2,
"title": "Another post",
"updated": "2020-06-15T09:24:58.253400Z"
},
{
"body": "Some content also..",
"created": "2020-06-15T09:24:58Z",
"id": 3,
"title": "Third article with awesome things",
"updated": "2020-06-15T09:25:25.602840Z"
}
]
虽然简陋,但是你已经成功完成了一个简单的接口。
创建管理员用户、在后台中添加数据是非常基础的内容,如果不清楚请参照 这篇文章。
如果你进入后台发现页面没有样式,那是因为静态文件未配置路由引起的。解决这个问题请参考 配置静态文件。
开发前端时会用到 npm(类似 Python 的包管理工具 Pip),这是 Node.js 官方提供的包管理工具。
所以准备工作的第一步,安装 Node.js,下载地址在官网,安装时基本就是一路 next。
完毕后打开命令行(依旧默认是 PowerShell),输入:
> npm -v
6.14.9
显示版本号就表示安装成功了。
npm 站点在国外,如果你遇到安装速度慢的问题,可以用指令
npm config set registry https://registry.npm.taobao.org
修改为国内镜像源。
接下来就可以安装 Vue 的命令行工具,它可以帮助我们方便的搭建 Vue 项目的骨架:
> npm install -g @vue/cli
# 这里省略一段神秘的安装文字...
> vue --version
@vue/cli 4.5.9
同样的,显示版本号就表示安装成功了。
深入了解见Vue-Cli文档。
如果运行命令报错"vue: 无法加载文件 C:\xxx\vue.ps1,因为在此系统上禁止运行脚本",则需要通过 PowerShell 解除
Execution_Policies
运行策略限制。方法见这里。
进入 Django 项目的根目录,用命令行工具搭建 Vue 骨架:
# 改为你的项目根路径
> cd D:\Developer\Py\drf_vue_blog
> vue create frontend
一定要选择安装 Vue 3:
前面说了,Vue 3 和 Vue 2 变化比较大,装错了后面章节的代码可能都跑不起来。
# 选择第二项,即安装 Vue3
Vue CLI v4.5.9
? Please pick a preset:
Default ([Vue 2] babel, eslint)
> Default (Vue 3 Preview) ([Vue 3] babel, eslint)
Manually select features
然后等待安装完成:
Vue CLI v4.5.9
Creating project in D:\Developer\Py\drf_vue_blog\frontend.
Installing CLI plugins. This might take a while...
...
added 1243 packages from 946 contributors in 22.141s
63 packages are looking for funding
run `npm fund` for details
Invoking generators...
Installing additional dependencies...
added 75 packages from 83 contributors in 9.281s
69 packages are looking for funding
run `npm fund` for details
Running completion hooks...
Generating README.md...
Successfully created project frontend.
Get started with the following commands:
$ cd frontend
$ npm run serve
出现这段文字说明 Vue 安装完成了。
与 Django 需要运行服务器类似,作为前后端分离的项目,在开发时前端同样也需要运行前端的服务器。
根据文字提示,进入 frontend
目录,运行 Vue 的开发服务器:
> cd frontend
> npm run serve
DONE Compiled successfully in 2134ms
App running at:
- Local: http://localhost:8080/
- Network: http://172.20.10.2:8080/
Note that the development build is not optimized.
To create a production build, run npm run build.
http://localhost:8080/
即可看到 Vue 的欢迎页面了。
进行后续章节的开发时,我们需要同时运行后端 http://127.0.0.1:8000/
和前端 http://localhost:8080/
两个服务器,别搞混了。
虽然现在前后端 Django + Vue 都有了,但还缺一个它们之间通信的手段。Vue 官方推荐的是 axios 这个前端库。
命令行进入 frontend
目录,安装 axios:
> npm install axios
喝口茶就安装完成了。
跨域问题是由于浏览器的同源策略(域名,协议,端口均相同)造成的,是浏览器施加的安全限制。说简单点,Vue 服务器端口(8080)和 Django 服务器端口(8000)不一致,因此无法通过 Javascript 代码请求后端资源。
解决办法有两种。
第一种方法是创建 frontend/vue.config.js
文件并写入:
module.exports = {
devServer: {
proxy: {
'/api': {
target: `http://127.0.0.1:8000/api`,
changeOrigin: true,
pathRewrite: {
'^/api': ''
}
}
}
}
};
这个 Vue 的配置文件给前端服务器设置了代理,即将 /api
地址的前端请求转发到 8000 端口的后端服务器去,从而规避跨域问题。
另一种方法是在后端引入 django-cors-middleware
这个库,在后端解决此问题。
此方法具体步骤百度很多,就不赘述了。
两种解决方法都可以,本文将选择第一种即前端代理的方法。
npm install element-plus --save
# 进入mian.ts,完整引入
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
app.use(ElementPlus)
本教程假定读者已经具有了 Javascript
/ Html
/ Css
等前端基础知识,因此不会展开讲相关内容。但为了理解 Vue 的基本结构,让我们来看三个重要的文件。
此文件路径位于 frontend/public/index.html
,内容如下:
DOCTYPE html>
<html lang="en">
<head>
...
head>
<body>
...
<div id="app">div>
body>
html>
这个页面是整个前端工程提供 html 的入口,里面的 不过在前端工程化的思想中,我们很少会直接去写这类 此文件位于 它的作用就是把后续你要写的 Vue 组件挂载到刚才那个 如果你有些前端的初始化配置,都可以写到这里。 此文件位于 仔细看一下,这个文件似乎就是对应 Vue 的欢迎页面嘛! Vue 采用组件化的思想,把同一个组件的内容打包到一起。比如这个默认的 以上就是 Vue 项目三个重要的文件,而对入门者来说,最重要的就是各种 翻一翻 frontend 中的每个文件,感受 Vue 项目的结构。 接下来就实际尝试一下编写文章列表页面了,通常这也是博客的首页。 首先把 像前面说的一样,Vue 把同一个组件的 让我们先从 当一个 Vue 实例被创建时,它将 比方说上面代码的 Axios 自动将请求得到的 Json 数据转换为 JavaScript 对象,所以你可以直接调用接口里的数据了。 如果写好上述代码,访问 如果你之前学过 Django 内置的模板语法,那么 Vue 的模板语法就不难理解。元素块中以 注意,很巧的是 Vue 默认同样也用双花括号 这部分纯粹就是 顺利的话(别忘了前后端服务器都要启动),现在你的页面应该时这样子:(通过后台添加一些测试文章) 虽然很简陋,但是成功把文章列表数据渲染出来了。 继续给列表数据添加内容,比如显示后端辛辛苦苦开发的标签和创建时间: 标签 创建时间时 方法 刷新页面,可以看到标签和日期都成功显示出来了。 记得在后台中添加适当的标签数据哦。 博客网站有页眉和页脚才比较美观,因此继续添加这部分内容: 没有新知识,唯一需要注意的就是样式中的 现在你的博客页面是这样子的: 是不是有点博文列表的意思了? 因为 vue-router 会用到文章的 id 作为动态地址,所以对 Django 后端做一点小更改: 简单的把文章的 id 值增加到接口数据中。 接下来就正式开始配置前端路由了。 首先把 vue-router 加载到 Vue 实例中: 和 Vue 2 不同的是,挂载路由实例时 Vue 3 采用函数式的写法,变得更加美观了。 由于后续页面会越来越多,为了避免 新建 新增文章详情页面: 页面暂时只有个壳子,一会儿来添加实际功能。 修改 一套组合拳,App.vue 看起来干净多了。 这些都搞好了之后,新建 各模式的详细介绍看文档。 搞定这些后,修改首页的组件代码: 调用 vue-router 不再需要常规的 在 Vue 中,属性前面的冒号 有一个小问题是由于 router 内部机制,之前给 Router 骨架就搭建完毕了。此时点击首页的文章标题链接后,应该就顺利跳转到一个只有页眉页脚的详情页面了。 注意查看浏览器控制栏,有任何报错都表明代码不正确。html
文件。main.js
frontend/src/main.js
,内容如下:import {createApp} from 'vue'
import App from './App.vue'
createApp(App).mount('#app');
index.html
中。App.vue
frontend/src/App.vue
,内容如下:<template>
<img alt="Vue logo" src="./assets/logo.png">
<HelloWorld msg="Welcome to Your Vue.js App"/>
template>
<script>
import HelloWorld from './components/HelloWorld.vue'
export default {
name: 'App',
components: {
HelloWorld
}
}
script>
<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
style>
App.vue
文件,明显 标签就对应传统的
html
, 标签对应
javascript
, 标签对应了
css
。
这个就是一个封装好的组件,路径位于 frontend/src/components/HelloWorld.vue
。.vue
文件,这就是你最主要的写代码的地方。
文章列表
App.vue
中的默认代码都删掉,写入以下代码:
<template>
<div v-for="article in info.results" v-bind:key="article.url" id="articles">
<div class="article-title">
{{ article.title }}
div>
div>
template>
<script>
import axios from 'axios';
export default {
name: 'App',
data: function () {
return {
info: ''
}
},
mounted() {
axios
.get('/api/article')
.then(response => (this.info = response.data))
}
}
script>
<style>
#articles {
padding: 10px;
}
.article-title {
font-size: large;
font-weight: bolder;
color: black;
text-decoration: none;
padding: 5px 0 5px 0;
}
style>
template
/ script
/ style
打包到一起。 脚本看起。
script
data
对象返回的所有属性加入到 Vue 的响应式系统中。更神奇的是,当这些属性的值发生改变时,视图将会产生“响应”,即自动更新为新的值。data
中的 info
属性在初始化时赋值了一个空字符串。当 Vue 加载完成后调用了生命周期的钩子 mounted()
方法,通过 axios
向 Django 后端获取到文章列表数据并赋值给 info
后,页面中关联的部分也会立即随之更新,而不用你手动去操作页面元素,这就是响应式的好处。
http://localhost:8080/
看不到任何内容,请检查后端服务器是否已启动,并查看浏览器控制台确保没有跨域相关的报错。template
v
打头的属性即是 Vue 的模板语法标记。 v-for
即循环可迭代元素(info.results
对应后端数据的 json 结构。请对照后端接口进行理解。),v-bind:key
给定了循环中每个元素的主键,作用是方便 Vue 渲染时对元素进行识别。{{ }}
定义它所持有的数据对象。所以这里的双花括号和 Django 模板语法没有任何关系,千万别搞混了。style
css
了,也就是规定了页面各元素的大小、位置、颜色等样式,比较基础就不展开讲了。优化界面
<template>
...
<div v-for="...">
<div>
<span
v-for="tag in article.tags"
v-bind:key="tag"
class="tag"
>
{{ tag }}
span>
div>
<div class="article-title">...div>
<div>{{ formatted_time(article.created) }}div>
div>
template>
<script>
...
export default {
...
data: function () {...},
mounted() {...}, // 注意添加这个逗号!
methods: {
formatted_time: function (iso_date_string) {
const date = new Date(iso_date_string);
return date.toLocaleDateString()
}
}
}
script>
<style>
...
.tag {
padding: 2px 5px 2px 5px;
margin: 5px 5px 5px 0;
font-family: Georgia, Arial, sans-serif;
font-size: small;
background-color: #4e4e4e;
color: whitesmoke;
border-radius: 5px;
}
style>
tag
和文章标题类似,用 v-for
循环取值即可。article.created
由于需要格式化,则用到点新东西:方法(即methods,注意看 scripts
中对其的定义)。方法名为 formatted_time()
,功能很简单,即把 iso 日期转换为人类容易理解的日期显示形式。methods
既可以在脚本中直接调用,也可以在模板中通过标签属性或者花括号调用,非常方便。
页眉和页脚
<template>
<div id="header">
<h1>My Drf-Vue Blogh1>
<hr>
div>
<div v-for="..." id="articles">...div>
<div id="footer">
<p>dusaiphoto.comp>
div>
template>
<style>
#app {
font-family: Georgia, Arial, sans-serif;
margin-left: 40px;
margin-right: 40px;
}
#header {
text-align: center;
margin-top: 20px;
}
#footer {
position: fixed;
left: 0;
bottom: 0;
height: 50px;
width: 100%;
background: whitesmoke;
text-align: center;
font-weight: bold;
}
...
style>
#app
,它是由 Vue 自动挂载的,因此覆盖了整个页面的元素。配置Vue-router
npm install vue-router@4
# article/serializers.py
class ArticleBaseSerializer(serializers.HyperlinkedModelSerializer):
id = serializers.IntegerField(read_only=True)
...
Router
// frontend/src/main.ts
...
import router from './router'
createApp(App).use(router).mount('#app');
App.vue
越发臃肿,因此必须优化文件结构。frontend/src/views/
目录,用来存放现在及将来所有的页面文件。在此目录新建 Home.vue
文件,把之前的首页代码稍加修改搬运过来:
<template>
<BlogHeader/>
<ArticleList/>
<BlogFooter/>
template>
<script>
import BlogHeader from '@/components/BlogHeader.vue'
import BlogFooter from '@/components/BlogFooter.vue'
import ArticleList from '@/components/ArticleList.vue'
export default {
name: 'Home',
components: {BlogHeader, BlogFooter, ArticleList}
}
script>
<template>
<BlogHeader/>
<BlogFooter/>
template>
<script>
import BlogHeader from '@/components/BlogHeader.vue'
import BlogFooter from '@/components/BlogFooter.vue'
export default {
name: 'ArticleDetail',
components: {BlogHeader, BlogFooter}
}
script>
App.vue
:
<template>
<router-view/>
template>
<script>
export default {
name: 'App'
}
script>
<style>
...
style>
App.vue
文件中大部分内容都搬走了,只剩一个新增的
标签,它就是各路径所代表的页面的实际渲染位置。比如你现在在 Home 页面,那么
则渲染的是 Home 中的内容。
frontend/src/router/index.js
文件用于存放路由相关的文件,写入:// frontend/src/router/index.js
import {createWebHistory, createRouter} from "vue-router";
import Home from "@/views/Home.vue";
import ArticleDetail from "@/views/ArticleDetail.vue";
const routes = [
{
path: "/",
name: "Home",
component: Home,
},
{
path: "/article/:id",
name: "ArticleDetail",
component: ArticleDetail
}
];
const router = createRouter({
history: createWebHistory(),
routes,
});
export default router;
routes
定义了所有需要挂载到路由中的路径,成员为路径 url 、路径名和路径的 vue 对象。详情页面的动态路由采用冒号 :id
的形式来定义。createRouter()
创建 router。参数里的 history
定义具体的路由形式,createWebHashHistory()
为哈希模式(具体路径在 # 符号后面);createWebHistory()
为 HTML5 模式(路径中没有丑陋的 # 符号),此为推荐模式,但是部署时需要额外的配置。
<template>
<div v-for="...">
<router-link
:to="{ name: 'ArticleDetail', params: { id: article.id }}"
class="article-title"
>
{{ article.title }}
router-link>
...
div>
template>
...
标签了,而是
。:to
属性指定了跳转位置,注意看动态参数 id 是如何传递的。:
表示此属性被”绑定“了。”绑定“的对象可以是某个动态的参数(比如这里的 id 值),也可以是 Vue 所管理的 data,也可以是 methods。总之,看到冒号就要明白这个属性后面跟着个变量或者表达式,没有冒号就是普通的字符串。冒号 :
实际上是 v-bind:
的缩写。
class="article-title"
写的 padding 样式会失效。解决方式是将其包裹在一个 div 元素中,在此 div 上重新定义 padding。想了解做法的见 Github 仓库中的源码。