玩轉 CMS 目前接手的內容管理系統(CMS)基於 ant-design-vue-pro(簡稱模板項目或ant-vue-pro) 開發的,經過許多次迭代,形成了現在的模樣(簡稱本地項目)。 假如讓一名新手接手這個項目,他會遇到很多問題,比如 .env 的作用、開發時後端介面沒有寫好如何聯調、樣式使用 ...
玩轉 CMS
目前接手的內容管理系統(CMS)基於 ant-design-vue-pro(簡稱模板項目
或ant-vue-pro
) 開發的,經過許多次迭代,形成了現在的模樣(簡稱本地項目
)。
假如讓一名新手接手這個項目,他會遇到很多問題,比如 .env 的作用、開發時後端介面沒有寫好如何聯調、樣式使用less還是 CSS Modules、表單和表格如何使用等等
技術是為產品服務,只需要能用技術做出項目,不需要所有技術、所有最佳實踐都清楚。好比中醫發展了好幾千年,許多本源的東西老中醫也是不清楚的,但我們摸索出一套規則,按照這個能治病,這個就很好。
本地項目使用的是 ant design vue 1.x 版本,基於 vue 2
本系列目的:讓新手快速接手這個 CMS 系統
樣式
Ant Design Pro 預設使用 less 作為樣式語言。
Tip: less 語法 —— 重要,不緊急(後續補上)。直接在 less 中用 css 語法也能完成項目,然後逐步的利用 less 功能。
vscode 搜索,發現 90% 以上都有 scoped
,樣式語言也確實是 less。
// 69處
<style
// 49處
<style lang="less" scoped>
// 15處
<style scoped>
樣式開發過程,要避免全局污染
,通過 scoped 特性和 css modules 設置組件樣式作用域。
<style lang="less" scoped>
.chart-trend {
display: inline-block;
font-size: 16px;
line-height: 24px;
}
</style>
Tip:
註
:避免在 scoped 中使用元素選擇器。比如轉成 button[data-v-xxxxxx]
會比類選擇器組合要慢,因為要匹配的元素太多了。
@import
在單文件組件的樣式中,通過 @import 引入 less 文件:
<template>
<div>
<p class="red">hello</p>
</div>
</template>
<style lang="less" scoped>
@import './index.less';
</style>
// index.less
.red{
color: red;
font-size: 23.5px;
}
請問 .red 是全局的還是局部的,是否會影響到其他頁面?
筆者測試發現,是局部
的。最後編譯出來是這樣:
<style type="text/css">.red[data-v-2b80bebf] {
color: red;
font-size: 23.5px;
}
</style>
Tip:網上有的說這麼寫是全局。
如果不加 scoped
則會全局生效。就像這樣:
<style lang="less">
@import './index.less';
</style>
導入寫法
- 從 ant-design-vue 庫的樣式文件中導入 index.less 文件
// index.less
// ~ant-design-vue/lib/style/index 表示從 ant-design-vue 庫的樣式文件中導入 index 文件
// 導入的是 index.less 文件,而不是 index.css
// 在 Less 中,通過 @import 關鍵字導入的文件可以是 Less 文件或 CSS 文件。如果導入的文件沒有指定尾碼名,Less 會嘗試導入同名的 .less 文件,如果不存在,Less 會嘗試導入同名的 .css 文件。
@import '~ant-design-vue/lib/style/index';
- 同一目錄下的名為chart.less的文件
<style lang="less" scoped>
// 同一目錄下的名為chart.less的文件。不存在,Less 將會繼續嘗試導入同名的chart.css文件
@import "chart";
</style>
@import '../index.less';
是 Less 的新語法格式,它不使用 url() 函數。更加簡潔和直觀。
// 舊語法
@import url('../index.less')
// 新語法
@import '../index.less';
使用的是支持新語法的 Less 版本,這兩種寫法是等價的。
@import "~@/components/index.less";
是一種在 Less 中導入模塊化組件的常見方式。
<style lang="less" scoped>
// 正確
@import "~@/components/test.less";
// 錯誤。less 並不會識別 @ 符號作為項目根目錄的表示
// @import "@/components/test.less";
</style>
樣式文件類別
在一個項目中,樣式文件根據功能不同,可以劃分為不同的類別
公共樣式
可以將樣式提取到一個公共文件,比如 Pro 提取的 src/global.less 然後在 main.js 將樣式引入 import './global.less'
工具樣式
src/utils/utils.less 這裡可以放置一些工具函數供調用,比如清除浮動 .clearfix。
// mixins for clearfix
// ------------------------
.clearfix() {
zoom: 1;
&::before,
&::after {
display: table;
content: ' ';
}
&::after {
height: 0;
clear: both;
font-size: 0;
visibility: hidden;
}
}
.clearfix() 混合器定義了清除浮動的樣式。然後,你可以通過 .clearfix() 在選擇器 .selector 中調用混合器,從而應用清除浮動的樣式:
.selector {
// 調用 .clearfix() 混合器
.clearfix();
}
通用模塊級
例如 src/layouts/BasicLayout.less,裡面包含一些基本佈局的樣式,被 src/layouts/BasicLayout.vue 引用,項目中使用這種佈局的頁面就不需要再關心整體佈局的設置。如果你的項目中需要使用其他佈局,也建議將佈局相關的 js 和 less 放在這裡 src/layouts
。
組件級
組件相關的樣式,有一些在頁面中重覆使用的片段或相對獨立的功能,你可以提煉成組件,相關的樣式也應該提煉出來放在組件中,而不是混淆在頁面里。
Tip:有時樣式配置特別簡單,也沒有重覆使用,你也可以用內聯樣式 style="{ fontSize: fontSizeVar }" 來設置。
覆蓋組件樣式
由於業務的個性化需求,我們經常會遇到需要覆蓋組件樣式的情況。請看示例:
<template>
<div class="test-wrapper">
<a-select v-model="name" style="width:400px">
<a-select-option value="1">Option 1</a-select-option>
<a-select-option value="2">Option 2</a-select-option>
<a-select-option value="3">Option 3</a-select-option>
</a-select>
</div>
</template>
<script>
export default {
data(){
return {
name: 'Option 1'
}
}
}
</script>
<style lang="less" scoped>
// 使用 scss, less 時,可以用 /deep/ 進行樣式穿透
.test-wrapper ::v-deep .ant-select {
font-size: 26px;
}
.test-wrapper /deep/ .ant-select {
font-weight: 700;
}
</style>
<style scoped>
/* 這裡註釋不可以用 `//` */
.test-wrapper >>> .ant-select {
color: blue
}
</style>
在 scss、less 中可以使用 /deep/
或::v-deep
進行樣式穿透,在css 中可以使用 >>>
穿透。
最終渲染成:
<style type="text/css">
.test-wrapper[data-v-2b80bebf] .ant-select {
font-size: 26px;
}
.test-wrapper[data-v-2b80bebf] .ant-select {
font-weight: 700;
}
</style>
<style type="text/css">
/* 這裡註釋不可以用 `//` */
.test-wrapper[data-v-2b80bebf] .ant-select {
color: blue
}
</style>
請求
axios
首先回顧下 axios 如何使用的。
在 vue-admin-template(基於 element-ui) 中使用 axios 有以下幾步(參考這裡):
- 安裝 axios 包
- 對 axios 進行封裝,比如封裝到 request.js 文件中。關鍵增加請求攔截器和響應攔截器,比如返回 403、500等都會通過 Message 組件提示給用戶
- 每個頁面(或模塊)引入 request.js,導出介面。例如 api/table.js
Tip: 以前我們研究的 spug 開源項目(基於react)中 axios 也是類似用法 —— react axios
ant-vue-pro 中axios 用法類似:
- 通過 src\utils\request.js 封裝 request.js
- 每個頁面(或模塊)引入 request.js,導出介面。例如登錄模塊對應
src\api\login.js
為了方便管理維護,統一的請求處理都放在 @/src/api
文件夾中,並且一般按照 model 緯度進行拆分文件,如:
api/
user.js
permission.js
goods.js
...
本地項目 api
本地項目的 api 大概是這樣:
import { axios } from '@/utils/request'
import cancelAxios from 'axios'
import qs from 'qs'
/* 取消請求 */
var CancelToken = cancelAxios.CancelToken
export let cancellistApi
// 列表
export function list (parameter) {
return axios({
url: '/acms/demo/list',
method: 'get',
// params 參數用於將數據通過查詢字元串的形式添加到請求的 URL 中。這種方式適用於 GET 請求
params: parameter,
cancelToken: new CancelToken(function (c) {
cancellistApi = c
}),
// paramsSerializer 是 axios 的一個配置選項,用於將請求參數序列化為 URL 查詢字元串格式
// 比如轉換開始結束時間的格式:rangeDate[]=2023-11-11&rangeDate[]=2023-12-03 轉成 rangeDate=2023-11-11&rangeDate=2023-12-03
paramsSerializer: function (params) {
return qs.stringify(params, {
arrayFormat: 'repeat'
})
}
})
}
// get請求
export function review (id) {
return axios({
url: `/acms/demo/detail/${id}`,
method: 'get'
})
}
// post請求
export function pass (data) {
return axios({
url: `/acms/demo/pass`,
method: 'post',
// data 參數則是將數據作為請求的正文發送給伺服器。這種方式適用於 POST、PUT、DELETE 等請求
// 請求中的 Content-Type 頭附帶的是 application/json 或 multipart/form-data 等適合傳遞數據的類型
data
})
}
// 刪除文章
export function delArticle (id) {
return axios({
url: `/acms/article/${id}`,
// DELETE 方法用於請求伺服器刪除指定的資源。它通常需要在請求中指定要刪除的資源的標識符。例如,使用 DELETE 方法可以刪除用戶賬號、刪除文章等。
method: 'delete'
})
}
// 上線文章
// PUT 方法用於向指定的 URL 發送數據,通常是用於更新伺服器上的資源
export function onlineArticle (id) {
return axios({
url: `/acms/article/online/${id}`,
method: 'put'
})
}
get、post、put、delete請求,有時引入 qs
包,用於將請求的參數對象序列化,比如處理開始時間和結束時間。
Tip: qs
是一個用於序列化和反序列化 URL 查詢字元串的 JavaScript 庫。比如:
- 序列化:將 JavaScript 對象序列化為 URL 查詢字元串的格式,以便作為請求參數添加到 URL 中。例如,將 { key1: 'value1', key2: 'value2' } 轉換為 key1=value1&key2=value2。
- 反序列化:將 URL 查詢字元串解析為 JavaScript 對象,方便進行參數的提取和處理。例如,將 key1=value1&key2=value2 轉換為 { key1: 'value1', key2: 'value2' }。
- 處理複雜參數:qs 支持處理複雜對象、數組等數據結構,可以將它們轉換為合適的 URL 查詢字元串格式,方便進行網路請求。
cancelAxios 用於取消請求。不過有的同事用法不對,他用在搜索的 input 框中,想實現輸入字元延遲查詢。可以用 .lazy 或 lodash 的延遲。
Tip: ant design vue 中 lazy(<a-input v-model.lazy=
)不起作用。根據場景可以使用lodash 中的防抖或節流。當調用 delayedRequest 函數時,如果在 1000 毫秒內沒有再次調用該函數,那麼延遲時間結束後,請求邏輯將會執行。
import { debounce } from 'lodash';
const delayedRequest = debounce(() => {
// 在這裡執行你的請求邏輯
}, 1000); // 延遲時間為 1000 毫秒
// 調用 delayedRequest 函數
delayedRequest();
async和Promise
Tip:有關 promise 和 async 的介紹請看筆者之前文章:Promise、async
在 ant-vue-pro 中只使用了 Promise,沒有使用 async
。
從 ./src/views 中搜索:
- async、await 都沒有
- promise 在13個文件中有 23 處。
用法大致如下:
// 模擬網路請求、卡頓 800ms
new Promise((resolve) => {
}).then(() => {
})
// 兩個都成功才進入 then
Promise.all([repositoryForm, taskForm]).then(values => {
}).catch(() => {
})
new Promise((resolve) => {
}).then(() => {
}).catch(() => {
// 總是會執行。比如關閉`載入中...`彈框
}).finally(() => {
})
筆者認為 async 也需要用起來,async 和 Promise 不是替代關係,各有其使用場景 —— 使用 promise 還是 async/await
本地項目的寫法
本地項目
的寫法有以下幾種:
- 取消發佈。只處理了成功的情況
// axios 在響應攔截器中已經處理了http 非 200 的請求,也處理的 5000、4000 等 token 過期或其他錯誤,最後到這裡通常是約定好的介面數據。
cancelPublish(params).then((res) => {
if (res.code === 0) {
this.getDataList()
this.$message.success(res.msg)
}
})
- 編輯。在 finally 中關閉關閉
載入中...
彈框
async editFn (contentType, params) {
this.loading = true
try {
const res = await updateArticle(params)
if (res.code === 0) {
// 請求成功...
}
} catch (err) {
} finally {
this.loading = false
}
},
- 只有一個非同步請求,並且需要處理錯誤情況。可以這麼寫:
fetchData(false).then(() => {
// do ...
}).catch(() => {
console.log('error')
})
如果不覺得 try...catch 麻煩,也可以這樣:
try {
let p = await fetchData(false)
// do ...
} catch (e) {
console.log('error')
}
註
:try...catch 除了可以捕獲語法報錯,還能捕獲 reject 。
const fetchData = new Promise((resolve, reject) => {
reject(11);
});
async function myAsyncFunction() {
try {
let p = await fetchData;
console.log('p', p);
// 其他代碼...
} catch (e) {
console.log('error', e);
}
}
myAsyncFunction();
// => error 11
需要註意的幾點
錯誤寫法
遮罩層沒有消失
async function request() {
console.log('開啟遮罩')
// 報錯就退出了
let json = await requestUserList() // {1}
// 處理數據...
console.log('關閉遮罩');
}
await 與並行
// 下麵這段代碼是串列
async function foo() {
let a = await createPromise(1)
let b = await createPromise(2)
}
可以通過下麵兩種方法改為並行:
// 方式一
async function foo() {
let p1 = createPromise(1)
let p2 = createPromise(2)
// 至此,兩個非同步操作都已經發出
await p1
await p2
}
// 方式二
async function foo() {
let [p1, p2] = await Promise.all([createPromise(1), createPromise(2)])
}
async 有時會比 Promise 更容易調試
promise.catch
以下兩段代碼等效
promise1.then(null, () => {
console.log('拒絕')
})
// 等價於
promise1.catch(() => {
console.log('拒絕')
})
- 鏈式捕獲錯誤
let p1 = new Promise((resolve, reject) => {
resolve('10') // {1}
})
// 三個完成處理程式都有可能出錯,我們可以在末尾添加一個已拒絕處理的程式對這個鏈式統一處理
p1.then(() => {
throw new Error('fail')
console.log(1)
}).then(() => {
console.log(2)
}).then(() => {
console.log(3)
}).catch(e => {
console.log(e.message)
})
// 輸出:fail
如果將 {1} 改成 reject(10),也會直接到 catch 中,這時 e 就是 10。
await 的返回值
await 命令後面是一個 Promise 對象。如果不是,會被轉為一個立即 resolve 的 Promise 對象。Promise 的解決值會被當作該 await 表達式的返回值。
async function fa() {
return await 1
}
// 等價於
async function fa() {
return await Promise.resolve(1)
}
// 等價於
async function foo() {
return await new Promise((resolve, reject) => {
resolve(1)
})
}
在看一個示例:
function resolveAfter2Seconds(x) {
return new Promise((resolve) => {
setTimeout(() => {
resolve(x);
}, 2000);
});
}
async function f1() {
let x = await resolveAfter2Seconds(10).then(res => {return 1});
console.log(x); // 1
}
f1();
每調用一次 then 就會創建一個新的 Promise。
async 函數中的 return
return 返回值,會成為 then() 方法回調函數的參數
async function foo() {
return 'hello'
}
foo().then(v => {
console.log(v)
})
// hello
Promise 執行器錯誤
每個執行器中都隱含一個 try-catch 塊,所以錯誤會被捕獲並傳入給已拒絕回調。以下兩段代碼等價:
let p1 = new Promise(function(resolve, reject){
throw new Error('fail')
})
p1.catch(v => {
console.log(v.message) // fail
})
let p1 = new Promise(function(resolve, reject){
try{
throw new Error('fail')
}catch(e){
reject(e)
}
})
...
env
.env 是一種用來存儲環境變數的文件。
模板項目的 env
在 ant-vue-pro 中一共有三個 .env 文件。
// .env
NODE_ENV=production
VUE_APP_PREVIEW=false
VUE_APP_API_BASE_URL=/api
// .env.development
NODE_ENV=development
VUE_APP_PREVIEW=true
VUE_APP_API_BASE_URL=/api
// .env.preview
NODE_ENV=production
VUE_APP_PREVIEW=true
VUE_APP_API_BASE_URL=/api
.env
- 在所有的環境中被載入
.env.[mode]
- 只在指定的模式中被載入
一個環境文件只包含環境變數的“鍵=值”對:
FOO=bar
VUE_APP_NOT_SECRET_CODE=some_value
FOO2 = bar # 等號前後加空格也可以
註
:只有 NODE_ENV
,BASE_URL
和以 VUE_APP_
開頭的變數預設可以被識別。比如 FOO=bar
就不會被識別,除非使用其他手段進行變數擴展。
三個模式
預設情況下,一個 Vue CLI 項目有三個模式:
- development 模式用於 vue-cli-service serve
- test 模式用於 vue-cli-service test:unit
- production 模式用於 vue-cli-service build 和 vue-cli-service test:e2e
比如配置如下:
// .env
NODE_ENV=production
VUE_APP_PREVIEW=false
VUE_APP_API_BASE_URL=/api
VUE_APP_address = 長沙
// .env.development
VUE_APP_PREVIEW=true
VUE_APP_API_BASE_URL=/api
tel = 2222
VUE_APP_NAME=peng 3
VUE_APP_tel = 1111
運行 npm run serve
(對應package.json中 "serve": "vue-cli-service serve",
),會依次載入 .env 和 .env.development,後者會將前者的值覆蓋,所以最後通過 process.env 輸出:
console.log(process.env)
{
BASE_URL: "/"
NODE_ENV: "development"
VUE_APP_API_BASE_URL: "/api"
VUE_APP_NAME: "peng 3"
VUE_APP_PREVIEW: "true"
VUE_APP_address: "長沙"
VUE_APP_tel: "1111"
}
比如:
- VUE_APP_address 從 .env 中得到
- tel 被忽略
- NODE_ENV 在 .env.development 中被自動加上該屬性
- VUE_APP_tel 中 = 前後有空格也能生效
Tip:每次修改 .env,需要重新啟動服務才會生效。
--mode
可以通過 --mode 覆寫預設的模式。比如本地開發我可以代理到測試的url,也像代理到預發佈的url,我可以這樣做:
增加 .env.pre:
VUE_APP_URL=/myapi
package.json 增加:
"scripts": {
"serve": "vue-cli-service serve",
+ "serve:pre": "vue-cli-service serve --mode pre",
通過 npm run serve:pre 就能操作預發佈環境的數據。現在輸出:
console.log(process.env)
{
BASE_URL: "/"
NODE_ENV: "development"
VUE_APP_API_BASE_URL: "/api"
VUE_APP_PREVIEW: "false"
VUE_APP_URL: "/myapi"
VUE_APP_address: "長沙"
}
註意,現在 NODE_ENV 是 development,這個值來自 .env,vue-cli 沒有給我們增加一個 NODE_ENV 的變數。
execSync
關於構建,有的人可能會通過配置一個 js 去執行,這樣能更靈活,比如運維需要你創建一個每次創建一個文件。就像這樣:
// package.json
"scripts": {
"build": "node src/libs/shell.js",
"build:pre": "node src/libs/shell.js pre",
"build:test": "node src/libs/shell.js test"
// shell.js
var dist = 'dist'
var d = new Date().getTime().toString()
var env = process.argv.splice(2)[0]
var writeFileSync = require('fs').writeFileSync
var execSync = require('child_process').execSync
if (env == 'test') {
execSync('vue-cli-service build --mode test')
} else if (env == 'pre') {
execSync('vue-cli-service build --mode pre')
} else {
execSync('vue-cli-service build --mode prod')
}
writeFileSync(dist + '/xx.txt', d)
Tip: execSync 是 Node.js 的 child_process 模塊提供的一個同步執行外部命令的函數。它允許通過 JavaScript 代碼來執行系統命令,並等待命令執行完成後再繼續執行後續代碼。
Mock
ant-vue-pro 使用的是 mockjs2(好像和 mockjs 是同一個東西)。
Tip: mockjs 不會在瀏覽器中看到請求發出,更多有關 mockjs 的使用方法,請看 這裡。
本地項目使用的 proxy,當後端沒有提供介面給前端時,前端還是需要自己去模擬數據。
筆者通過如下方式給模板項目添加 mockjs。
首先模板項目中有 ant-vue-pro 中的 mockjs2 包,直接跳過安裝包。
創建 src/mock/index.js:
// 判斷環境不是 prod 時載入 mock 服務
if (process.env.NODE_ENV !== 'production') {
console.log('[antd-pro] mock mounting')
const Mock = require('mockjs2')
require('./skin.js')
Mock.setup({
timeout: 100 // 設置所有請求的響應時間為100ms
})
}
// skin.js
import Mock from 'mockjs2'
const navList = {
"code": 0,
"msg": "查詢成功",
"error": "",
"url": null,
"data": [
{...},
],
"success": true
};
Mock.mock(/\/mockjs-cms\/channel\/list\/navigation/, 'get', navList)
main.js 中引入 src/mock/index:
import './mock/index.js'
最後是發起請求:
export function queryNavigation() {
return axios({
url: `/mockjs-cms/channel/list/navigation`,
method: 'get',
})
}
註:中途筆者遇到兩個問題:
- mockjs 返回了導航數據,但頁面沒有顯示導航。將
timeout: 800
改成timeout: 100
。 - 本地開發時,保存
mock/index.js
(按 ctrl + s) 文件 vscode 不會觸發自動編譯,在別的文件中保存會觸發自動編譯。最後引入這個資源,比如在 main.js 中引入,然後重啟服務或vscode即可實現保存自動編譯。
另外公司使用了 eolink,後端定義好介面後,就有一個簡易 Mock 鏈接
,開發階段前端可以這樣:
export function queryNavigation() {
return axios({
// 簡易 Mock 鏈接
url: 'https://mockapi.eolink.com/81F5kJv4c4f5ff6d3b8a7880xxxx',
method: 'get',
})
}
出處:https://www.cnblogs.com/pengjiali/p/18021782
本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接。