学习使用VUE3+Django+GraphQL实现简单的Blog网站
这周每天花点时间学习使用VUE3+Django+GraphQL的使用,按照RealPython的网站的教程走了一遍,踩了一遍坑.
Realpython上的教程使用的是Vue2的Vue-CLI模块,Vue本身已经进化到VUE3,并且推荐使用Vite代替Vue-CLI.我按照教程上的步骤将代码转化为VUE3+Vite+Composition API模式.
在这里重新整理一下教程,将遇见的坑也整理如下: 原英文的URL在这里 Build a Blog Using Django, Vue, and GraphQL(https://realpython.com/python-django-blog/)
这里的代码可以在Github上找到 https://github.com/magicduan/django_vue_graphql/releases/tag/base_0.1
Step1 : Setup a Django Blog
安装Django
Python环境下执行 pip install Django
生成Django Backend Project
django-admin startproject backend .
django-admin将生成Django backend的项目:
目录结果如下:
dvg
└── backend
├── manage.py
├── requirements.txt
└── backend
├── __init__.py
├── asgi.py
├── settings.py
├── urls.py
└── wsgi.py
Run Django Migrate
进入 backend目录
python manage.py migrate
Create Super User
python manage.py createsuperuser
启动Django Server,检查第一步成果
python manage.py runserver
在浏览器中访问http://localhost:8000,和http://localhost:8000/admin 确认Django Server已经正常运行.
## 我使用的是vscode的开发环境,在vscode中创建python的virtual enviroment.
Step 2: Create the Django Blog Admin
创建Django Blog App
python manage.py startapp blog
命令执行后的blog目录结构如下:
blog
├── __init__.py
├── admin.py
├── apps.py
├── migrations
│ └── __init__.py
├── models.py
├── tests.py
└── views.py
在Backend Project中装载blog App
修改backend的setting.py的INSTALL_APPS,插入 “blog”
INSTALLED_APPS = [ ... "blog", ]
创建Blog数据Model
- Profile Model: 用于记录Blog用户信息
- Tag Model:用于Blog的标签
- Posts:发表的Blog
修改blog下的models.py, 修改内容如下:
import Django模块
from django.db import models from django.conf import settings
Profile Model
class Profile(models.Model): user = models.OneToOneField( settings.AUTH_USER_MODEL, on_delete=models.PROTECT, ) website = models.URLField(blank=True) bio = models.CharField(max_length=240, blank=True) def __str__(self): return self.user.get_username()
Tag Model
class Tag(models.Model): name = models.CharField(max_length=50, unique=True) def __str__(self): return self.name
Posts Model
class Post(models.Model): class Meta: ordering = ["-publish_date"] title = models.CharField(max_length=255, unique=True) subtitle = models.CharField(max_length=255, blank=True) slug = models.SlugField(max_length=255, unique=True) body = models.TextField() meta_description = models.CharField(max_length=150, blank=True) date_created = models.DateTimeField(auto_now_add=True) date_modified = models.DateTimeField(auto_now=True) publish_date = models.DateTimeField(blank=True, null=True) published = models.BooleanField(default=False) author = models.ForeignKey(Profile, on_delete=models.PROTECT) tags = models.ManyToManyField(Tag, blank=True)
将Blog数据Model加入admin模块实现数据模块的数据的增删修改等操作
修改blog/amdin.py
Profile和Tag模块数据项较少,直接使用系统的内容,加入如下代码:
@admin.register(Profile) class ProfileAdmin(admin.ModelAdmin): model = Profile @admin.register(Tag) class TagAdmin(admin.ModelAdmin): model = Tag
Posts的内容比较多,对admin的显示内容进行简单定制:
@admin.register(Post) class PostAdmin(admin.ModelAdmin): model = Post list_display = ( "id", "title", "subtitle", "slug", "publish_date", "published", ) list_filter = ( "published", "publish_date", ) list_editable = ( "title", "subtitle", "slug", "publish_date", "published", ) search_fields = ( "title", "subtitle", "slug", "body", ) prepopulated_fields = { "slug": ( "title", "subtitle", ) } date_hierarchy = "publish_date" save_on_top = True
将Blog的Model数据Migrate到数据库中
python manage.py makemigrations
python manage.py migrate
至此Blog的数据输入部分已经在Django上已经实现了. 在Browser上进入 http://localhost:8000/admin中可以对Profile,Posts,Tag进行对应的增删修改等操作
Step3 配置GraphQL
安装Django GraphQL模块 Graphene-Django
pip install graphene-django
将graphene-django模块加载到Django的setting.py的INSTALL_APP中
INSTALLED_APPS = [ ... "blog", "graphene_django", ]
配置Graphene-Django
- 在setting.py中加入Graphene-Django的scheme的配置
GRAPHENE = { "SCHEMA": "blog.schema.schema", }
- 配置GprahQL的URL
修改backend/urls.py 的urlpatterns
from django.views.decorators.csrf import csrf_exempt from graphene_django.views import GraphQLView
urlpatterns = [ ... path("graphql", csrf_exempt(GraphQLView.as_view(graphiql=True))), ]
- 在blog目录下创建文件schema.py, 配置GraphQL的schema
from django.contrib.auth import get_user_model from graphene_django import DjangoObjectType from blog import models
GraphQL Model对应Blog的数据Model
class UserType(DjangoObjectType): class Meta: model = get_user_model() class AuthorType(DjangoObjectType): class Meta: model = models.Profile class PostType(DjangoObjectType): class Meta: model = models.Post class TagType(DjangoObjectType): class Meta: model = models.Tag
生成GraphQL需要的query
class Query(graphene.ObjectType): all_posts = graphene.List(PostType) author_by_username = graphene.Field(AuthorType, username=graphene.String()) post_by_slug = graphene.Field(PostType, slug=graphene.String()) posts_by_author = graphene.List(PostType, username=graphene.String()) posts_by_tag = graphene.List(PostType, tag=graphene.String()) def resolve_all_posts(root, info): return ( models.Post.objects.prefetch_related("tags") .select_related("author") .all() ) def resolve_author_by_username(root, info, username): return models.Profile.objects.select_related("user").get( user__username=username ) def resolve_post_by_slug(root, info, slug): return ( models.Post.objects.prefetch_related("tags") .select_related("author") .get(slug=slug) ) def resolve_posts_by_author(root, info, username): return ( models.Post.objects.prefetch_related("tags") .select_related("author") .filter(author__user__username=username) ) def resolve_posts_by_tag(root, info, tag): return ( models.Post.objects.prefetch_related("tags") .select_related("author") .filter(tags__name__iexact=tag) )
link前面的blog.schema.schema的变量
schema = graphene.Schema(query=Query)
查看GraphQL配置效果
在浏览器上进入http://localhost:8000/graphql 将进入Graphql的Web执行界面.
输入allPosts的Query可以取得all posts的json数据
{
allPosts {
title
subtitle
author {
user {
username
}
}
tags {
name
}
}
}
Step4 配置Backend Server的访问允许模块Django-cors-headers
由于Backend与Frontend是不同的服务,端口也是不同,为了防止浏览器组织不同源的数据访问,需要在backend端安装Django-cors-headers模块允许来自Frontend的访问.
pip install django-cors-headers
在setting.py的INSTALL_APP中加载Django-cors-headers模块
INSTALLED_APPS = [ ... "corsheaders", ]
在“corsheaders.middleware.CorsMiddleware“加入middleware
MIDDLEWARE = [ "corsheaders.middleware.CorsMiddleware", ... ]
配置Django-cors-headers
CORS_ORIGIN_ALLOW_ALL = False CORS_ORIGIN_WHITELIST = ("http://localhost:8080",)
至此Backend端的配置已经完成,下面我们开始配置Frontend的Vue
Step5 安装Vue
这里开始我们使用Vue3 + Vite + Composition API来实现Vue的Frontend开发,与原英文版的配置等操作开始有些不同
参考 vue的官方安装文档https://vuejs.org/guide/quick-start.html
安装node.js
下载node.js, 正常安装node.js
生成frontend Vue Project
npm init vue@latest
这条命令执行后安装命令提示进行选择
✔ Project name: … <your-project-name> ✔ Add TypeScript? … No / Yes ✔ Add JSX Support? … No / Yes ✔ Add Vue Router for Single Page Application development? … No / Yes ✔ Add Pinia for state management? … No / Yes ✔ Add Vitest for Unit testing? … No / Yes ✔ Add Cypress for both Unit and End-to-End testing? … No / Yes ✔ Add ESLint for code quality? … No / Yes ✔ Add Prettier for code formatting? … No / Yes
其中project name输入 frontend, TypeScript:Yes, JSX:No; Vue Router:Yes; Pinia:No; Vitest:Yes; Cypress:Yes; ESLint:Yes; Prettier:Yes
输入后将创建一个frontend的Vue3 Project.
安装vscode的volar插件
在vscode中打开frontend Folder, 安装Vue的Volar插件,注意Volar插件与Vue2的Vetur插件有冲突,需要禁用 Vetur插件
启动Vue frontend 服务
cd frontend
npm install npm run dev
命令会提示frontend的端口,缺省端口是系统自选的,我的端口是5173,为了配置自己想要的端口可以在vscode中修改vite的配置文件,也可以执行启动时指定port
命令制定port 8080
npm run dev -- --port 8080
修改vite.config.ts文件,加入port相关代码, 在vscode terminal中运行npm run dev命令会将缺省的端口配置为8080
import { fileURLToPath, URL } from 'node:url' import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' // https://vitejs.dev/config/ export default defineConfig({ server:{ port:8080 }, plugins: [vue()], resolve: { alias: { '@': fileURLToPath(new URL('./src', import.meta.url)) } } })
在浏览器中进入: http://localhost:8080将看到确实的Vue的frontend页面
配置Router
修改frontend的src/router/index.ts配置router
加入author、post、allpost、tag的路由
import { createRouter, createWebHistory } from 'vue-router' import HomeView from '../views/HomeView.vue' const router = createRouter({ history: createWebHistory(import.meta.env.BASE_URL), routes: [ { path: '/', name: 'home', component: HomeView }, { path: '/about', name: 'about', // route level code-splitting // this generates a separate chunk (About.[hash].js) for this route // which is lazy-loaded when the route is visited. component: () => import('../views/AboutView.vue') }, { path: '/author/:username', name: 'AuthorView', component: ()=>import("../views/AuthorView.vue") }, { path: '/post/:slug', name: 'PostView', component: ()=>import("../views/PostView.vue") }, { path: '/tag/:tag', name: 'PostByTag', component: ()=>import("../views/PostsByTag.vue") }, { path: '/posts', name: 'AllPosts', component: ()=>import("../views/AllPosts.vue") } ] }) export default router
Router已经加入了但是对应的view和component Vue文件还没有产生,npm run dev会报告对应的view不存在的错误,可以先忽略进入下一步.
Step7 编写Componet和View的代码
Vue提src下有compnents和views目录, components主要用来放重复使用的Vue Component, View主要对应的Browser页面
AuthorLink Component
AuthorLink这个Component我们主要用来根据传入的Author显示Author的信息,在src/components目录下创建AuthorLink.vue
<script setup lang="ts"> import {computed } from 'vue'; const props = defineProps({ author:{ type: Object, required:true} }) const displayName = computed(()=>{ return ( props.author.user.firstName && props.author.user.lastName && `${ props.author.user.firstName} ${props.author.user.lastName}` ) || `${props.author.user.username}` }) </script> <template> <router-link :to="`/author/${author.user.username}`" >{{ displayName }}</router-link> </template>
AuthorLink这个Component需要属性 author, 使用方法为 <AuthorLink :author="xxx" />
PostList Component
PostList Component(PostList.vue)用来显示所有Published Post. 具有属性:posts( Posts的数组), showAuthor(是否显示Author的Boolean)
<script setup lang="ts"> import AuthorLink from '../components/AuthorLink.vue' import {computed} from 'vue' const props = defineProps({ posts:{type:Array,required: false, default: true}, showAuthor: {type: Boolean,required: false,default: true} }) const publishedPosts = computed(()=>{ return props.posts.filter((post) => post.published) }) function displayableDate (date:string) { return new Intl.DateTimeFormat( 'en-US', { dateStyle: 'full' }, ).format(new Date(date)) } </script> <template> <div> <ol class="post-list"> <li class="post" v-for="post in publishedPosts" :key="post.title"> <span>{{ post.title }}</span> <span class="post__title"> <router-link :to="`/post/${post.slug}`" >{{ post.title }}: {{ post.subtitle }}</router-link> </span> <span v-if="showAuthor"> by <AuthorLink :author="post.author" /> </span> <div class="post__date">{{ displayableDate(post.publishDate) }}</div> <p class="post__description">{{ post.metaDescription }}</p> <ul> <li class="post__tags" v-for="tag in post.tags" :key="tag.name"> <router-link :to="`/tag/${tag.name}`">#{{ tag.name }}</router-link> </li> </ul> </li> </ol> </div> </template>
<style> .post-list { list-style: none; } .post { border-bottom: 1px solid #ccc; padding-bottom: 1rem; } .post__title { font-size: 1.25rem; } .post__description { color: #777; font-style: italic; } .post__tags { list-style: none; font-weight: bold; font-size: 0.8125rem; } </style>
在vscode中 post属于Object类型, post相关的会有红色的波浪线提示错误,实际执行中不会出错,可以忽略.
AllPosts View
在src/views下加入AllPosts(AllPosts.vue)显示所有的Posts,其中调用PostList Component
这里我们先不加入GraphQL代码,将allPosts设置为null, 后续在加入GraphQL的Query
<script setup lang="ts"> import PostList from '../components/PostList.vue' import { computed } from '@vue/reactivity'; const allPosts = computed(() =>{return null}) </script> <template> <div> <h2 >Recent Posts</h2> <PostList v-if="allPosts" :posts="allPosts" ></PostList> </div> </template>
PostsByTag View
PostByTag(PostsByTag.vue)根据Tag显示Posts列表
<script setup lang="ts"> import PostList from '../components/PostList.vue'
import { computed } from '@vue/reactivity';
const allPosts = computed(() =>{return null})
</script>
<template>
<div>
<h2 >Recent Posts</h2>
<PostList v-if="allPosts" :posts="allPosts" ></PostList>
</div>
</template>
AuthorView
AuthorView(src/views/AuthorView.vue)
<script setup lang="ts"> import PostList from '../components/PostList.vue' import {ref,computed} from 'vue' import { useRoute } from 'vue-router'; let p_username = useRoute().params.username const author = computed(() => null) function getDisplayName(){ if (author && author.value.user){ return ( author.value.user.firstName && author.value.user.lastName && `${author.value.user.firstName} ${author.value.user.lastName}` ) || `${author.value.user.username}` }else{ return "" } } </script> <template> <div v-if="author"> <h2>{{ getDisplayName() }}</h2> <a :href="author.website" target="_blank" rel="noopener noreferrer" >Website</a> <p>{{ author.bio }}</p> <h3>Posts by {{ getDisplayName() }}</h3> <PostList :posts="author.postSet" :showAuthor="false" /> </div> </template>
Post View
PostView(src/view/PostView.vue)
<script setup lang="ts"> import AuthorLink from '../components/AuthorLink.vue' import { useRoute } from 'vue-router' import { computed } from '@vue/reactivity' let postSlug = useRoute().params.slug const post = computed(()=>null) function displayableDate (date:string) { return new Intl.DateTimeFormat( 'en-US', { dateStyle: 'full' }, ).format(new Date(date)) } </script> <template> <div class="post" v-if="post"> <h2>{{ post.title }}: {{ post.subtitle }}</h2> By <AuthorLink :author="post.author" /> <div>{{ displayableDate(post.publishDate) }}</div> <p class="post__description">{{ post.metaDescription }}</p> <article> {{ post.body }} </article> <ul> <li class="post__tags" v-for="tag in post.tags" :key="tag.name"> <router-link :to="`/tag/${tag.name}`">#{{ tag.name }}</router-link> </li> </ul> </div> </template>
将Posts Link加入AppVue.vue中
AppVue.vue是Create Vue项目时自动生成的,我们加入Posts的Link就可以(红色的微新加入的posts的link)
<div class="wrapper">
<HelloWorld msg="You did it!" />
<nav>
<RouterLink to="/">Home</RouterLink>
<RouterLink to="/about">About</RouterLink>
<RouterLink to="/posts">posts</RouterLink>
</nav>
</div>
至此Vue的页面部分基本完成,还没有与Backend的相联系,我们下一步就是配置GraphQL相关的代码
Step8 获取Backend端的GraphQL数据
配置Vue-Apollo Client模块取Backend数据
Vue Apollo的Install参考Vue Apollo的官方文档
安装Apollo-Client: 在frontend目录下执行@apollo/client安装命令
npm install --save graphql graphql-tag @apollo/client
安装@vue/apollo-composable
npm install --save @vue/apollo-composable
配置Vue Apollo
在main.ts中加入下列代码:
import { DefaultApolloClient } from '@vue/apollo-composable' import { ApolloClient, createHttpLink, InMemoryCache } from '@apollo/client/core' // HTTP connection to the API const httpLink = createHttpLink({ uri: 'http://localhost:8000/graphql', }) // Cache implementation const cache = new InMemoryCache() // Create the apollo client const apolloClient = new ApolloClient({ link: httpLink, cache, }) const app = createApp({ setup () { provide(DefaultApolloClient, apolloClient) }, render: () => h(App), })
将原有的 app = createApp(App)替换为目前的代码
加入相关的GraphQL Query处理:
AllPosts Query
在AllPosts.vue中加入Query代码
import gql from 'graphql-tag'; import { useQuery } from '@vue/apollo-composable'; import { computed } from '@vue/reactivity'; const {result,loading,error} = useQuery(gql` query getAllPosts{ allPosts { title subtitle publishDate published metaDescription slug author { user { username firstName lastName } } tags { name } } } `) const allPosts = computed(() =>{return result.value?.allPosts})
注意result.value?.allPosts其中?.的处理,由于取数据是异步执行的,result会出现为undeifned的状态,所以使用?.来进行处理
在Template的代码中加入loading相关处理,防止数据没有被取出来是出现异常错误
<template> <div> <h2 v-if="loading">Loading....</h2> <div v-else> <h2 >Recent Posts</h2> <PostList v-if="allPosts" :posts="allPosts" ></PostList> </div> </div> </template>
Author Query
AuthorView.vue的script部分加入Author相关Query
let p_username = useRoute().params.username const {result,loading,error} = useQuery(gql` query getAuthorByUsername($username:String!){ authorByUsername(username: $username) { website bio user { firstName lastName username } postSet { title subtitle publishDate published metaDescription slug tags { name } } } } `,{username:p_username}) const author = computed(() => result.value?.authorByUsername)
template中如post类似加入loading相关处理
PostsByTag Query
PostsByTag.vue中加入PostsByTag的Query处理
import gql from 'graphql-tag'; import { useQuery } from '@vue/apollo-composable'; import { useRoute } from 'vue-router'; let p_tag = useRoute().params.tag const {result,loading,error} = useQuery(gql` query getPostsBytag($tag:String!){ postsByTag(tag: $tag) { title subtitle publishDate published metaDescription slug author { user { username firstName lastName } } tags { name } } }`, { tag: p_tag } ) const posts = computed(()=>result.value?.postsByTag)
Post Query
在PostView.vue中加入Post Query
import { useRoute } from 'vue-router' import { useQuery } from '@vue/apollo-composable' import gql from 'graphql-tag' import { computed } from '@vue/reactivity' let postSlug = useRoute().params.slug const {result,loading,error} = useQuery(gql` query getPostBySlug($slug: String!) { postBySlug(slug: $slug) { title subtitle publishDate metaDescription slug body author { user { username firstName lastName } } tags { name } } } `,{slug:postSlug}) const post = computed(()=>result.value?.postBySlug)
至此所有的代码配置结束,我们可以使用http://localhost:8000/admin增加Posts, Author,Tag等内容,然后通过http://localhost:8080查询相关的数据
对于Vue的调试,我试图使用vscode + Chrome进行Debug,用起来很是不顺,最后还是放弃了,直接在代码中用Console.log输出内容或者将取得数据通过{{ xxx }}显示到页面中查看结果
来的更方便一些, Console.log的内容在Chrome上也是通过Chrome的“视图-开发者-开发者工具”来进行查看, 可以在Chrome安装vue-devtool插件,目前还没发现有什么大用.