代码地址:
文章目录
1. 环境
node 和 npm 版本
node版本:v16.17.0
npm版本:8.15.0
nvm
可使用nvm切换node环境,参考链接:nvm安装,nvm的使用,nvm常用命令,nvm安装node报错,nvm切换不了,等系列集合
2. 初始化项目
2.1 创建项目
npm init vite@latest easyblog-front-admin
2.2 使用router
安装路由
npm i vue-router@4
创建router/index.js
创建router/index.js,如下:
import { createWebHistory, createRouter } from 'vue-router'
const routes = [
{
name: '登录',
path: '/login',
component: () => import('@/views/Login.vue')
},
{
name: '登录',
path: '/blog/:blogId',
component: () => import('@/views/Blog.vue')
}
]
const router = createRouter({
routes,
history: createWebHistory()
})
export default router
使用路由
<script setup>
import { reactive, ref, watch } from 'vue'
import { useRoute,useRouter } from 'vue-router'
const route = useRoute()
const router = useRouter()
// 获取路由params参数
const blogId = ref(route.params.blogId)
const currPath = ref(null)
// 监听路由变化
watch(route, (newVal,oldVal) => {
currPath.value = newVal.path
}, {immediate: true})
const activePath = ref('')
watch(
() => router,
(newVal, oldValue) => {
activePath.value = newVal.currentRoute.value.meta.activePath;
},
{ immediate: true, deep: true }
);
// 路由跳转
// router.push('/')
2.3 配置代理及@
安装path
npm i path
vite.config.json
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue()],
server: {
hmr: true,
port: 3001,
proxy: {
'/api': {
target:'http://localhost:8081',
secure: false,
changeOrigin: true,
pathRewrite: {
'^/api':'/api'
}
}
}
},
resolve: {
alias: {
'@':path.resolve(__dirname,'./src')
}
}
})
main.js
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
import router from '@/router'
const app = createApp(App)
app.use(router)
app.mount('#app')
vscode配置@符号可自动跳转,在项目根目录下增加这个文件jsconfig.json,然后重启vscode
{
"compilerOptions": {
"baseUrl": "./",
"paths": {
"@/*": [
"src/*"
],
}
},
"exclude": [
"node_modules",
"dist"
],
"include": [
"src/**/*"
]
}
2.4 使用element-plus
安装element-plus
npm install element-plus --save
引入element-plus
import { createApp } from 'vue'
import './style.css'
/* 导入element-plus的样式 */
import 'element-plus/dist/index.css'
import App from './App.vue'
import router from '@/router'
/* 导入element-plus */
import ElementPlus from 'element-plus'
const app = createApp(App)
app.use(router)
/* 使用element-plus */
app.use(ElementPlus)
app.mount('#app')
安装sass
安装sass及sass-loader以支持scss语法
npm i sass --save
npm i sass-loader --save
login.vue
<template>
<div class="login-body">
<el-button type="primary">登录</el-button>
</div>
</template>
<script setup>
</script>
<style lang="scss">
.login-body {
height: 100vh;
background-image: url(@/assets/login-bg.jpg);
background-repeat: no-repeat;
background-size: cover;
background-position: center;
}
</style>
App.vue
<script setup>
</script>
<template>
<div>
123
<router-view/>
</div>
</template>
<style scoped>
</style>
2.5 使用axios
安装axios
npm i axios
封装axios
import axios from 'axios'
import messager from '@/utils/messager'
const contentTypeForm = "application/x-www-form-urlencoded;charset=UTF-8";
const contentTypeJson = "application/json";
const contentTypeFile = "multipart/form-data";
const Request = (config)=>{
let { url, params, dataType='form', showLoading} = config;
let contentType = contentTypeForm
if(dataType == 'json') {
contentType = contentTypeJson
} else if(dataType == 'file') {
contentType = contentTypeFile
let param = new FormData()
for (const key in params) {
param.append(key, param[key])
}
params = param
}
const instance = axios.create({
baseURL: '/api',
timeout: 10 * 1000,
headers: {
'Content-Type': contentType,
'X-Requested-With': 'XMLHttpRequest'
}
})
instance.interceptors.request.use(
config=>{
return config
}
)
instance.interceptors.response.use(
response => {
const responseData = response.data
if(responseData.status == 'error') {
if(responseData.code == 901) {
setTimeout(()=>{
},2000)
return Promise.reject('登录超时')
} else {
messager.error(responseData.info)
return Promise.reject(responseData.info)
}
} else {
return Promise.resolve(responseData.data)
}
},
err=> {
return Promise.reject('网络异常')
}
)
return instance.post(url,params)
}
export default Request
main.js中注册
import { createApp } from 'vue'
import './style.css'
import 'element-plus/dist/index.css'
import App from './App.vue'
import router from '@/router'
import ElementPlus from 'element-plus'
import Request from '@/utils/request'
const app = createApp(App)
// 注册Request到全局,后面在vue组件中, 可以通过getCurrentInstance()方法获取proxy,从而获取到该Request
app.config.globalProperties.Request = Request
app.config.globalProperties.websiteInfo = {
title:'easyBlog'
}
app.use(router)
app.use(ElementPlus)
app.mount('#app')
发送请求
<template>
{{ proxy.websiteInfo.title }}
</template>
<script setup>
import { getCurrentInstance } from 'vue'
const {proxy} = getCurrentInstance()
const formData = reactive({
username: '18666666666',
password: '123456'
})
proxy.Request({
url: api.login,
params: {
account: formData.username,
password: formData.password,
checkCode:formData.checkCode,
rememberMe:formData.remeberMe
},
}).then(data=>{
proxy.messager.success('登陆成功')
// console.log(data);
router.push('/')
})
</script>
2.6 使用iconfont
下载iconfont相关文件
将iconfont中指定的项目下载到本地,解压到src/assets/iconfont目录下
main.js中引入
import '@/assets/iconfont/iconfont.css'
组件中使用方法
<el-form-item size="large" prop="username">
<el-input v-model="formData.username" placeholder="请输入用户名">
<template #prefix>
<i class="iconfont icon-account"></i>
</template>
</el-input>
</el-form-item>
2.7 使用vuex
安装vuex
npm i vuex@4
创建store/index.js
import { createStore } from 'vuex'
// 引入封装了axios发送请求的工具
import Request from '@/utils/request'
const store = createStore({
// 维护的数据,保存在state
state: {
userInfo:{
nickName:'',
avatar: ''
}
},
// 唯一可以修改state的地方(建议)
mutations: {
updateUserInfo(state,userInfo) {
state.userInfo = userInfo
}
},
// actions可以处理异步,其中定义的方法默认也会将返回结果封装为Promise
actions: {
getUserInfo({commit},payload) {
debugger
return new Promise(async function(resolve, reject) {
try {
// 发送请求,获取当前用户信息
let data = await Request({
url: 'getUserInfo'
})
debugger
// 调用commit,让commit去更新state
commit('updateUserInfo', {
nickName: data.nickName,
avatar: '/api/file/getImage/' + data.avatar
})
resolve(data)
} catch (error) {
// 假设请求路径不存在, 需要修改当前Promise的状态为失败, 以便回调调用此action时指定的失败回调函数
reject(error)
}
})
}
},
// 方便直接访问,类似于计算属性
getters: {
userInfo: (state) => state.userInfo
}
})
export default store
main.js中注册
import { createApp } from 'vue'
import './style.css'
import 'element-plus/dist/index.css'
import ElementPlus from 'element-plus'
import App from './App.vue'
import router from '@/router'
// 引入store
import store from '@/store'
const app = createApp(App)
app.use(router)
// 使用store
app.use(store)
app.use(ElementPlus, {locale})
app.mount('#app')
使用store
MyInfo.vue
<template>
<el-form-item>
<el-button @click="saveUserInfo" type="success">保存</el-button>
</el-form-item>
<template>
<script setup>
// 引入store
import { useStore } from 'vuex'
const store = useStore()
const saveUserInfo = () => {
userInfoFormRef.value.validate(async (valid,fields)=>{
if(!valid) {
proxy.Messager.error('请按要求填写')
return
}
let result = await proxy.Request({
url: api.saveMyInfo,
params: {
avatar: formData.value.avatar,
nickName: formData.value.nickName,
phone: formData.value.phone,
editorType: formData.value.editorType,
profession: formData.value.profession,
introduction: formData.value.introduction,
},
});
proxy.Messager.success('保存成功')
// 这里其实也可以用await的,因为外面这个函数有async修饰了
store.dispatch('getUserInfo', {nickName: formData.value.nickName,avatar: formData.value.avatar})
.then(res=>{ console.log('派发action成功',res);})
.catch(err=>{ console.log('派发action失败',err);})
})
}
</script>
framework.vue
<template>
<!-- 计算属性 -->
<span>{{ userInfo.nickName }}</span>
<img :src="userInfo.avatar">
<!-- watch + ref -->
<el-input v-model="userInfo2.nickName"></el-input>
<span>{{ userInfo2 }}}</span>
</template>
<script setup>
import { reactive, ref, watch,computed } from 'vue'
import { useStore } from 'vuex'
const store = useStore()
// 使用计算属性
const userInfo = computed(()=>{
return store.state.userInfo
})
const userInfo2 = ref({})
// 使用watch监视store
watch(
()=>store.state.userInfo,
(newVal,oldVal)=>{
console.log('userInfo变化了' + JSON.stringify(newVal));
userInfo2.value = newVal;
},
{immediate:true, deep:true}
)
</script>
2.8 部署
打包
执行打包命令,会得到一个dist文件夹
npm run build
配置nginx,并启动
windows 下杀死所有nginx进程的命令 :taskkill /f /t /im nginx.exe
windows 下启动nginx:start nginx.exe
nginx部署vue项目配置
server {
# 监听80端口
listen 80;
server_name localhost;
charset utf-8;
# 配置前端访问的路径,dist包放到此文件夹下
location / {
alias D:/document/easyblog/learn/easyblog-front-admin/dist/;
try_files $uri $uri/ /index.html; # 解决页面刷新导致404的为题
index index.html index.htm;
}
# 将前端发送到80端口,并且以/api开头的请求,转发到8081端口的后台服务去(后台项目监听8081端口)
location /api {
proxy_pass http://localhost:8081/api; # 端口的后面有没有加“/”是很重要的,这个/指的是端口后面有没有带/,而不是末尾有没有带/
# 这里的..:8081/api要理解为带了/
# 既然带了/,那前面以/api匹配到的请求,比如匹配了/api/a/b这个请求,那么就会把/api匹配之外的所有字符 拼接到 proxy_pass指定的路径后面,即:..:8081/api/a/b
# 假设,这个location后面接的是/api/,proxy_pass是http://localhost:8081/api,那么如果请求的是/api/a/b,那么转发的是:http://localhost:8081/apia/b,注意前面的/就没了
# 如果proxy_pass后面没有带/,proxy_pass就一定是http://localhost:8081,(因为如果是/api,那也叫作带了/)
# 由于后面没有带/,如果请求的是/api/a/b,那么会把后面匹配的全部拼接到后面去,转发的就是http://localhost:8081/api/a/b
# 如果proxy_pass配置的 是http://localhost:8081/,location是/api,
# 那么如果请求的是/api/a/b,那么转发的路径就是http://localhost:8081//a/b,注意,这就多了一个/了
proxy_set_header x-forward-for $remote_addr;
}
}
windows下操作nginx
运行命令:start nginx 启动nginx服务
运行命令:nginx -s stop 停止nginx服务
运行命令:nginx -s reload 重载配置
运行命令:taskkill /f /t /im nginx.exe 关闭nginx其他服务,这样才能彻底关闭
访问
浏览器输入 localhost,即可!
2.9 代码高亮
安装hignlight.js
npm i highlight.js
模板中使用highlight.js
<template>
<div class="main-body article-wrapper" style="{ 'min-height': 'calc(100% - 10px)' }">
<div class="main-body-left">
<div class="title">
<h2>{{ blogDetail.title }}</h2>
</div>
<div class="article-extra-info">
<span class="time">{{ blogDetail.createTime }}</span>
<span>作者: <a href="#">{{ blogDetail.nickName }}</a></span>
<span>分类专栏: <a href="#">{{ blogDetail.categoryName }}</a></span>
</div>
<div class="article-content">
<div v-html="blogDetail.content"></div>
</div>
</div>
<div class="main-body-right">
456
</div>
</div>
</template>
<script setup>
import { ref, reactive, getCurrentInstance,nextTick } from 'vue'
import hljs from "highlight.js";
import "highlight.js/styles/atom-one-dark-reasonable.css"; //样式
import { useRoute } from 'vue-router'
const route = useRoute()
const { proxy } = getCurrentInstance()
const api = {
getBlogDetail: "view/getBlogDetail",
loadCategory: "/view/loadCategory",
};
const blogDetail = ref({})
onUpdated(()=>{
console.log('渲染更新了呀~');
})
async function getBlogDetail() {
let result = await proxy.Request({
url: api.getBlogDetail,
params: {
blogId: route.params.blogId
}
})
blogDetail.value = result
nextTick(() => {
console.log('nextTick');
//高亮显示
let blocks = document.querySelectorAll("pre code");
blocks.forEach((block) => {
hljs.highlightElement(block);
});
});
}
getBlogDetail()
</script>
<style lang="scss" scoped>
.main-body-left {
/* box-sizing: border-box; */
padding: 20px 20px;
}
.title {
margin-bottom: 10px;
h2 {
font-size: 1.6em;
font-weight: 600;
line-height: 1.48;
margin: 0;
}
}
.article-extra-info {
font-size: 13px;
color: #82868e;
span {
margin-right: 12px;
}
a {
color: #1890ff;
}
}
.article-content {
border-radius: 4px;
margin-top: 20px;
padding: 5px 25px;
background-color: #fff;
box-shadow: 0 3px 5px 0px rgb(0 0 0 / 10%);
}
.article-wrapper {
width: 1160px;
margin: auto;
}
</style>
封装table组件,并全局注册
封装Dialog组件,并全局注册
表单、Dialog和表单、Dialog和富文本编辑器&markdown编辑器,新增/修改/回显,特别是编辑和新增时内容的回显表单校验的移除和编辑器内容的回显,
数据传输blog->table->pagination,在table中修改blog传过来的数据
父子组件传值defineProps
defineEmits
defineExpose
封面上传,v-model双向绑定
监听浏览器窗口变化,修改表格高度
vue3通过ref引用dom,但是如果要拿到里面的方法,必须要子组件defineExpose
全局引入nextTick,以及nextTick用法
window.onresize监听浏览器窗口变化
Object.assign
splice实现上移下移
父给子传值通过传递props,对于传过来的基本数据类型,子组件不能直接修改;对于传过来的对象数据类型(响应式数据),子组件可以修改对象内部属性的值
axios上传文件写法
封装markdown编辑器v-md-editor,数据绑定
封装富文本编辑器,数据绑定
封装Window.vue
在组件内部引入样式和在main.js引入样式有什么区别
style的scoped作用
v-if渲染,原来数据是否还在的问题,组件有没有销毁和v-show的区别
在vue文件组件中引入一个.css文件,最终是放入head中的style标签里面
Object.assign可以将一个对象中的值拷贝给reactive的对象
Object.assign可以将一个对象中的值拷贝给ref的value
Object.assign将一个ref的value值赋值给一个{}
Object.assign将一个reactive的值赋值给一个{}
用户数据输入和后台交互的,最好用ref去绑定,一些设置相关的用reactive,不然操作起来很麻烦(补充:可以把对象用一个key绑定,添加到reactive中,即const user = reactive({info:{‘name’:‘zzhua’}}),修改的时候,就可以user.info = {‘name’:‘zj’}去改了);
v-model绑定输入框,当输入内容时,会触发重新渲染吗?
当通过v-if显示元素时候,接口数据还没返回,但是显示的元素里面需要用到接口返回的数据,接口数据又是通过await axios发的,那这个时候元素是否会显示出来?
v-model绑定值给子组件,子组件通过事件触发的方式通知父组件改值,引发重新渲染?验证一下。还有就是重新渲染时,原来的元素状态是否有变化,子组件的某些值是初始值吗?
测试下v-if和v-show修改时,能否立即获取到dom
/*
场景:
弹窗中有表单,填写表单内容后,添加到表格中,表格的数据可通过弹窗修改,新增和修改用的同一个弹窗
问题:
表单项的校验方式是change,
先打开一个有数据的表单弹框, 然后关闭它的时候, 清除掉表单数据,
由于修改了表单数据, 并且表单数据为空, 触发校验, 下面会有校验错误提示, 尽管这个时候dialog已经被隐藏了
但是当继续打开一个新增表单数据弹框的时候, 校验错误提示仍然还在(如果继续打开的是一个修改表单项数据弹框,校验提示是会消失的)
*/
const showEdit = (type, data) => {
dialogConfig.show = true;
// 第一次的时候, dialog中的这个表单还没有被渲染出来, 所以会拿不到这个表单ref,所以调用不了ref的任何方法。
// 如果拿不到,说明是第一次打开这个框,既然是第一次,它本身肯定是没有校验的,就不需要清除校验结果
formDataRef.value && formDataRef.value.clearValidate() // 清空上一次的校验结果
if(type === 'add') {
dialogConfig.title = "新增分类";
formData.value = {}
} else {
dialogConfig.title = "编辑分类";
formData.value = JSON.parse(JSON.stringify(data))
}
// 或者使用下面的nextTick
/* nextTick(()=>{
formDataRef.value.clearValidate() // 清空上一次的校验结果
if(type === 'add') {
dialogConfig.title = "新增分类";
} else {
dialogConfig.title = "编辑分类";
formData.value = JSON.parse(JSON.stringify(data))
}
}) */
}
折叠面板不使用max-height的方式,先height:auto,获取到元素的auto,然后再从0到指定高度的过渡
document.location.reload()刷新页面
document.location.reload()刷新页面
封装markdown
封装wangeditor:样式-div包裹toolbar和editor,div的高度设置为height:100%;width100%,editor的高度设置为calc100%-81px,这样当div的高度变化时,编辑器的高度也会变化,然后把它封装为一个组件,使用的时候,再用一个div包裹这个组件,这个div就可以设置具体的 高度了。
flex布局:一个固定大小的div1包裹着一个div2,div2开启了flex布局,但是没有设置宽高,我们会自然的以为div2的宽度最大就是div1的宽度,但是,如果div2中放置了多个容器,容器的宽度加起来就是div2的宽度,这个宽度是有可能超过div1的宽度的,解决办法就是给div1设置具体的宽度
const定义的常量,不会像var一样提到最上面。
element-ui的model绑定的响应式数据只能绑定到1个表单上面,如果再绑定到其它表单的model上,这个表单的validate方法会失效
挂载在app.config.gloablProperties的东西,在vue中可以通过getCurrentInstance拿到,可是在js文中怎么拿到
是先渲染完子组件还是先父组件,碰到作用域插槽呢?
@keyup.enter.native监听enter键
搜索条件只有一个输入框时,如果使用了@keyup.enter.native=“handleQuery” 原始键盘回车事件来触发搜索操作,会对整个页面都进行刷新,想让页面不刷新,可在el-form位置添加 @submit.native.prevent 原始提交事件即可(@submit.native.prevent,其中.native 表示对一个组件绑定系统原生事件
.prevent 表示提交以后不刷新页面)
el-form添加自定义校验规则,一定要调用callback(),否则调用表单的validate校验方法时,不会走后面的逻辑了
el-row的样式中默认带了margin-left:-5px,margin-right:-5px,把它们重置下,不然老是因为宽度增加了而出现滚动条
直接通过绑定style属性,给元素添加样式
直接将具体的高度值绑定给el-table的height属性,具体的高度值可以通过自己手动计算得到,比如window.innerHeight-上面预留高度-下边预留高度,这样表格能比较好的展现,除此之外,还可以设置一个监听浏览器窗口变化的函数,去动态的修改绑定给el-table的height的属性的值
document.title=‘小标题名’
document.body.scrollHeight 整个文档的高度
window.innerHeight 浏览器可视窗口的高度
document.location.href 可读可写属性地址栏,可结合dom元素的scrollIntoView一起,在跳转页面时候,将页面滚动到指定的元素
document.getElementById(‘h28’).scrollIntoView({‘behavior’:‘smooth’}) 将dom平滑滚动到浏览器可视区域
tocbot根据dom中的h标签生成目录内容
prisms高亮
开发小而美的个人博客之页面插件集成
解析html,根据h标签,组装成目录结构数据,封装到el-tree中
在下面2个组件中,Member.vue中使用了MemberItem.vue这个组件,其中父组件,在setup的时候,teamUserList就已经有值了,只不过是null,所以这个时候,应该算父组件已经mounted挂载完毕了,只不过,在setup里面,开了一个promise,等到请求完成的时候,修改teamUserList,这个时候,就要重新渲染,渲染完成之后,执行nextTick(注意这个nextTick的用法,它书写的位置是放在刚刚修改完响应式数据之后的,因为js是单线程,nextTick会向全局注册这个执行函数,然后上面的任务执行完了,开始执行渲染任务,渲染完毕了,nextTick中传入的函数就会执行了)
<!-- Member.vue -->
<template>
<div class="category-wrapper">
<MemberItem :user="user" v-for="user, index in teamUserList" :key="index"></MemberItem>
</div>
</template>
<script setup>
import { ref,reactive,getCurrentInstance,nextTick,onMounted } from 'vue'
import MemberItem from '@/views/member/MemberItem.vue'
const { proxy } = getCurrentInstance()
const teamUserList = ref(null)
async function loadTeamUserList() {
let result = await proxy.Request({
url:'/view/loadTeamUser'
})
teamUserList.value = result
nextTick(()=>{
let id = location.href.substring(location.href.lastIndexOf('#'))
let dom = document.querySelector(id)
dom.scrollIntoView({'behavior':'smooth'})
})
}
loadTeamUserList()
onMounted(()=>{ console.log('这里执行晚于上面的nextTick') })
</script>
<!-- MemberItem.vue -->
<template>
<div class="user-info" :id="'user-info-' + user.userId">
<a class="avatar" href="#">
<img :src="proxy.globalInfo.imageUrl + user.avatar" alt="">
</a>
<div class="user-detail">
<div class="nick-name">
{{ user.nickName }}
</div>
<div class="user-job">
<span class="profession">职位: {{ user.profession }}</span>
<span>博客: {{ user.blogCount }}</span>
</div>
<div class="intro-wrapper" ref="userIntroRef" :id="'userIntro' + user.userId" >
<div :class="{'max-height-limit':limitFlag}" v-html="user.introduction"></div>
<span @click="limitFlag=!limitFlag" v-if="isExpandableShow" class="expandable">
{{ limitFlag?'展开':'收起' }}
<i :class="['iconfont icon-open',{'r180':!limitFlag}]"></i>
</span>
</div>
</div>
</div>
</template>
<script setup>
import { ref, reactive, getCurrentInstance,onMounted } from 'vue'
const { proxy } = getCurrentInstance()
const userIntroRef = ref(null)
const props = defineProps({
user: {
type: Object
}
})
const isExpandableShow = ref(false)
const limitFlag = ref(false)
onMounted(()=>{
console.log(userIntroRef.value.offsetHeight);
if(userIntroRef.value.offsetHeight >= 100) {
isExpandableShow.value = true
limitFlag.value = true
}
})
</script>
使用vue3的ref去引用dom的时候,如果ref写的位置是原始的html元素,那么可以拿到这个dom,如果是组件,拿到的是undefined。原因是ref需要组件将需要暴露的东西使用defineExpose暴露才能拿到。