我們再來看一下管理類的設計。
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)的構建輸出路徑。
其他設定方式可以看這裡:https://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的功能提交資料,回撥函式裡面,使用的狀態管理的功能提醒討論列表重新整理資料。
原始碼
https://gitee.com/naturefw/vue3-blog