学习使用VUE3+Django+GraphQL实现简单的Blog网站

  • 学习使用VUE3+Django+GraphQL实现简单的Blog网站已关闭评论
  • 226 次浏览
  • A+
所属分类:Web前端
摘要

这周每天花点时间学习使用VUE3+Django+GraphQL的使用,按照RealPython的网站的教程走了一遍,踩了一遍坑.

这周每天花点时间学习使用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

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

  1. Profile Model: 用于记录Blog用户信息
  2. Tag Model:用于Blog的标签
  3. 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

  import gql from 'graphql-tag';
  import { useQuery } from '@vue/apollo-composable';
  import { useRoute } from 'vue-router';

  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插件,目前还没发现有什么大用.