一、开发规范&流程
1.1 新增view
1、添加菜单

这里要把组件路径写对,不然会加载不出来页面。
2、创建文件夹
在前端代码src→views文件下 创建对应的文件夹,一般性一个路由对应一个文件, 该模块下的功能就建议在本文件夹下创建一个新文件夹,各个功能模块维护自己的utils或components组件
在src→views下创建service/msg/index.vue
刷新页面 
1.2 新增api
在src→api文件夹下创建本模块对应的 api 服务,如在src→api下新建service/msg.js 
1.3 新增组件
在全局的 src→components写一些全局的组件,如富文本,各种搜索组件,封装的分页组件等等能被公用的组件。 每个页面或者模块特定的业务组件则会写在当前 src→view下。 如:src/views/service/msg/components/xxx.vue 这样拆分大大减轻了维护成本。
1.4 新增样式
页面样式与组件相同,全局的src→style放置全局公用的样式,每一个页面的样式就写在当前views下,注意我们上一篇提到的scoped,加上后就只会作用在当前组件内,从而避免造成全局样式污染。
二、请求流程
请求流程部分内容来自官方文档
2.1 交互流程
完整的前端 UI 交互到服务端处理流程如下: 1.UI 组件交互操作; 2.调用统一管理的 api service 请求函数; 3.使用封装的 request.js 发送请求; 4.获取服务端返回; 5.更新 data; 为了方便管理维护,统一的请求处理都放在src→api文件夹中,并且一般按照 model 维度进行拆分文件 
src→utils→request.js是基于axios的封装,便于统一处理POST,GET等请求参数,请求头,以及错误提示信息等。它封装了全局request拦截器、response拦截器、统一的错误处理、统一做了超时处理、baseURL设置等。
javascript
// 导入所需模块
import axios from 'axios'
import { ElNotification , ElMessageBox, ElMessage, ElLoading } from 'element-plus'
import { getToken } from '@/utils/auth'
import errorCode from '@/utils/errorCode'
import { tansParams, blobValidate } from '@/utils/ruoyi'
import cache from '@/plugins/cache'
import { saveAs } from 'file-saver'
import useUserStore from '@/store/modules/user'
let downloadLoadingInstance
// 是否显示重新登录
export let isRelogin = { show: false }
axios.defaults.headers['Content-Type'] = 'application/json;charset=utf-8'
// 创建axios实例
const service = axios.create({
// axios中请求配置有baseURL选项,表示请求URL公共部分
baseURL: import.meta.env.VITE_APP_BASE_API,
// 超时
timeout: 10000
})
/**
* 请求拦截器
* 在发送请求之前做些什么
* @param {Object} config - axios请求配置对象
* @returns {Object} 配置对象
*/
// request拦截器
service.interceptors.request.use(config => {
// 是否需要设置 token
const isToken = (config.headers || {}).isToken === false
// 是否需要防止数据重复提交
const isRepeatSubmit = (config.headers || {}).repeatSubmit === false
if (getToken() && !isToken) {
// 让每个请求携带自定义token 请根据实际情况自行修改
config.headers['Authorization'] = 'Bearer ' + getToken()
}
// get请求映射params参数
if (config.method === 'get' && config.params) {
let url = config.url + '?' + tansParams(config.params)
url = url.slice(0, -1)
config.params = {}
config.url = url
}
if (!isRepeatSubmit && (config.method === 'post' || config.method === 'put')) {
const requestObj = {
url: config.url,
data: typeof config.data === 'object' ? JSON.stringify(config.data) : config.data,
time: new Date().getTime()
}
// 请求数据大小
const requestSize = Object.keys(JSON.stringify(requestObj)).length
// 限制存放数据5M
const limitSize = 5 * 1024 * 1024
if (requestSize >= limitSize) {
console.warn(`[${config.url}]: ` + '请求数据大小超出允许的5M限制,无法进行防重复提交验证。')
return config
}
const sessionObj = cache.session.getJSON('sessionObj')
if (sessionObj === undefined || sessionObj === null || sessionObj === '') {
cache.session.setJSON('sessionObj', requestObj)
} else {
// 请求地址
const s_url = sessionObj.url
// 请求数据
const s_data = sessionObj.data
// 请求时间
const s_time = sessionObj.time
// 间隔时间(ms),小于此时间视为重复提交
const interval = 1000
if (s_data === requestObj.data && requestObj.time - s_time < interval && s_url === requestObj.url) {
const message = '数据正在处理,请勿重复提交'
console.warn(`[${s_url}]: ` + message)
return Promise.reject(new Error(message))
} else {
cache.session.setJSON('sessionObj', requestObj)
}
}
}
return config
}, error => {
console.log(error)
Promise.reject(error)
})
/**
* 响应拦截器
* 对响应数据做点什么
* @param {Object} res - axios响应对象
* @returns {Object|Promise} 处理后的响应数据或拒绝的Promise
*/
// 响应拦截器
service.interceptors.response.use(res => {
// 未设置状态码则默认成功状态
const code = res.data.code || 200
// 获取错误信息
const msg = errorCode[code] || res.data.msg || errorCode['default']
// 二进制数据则直接返回
if (res.request.responseType === 'blob' || res.request.responseType === 'arraybuffer') {
return res.data
}
if (code === 401) {
if (!isRelogin.show) {
isRelogin.show = true
ElMessageBox.confirm('登录状态已过期,您可以继续留在该页面,或者重新登录', '系统提示', { confirmButtonText: '重新登录', cancelButtonText: '取消', type: 'warning' }).then(() => {
isRelogin.show = false
useUserStore().logOut().then(() => {
location.href = '/index'
})
}).catch(() => {
isRelogin.show = false
})
}
return Promise.reject('无效的会话,或者会话已过期,请重新登录。')
} else if (code === 500) {
ElMessage({ message: msg, type: 'error' })
return Promise.reject(new Error(msg))
} else if (code === 601) {
ElMessage({ message: msg, type: 'warning' })
return Promise.reject(new Error(msg))
} else if (code !== 200) {
ElNotification.error({ title: msg })
return Promise.reject('error')
} else {
return Promise.resolve(res.data)
}
},
error => {
console.log('err' + error)
let { message } = error
if (message == "Network Error") {
message = "后端接口连接异常"
} else if (message.includes("timeout")) {
message = "系统接口请求超时"
} else if (message.includes("Request failed with status code")) {
message = "系统接口" + message.substr(message.length - 3) + "异常"
}
ElMessage({ message: message, type: 'error', duration: 5 * 1000 })
return Promise.reject(error)
}
)
/**
* 通用下载方法
* @param {string} url - 下载接口地址
* @param {Object} params - 请求参数
* @param {string} filename - 下载文件名
* @param {Object} config - 其他配置项
* @returns {Promise} 下载Promise
*/
// 通用下载方法
export function download(url, params, filename, config) {
downloadLoadingInstance = ElLoading.service({ text: "正在下载数据,请稍候", background: "rgba(0, 0, 0, 0.7)", })
return service.post(url, params, {
transformRequest: [(params) => { return tansParams(params) }],
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
responseType: 'blob',
...config
}).then(async (data) => {
const isBlob = blobValidate(data)
if (isBlob) {
const blob = new Blob([data])
saveAs(blob, filename)
} else {
const resText = await data.text()
const rspObj = JSON.parse(resText)
const errMsg = errorCode[rspObj.code] || rspObj.msg || errorCode['default']
ElMessage.error(errMsg)
}
downloadLoadingInstance.close()
}).catch((r) => {
console.error(r)
ElMessage.error('下载文件出现错误,请联系管理员!')
downloadLoadingInstance.close()
})
}
export default service
登录请求示例
javascript
// 导入request.js
import request from '@/utils/request'
// 登录方法
export function login(username, password, code, uuid) {
const data = {
username,
password,
code,
uuid
}
return request({
// 向/login发送请求
url: '/login',
// 添加请求头
headers: {
isToken: false,
repeatSubmit: false
},
// post请求
method: 'post',
// 发送数据
data: data
})
}
三、引入依赖
参考官方文档 除了element-ui组件以及脚手架内置的业务组件,有时还需要引入其它外部组件,这里以引入vue-count-to(在Vue3版本的依赖中已没有vue-count-to)为例进行介绍。
在终端输入下面的命令完成安装:
shell
$ npm install vue-count-to --save
加上 --save 参数会自动添加依赖到package.json中
四、组件
4.1 注册组件
4.1.1 局部注册
在组件的 components 选项中注册,仅在当前组件内可用
javascript
import MyComponent from './MyComponent.vue'
export default {
components: {
// 局部注册组件
MyComponent
}
}
4.1.2全局注册
使用 app.component() 方法注册,注册后可在应用的任何地方使用
javascript
import { createApp } from 'vue'
import MyCompoent from './MyCompoent.vue'
const app = createApp({})
// 全局注册组件
app.component('MyComponent', MyComponent)
4.2 组件通信
4.2.1 父子组件通信
Props(父传子)
javascript
<!-- 父组件 -->
<template>
<ChildComponent :message="parentMessage" />
</template>
<script setup>
import { ref } from 'vue'
import ChildComponent from './ChildComponent.vue'
const parentMessage = ref('Hello from parent')
</script>
javascript
<!-- 子组件 -->
<template>
<p>{{ message }}</p>
<button @click="handleClick">点击</button>
</template>
<script setup>
// 定义接收的 props
const props = defineProps({
message: {
type: String,
required: true
}
})
// 向父组件发送事件
const emit = defineEmits(['childEvent'])
const handleClick = () => {
emit('childEvent', '数据从子组件传来')
}
</script>
emits(子传父)
- 使用 defineEmits 声明可触发的事件
- 父组件通过 v-on 监听子组件事件
ruoyi中的应用: 在src→components→Pagination→index.vue中
javascript
<template>
<div :class="{ 'hidden': hidden }" class="pagination-container">
// 事件绑定
<el-pagination
:background="background"
// 双向绑定当前页码
v-model:current-page="currentPage"
// 双向绑定每页条数
v-model:page-size="pageSize"
:layout="layout"
:page-sizes="pageSizes"
:pager-count="pagerCount"
:total="total"
// 监听每页条数改变事件
@size-change="handleSizeChange"
// 监听当前页码改变事件
@current-change="handleCurrentChange"
/>
</div>
</template>
<script setup>
import { scrollTo } from '@/utils/scroll-to'
// Props 定义(父组件向子组件传递数据)
const props = defineProps({
// 定义 total prop,父组件必须传递数据总数,类型为数字
total: {
required: true,
type: Number
},
// 定义 page prop,接收当前页码,默认值为1
page: {
type: Number,
default: 1
},
// 定义 limit prop,接收每页显示条数,默认值为20
limit: {
type: Number,
default: 20
},
pageSizes: {
type: Array,
default() {
return [10, 20, 30, 50]
}
},
// 移动端页码按钮的数量端默认值5
pagerCount: {
type: Number,
default: document.body.clientWidth < 992 ? 5 : 7
},
layout: {
type: String,
default: 'total, sizes, prev, pager, next, jumper'
},
background: {
type: Boolean,
default: true
},
autoScroll: {
type: Boolean,
default: true
},
hidden: {
type: Boolean,
default: false
}
})
// 声明可触发事件
const emit = defineEmits()
// 创建currentPage计算属性
const currentPage = computed({
// 返回父组件page的值
get() {
return props.page
},
// 当值改变时,通过 update:page 事件通知父组件更新
set(val) {
emit('update:page', val)
}
})
// 创建pageSize计算属性
const pageSize = computed({
// 返回父组件传的limit值
get() {
return props.limit
},
// 当值改变时,通过 update:limit 事件通知父组件更新
set(val){
emit('update:limit', val)
}
})
// 事件处理函数(子组件向父组件通信)
function handleSizeChange(val) {
if (currentPage.value * val > props.total) {
currentPage.value = 1
}
// 通过 pagination 事件将新的分页信息传递给父组件
emit('pagination', { page: currentPage.value, limit: val })
if (props.autoScroll) {
scrollTo(0, 800)
}
}
// 处理当前页码改变事件
function handleCurrentChange(val) {
// 通过 pagination 事件将新的分页信息传递给父组件
emit('pagination', { page: val, limit: pageSize.value })
if (props.autoScroll) {
scrollTo(0, 800)
}
}
</script>
<style scoped>
.pagination-container {
background: #fff;
}
.pagination-container.hidden {
display: none;
}
</style>
示例: 1、新建测试菜单 
2、新建vue文件 
3、父子组件通信代码
javascript
<!-- 父组件 -->
<template>
<div style="text-align: center; font-size: 20px">
测试页面
<testa :name="name" @ok="ok"></testa>
子组件传来的值 : {{ message }}
</div>
</template>
<script setup> import { ref } from 'vue'
import testa from "./a.vue";
// 响应式数据,初始化name message
const name = ref("若依")
const message = ref("")
// 方法定义,将接收的messageValue的值赋给message
const ok = (messageValue) => {
message.value = messageValue
}
</script>
javascript
<!-- 子组件 -->
<template>
<div>
这是a组件 name:{{ name }}
<button @click="click">发送</button>
</div>
</template>
<script setup>import { ref } from 'vue'
// 定义props
const props = defineProps({
name: {
type: String,
default: ""
},
})
// 定义emit
const emit = defineEmits(['ok'])
// 响应式数据
const message = ref("我是来自子组件的消息")
// 方法定义
const click = () => {
emit('ok', message.value)
}
</script>
其中 <testa :name="name" @ok="ok">
- :name="name" 向子组件传递数据,将父组件name响应式变量的值传递给子组件的name prop
- @ok="ok"
- 监听子组件触发的ok事件
- 当子组件通过emit('ok',data)触发事件时,执行父组件的ok方法
4、实现效果
点击发送按钮后 