前言
欢迎来到使用Django&Channels、Vue3&WebSocket实现一个即时聊天室教程的第二部分。本教程将花一些时间介绍聊天室的基础设计和具体实现,接着通过ajax技术将前后端接通,实现基本的前后端交互。
我们将使用现阶段较为流行的css框架--Tailwindcss来编写页面,它的出现使我们可以大量减少基础css代码的编写,十分方便。
本次教程的待办事项如下:
- 初始化vue项目
- 编写静态页面
- 配置路由
- 前后端交互
- pinia的配置与使用
初始化vue项目
使用命令创建vue3项目
npm init vue@latest
在提示下填下项目名和相关配置:
此时vue项目已经创建完成,接下来我们可以启动项目查看
cd client
yarn install
Tips
为了避免与其他项目在同时运行时出现冲突,我们可以在vite.config.js中设置指定端口:
export default defineConfig({
plugins: [vue()],
// 设置端口号
server: {
port: 3000,
},
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
},
},
})
修改完成之后,使用命令启动项目:
yarn dev
启动完成后,在http://localhost:3000下会看到如下页面:
看到这个页面,说明vue项目已经成功创建。
编写页面
在进行下一步之前,简单描述一下我们接下来要编写的页面。
- HomeView:主页
- LoginView:登录页
- SignupView:注册页
- NavBar:导航栏
本部分所需的页面就是这些,后面随着课程的推进会有变动。
开始编写页面之前,我们先安装一下Tailwind CSS
安装Tailwind CSS
安装TailwindCSS以及其依赖项
yarn add tailwindcss postcss autoprefixer
生成配置文件
npx tailwindcss init -p
修改配置文件tailwind.config.js
设置content内的内容,您可以根据自己的开发需求添加:
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ['./index.html', './src/**/*.{vue,js}'],
theme: {
extend: {},
},
plugins: [],
}
修改src/assets中的main.css
@tailwind base;
@tailwind components;
@tailwind utilities;
这个文件后面会用到,现在先保持初始配置。
上述配置完成后,tailwindcss就可以使用了。接下来就是编写页面了。
准备工作
编写页面之前,先把项目原有的页面和组件清除,换上我们即将要开发的内容。
清理完成之后,文件结构应该是这样:
📦 client
┣ 📂.vscode
┣ 📂node_modules
┣ 📂public
┣ 📂src
┃ ┣ 📂assets
┃ ┃ ┣ 📜logo.svg
┃ ┃ ┗ 📜main.css
┃ ┣ 📂components
┃ ┃ ┗ 📜Navbar.vue
┃ ┣ 📂router
┃ ┃ ┗ 📜index.js
┃ ┣ 📂views
┃ ┃ ┣ 📜HomeView.vue
┃ ┃ ┣ 📜LoginView.vue
┃ ┃ ┗ 📜SignUpView.vue
┃ ┣ 📜App.vue
┃ ┗ 📜main.js
┃ 📜.gitignore
┃ 📜index.html
┃ 📜package.json
┃ 📜postcss.config.js
┃ 📜README.md
┃ 📜tailwind.config.js
┃ 📜vite.config.js
┗ 📜yarn.lock
然后在main.css编写全局样式,为每个页面添加背景颜色:
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
body {
@apply bg-teal-600;
}
}
恭喜!准备工作已经完成,让我们开始编写页面吧!
HomeView
主页其实就只写了一个标题,稍微设置了一下移动端样式:
<template>
<div class="p-10 text-center lg:p-20">
<h1 class="text-3xl text-white lg:text-6xl">Django-chat</h1>
</div>
</template>
SignUpView
在这里编写了一个简单的表单,由标题、输入框和按钮组成。设置了一下移动端样式:
<template>
<div class="text-center p-10 lg:p-20">
<h1 class="text-3xl text-white lg:text-6xl">注册</h1>
</div>
<div class="px-4 mx-auto lg:w-1/4">
<div class="mb-5">
<input
type="text"
class="w-full mt-2 px-4 py-2 rounded-xl focus:outline-none"
placeholder="Email"
/>
</div>
<div class="mb-5">
<input
type="text"
class="w-full mt-2 px-4 py-2 rounded-xl focus:outline-none"
placeholder="用户名"
/>
</div>
<div class="mb-5">
<input
type="password"
class="w-full mt-2 px-4 py-2 rounded-xl focus:outline-none"
placeholder="密码"
/>
</div>
<div class="mb-5">
<input
type="password"
class="w-full mt-2 px-4 py-2 rounded-xl focus:outline-none"
placeholder="重复密码"
/>
</div>
<div id="err-msg" class="py-2 text-sm text-white"></div>
<div class="flex gap-5">
<button
class="px-5 py-3 rounded-xl text-white bg-teal-800 hover:bg-teal-700"
>
注册
</button>
<button
class="px-5 py-3 rounded-xl text-white bg-teal-800 hover:bg-teal-700"
>
已有账号?去登录
</button>
</div>
</div>
</template>
LoginView
登录页面与注册页面组成大致相同,这里不再赘述:
<template>
<div class="text-center p-10 lg:p-20">
<h1 class="text-3xl text-white lg:text-6xl">登录</h1>
</div>
<div class="px-4 mx-auto lg:w-1/4">
<div class="mb-5">
<input
type="text"
class="w-full mt-2 px-4 py-2 rounded-xl focus:outline-none"
placeholder="Email"
/>
</div>
<div class="mb-5">
<input
type="password"
class="w-full mt-2 px-4 py-2 rounded-xl focus:outline-none"
placeholder="密码"
/>
</div>
<div class="flex gap-5">
<button
class="px-5 py-3 rounded-xl text-white bg-teal-800 hover:bg-teal-700"
>
登录
</button>
<button
class="px-5 py-3 rounded-xl text-white bg-teal-800 hover:bg-teal-700"
>
没有账号?去注册
</button>
</div>
</div>
</template>
NavBar
我们还需要制作一个导航栏,可以在其中添加登录、注册等页面链接。由于页面构成不难,在这里直接给出代码:
<template>
<nav class="flex items-center justify-between px-4 py-6 bg-teal-800">
<div>
<router-link to="/" class="text-xl text-white">Django-Chat</router-link>
</div>
<div class="flex items-center space-x-4">
<router-link to="/login" class="text-white hover:text-gray-200"
>登录</router-link
>
<router-link
to="/signup"
class="px-5 py-3 rounded-xl text-white bg-teal-600 hover:bg-teal-700"
>注册</router-link
>
</div>
<div class="flex items-center space-x-4">
<router-link to="/rooms" class="text-white hover:text-gray-200"
>房间</router-link
>
<button
class="px-5 py-3 rounded-xl text-white bg-teal-600 hover:bg-teal-700"
>
登出
</button>
</div>
</nav>
</template>
将其添加至App.vue:
<script setup>
import Navbar from './components/Navbar.vue'
</script>
<template>
<Navbar />
<RouterView />
</template>
配置路由
vue-router的配置比较简单,我们直接根据我们已经编写好的页面配置即可,配置如下:
import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '../views/HomeView.vue'
import LoginView from '../views/LoginView.vue'
import SignUpView from '../views/SignUpView.vue'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
name: 'home',
component: HomeView,
},
{
path: '/login',
name: 'login',
component: LoginView,
},
{
path: '/signup',
name: 'signup',
component: SignUpView,
},
],
})
export default router
前后端交互
提起前后端交互,不得不提到Axios:
Axios 是一个基于 promise 网络请求库,作用于node.js 和浏览器中。 它是 isomorphic 的(即同一套代码可以运行在浏览器和node.js中)。在服务端它使用原生 node.js http 模块, 而在客户端 (浏览端) 则使用 XMLHttpRequests。
我们将使用Axios连通前后端。
安装与封装axios
首先让我们安装axios
yarn add axios
安装完成后,我们需要封装一下axios。封装axios的好处有很多,比如提高代码的可维护性、可扩展性和复用性等。我们在src中创建文件夹composables,创建文件useAxios.js文件用于各组件可以全局调用axios,封装的代码如下:
import axios from 'axios'
function useAxios() {
const instance = axios.create({
baseURL: 'http://127.0.0.1:8000/',
timeout: 5000,
})
instance.interceptors.request.use(
(config) => {
const token = localStorage.getItem('user.token')
if (token) {
config.headers['token'] = 'Token ' + token
}
return config
},
(error) => {
return Promise.reject(error)
}
)
instance.interceptors.response.use(
(response) => {
return response.data
},
(error) => {
if (error.response.status == 400) {
return error.response.data
}
return Promise.reject(error)
}
)
return instance
}
export default useAxios
封装主要是为了给每个请求统一请求地址和请求超时时间,简单设置了两个拦截器:
- 请求拦截器:用于在每个请求被发送之前添加额外的配置或标头。在这里,你检查了localStorage中是否存在用户令牌(token),如果有,就将它添加到请求头中。
- 响应拦截器:用于在接收到响应后对其进行处理。在成功的情况下,请求只返回响应的数据部分(response.data),以使调用者只获取数据而不是整个响应对象。在错误的情况下,检查了错误的响应状态,如果是400错误,返回了错误的响应数据(error.response.data)。
配置后端跨域
在发起请求之前,我们需要返回后端代码配置跨域,否则将无法成功发起请求。
让我们回到后端,安装并配置django-cors-headers
在后端输入以下命令:
# server/
env\Scripts\activate
pip install django-cors-headers
接着在server/chat_api/settings.py里配置跨域
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"accounts",
"rest_framework",
"rest_framework.authtoken",
# 跨域
"corsheaders",
]
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
# 放的位置要注意
"corsheaders.middleware.CorsMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
]
CORS_ALLOWED_ORIGINS = ["http://localhost:3000"] # 前端地址
CORS_ALLOW_METHODS = [
"DELETE",
"GET",
"OPTIONS",
"PATCH",
"POST",
"PUT",
]
CORS_ALLOW_HEADERS = [
"accept",
"accept-encoding",
"authorization",
"content-type",
"dnt",
"origin",
"user-agent",
"x-csrftoken",
"x-requested-with",
]
这样就配置完成了。
实现注册功能
在SignUpView里添加JS代码,实现前后端交互,修改后的代码如下:
<template>
<div class="text-center p-10 lg:p-20">
<h1 class="text-3xl text-white lg:text-6xl">注册</h1>
</div>
<div class="px-4 mx-auto lg:w-1/4">
<div class="mb-5">
<input
type="text"
class="w-full mt-2 px-4 py-2 rounded-xl focus:outline-none"
placeholder="Email"
v-model="user.email"
/>
</div>
<div class="mb-5">
<input
type="text"
class="w-full mt-2 px-4 py-2 rounded-xl focus:outline-none"
placeholder="用户名"
v-model="user.username"
/>
</div>
<div class="mb-5">
<input
type="password"
class="w-full mt-2 px-4 py-2 rounded-xl focus:outline-none"
placeholder="密码"
v-model="user.password"
/>
</div>
<div class="mb-5">
<input
type="password"
class="w-full mt-2 px-4 py-2 rounded-xl focus:outline-none"
placeholder="重复密码"
v-model="user.repassword"
/>
</div>
<div ref="errMsg" class="py-2 text-sm text-white"></div>
<div class="flex gap-5">
<button
@click="signup"
class="px-5 py-3 rounded-xl text-white bg-teal-800 hover:bg-teal-700"
>
注册
</button>
<button
@click="toLogin"
class="px-5 py-3 rounded-xl text-white bg-teal-800 hover:bg-teal-700"
>
已有账号?去登录
</button>
</div>
</div>
</template>
<script setup>
import { reactive, ref } from 'vue'
import { useRouter } from 'vue-router'
import useAxios from '../composables/useAxios'
const router = useRouter()
const axios = useAxios()
const user = reactive({
email: '',
username: '',
password: '',
repassword: '',
})
const errMsg = ref(null)
async function signup() {
if (user.repassword !== user.password) {
errMsg.value.innerHTML = '两次密码不一致,请重新输入!'
errMsg.value.classList.add(
'mb-5',
'text-center',
'bg-red-500',
'rounded-lg'
)
return
} else if (
user.email.length == 0 ||
user.username.length == 0 ||
user.password.length == 0 ||
user.repassword.length == 0
) {
errMsg.value.innerHTML = '表单未填写完整!'
errMsg.value.classList.add(
'mb-5',
'text-center',
'bg-red-500',
'rounded-lg'
)
return
} else {
errMsg.value.innerHTML = ''
errMsg.value.classList.add('hidden')
}
const userForm = new FormData()
userForm.append('email', user.email)
userForm.append('username', user.username)
userForm.append('password', user.password)
await axios.post('auth/signup/', userForm).then((res) => {
const error = res.errors && res.errors.length > 0 ? res.errors[0] : null
if (error) {
errMsg.value.innerHTML = '该邮箱已被使用!'
errMsg.value.classList.remove('hidden')
errMsg.value.classList.add(
'mb-5',
'text-center',
'bg-red-500',
'rounded-lg'
)
return
} else {
router.push({ name: 'login' })
}
})
}
function toLogin() {
router.push({ name: 'login' })
}
</script>
这里主要说说JS部分。这里通过调用errMsg元素做了一个简单的表单验证,给该元素在不同情况下显示出不同的状态。然后通过使用封装好的axios发起请求,根据读取请求的返回值判断并执行请求成功或失败后的相关逻辑。成功就跳转登录,失败就返回错误并显示在页面上。
实现登录功能
登录功能与注册功能相似,在LoginView.vue中编写以下代码:
<template>
<div class="text-center p-10 lg:p-20">
<h1 class="text-3xl text-white lg:text-6xl">登录</h1>
</div>
<div class="px-4 mx-auto lg:w-1/4">
<div class="mb-5">
<input
type="text"
class="w-full mt-2 px-4 py-2 rounded-xl focus:outline-none"
placeholder="Email"
v-model="user.email"
/>
</div>
<div class="mb-5">
<input
type="password"
class="w-full mt-2 px-4 py-2 rounded-xl focus:outline-none"
placeholder="密码"
v-model="user.password"
/>
</div>
<div class="flex gap-5">
<button
@click="login"
class="px-5 py-3 rounded-xl text-white bg-teal-800 hover:bg-teal-700"
>
登录
</button>
<button
@click="toRegister"
class="px-5 py-3 rounded-xl text-white bg-teal-800 hover:bg-teal-700"
>
没有账号?去注册
</button>
</div>
</div>
</template>
<script setup>
import { reactive } from 'vue'
import { useRouter } from 'vue-router'
import useAxios from '../composables/useAxios'
const router = useRouter()
const axios = useAxios()
const user = reactive({
email: '',
password: '',
})
function toRegister() {
router.push({ name: 'signup' })
}
async function login() {
const userForm = new FormData()
userForm.append('email', user.email)
userForm.append('password', user.password)
await axios.post('auth/login/', userForm).then((res) => {
console.log('res :>> ', res)
localStorage.setItem('user.token', res.user.token)
router.push({ name: 'home' })
})
}
</script>
这里主要是通过设置浏览器存储的方式记录来确定登录状态,这种方式并不是很好,后面我们会使用pinia来对用户状态进行缓存。
实现登出功能
实现登出功能也非常简单,由于后面要通过pinia来控制缓存,这里先把使用本地存储进行登出的代码先贴出来供大家参考:
function logout() {
localStorage.setItem('user.token', '')
router.push({ name: 'home' })
}
pinia的使用
设置全局状态
由于在创建vue项目的时候已经安装好pinia了,现在我们直接开始设置用户的状态:
import { defineStore } from 'pinia'
export const useUserStore = defineStore({
id: 'user',
state: () => ({
user: {
isAuthenticated: false,
id: null,
name: null,
email: null,
token: null,
},
}),
actions: {
initStore() {
console.log('initStore')
const token = localStorage.getItem('user.token')
if (token) {
this.user.token = localStorage.getItem('user.token')
this.user.email = localStorage.getItem('user.email')
this.user.id = localStorage.getItem('user.id')
this.user.name = localStorage.getItem('user.name')
this.user.isAuthenticated = true
console.log('用户' + this.user.name + '登录成功')
}
},
setToken(data) {
console.log('setToken', data)
this.user.token = data.token
this.user.email = data.email
this.user.name = data.name
this.user.id = data.user_id
this.user.isAuthenticated = true
localStorage.setItem('user.token', data.token)
localStorage.setItem('user.email', data.email)
localStorage.setItem('user.name', data.name)
localStorage.setItem('user.id', data.user_id)
console.log('用户的token已设置:' + localStorage.getItem('user.token'))
},
removeToken(user) {
console.log('removeToken')
this.user.token = null
this.user.email = null
this.user.name = null
this.user.id = null
this.user.isAuthenticated = null
localStorage.setItem('user.token', '')
localStorage.setItem('user.email', '')
localStorage.setItem('user.name', '')
localStorage.setItem('user.id', '')
},
},
})
代码比较清晰,理解起来不难。我直接简述一下这个文件的内容干了些啥:
states:存放当前用户状态,有以下字段:
user.id,用户的IDuser.email,用户的账号(邮箱)user.username,用户的昵称user.token,用户的验证令牌user.isAuthenticated,判断用户是否处于登录状态
actions:定义了一些方法用于操作用户的状态,有以下方法:
initStore:初始化状态,确认用户是否登录setToken:用户登录的时候执行,添加用户相关信息的同时设置用户为在线状态removeToken:用户登出的时候执行,移除用户相关信息的同时设置用户为离线状态
在组件内使用
LoginView中的登录函数
async function login() {
const userForm = new FormData()
userForm.append('email', user.email)
userForm.append('password', user.password)
await axios.post('auth/login/', userForm).then((res) => {
console.log('res :>> ', res)
// 使用pinia设置全局状态
userStore.setToken(res.user)
router.push({ name: 'home' })
})
}
Navbar中的导航切换
通过user.isAuthenticated的值完成导航栏的切换。
<template>
<nav class="flex items-center justify-between px-4 py-6 bg-teal-800">
<div>
<router-link to="/" class="text-xl text-white">Django-Chat</router-link>
</div>
<div
v-if="!userStore.user.isAuthenticated"
class="flex items-center space-x-4"
>
<router-link to="/login" class="text-white hover:text-gray-200"
>登录</router-link
>
<router-link
to="/signup"
class="px-5 py-3 rounded-xl text-white bg-teal-600 hover:bg-teal-700"
>注册</router-link
>
</div>
<div v-else class="flex items-center space-x-4">
<router-link to="/rooms" class="text-white hover:text-gray-200"
>房间</router-link
>
<button
@click="logout"
class="px-5 py-3 rounded-xl text-white bg-teal-600 hover:bg-teal-700"
>
登出
</button>
</div>
</nav>
</template>
<script setup>
import { useRouter } from 'vue-router'
import { useUserStore } from '../stores/user'
const router = useRouter()
const userStore = useUserStore()
function logout() {
userStore.removeToken()
router.push({ name: 'home' })
}
</script>
App.vue的全局状态
使用initStore管理Vue应用的全局状态,它可以发挥以下作用:
- 初始化用户数据
- 保持用户登录状态
- 避免频繁登录
- ...
下面看看pinia在App.vue的应用
<script setup>
import Navbar from './components/Navbar.vue'
import { useUserStore } from './stores/user'
const userStore = useUserStore()
userStore.initStore()
</script>
<template>
<Navbar />
<RouterView />
</template>
以上配置完成之后,我们现在的应用程序已经可以实现前后端交互了!
写在最后
下一部分将是本教程的重点内容:channels的使用,它是如何处理并实现即时通信的?它在与websocket的碰撞中又会擦出什么样的火花?各位看官别走开,后面的教程为您揭晓!
本章节的代码:github