学习使用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

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

 

posted @ 2023-04-07 17:14  magicduan  阅读(655)  评论(0编辑  收藏  举报