前段時間和朋友做了一個區域網考試系統,總共有3個端:考生端、監考端、管理端。 框架與相關的庫 先簡單說明一下我使用的框架和相關的庫: 構建工具:Vite 框架:Vue3 UI組件庫:element-plus 網路請求庫:axios 路由跳轉:vue-router 狀態管理:pinia CSS擴展語言 ...
前段時間和朋友做了一個區域網考試系統,總共有3個端:考生端、監考端、管理端。
框架與相關的庫
先簡單說明一下我使用的框架和相關的庫:
構建工具:Vite
框架:Vue3
UI組件庫:element-plus
網路請求庫:axios
路由跳轉:vue-router
狀態管理:pinia
CSS擴展語言:sass
其它與項目功能需求相關的庫這裡就不一一列出了
多端非根路徑部署
考慮到每一個用戶理論上只會使用其中一個端,如果將三個端綁定在一個Vue項目上,則會導致“捆綁銷售”。因此,將三個端用三個Vue項目完成,然後讓後端開發人員使用nginx配置好映射。最後我需要再寫一個根路徑的入口頁面,用於跳轉到三個端。
/
:根路徑,頁面的內容主要是三個按鈕,分別跳轉到三個端;/admin
:管理端;/teacher
:監考端;/student
:考生端。
三個端的路徑經由nginx配置之後,指向三個Vue項目的index.html
,然後再載入各自的main.js
。
與以往將前端項目部署在根路徑的情況不同,將前端項目部署在非根路徑需要做相關配置。
主要是需要修改vite.config.js
和vue-router
的配置文件。
以管理端為例,由於其項目部署在/admin
,因此需要配置項目的base
。
vite.config.js
export default defineConfig({
...
base: '/admin/',
...
})
vue-router
配置文件
const router = createRouter({
...
history: createWebHistory(import.meta.env.BASE_URL),
...
})
使用history模式,需要後端在nginx上做配置。而createWebHistory
函數的參數需要傳入base
,即上面配置的/admin/
。
而餘下的routes
配置,就根據以往的編寫方式就可以。
例如,管理端的登錄頁面,在配置了base: '/admin/'
的情況下,在配置登錄頁面的路由的時候,只需要寫/login
:
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
redirect: {name: 'login'}
},
{
path: '/login',
name: 'login',
component: ()=>import('../views/LoginView.vue')
}
]
})
實際上,從整個考試系統的角度看來,它匹配到的路徑應該是:/admin/login
。
這是因為/admin/
會先被nginx的配置捕獲到,然後指向管理端這個Vue項目,返回管理端的index.html
和main.js
給用戶(該系統的管理員),然後路徑後續的/login
會因為main.js
中引入的路由配置文件,匹配上LoginView.vue
,即登錄頁面。
盒子的最大寬度
頁面中的文字依據來源可以分為兩種:
- 靜態文本:即本身固化在代碼中的文本;
- 動態文本:由用戶輸入並顯示在頁面中的文本。
靜態文本,例如側邊導航欄的按鈕的文本,文本的字數是固定的。因此,側邊導航欄的寬度可以寫成固定的。
而動態文本,是由用戶輸入的,並且大多數時候沒有嚴格的字數限制。
我一開始犯了一個錯,就是只使用flex:3
和flex:7
簡單地將頁面分為左右佈局,然後左邊是一個列表,每一項都是一行用戶輸入的數據,即不做換行處理。
當用戶輸入了長文本之後,左邊的列表會被子元素撐大,從而導致頁面的左右佈局比例被破壞。
因此這裡由用戶輸入的數據構成的列表,應該使用css設置一個max-width
,限制其最大寬度。
對象的深拷貝
使用JSON簡單地實現了對象的深拷貝
// 存儲對象的數組
list: []
// 添加新對象
list.push(JSON.stringify(newItem))
// 獲取對象
function getItem(params){
...do some search
return Json.parse(target)
}
pinia 實現試題管理模塊
這裡的試題是指添加試題時的階段,即需要提供讀與寫操作。
- state:
state: ()=>({
// 題目列表,存儲題目對象,使用JSON簡單實現了對象的深拷貝
qList: [],
// 當前編輯的題目的指針
currIdx: -1
}),
- getter:(返回常用數據)
getters: {
// 題目數量
count(){
return this.qList.length
},
// 當前編輯的題目是否存在“上一題”
hasPrev(){
return this.currIdx>0
},
hasNext(){
return this.currIdx<this.count
}
},
- actions:向外提供操作方法
actions: {
// 初始化
init(){
this.qList.length = 0
this.currIdx = 0
},
// 寫操作
saveQuestion(q){
this.qList[this.currIdx] = JSON.stringify(q)
},
// 前一道題
goPrevQuestion(){
if(this.hasPrev){
return JSON.parse(this.qList[--this.currIdx] || "")
}
},
// 後一道題
goNextQuestion(){
const q = this.qList[++this.currIdx]
return q===undefined?undefined:JSON.parse(q)
},
// 上傳題目列表到後端
async uploadQuestionList(){
for await (let q of this.qList){
q = JSON.parse(q)
if(this.checkCompleteness(q)){
await uploadQuestion(q)
}
}
},
checkCompleteness(q){
// 用於檢查一道題目是否設置完整
},
isEmpty(q){
// 用於檢查一道題目是否沒有填寫任何內容
}
}
上述代碼中的checkCompleteness
和isEmpty
函數的實現涉及到試題對象的設計,較為複雜,這裡不給出代碼。
上傳題目列表到後端的操作中,為了實現按順序上傳,需要使用for await(... of ...)
,而不能使用foreach await
,後者無法保證上傳順序。
vite打包配置
在vite.config.js
中,通過如下配置,可以去除代碼中的console.log
,避免將數據帶到生產環境,同時將js
文件和assets
文件打包到不同文件夾。
export default defineConfig({
...
build: {
terserOptions: {
compress: {
// 生產環境時移除console.log調試代碼
drop_console:true,
drop_debugger: true
}
},
rollupOptions: {
output: {
//對靜態文件進行打包處理(文件分類)
chunkFileNames: 'assets/js/[name]-[hash].js',
entryFileNames: 'assets/js/[name]-[hash].js',
assetFileNames: 'assets/[ext]/[name]-[hash].[ext]'
}
}
}
...
})
文件下載功能
項目中有需求是:用戶點擊按鈕之後下載文件。使用js實現:
// 下載文件
const downloadFile = () => {
const tempDom = document.createElement('a')
tempDom.href = "/file/demo.txt"
tempDom.download = 'fileName.txt'
tempDom.click()
}
這裡創建了一個DOM對象,路徑href
是伺服器上的文件路徑,download
屬性的字元串是用戶下載到的文件名。
pdf預覽功能
我寫了一個pdf-previewer.html
文件,並放在根路徑下,然後每次不同端的項目中,需要訪問pdf文件的時候,就調用:
window.open('/pdf-preview.html?url='+path)
path是後端傳過來的文件路徑。
在pdf-previewer.html
中,
- 使用
iframe
標簽; - 封裝了
getQueryVariable
函數,用來獲取訪問地址攜帶的參數(即文件的地址); - 為瞭解決緩存問題(利用iframe打開pdf後,當再次利用iframe打開另一個pdf時會顯示第一份pdf,原因是瀏覽器對url的緩存處理),在url上添加時間戳。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PDF預覽視窗</title>
<link rel="icon" href="/icon.png">
<style>
*{
margin: 0;
padding: 0;
box-sizing: border-box;
}
body{
width: 100%;
height: 100vh;
overflow: hidden;
}
</style>
</head>
<body>
<iframe id="viewer" src="" style="width: 100%;height: 100vh;" frameborder="0"></iframe>
<script>
window.addEventListener ('load', () => {
function getQueryVariable(variable) {
let query = window.location.search.substring(1);
let vars = query.split("&");
for (let i = 0; i < vars.length; i++) {
let pair = vars[i].split("=");
if (pair[0] === variable) { return pair[1]; }
}
return (false);
}
let path = getQueryVariable('url')
const fresh = new Date().getTime()
path += '?fresh=' + fresh
document.getElementById('viewer').setAttribute('src', path)
});
</script>
</body>
</html>
常用Message封裝
el-message
組件對於反饋功能很常用,封裝成函數:
import { ElMessage } from 'element-plus'
const showError = (msg)=>{
return ElMessage({
type: 'error',
message: msg
})
}
const showSuccess = (msg)=>{
return ElMessage({
type: 'success',
message: msg
})
}
const showInfo = (msg)=>{
return ElMessage({
type: 'info',
message: msg
})
}
export { showError, showSuccess, showInfo }
使用CSS常量
使用CSS常量記錄常用的尺寸、顏色,可以改一處,而變全局。
以下常量是我的項目中的一部分顏色,僅供參考,不具有普適性。
:root {
--main-color: #31364d;
--header-height: 60px;
--border-color: #DCDFE6;
--border-color-light: #E4E7ED;
--border-color-darker: #CDD0D6;
--page-background: #F2F3F5;
}