Ithy Logo

使用Vue与Django构建支持Markdown格式的博客网站

A Complete Beginner's Guide to Django - Part 6

项目概述

构建一个使用Vue.js与Django的博客网站,同时支持使用Markdown格式编写文章,是一个集成前后端技术的综合性项目。Vue.js提供了动态、响应式的前端用户界面,而Django则作为强大的后端框架,负责数据管理与API服务。Markdown的引入,使得内容创建更加简洁和高效。

技术栈选择

后端

  • Django:提供后端框架和数据管理。
  • Django Rest Framework (DRF):用于构建RESTful API。
  • django-cors-headers:解决跨域请求问题。
  • django-mdeditordjango-markdownx:支持Markdown内容编辑和存储。

前端

  • Vue.js:构建用户界面和单页应用(SPA)。
  • Vue Router:管理前端路由。
  • Axios:处理HTTP请求,与后端API交互。
  • mavon-editorvue-markdown-loader:集成Markdown编辑和渲染功能。

数据库

  • SQLite(默认)或其他关系型数据库如PostgreSQL。

后端设置:Django

1. 环境准备

确保已安装Python和pip。然后,创建并激活一个虚拟环境,并安装必要的依赖包:

bash
pip install django
pip install djangorestframework
pip install django-cors-headers
pip install markdown
pip install django-mdeditor  # 或者使用 django-markdownx
    

2. 创建Django项目和应用

启动Django项目并创建一个新的应用来管理博客文章:

bash
django-admin startproject blog_backend
cd blog_backend
python manage.py startapp blog
    

3. 配置项目设置

blog_backend/settings.py中,添加已安装的应用和配置中间件:

python
INSTALLED_APPS = [
    # Django默认应用
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',

    # 第三方应用
    'rest_framework',
    'corsheaders',
    'mdeditor',  # 如果使用 django-mdeditor
    # 'markdownx',  # 如果使用 django-markdownx

    # 自定义应用
    'blog',
]

MIDDLEWARE = [
    'corsheaders.middleware.CorsMiddleware',  # CORS中间件需放在最上面
    'django.middleware.common.CommonMiddleware',
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

# 配置CORS
CORS_ALLOW_ALL_ORIGINS = True  # 开发阶段允许所有来源,生产环境应具体设置

# 配置静态文件和媒体文件
STATIC_URL = '/static/'
MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media/')

# Django Rest Framework配置
REST_FRAMEWORK = {
    'DEFAULT_PERMISSION_CLASSES': [
        'rest_framework.permissions.AllowAny',
    ]
}
    

4. 定义数据模型

blog/models.py中定义博客文章相关的模型,包括分类、标签和文章内容:

python
from django.db import models
from mdeditor.fields import MDTextField  # 如果使用 django-mdeditor

class BlogCategory(models.Model):
    title = models.CharField(max_length=50, verbose_name='分类名称', default='')
    href = models.CharField(max_length=100, verbose_name='分类路径', default='')

    def __str__(self):
        return self.title

    class Meta:
        verbose_name = '文章分类'
        verbose_name_plural = '文章分类'

class Tag(models.Model):
    tag = models.CharField(max_length=20, verbose_name='标签')

    def __str__(self):
        return self.tag

    class Meta:
        verbose_name = '标签'
        verbose_name_plural = '标签'

class BlogPost(models.Model):
    title = models.CharField(max_length=200, verbose_name='文章标题', unique=True)
    category = models.ForeignKey(BlogCategory, blank=True, null=True, verbose_name='文章分类', on_delete=models.DO_NOTHING)
    is_top = models.BooleanField(default=False, verbose_name='是否置顶')
    is_hot = models.BooleanField(default=False, verbose_name='是否热门')
    summary = models.CharField(max_length=500, verbose_name='内容摘要', default='')
    content = MDTextField(verbose_name='内容')  # 使用Markdown字段
    views_count = models.IntegerField(default=0, verbose_name="查看数")
    comments_count = models.IntegerField(default=0, verbose_name="评论数")
    tags = models.ManyToManyField(to=Tag, related_name="tag_post", blank=True, verbose_name="标签")

    @property
    def tag_list(self):
        return ','.join([tag.tag for tag in self.tags.all()])

    def __str__(self):
        return self.title

    class Meta:
        verbose_name = '博客文章'
        verbose_name_plural = '博客文章'

class Site(models.Model):
    name = models.CharField(max_length=50, verbose_name='站点名称', unique=True)
    avatar = models.CharField(max_length=200, verbose_name='站点图标')
    slogan = models.CharField(max_length=200, verbose_name='站点标语')

    def __str__(self):
        return self.name

    class Meta:
        verbose_name = '站点信息'
        verbose_name_plural = '站点信息'
    

5. 注册模型到管理界面

blog/admin.py中注册模型,以便通过Django管理界面进行管理:

python
from django.contrib import admin
from .models import BlogCategory, Tag, BlogPost, Site

@admin.register(BlogCategory)
class BlogCategoryAdmin(admin.ModelAdmin):
    list_display = ['id', 'title', 'href']

@admin.register(Tag)
class TagAdmin(admin.ModelAdmin):
    list_display = ['id', 'tag']

@admin.register(BlogPost)
class BlogPostAdmin(admin.ModelAdmin):
    list_display = ['title', 'category', 'is_top', 'is_hot', 'views_count', 'comments_count']
    search_fields = ('title',)

@admin.register(Site)
class SiteAdmin(admin.ModelAdmin):
    list_display = ['name', 'avatar', 'slogan']
    

6. 数据库迁移

执行迁移命令以应用模型到数据库:

bash
python manage.py makemigrations
python manage.py migrate
    

7. 创建序列化器

blog/serializers.py中定义序列化器,将模型数据转换为JSON格式:

python
from rest_framework import serializers
from .models import BlogPost, BlogCategory, Tag, Site

class BlogCategorySerializer(serializers.ModelSerializer):
    class Meta:
        model = BlogCategory
        fields = ['id', 'title', 'href']

class TagSerializer(serializers.ModelSerializer):
    class Meta:
        model = Tag
        fields = ['id', 'tag']

class BlogPostSerializer(serializers.ModelSerializer):
    category = BlogCategorySerializer()
    tags = TagSerializer(many=True)

    class Meta:
        model = BlogPost
        fields = ['id', 'title', 'category', 'is_top', 'is_hot', 'summary', 'content', 'views_count', 'comments_count', 'tags']

class SiteSerializer(serializers.ModelSerializer):
    class Meta:
        model = Site
        fields = ['id', 'name', 'avatar', 'slogan']
    

8. 创建视图

blog/views.py中定义API视图,使用Django Rest Framework的泛型视图:

python
from rest_framework import generics
from .models import BlogPost, BlogCategory, Tag, Site
from .serializers import BlogPostSerializer, BlogCategorySerializer, TagSerializer, SiteSerializer

class BlogPostListCreateView(generics.ListCreateAPIView):
    queryset = BlogPost.objects.all()
    serializer_class = BlogPostSerializer

class BlogPostDetailView(generics.RetrieveUpdateDestroyAPIView):
    queryset = BlogPost.objects.all()
    serializer_class = BlogPostSerializer

class BlogCategoryListCreateView(generics.ListCreateAPIView):
    queryset = BlogCategory.objects.all()
    serializer_class = BlogCategorySerializer

class TagListCreateView(generics.ListCreateAPIView):
    queryset = Tag.objects.all()
    serializer_class = TagSerializer

class SiteDetailView(generics.RetrieveUpdateAPIView):
    queryset = Site.objects.all()
    serializer_class = SiteSerializer
    

9. 设置URL路由

blog/urls.py中定义应用的URL路由:

python
from django.urls import path
from .views import (
    BlogPostListCreateView,
    BlogPostDetailView,
    BlogCategoryListCreateView,
    TagListCreateView,
    SiteDetailView
)

urlpatterns = [
    path('posts/', BlogPostListCreateView.as_view(), name='post-list-create'),
    path('posts//', BlogPostDetailView.as_view(), name='post-detail'),
    path('categories/', BlogCategoryListCreateView.as_view(), name='category-list-create'),
    path('tags/', TagListCreateView.as_view(), name='tag-list-create'),
    path('site/', SiteDetailView.as_view(), name='site-detail'),
]
    

然后,在主项目的blog_backend/urls.py中包含应用的URL:

python
from django.contrib import admin
from django.urls import path, include
from django.conf import settings
from django.conf.urls.static import static

urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/', include('blog.urls')),
    path('mdeditor/', include('mdeditor.urls')),  # 如果使用 django-mdeditor
    # path('markdownx/', include('markdownx.urls')),  # 如果使用 django-markdownx
]

if settings.DEBUG:
    urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
    

10. 启动开发服务器

运行以下命令启动Django开发服务器:

bash
python manage.py runserver
    

访问http://localhost:8000/admin/,使用超级用户账户登录管理界面,管理博客内容。

前端设置:Vue.js

1. 创建Vue项目

使用Vue CLI创建一个新的Vue.js项目:

bash
npm install -g @vue/cli
vue create blog_frontend
cd blog_frontend
    

2. 安装必要的依赖

安装Axios用于HTTP请求,mavon-editor作为Markdown编辑器,以及Vue Router进行路由管理:

bash
npm install axios mavon-editor
npm install vue-router
    

3. 配置Vue Router

src/router/index.js中设置路由:

javascript
import Vue from 'vue';
import Router from 'vue-router';
import Home from '@/views/Home.vue';
import ArticleDetail from '@/views/ArticleDetail.vue';
import CreateArticle from '@/views/CreateArticle.vue';

Vue.use(Router);

export default new Router({
    mode: 'history',
    routes: [
        {
            path: '/',
            name: 'Home',
            component: Home
        },
        {
            path: '/article/:id',
            name: 'ArticleDetail',
            component: ArticleDetail
        },
        {
            path: '/create',
            name: 'CreateArticle',
            component: CreateArticle
        }
    ]
});
    

src/main.js中引入路由:

javascript
import Vue from 'vue';
import App from './App.vue';
import router from './router';
import mavonEditor from 'mavon-editor';
import 'mavon-editor/dist/css/index.css';

Vue.use(mavonEditor);

Vue.config.productionTip = false;

new Vue({
    router,
    render: h => h(App),
}).$mount('#app');
    

4. 创建Vue组件

a. Home.vue - 文章列表页面

src/views/Home.vue中实现文章列表:

vue





    

b. ArticleDetail.vue - 文章详情页面

src/views/ArticleDetail.vue中实现文章详情展示:

vue





    

c. CreateArticle.vue - 创建文章页面

src/views/CreateArticle.vue中实现文章创建功能:

vue





    

5. 集成Markdown编辑器和渲染

使用组件在创建文章页面提供Markdown编辑功能,在文章详情页将Markdown内容渲染为HTML。

a. 安装并配置mavon-editor

已在项目初始化阶段安装mavon-editor,并在src/main.js中引入和使用。

b. 使用v-html渲染Markdown内容

在文章详情页面,通过v-html指令将渲染后的HTML内容显示出来,同时确保内容的安全性已被后端处理:

vue

6. 运行Vue开发服务器

启动前端开发服务器:

bash
npm run serve
    

访问http://localhost:8080/,应能看到博客的首页,列出所有文章,并能通过链接访问详情页或创建新文章。

集成与运行项目

确保Django后端服务器(http://localhost:8000/)和Vue前端服务器(http://localhost:8080/)均已启动。前端通过Axios与后端API进行通信,实现数据的获取和提交。

安全性考虑

1. 防止XSS攻击

由于前端使用v-html渲染后端提供的HTML内容,需确保后端在存储和提供内容时,对Markdown内容进行了适当的过滤和清理,防止恶意脚本注入。

2. 配置CORS策略

settings.py中正确配置CORS,限制允许的前端域名,避免跨域安全问题。

3. 用户认证与授权

当前项目示例未涉及用户认证,建议在生产环境中添加JWT或其他认证机制,保护API端点,确保只有授权用户可以创建、编辑或删除文章。

功能扩展与最佳实践

1. 用户认证

引入Django的认证系统或使用django-rest-framework-simplejwt实现JWT认证,保护API并管理用户权限。

2. 分页与搜索功能

在后端API中添加分页支持,提高数据加载效率;在前端实现分页控件。此外,增加搜索功能,使用户能够按标题或内容搜索文章。

3. 富文本编辑器集成

如果需要更丰富的编辑功能,可以考虑集成更高级的Markdown编辑器,如vue3-markdown-editor,提供实时预览、语法高亮等功能。

4. 部署与优化

在生产环境中,需优化前后端的部署,使用Nginx或其他服务器代理,配置HTTPS,优化静态资源加载,确保网站的性能与安全。

完整代码示例

以下是关键文件的完整代码示例:

后端:blog/models.py

python
from django.db import models
from mdeditor.fields import MDTextField

class BlogCategory(models.Model):
    title = models.CharField(max_length=50, verbose_name='分类名称', default='')
    href = models.CharField(max_length=100, verbose_name='分类路径', default='')

    def __str__(self):
        return self.title

    class Meta:
        verbose_name = '文章分类'
        verbose_name_plural = '文章分类'

class Tag(models.Model):
    tag = models.CharField(max_length=20, verbose_name='标签')

    def __str__(self):
        return self.tag

    class Meta:
        verbose_name = '标签'
        verbose_name_plural = '标签'

class BlogPost(models.Model):
    title = models.CharField(max_length=200, verbose_name='文章标题', unique=True)
    category = models.ForeignKey(BlogCategory, blank=True, null=True, verbose_name='文章分类', on_delete=models.DO_NOTHING)
    is_top = models.BooleanField(default=False, verbose_name='是否置顶')
    is_hot = models.BooleanField(default=False, verbose_name='是否热门')
    summary = models.CharField(max_length=500, verbose_name='内容摘要', default='')
    content = MDTextField(verbose_name='内容')
    views_count = models.IntegerField(default=0, verbose_name="查看数")
    comments_count = models.IntegerField(default=0, verbose_name="评论数")
    tags = models.ManyToManyField(to=Tag, related_name="tag_post", blank=True, verbose_name="标签")

    @property
    def tag_list(self):
        return ','.join([tag.tag for tag in self.tags.all()])

    def __str__(self):
        return self.title

    class Meta:
        verbose_name = '博客文章'
        verbose_name_plural = '博客文章'

class Site(models.Model):
    name = models.CharField(max_length=50, verbose_name='站点名称', unique=True)
    avatar = models.CharField(max_length=200, verbose_name='站点图标')
    slogan = models.CharField(max_length=200, verbose_name='站点标语')

    def __str__(self):
        return self.name

    class Meta:
        verbose_name = '站点信息'
        verbose_name_plural = '站点信息'
    

后端:blog/serializers.py

python
from rest_framework import serializers
from .models import BlogPost, BlogCategory, Tag, Site

class BlogCategorySerializer(serializers.ModelSerializer):
    class Meta:
        model = BlogCategory
        fields = ['id', 'title', 'href']

class TagSerializer(serializers.ModelSerializer):
    class Meta:
        model = Tag
        fields = ['id', 'tag']

class BlogPostSerializer(serializers.ModelSerializer):
    category = BlogCategorySerializer()
    tags = TagSerializer(many=True)

    class Meta:
        model = BlogPost
        fields = ['id', 'title', 'category', 'is_top', 'is_hot', 'summary', 'content', 'views_count', 'comments_count', 'tags']

class SiteSerializer(serializers.ModelSerializer):
    class Meta:
        model = Site
        fields = ['id', 'name', 'avatar', 'slogan']
    

前端:src/router/index.js

javascript
import Vue from 'vue';
import Router from 'vue-router';
import Home from '@/views/Home.vue';
import ArticleDetail from '@/views/ArticleDetail.vue';
import CreateArticle from '@/views/CreateArticle.vue';

Vue.use(Router);

export default new Router({
    mode: 'history',
    routes: [
        {
            path: '/',
            name: 'Home',
            component: Home
        },
        {
            path: '/article/:id',
            name: 'ArticleDetail',
            component: ArticleDetail
        },
        {
            path: '/create',
            name: 'CreateArticle',
            component: CreateArticle
        }
    ]
});
    

前端:src/main.js

javascript
import Vue from 'vue';
import App from './App.vue';
import router from './router';
import mavonEditor from 'mavon-editor';
import 'mavon-editor/dist/css/index.css';

Vue.use(mavonEditor);

Vue.config.productionTip = false;

new Vue({
    router,
    render: h => h(App),
}).$mount('#app');
    

前端:src/views/Home.vue

vue





    

前端:src/views/ArticleDetail.vue

vue





    

前端:src/views/CreateArticle.vue

vue





    

参考资料

总结

通过上述步骤,您已成功搭建了一个基于Vue和Django的博客网站,支持使用Markdown格式编写和展示文章。前后端分离的架构增强了项目的可维护性和扩展性,Markdown的使用则简化了内容的编辑与管理。未来,您可以根据需求进一步优化和扩展功能,如集成用户认证、添加评论系统、实现文章分类与标签的高级管理等。


Last updated January 7, 2025
Search Again