技术选型
Vue3 + Vue-router + Pinia + ElementPlus
代码地址
https://gitee.com/galen.zhang/vue3-demo/tree/master/tech-blog
Mock后台服务器代码
https://gitee.com/galen.zhang/vue3-demo/tree/master/mock-server
cd mock-server
npm install
npm run dev
npm create vue@latest
√ Project name: ... tech-blog
√ Add TypeScript? ... No
√ Add JSX Support? ... No
√ Add Vue Router for Single Page Application development? ... Yes
√ Add Pinia for state management? ... Yes
√ Add Vitest for Unit Testing? ... No
√ Add an End-to-End Testing Solution? » No
√ Add ESLint for code quality? ... Yes
√ Add Prettier for code formatting? ... Yes
cd tech-blog
npm install
npm run format
npm run dev
浏览器访问
http://localhost:5173/
官方文档
https://element-plus.org/zh-CN/guide/installation.html
安装Element Plus
npm install element-plus --save
npm install @element-plus/icons-vue
在 src/main.js
中引入Element Plus
import './assets/main.css'
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
import App from './App.vue'
import router from './router'
const app = createApp(App)
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component)
}
app.use(ElementPlus)
app.use(createPinia())
app.use(router)
app.mount('#app')
修改文件 src/router/index.js
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',
component: () => import('../views/AboutView.vue')
},
{
path: '/blog/:id',
component: () => import('../views/BlogDetailView.vue')
},
{
path: '/blog/:id/edit',
component: () => import('../views/CreateEditBlogView.vue')
},
{
path: '/create',
component: () => import('../views/CreateEditBlogView.vue')
},
{
path: '/myBlog',
component: () => import('../views/MyBlogView.vue')
},
{
path: '/register',
component: () => import('../views/RegisterView.vue')
},
{
path: '/login',
component: () => import('../views/LoginView.vue')
}
]
})
export default router
npm install axios --save
// 进度条
npm install nprogress --save
封装网络请求工具类 src/utils/request.js
import axios from 'axios'
import NProgress from 'nprogress'
import 'nprogress/nprogress.css'
export const serverUrl = 'http://localhost:3000'
const service = axios.create({
baseURL: serverUrl,
timeout: 5000
})
// Add a request interceptor 全局请求拦截
service.interceptors.request.use(
function (config) {
// Do something before request is sent
const token = localStorage.getItem('token')
if (token) {
config.headers['Authorization'] = `Bearer ${token}`
}
NProgress.start() // 启动进度条
// 此处还可以设置token
return config
},
function (error) {
// Do something with request error
return Promise.reject(error)
}
)
// Add a response interceptor 全局相应拦截
service.interceptors.response.use(
function (response) {
// Any status code that lie within the range of 2xx cause this function to trigger
// Do something with response data
NProgress.done()
// 如果是固定的数据返回模式,此处可以做继续完整的封装
const resData = response.data || {}
if (resData.code == '0') {
return resData.data
}
return Promise.reject(resData.message)
},
function (error) {
// Any status codes that falls outside the range of 2xx cause this function to trigger
// Do something with response error
NProgress.done()
// 此处需要对返回的状态码或者异常信息作统一处理
return Promise.reject(error)
}
)
export const get = (url, params) => {
return service.get(url, {
params
})
}
export const post = (url, data) => service.post(url, data)
export const put = (url, data) => service.put(url, data)
export const del = (url, data) => service.delete(url)
博客后台请求接口api文件 src/api/blogApi.js
import { get, post, put, del } from "../utils/request";
// 获取博客列表
export async function getBlogList(opt = {}) {
const { page = 1, pageSize = 10, category = '', keyword = '', my = false } = opt;
return get(`/api/blogs?page=${page}&pageSize=${pageSize}&category=${category}&keyword=${keyword}&my=${my}`);
}
// 创建博客
export async function createBlog(data) {
return post('/api/blogs', data);
}
// 编辑博客
export async function editBlog(id, data) {
return put(`/api/blogs/${id}`, data);
}
// 删除博客
export async function deleteBlog(id) {
return delete(`/api/blogs/${id}`);
}
// 根据 ID 获取单个博客
export async function getBlogById(id) {
return get(`/api/blogs/${id}`);
}
// 点赞博客
export async function likeBlog(id) {
return post(`/api/blogs/${id}/like`);
}
// 取消点赞博客
export async function unlikeBlog(id) {
return del(`/api/blogs/${id}/like`);
}
// 收藏博客
export async function favoriteBlog(id) {
return post(`/api/blogs/${id}/favorite`);
}
// 取消收藏博客
export async function unfavoriteBlog(id) {
return del(`/api/blogs/${id}/favorite`);
}
export default {
getBlogList,
createBlog,
editBlog,
deleteBlog,
getBlogById,
likeBlog,
unlikeBlog,
favoriteBlog,
unfavoriteBlog
}
修改页面标题
添加文件 src/hooks/usePageTitle.js
import { ref, isRef, onMounted, onBeforeUnmount, watchEffect } from 'vue'
const NAME = 'TechBlog'
function usePageTitle(title) {
const originalTitle = ref(document.title)
// 更新网页标题
const updatePageTitle = () => {
const titleValue = isRef(title) ? title.value : title
if (!titleValue) {
return
}
document.title = titleValue + ' - ' + NAME
}
if (isRef(title)) {
watchEffect(updatePageTitle)
}
// 在组件挂载时更新网页标题
onMounted(() => {
updatePageTitle()
})
// 在组件卸载时恢复原始网页标题
onBeforeUnmount(() => {
document.title = originalTitle.value
})
return {
updatePageTitle
}
}
export default usePageTitle
在页面上使用,如登录页面
添加文件 src/components/NavMenu.vue
添加文件 src/components/SearchInput.vue
form表单提交数据 src/views/RegisterView.vue
新用户注册
注册
已有账号?去登录
在用户登录后,存储token到本地 src/views/LoginView.vue
用户登录
登录
没有账号?去注册
Pinia使用例子
src/stores/counter.js
import { ref, computed } from 'vue'
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
const doubleCount = computed(() => count.value * 2)
function increment() {
count.value++
}
return { count, doubleCount, increment }
})
在页面中使用 counter
{{ counterStore.count }}
{{ counterStore.doubleCount }}
添加文件 src/stores/user.js
import { reactive } from 'vue'
import { defineStore } from 'pinia'
export const useUserStore = defineStore('user', () => {
const userInfo = reactive({
username: '',
nickname: ''
})
function setUserInfo({ username, nickname }) {
userInfo.username = username
userInfo.nickname = nickname
}
function clearUserInfo() {
userInfo.username = ''
userInfo.nickname = ''
}
return { userInfo, setUserInfo, clearUserInfo }
})
在首页 src/App.vue
中获取用户信息
添加文件 src/components/UserInfo.vue
{{userInfo.nickname || userInfo.username}}
创建博客
我的博客
注销
登录
添加文件 src/hooks/useNavToHome.js
import { watch } from 'vue';
import { useRouter } from 'vue-router';
import { useUserStore } from '@/stores/user';
function useNavToHome() {
const router = useRouter();
const { userInfo } = useUserStore()
// 监听 userInfo.username ,如果有值,说明用户已经登录,跳转到首页
watch(() => userInfo.username, (val) => {
if (val) {
router.push('/');
}
}, { immediate: true });
}
export default useNavToHome
在注册、登录页面中添加,如果已登录则跳转到首页
import useNavToHome from '@/hooks/useNavToHome'
useNavToHome()
添加卡片式组件,展示博客关键信息 src/components/BlogCard.vue
{{ blog.title }}
{{ blog.description }}
{{ blog.likes }}
{{ blog.favorites }}
{{ blog.comments }}
列表分页显示 src/views/HomeView.vue
博客内容存储为marddown格式的文本
安装markdown工具 markdown-it
npm install markdown-it --save
博客详情页 src/view/BlogDetailView.vue
{{ blogInfo.title }}
{{ formatDate(blogInfo.createdAt) }}
{{ blogInfo.category }}
添加文件 src/components/ListPage.vue
使用table表格展示列表数据 src/views/MyBlogView.vue
我的博客
创建博客
{{ row.title }}
编辑
删除
添加文件 src/views/CreateEditBlogView.vue
{{ title }}
提交
标题
类型
打包项目
npm run build
可以查看打包后每个文件的大小
安装 Import Cost
插件,可以在每个vue文件中,查看第三方依赖库的文件大小
Vue、Element-Plus 可以拆分出来,减小首页index文件的大小
修改配置文件 vite.config.js
build: {
rollupOptions: {
output: {
manualChunks: {
// 将 Vue 和 Element Plus 单独打包为一个公共块
vue: ['vue'],
'element-plus': ['element-plus'],
'markdown-it': ['markdown-it']
}
}
}
}