我们再来看一下管理类的设计。
Composition API,就是组合API的意思,那么是不是应该把js代码分离出来,做成独立的管理类的形式呢?
这样代码可以更整洁一些,主要是setup里面的代码就不会乱掉了。
管理类
import webSQLHelp from '../store/websql-help' import { blog, blogForm, blogList, articleList, discuss, discussList } from './blogModel' import blogStateManage from '../model/blogState' // 连接数据库 const help = new webSQLHelp('vite2-blog', 1.0, '测试用的博客数据库') // =====================数据库============================== /** * 建立 vite2-blog 数据库,blog表、discuss表 * @returns 建立数据库和表 */ export const databaseInit = () => { help.createTable('blog', blog()) help.createTable('discuss', discuss()) } /** * 删除:blog表、discuss表 * @returns 删除表 */ export const deleteBlogTable = () => { help.deleteTable('blog') help.deleteTable('discuss') } /** * 博客的管理类 * @returns 添加、修改、获得列表等 */ export const blogManage = () => { // =====================博文============================== /** * 添加新的博文 * @param { object } blog 博文 的 model * @return {*} promise,新博文的ID */ const addNewBlog = (blog) => { return new Promise((resolve, reject) => { const newBlog = {} Object.assign(newBlog, blog, { addTime: new Date(), // 添加时间 viewCount: 0, // 浏览量 agreeCount: 0, // 点赞数量 discussCount: 0 // 讨论数量 }) help.insert('blog', newBlog).then((id) => { resolve(id) }) }) } /** * 修改博文 * @param { object } blog 博文 的 model * @return {*} promise,修改状态 */ const updateBlog = (blog) => { return new Promise((resolve, reject) => { help.update('blog', blog, blog.ID).then((state) => { resolve(state) }) }) } /** * 根据博文ID获取博文,编辑博文、显示博文用 * @param { number } id 博文ID * @returns */ const getArtcileById = (id) => { return new Promise((resolve, reject) => { help.getDataById('blog', id).then((data) => { if (data.length > 0) { resolve(data[0]) } else { console.log('没有找到记录', data) resolve({}) } }) }) } /** * 依据分组ID获取博文列表,编辑博文列表用。 * @param {number} groupId 分组ID * @returns */ const getBlogListByGroupId = (groupId) => { return new Promise((resolve, reject) => { help.select('blog', articleList(), {groupId: [401, groupId]}) .then((data) => { resolve(data) }) }) } // 状态管理 const { getBlogState } = blogStateManage() const blogState = getBlogState() /** * 依据状态,分页查询博文 * @returns 博文列表 */ const getBlogList = () => { // 根据状态设置查询条件和分页条件 const _query = blogState.findQuery || {} _query.state = [401, 2] // 显示发布的博文,设置固定查询条件 return new Promise((resolve, reject) => { help.select('blog', blogList(), _query, blogState.page).then((data) => { resolve(data) }) }) } const getBlogCount = () => { // 根据状态设置查询条件和分页条件 const _query = blogState.findQuery || {} _query.state = [401, 2] // 显示发布的博文,设置固定查询条件 return new Promise((resolve, reject) => { help.getCountByWhere('blog', _query).then((count) => { resolve(count) }) }) } // =====================讨论============================== /** * 添加一个新讨论 * @param {object}} discuss 讨论的model * @returns */ const addDiuss = (discuss) => { return new Promise((resolve, reject) => { const newDiscuss = {} Object.assign(newDiscuss, discuss, { addTime: new Date(), // 添加时间 agreeCount: 0 // 点赞数量 }) help.insert('discuss', newDiscuss).then((id) => { resolve(id) }) }) } /** * 依据博文ID获取讨论列表。 * @param {number} blogId 分组ID * @returns */ const getDiscussListByBlogId = (blogId) => { return new Promise((resolve, reject) => { help.select('discuss', discussList(), {blogId: [401, blogId]}) .then((data) => { resolve(data) }) }) } return { addDiuss, // 添加新讨论 getDiscussListByBlogId, // 依据博文ID获取讨论列表。 addNewBlog, // 添加 新博文 updateBlog, // 修改博文 getArtcileById, // 根据博文ID获取博文 getBlogListByGroupId, // 获取博文列表 getBlogList, // 获取博文列表 getBlogCount // 统计数量 } } 复制代码
其实应该分成两个类,一个是博文的管理类,一个是讨论的管理类,以后还可以有分组的管理类。 现在因为讨论相关的只有两个函数,所以就没有分开。
把需要的功能集中起来,便于管理和复用,减少组件里面的代码,也便于代码的升级更换。 比如现在是把数据保存在前端的webSQL里面,那么以后要提交到后端怎么办? 只需要在这里改代码即可,不需要修改xxx.vue里面的代码。
把变化限制在最小的范围内。
编码
设计好了之后可以动手编码了,先看一下文件结构:
文件结构
个人感觉还是比较清晰的。
config设置
import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' // https://vitejs.dev/config/ export default defineConfig({ plugins: [vue()], base: '/vue3-blog/', // 修改发布网站的目录 build: { outDir: 'blog' // 修改打包的默认文件夹 } }) 复制代码
- base,设置发布网站的目录。
发布的时候默认项目会部署在网站根目录,如果不是根目录的话,可以使用 base 来更改。
- build.outDir
修改默认(dist)的构建输出路径。
其他设置方式可以看这里:cn.vitejs.dev/config/,内容非…
路由设置
src/router/index.js
import { createRouter, createWebHistory } from 'vue-router' import Home from '../views/home.vue' const routes = [ { path: '/', name: 'home', component: Home }, { path: '/write', name: 'write', component: () => import('../views/write.vue') }, { path: '/blogs/:id', name: 'blogs', props: true, component: () => import('../views/blog.vue') }, { path: '/groups/:groupId', name: 'groups', props: true, component: Home } ] const router = createRouter({ history: createWebHistory(), routes }) export default router 复制代码
除了 createWebHistory 的参数要去掉之外,没啥变化。 路由设置也很简单,只有首页、编写博文、博文详细、分组显示博文这四项。
网页入口
/index.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <link rel="icon" href="/favicon.ico" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>vite2 + vue3 做的简单的个人博客</title> </head> <body> <div id="app"></div> <script type="module" src="/src/main.js"></script> </body> </html> 复制代码
非常简洁,我们可以设置一个标题,用 type="module" 的方式加载入口js文件。其他的可以按照需要自行设置。
代码入口
/src/main.js
import { createApp, provide, reactive } from 'vue' import App from './App.vue' import router from './router' // 路由 // UI库 import ElementPlus from 'element-plus' import 'element-plus/lib/theme-chalk/index.css' import 'dayjs/locale/zh-cn' import locale from 'element-plus/lib/locale/lang/zh-cn' // Markdown 编辑插件 import VueMarkdownEditor from '@kangc/v-md-editor' import '@kangc/v-md-editor/lib/style/base-editor.css' import vuepressTheme from '@kangc/v-md-editor/lib/theme/vuepress.js' import '@kangc/v-md-editor/lib/theme/style/vuepress.css' VueMarkdownEditor.use(vuepressTheme) // markdown 显示插件 import VMdPreview from '@kangc/v-md-editor/lib/preview' import '@kangc/v-md-editor/lib/style/preview.css' // 引入你所使用的主题 此处以 github 主题为例 // import githubTheme from '@kangc/v-md-editor/lib/theme/github' VMdPreview.use(vuepressTheme) // 建立数据库 import { databaseInit, deleteBlogTable } from './model/blogManage' // deleteBlogTable() databaseInit() // 注入状态 import { blogState } from './model/blogState' const state = reactive(blogState) createApp(App) .provide('blogState', state) // 注入状态 .use(router) // 路由 .use(ElementPlus, { locale, size: 'small' }) // UI库 .use(VueMarkdownEditor) // markDown编辑器 .use(VMdPreview) // markDown 显示 .mount('#app') 复制代码
这里的代码稍微有点长,除了常规操作外,还使用了 MarkdownEditor 用于编辑博文,这个部分代码有点多。
然后又加入了设计webSQL数据库的代码,以及自己用 provide 实现的简易的状态管理。
首页、博文列表
模板部分:
<template> <!--博文列表--> <el-row :gutter="12"> <el-col :span="5"> <!--分组--> <blogGroup :isDetail="true"/> </el-col> <el-col :span="18"> <el-card shadow="hover" v-for="(item, index) in blogList" :key="'bloglist_' + index" > <template #header> <div class="card-header"> <router-link :to="{name:'blogs', params:{id:item.ID}}"> {{item.title}} </router-link> <span class="button">({{dateFormat(item.addTime).format('YYYY-MM-DD')}})</span> </div> </template> <!--简介--> <div class="text item" v-html="item.introduction"></div> <hr> <i class="el-icon-view"></i> {{item.viewCount}} <i class="el-icon-circle-check"></i> {{item.agreeCount}} <i class="el-icon-chat-dot-square"></i> {{item.discussCount}} </el-card> <!--没有找到数据--> <el-empty description="没有找到博文呢。" v-if="blogList.length === 0"></el-empty> <el-pagination background layout="prev, pager, next" v-model:currentPage="blogState.page.pageIndex" :page-size="blogState.page.pageSize" :total="blogState.page.pageTotal"> </el-pagination> </el-col> </el-row> </template> 复制代码
模板部分没啥变化,还是老样子,使用 el-row 做了一个简单的布局:
- 左面,blogGroup 显示分组的组件。
- 右面,用 el-card 做了一个列表,用于显示博文。
- 下面,用 el-pagination 实现分页功能。
代码部分:
<script setup> import { watch, reactive } from 'vue' import { useRoute } from 'vue-router' import blogGroup from '../components/blog-group.vue' import blogStateManage from '../model/blogState' import { blogManage } from '../model/blogManage' // 日期格式化 const dateFormat = dayjs // 博文管理 const { getBlogList, getBlogCount } = blogManage() // 状态管理 const { getBlogState } = blogStateManage() // 博文的状态 const blogState = getBlogState() // 博文列表 const blogList = reactive([]) 【后面就不写这些引入的代码了】 /** * 按照首页、分组、查询显示博文列表。 * 显示第一页,并且统计总记录数 */ const showBlog = () => { // 分组ID let groupId = blogState.currentGroupId if (groupId === 0) { // 首页,清空查询条件,显示第一页 blogState.findQuery = {} blogState.page.pageIndex = 1 } else { // 分组的博文列表,设置分组条件,显示第一页 blogState.findQuery = { groupId: [401, groupId] } blogState.page.pageIndex = 1 } // 统计符合条件的总记录数 getBlogCount().then((count) => { blogState.page.pageTotal = count }) // 获取第一页的数据 getBlogList().then((data) => { blogList.length = 0 blogList.push(...data) }) } const route = useRoute() // 如果是首页,把 当前分组ID设置为 0 ,以便于显示所有分组的博文。 watch(() => route.fullPath, () => { if (route.fullPath === '/' || route.fullPath === '/blog') { blogState.currentGroupId = 0 } }) // 监控选择的分组的ID watch(() => blogState.currentGroupId, () => { showBlog() }) // 监听页号的变化,按照页号显示博文列表 watch(() => blogState.page.pageIndex, () => { getBlogList().then((data) => { blogList.length = 0 blogList.push(...data) }) }) // 默认执行一遍 showBlog() 复制代码
代码有点长,这说明了啥呢?还有优化的空间。
- script setup
vite2 建立的项目,默认推荐的是这种方式,其实 vite2 也是支持 export default { setup (props, ctx) { }} 这种写法的。 当然 vue-cli 建立的项目也是支持 script setup 这种方式。所以用哪一种可以看个人喜好。
script setup 更简洁,省去了好多“麻烦”,比如组件引入部分,import 就好,不需要再次注册了。 const 后也不用 return 了,模板可以直接读取到。
- 各种js类
基于这种“散养”方式,所以必须写各种单独的js文件来实现基础功能,然后在 setup 里面整合,否则 setup 就没法看了。
- watch等
watch、ref、reactive这些的用法没有改变。
看一下效果:
后端出身,不会css,也没有艺术细胞所以比较难看,还望谅解
表单 发布博文
这里借鉴一下“简书”的编辑方式,个人感觉还是很方便的,左面是分组目录,中间的选择的分组的博文列表,右面是编辑博文的区域。
<template> <el-row :gutter="12"> <el-col :span="4"> <!--分组--> <blogGroup/> </el-col> <el-col :span="5"> <!--标题列表--> <blogArticle/> </el-col> <el-col :span="14"> <!--写博文--> <el-input style="width:90%" :show-word-limit="true" maxlength="100" placeholder="请输入博文标题,最多100字" v-model="blogModel.title" /> <el-button type="primary" plain @click="submit"> 发布文章 </el-button> {{dateFormat(blogModel.addTime).format('YYYY-MM-DD HH:mm:ss')}} <v-md-editor :include-level="[1, 2, 3, 4]" v-model="blogModel.concent" :height="editHeight+'px'"></v-md-editor>1 </el-col> </el-row> </template> 复制代码
- blogGroup
博文分组的组件,显示分组列表,便于我们选择分组。
- blogArticle
博文列表,选择分组后,显示分组里面的博文列表。在这里可以添加博文,点击博文标题,可以在右面加载博文的表单,进行博文编辑。
用过简书的编辑方式之后,感觉这个还是非常方便的。
代码部分:
【引入的代码略】 // 组件 import blogGroup from '../components/blog-group.vue' import blogArticle from '../components/blog-article.vue' // 可见的高度 const editHeight = document.documentElement.clientHeight - 200 // 管理 const { updateBlog, getArtcileById } = blogManage() // 表单的model const blogModel = reactive(blogForm()) // 监控编辑文章的ID watch(() => blogState.editArticleId, (v1, v2) => { getArtcileById(v1).then((data) => { Object.assign(blogModel, data) }) }) // 发布文章 const submit = () => { blogModel.ID = blogState.editArticleId blogModel.state = 2 // 改为发布状态 updateBlog(blogModel).then((id) => { // 通知列表 }) } 复制代码
- watch(() => blogState.editArticleId
监听要编辑的博文ID,然后加载博文数据绑定表单,编辑之后用 submit 发布博文。
这里还需要一个自动保存草稿的功能,以后再完善。
- submit
发布博文,其实这里是修改博文,因为添加的工作是在 blogArticle 组件里面实现的。
- updateBlog
调用管理类里面的方式实现发布博文的功能。
各个平台的发文方式也体验了一下,还是喜欢这种方式,所以个人博客也采用这种方式来实现编辑博文的功能。
看一下效果:
目录导航:
v-md-editor 提供的目录导航功能,还是非常给力的,看着大纲编写,思路清晰多了。
博文内容 + 讨论
<template> <el-row :gutter="12"> <el-col :span="5"> <!--分组--> <blogGroup :isDetail="true"/> </el-col> <el-col :span="18"> <!--显示博文--> <h1>{{blogInfo.title}}</h1> ({{dateFormat(blogInfo.addTime).format('YYYY-MM-DD')}}) <v-md-preview :text="blogInfo.concent"></v-md-preview> <hr> <!--讨论列表--> <discussList :id="id"/> <!--讨论表单--> <discussForm :id="id"/> </el-col> </el-row> </template> 复制代码
【引入的代码略】 // 组件的属性,博文ID const props = defineProps({ id: String }) // 管理 const { getArtcileById } = blogManage() // 表单的model const blogInfo = reactive({}) getArtcileById(props.id).then((data) => { Object.assign(blogInfo, data) }) 复制代码
这个代码就很简单了,因为只实现了基本的发讨论和显示讨论的功能,其他暂略。
看看效果:
好吧,这个讨论做的蛮敷衍的,其实有好多想法,只是篇幅有限,以后再介绍。
组件级别的代码
虽然在vue里面,除了js文件,就是vue文件了,但是我觉得还是应该细分一下。 比如上面都是是页面级的代码,下面这些是“组件”级别的代码了。
博文分组
多次提到的博文分组。
<template> <!--分组,分为显示状态和编辑状态--> <el-card shadow="hover" v-for="(item, index) in blogGroupList" :key="'grouplist_' + index" > <template #header> <div class="card-header"> <span>{{item.label}}</span> <span class="button"></span> </div> </template> <div class="text item" style="cursor:pointer" v-for="(item, index) in item.children" :key="'group_' + index" @click="changeGroup(item.value)" > {{item.label}} </div> </el-card> </template> 复制代码
暂时先用 el-card 来实现,后期会改成 NavMenu 来实现。
【引入的代码略】 // 组件的属性 const props = defineProps({ isDetail: Boolean }) /** * 博文的分组列表 */ const blogGroupList = reactive([ { value: '1000', label: '前端', children: [ { value: '1001', label: 'vue基础知识', }, { value: '1002', label: 'vue组件', }, { value: '1003', label: 'vue路由', } ] }, { value: '2000', label: '后端', children: [ { value: '2001', label: 'MySQL', }, { value: '2002', label: 'web服务', } ] } ]) // 选择分组 const { setCurrentGroupId } = blogStateManage() const router = useRouter() const changeGroup = (id) => { setCurrentGroupId(id) // 判断是不是要跳转 // 首页、编辑页不跳,博文详细页面调整 if (props.isDetail) { // 跳转到列表页 router.push({ name: 'groups', params: { groupId: id }}) } } 复制代码
分组数据暂时写死了,没有做成可以维护的方式,以后再完善。
博文列表,编辑用
<template> <!--添加标题--> <el-card shadow="hover"> <template #header> <div class="card-header"> <el-button @click="addNewArticle" >添加新文章</el-button> <span class="button"></span> </div> </template> <div class="text item" style="cursor:pointer" v-for="(item, index) in blogList" :key="'article_' + index" @click="changeArticle(item.ID)" > {{item.ID}}:{{item.title}} ({{dateFormat(item.addTime).format('YYYY-MM-DD')}}) </div> <el-empty description="该分类里面还没有文章呢。" v-if="blogList.length === 0"></el-empty> </el-card> </template> 复制代码
用 el-card 做个列表,上面是 添加博文的按钮,下面是博文列表,单击可以进行修改。
【引入的代码略】 // 博文列表 const blogList = reactive([]) // 博文管理 const { addNewBlog, getBlogListByGroupId } = blogManage() // 状态管理 const { getBlogState, setEditArticleId } = blogStateManage() // 博文的状态 const blogState = getBlogState() // 更新列表 const load = () => { getBlogListByGroupId(blogState.currentGroupId).then((data) => { blogList.length = 0 blogList.push(...data) }) } load() // 监控选择的分组的ID watch(() => blogState.currentGroupId, () => { load() }) // 添加新文章,仅标题、时间 const addNewArticle = () => { const newArticle = blogForm() // 选择的分组ID newArticle.groupId = blogState.currentGroupId // 用日期作为默认标题 newArticle.title = dayjs(new Date()).format('YYYY-MM-DD') addNewBlog(newArticle).then((id) => { // 设置要编辑的文章ID setEditArticleId(id) // 通知列表 newArticle.ID = id blogList.unshift(newArticle) }) } // 选择要编辑的文章 const changeArticle = (id) => { setEditArticleId(id) } 复制代码
讨论列表
<el-card shadow="hover" v-for="(item, index) in discussList" :key="'bloglist_' + index" > <template #header> <div class="card-header"> {{item.discusser}} <span class="button">({{dateFormat(item.addTime).format('YYYY-MM-DD')}})</span> </div> </template> <!--简介--> <div class="text item" v-html="item.concent"></div> <hr> <i class="el-icon-circle-check"></i> {{item.agreeCount}} </el-card> <!--没有找到数据--> <el-empty description="没有讨论呢,抢个沙发呗。" v-if="discussList.length === 0"></el-empty> 复制代码
还是用 el-card 做个列表,el-empty 做一个没有讨论的提示。
【引入的代码略】 // 组件的属性 const props = defineProps({ id: String }) // 管理 const { getDiscussListByBlogId } = blogManage() // 获取状态 const { getBlogState } = blogStateManage() const blogState = getBlogState() // 表单的model const discussList = reactive([]) getDiscussListByBlogId(props.id).then((data) => { discussList.push(...data) }) watch(() => blogState.isReloadDiussList, () => { getDiscussListByBlogId(props.id).then((data) => { discussList.length = 0 discussList.push(...data) }) }) 复制代码
因为功能比较简单,所以代码也很简单,获取讨论数据绑定显示即可,暂时没有实现分页功能。
讨论表单
<el-form style="width:400px;" label-position="top" :model="dicussModel" label-width="80px" > <el-form-item label="昵称"> <el-input v-model="dicussModel.discusser"></el-input> </el-form-item> <el-form-item label="内容"> <el-input type="textarea" v-model="dicussModel.concent"></el-input> </el-form-item> <el-form-item> <el-button type="primary" @click="submit">发表讨论</el-button> <el-button>取消</el-button> </el-form-item> </el-form> 复制代码
用 el-form 做个表单。
【引入的代码略】 // 组件的属性 const props = defineProps({ id: String }) // 管理 const { addDiuss } = blogManage() // 获取状态 const { getBlogState, setReloadDiussList } = blogStateManage() const blogState = getBlogState() // 表单的model const dicussModel = reactive(discuss()) // 发布讨论 const submit = () => { dicussModel.blogId = props.id // 这是博文ID addDiuss(dicussModel).then((id) => { // 可以想象成 axios 的提交 // 通知列表 setReloadDiussList() }) } 复制代码
分成多个组件,每个组件的代码就可以非常少了,这样便于维护。 发布讨论的函数,先使用blogManage的功能提交数据,回调函数里面,使用的状态管理的功能提醒讨论列表刷新数据。