前言
- 時過境遷,我們見證了諸如 webpack、Rollup 和 Parcel 等工具的變遷,它們極大地改善了前端開發(fā)者的開發(fā)體驗。
- 然而,當(dāng)我們開始構(gòu)建越來越大型的應(yīng)用時,需要處理的 JavaScript 代碼量也呈指數(shù)級增長。包含數(shù)千個模塊的大型項目相當(dāng)普遍?;?JavaScript 開發(fā)的工具就會開始遇到性能瓶頸:通常需要很長時間(甚至是幾分鐘!)才能啟動開發(fā)服務(wù)器,即使使用模塊熱替換(HMR),文件修改后的效果也需要幾秒鐘才能在瀏覽器中反映出來。如此循環(huán)往復(fù),遲鈍的反饋會極大地影響開發(fā)者的開發(fā)效率和幸福感。
- Vite 旨在利用生態(tài)系統(tǒng)中的新進展解決上述問題:瀏覽器開始原生支持 ES 模塊,且越來越多 JavaScript 工具使用編譯型語言編寫。
以上引用了Vite官網(wǎng)的描述
為什么要記錄這篇文章而且還很詳細呢?那是因為進入當(dāng)前這家公司的時候,這邊連基本的前端架構(gòu)都是沒有的,每次做項目都是復(fù)用網(wǎng)上那些很臃腫的代碼模版(做一套扔一套那種,沒有保留通用前端模板框架的)。然后這個項目組之前都是vue2做開發(fā)、最近領(lǐng)導(dǎo)說以后項目考慮使用vue3來開發(fā)了,那我作為這項目組唯一的純前端人員(其他都是C#大佬,而且很多項目都是前后端不分離那種)就得考慮從頭搭建一套自定義的vue3架構(gòu)啦,結(jié)合之前項目的經(jīng)驗,從零開始搭建了這套前端架構(gòu)(由于沒有大佬帶,只能自個一點點查看資料且一步步摸索,期間也遇到了不少問題),如果有優(yōu)化的地方,還請各位大佬多多評論交流哈。抱拳!
那么接下來咱們一步步來構(gòu)建vite
+vue3
+ts
吧~
Tips:本篇文章是按照配置一步步配置的,所以稍微較長,如果是新手建議往下一步步配置,花費的時間也不會很長
一、構(gòu)建基礎(chǔ)項目模板
- 首先需要安裝Node
先看下安裝node后版本
node -v
Tips 由于vite在node低版本不支持,所以建議安裝最新的穩(wěn)定版本,或者是 14.18+
- 安裝Vue最新版本基礎(chǔ)模板
當(dāng)前基礎(chǔ)項目模板是按照vue最新版本構(gòu)建的(其他安裝方式可異步vite官網(wǎng)查看更多)
安裝命令如下:
npm init vue@latest
執(zhí)行以上會有部分配置需要選擇,本項目具體選擇如下圖:
執(zhí)行完后生成目錄如圖:
- 安裝依賴
npm i
- 項目執(zhí)行
npm run dev
- 訪問頁面
按照上述操作后訪問 http://localhost:5173/ 即顯示下面內(nèi)容啦
因為當(dāng)前demo是基于element-plus
,先安裝npm i element-plus -S
并在main.js
應(yīng)用
import ElementPlus from 'element-plus'
import 'element-plus/theme-chalk/index.css'
...
const app = createApp(App)
...
app.use(ElementPlus)
二、根據(jù)基礎(chǔ)模版搭建優(yōu)化項目結(jié)構(gòu)(持續(xù)更新中)
由于初始化的基礎(chǔ)項目結(jié)構(gòu)比較簡單,所以一般都需要根據(jù)個人習(xí)慣,優(yōu)化結(jié)構(gòu)及配置相關(guān)信息
A、環(huán)境配置優(yōu)化
上面的基礎(chǔ)模板可看到,并沒看到不同環(huán)境變量,其實是有測試環(huán)境和生產(chǎn)環(huán)境的,具體可以 使用import.meta打印出看下。
在main.ts
輸出import.meta.env
然后執(zhí)行npm run dev
,查看頁面控制臺可看到MODE
的值為"development"
執(zhí)行npm run build-only
,把構(gòu)建的dist內(nèi)的文件放置服務(wù)器。
- 本地開發(fā)推薦使用
nginx
(推薦使用),可參考Nginx在Window與Mac環(huán)境的使用及配置詳情 - 還可使用
express-generator
開啟服務(wù) - 當(dāng)然也可以直接在開發(fā)測試訪問
http://localhost:5173/dist/index.html
,但這種需要修改對應(yīng)文件引入路徑,想了解的可自行研究下哈
然后訪問可看到控制臺打印出MODE
的值為"production"
但由于項目開發(fā)到上線,可能都存在有不同環(huán)境需求,所以咱們就需要配置環(huán)境變量以對應(yīng)不同環(huán)境的Api,而且配置好之后,如無特殊修改,并不需要再修改環(huán)境配置相關(guān)的,這樣就可避免部署構(gòu)建后環(huán)境錯誤的情況啦
正常流程一般分為
- 開發(fā)環(huán)境(可根據(jù)實際開發(fā)情況鏈接對應(yīng)環(huán)境的Api)
- sit環(huán)境(測試)
- uat環(huán)境(業(yè)務(wù)驗收)
- prod環(huán)境(線上環(huán)境)
先把環(huán)境變量這些配置好,在根目錄新建不同環(huán)境的文件(具體配置的key默認VITE_
開頭,也可配置envPrefix'
值自定義)
- .env(這個是公共環(huán)境配置,都會獲取這里面的配置信息)
- .env.development
- .env.production
- .env.sit
- .env.uat
.env文件是共有配置,所以即使配置配置對應(yīng)執(zhí)行腳本環(huán)境也會獲取
# 公有全局環(huán)境配置
VITE_STORE_NAME = invade-
想要執(zhí)行不同命令讀取對應(yīng)環(huán)境配置文件,還需要再在package.json
配置對應(yīng)腳本,執(zhí)行腳本后vite會根據(jù)編譯的命令讀取對應(yīng)環(huán)境文件的,新增具體如下
{
...
"scripts": {
"dev": "vite serve --mode development",
"dev-sit": "vite serve --mode sit",
"dev-uat": "vite serve --mode uat",
"dev-prod": "vite serve --mode production",
"sit": "vite build --mode sit",
"uat": "vite build --mode uat",
"prod": "vite build --mode production",
...
},
...
}
-
vite serve
對應(yīng)的是本地開發(fā)聯(lián)調(diào)測試用的,可能不同環(huán)境都得在本地聯(lián)調(diào)測試問題,所以存在dev-sit
這些,如果不指定--mode
后面的值,默認為development
-
vite build
是構(gòu)建打包執(zhí)行的,如果不指定--mode
后面的值,默認為production
-
--mode
后面跟著的就是配置的環(huán)境變量值,即對應(yīng)上面截圖控制臺看到的MODE
的值
不過咱感覺項目結(jié)構(gòu)看起來沒那么優(yōu)雅(主要不想更目錄文件太多,影響閱讀)。
根據(jù)官網(wǎng)提供配置,可自定義個文件夾,放置這些環(huán)境配置文件,那咱們就在根目錄新建文件夾env(這個文件名稱可自定義,對應(yīng)下面envDir配置的值),把環(huán)境配置的文件都移進去
且需要修改vite.config.ts
配置,指定envDir: 'env',
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'
// https://vitejs.dev/config/
export default defineConfig({
// env配置文件變量前綴, 默認 VITE_,可自行定義
envPrefix: 'VITE_',
// env配置文件夾位置
envDir: 'env',
plugins: [vue(), vueJsx()],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
}
})
例如 .env.development
配置
# 本地開發(fā)環(huán)境的代理配置配置, 任何代理請寫好注釋
# 接口地址
VITE_PROXY_API=http://localhost:8000
# 構(gòu)建后文件引用 相對路徑
VITE_PUBLIC_PATH=/
# 輸出路徑
VITE_OUT_DIR=/dist/
執(zhí)行 npm run dev
控制臺打印import.meta.env
,可看到.env
和.env.development
配置的信息,如下
例如 .env.sit
配置
# 本地開發(fā)環(huán)境的代理配置配置, 任何代理請寫好注釋
# 接口地址
VITE_PROXY_API=http://localhost:8001
# 構(gòu)建后文件引用 相對路徑
VITE_PUBLIC_PATH=/
# 輸出路徑
VITE_OUT_DIR=/dist/
執(zhí)行 npm run dev-sit
(當(dāng)然執(zhí)行npm run sit
構(gòu)建后打印的也是一致的,其他環(huán)境下的也是如此)
控制臺打印import.meta.env
,可看到.env
和.env.sit
配置的信息,且VITE_PROXY_API
的值同dev環(huán)境的不一樣,如下
B、優(yōu)化生產(chǎn)構(gòu)建后 console和debugger關(guān)閉
由于生產(chǎn)一般避免控制臺打印相關(guān)信息,vite也有這種配置優(yōu)化,話不多說,直接上配置
import { defineConfig } from 'vite'
// https://vitejs.dev/config/
export default defineConfig({
...
build: {
minify: "terser", // 必須開啟:使用 terserOptions 才有效果
terserOptions: {
compress: {
drop_console: true,
drop_debugger: true,
},
},
}
})
如果這樣配置后直接構(gòu)建會報錯,提示terser not found.
提示需要安裝這個terser
,可執(zhí)行npm i terser -D
安裝
安裝完terser后再次構(gòu)建成功。然后訪問構(gòu)建的內(nèi)容發(fā)現(xiàn)console.log(import.meta.env)
沒打印出來,說明配置生效了
不過這樣配置發(fā)現(xiàn)如果測試環(huán)境想要看到打印信息判斷錯誤等,還得修改配置再次構(gòu)建下,明顯不友好,那么我們稍微優(yōu)化下只在prod
環(huán)境下生效,即在defineConfig
使用回調(diào)函數(shù)的回參來配置(查看源碼可看到defineConfig入?yún)⒂袃煞N),如下
import { defineConfig, loadEnv } from 'vite'
// https://vitejs.dev/config/
export default defineConfig(({ command, mode, ssrBuild }) => {
const env = loadEnv(mode, `${process.cwd()}/env`, '')
// 為什么要這樣做,是為了 process.env和mode一致性
Object.assign(process.env, env, { NODE_ENV: mode })
return {
mode,
...
build: {
minify: "terser", // 必須開啟:使用 terserOptions 才有效果
terserOptions: {
compress: {
drop_console: process.env.NODE_ENV === 'production' ? true : false, // 也可直接使用mode進行判斷
drop_debugger: process.env.NODE_ENV === 'production' ? true : false,
},
},
}
}
})
可看到有三個參數(shù),ssrBuild
為可選
command: 'build' | 'serve'; // 二選一,默認為serve ,可在執(zhí)行腳本配置(配置在vite 后面) 例如 "dev": "vite serve --mode development",
mode: string; // 自定義 一般會設(shè)置為 development | sit | uat | production
/**
* @experimental
*/
ssrBuild?: boolean; // 看起來應(yīng)該是配置SSR模式使用的,后面有機會再ssr模式下詳細記錄下
結(jié)合上面配置,在構(gòu)建sit | uat
的時候打印還在,而構(gòu)建production
沒有了,說明咱們配置成功啦。
那么為什么不直接使用process.env.NODE_ENV
直接判斷呢?因為在開發(fā)環(huán)境process.env.NODE_ENV
默認是development
,即使跑的是dev-sit
也還是一樣為development
。我們看個截圖,所以需要使用回調(diào)函數(shù)根據(jù)配置的--mode
進行判斷
C、優(yōu)化文件引用路徑
在很多時候,不同頁面需要使用同一個組件,且引入的路徑可能不一致,那么怎樣才能統(tǒng)一呢?其實在初始化構(gòu)建項目之后,框架本身已經(jīng)生成了最基本的文件指向了,看下配置
import { fileURLToPath, URL } from 'node:url'
...
import { defineConfig } from 'vite'
// https://vitejs.dev/config/
export default defineConfig({
...
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
},
...
})
即在defineConfig
配置resolve
引用相關(guān)配置指向,如果指向 ./src
不能滿足需要,可以自行自定義下(咱得習(xí)慣是使用一個@即可,前提需要把項目目錄結(jié)構(gòu)合理化,即不要過于深入嵌套且按模塊分好目錄)
使用方式,在所需要的文件引入,例如在/src/components/index/demo.ts引入/src/views/HomeView.vue文件
以下代碼可看出多層 ../
引入的文件使用了alias
配置@
指向./src
,即可避免文件引入錯誤等情況
// import HomeView from '../../views/HomeView.vue'
import HomeView from '@/views/HomeView.vue'
D、優(yōu)化項目全局文件類型聲明配置(ts的泛型)
-
.d.ts
文件,它是用來做類型聲明(declare)。它僅僅用來做類型檢測,告知TypeScript我們有哪些類型; -
d
是 (declare,聲明) -
declare module
** 聲明文件 -
declare namespace
** 聲明命名空間
初始化構(gòu)建出來的根目錄有個
env.d.ts
文件,會在tsconfig.app.json
文件引入(如果未配置,有些文件引入會報錯,因為沒有在任何地方聲明。而有些是會自帶聲明,例如document
在lib.dom.d.ts
文件中進行了聲明了,具體可按下ctrl
鍵盤并鼠標左鍵點擊查看即可)
如果有多個這種配置文件,就得在tsconfig.app.json
文件手動一個個引入進來,例如新增個wechat.d.ts
文件配置weixin-js-sdk
全局。
{
"extends": "@vue/tsconfig/tsconfig.dom.json",
"include": ["env.d.ts", "src/**/*", "src/**/*.vue", "wechat.d.ts"],
...
}
其實咱們也可以直接把此配置直接配置在env.d.ts
文件內(nèi),可是如果有更多配置咋辦呢?不就出現(xiàn)了很多配置在一起,可能修改或新增某個配置的時候,不小心刪除了其他的配置,那怎樣避免這種情況呢?
/// <reference types="vite/client" />
declare module 'weixin-js-sdk'
可以看到env.d.ts
配置文件以.d.ts
結(jié)尾,那咱就優(yōu)化下上面配置,先在根目錄新建個types
文件夾,然后把env.d.ts
文件放進來,在修改下tsconfig.app.json
文件(一般修改了env.d.ts
會自動修改在引入的位置),即"env.d.ts"
修改成"types/env.d.ts"
了
{
"extends": "@vue/tsconfig/tsconfig.dom.json",
"include": ["types/env.d.ts", "src/**/*", "src/**/*.vue"],
...
}
然后優(yōu)化tsconfig.app.json
配置"types/env.d.ts"
改為"types/*.d.ts"
即動態(tài)引入types文件夾下所有.d.ts
結(jié)尾的文件,如下
{
"extends": "@vue/tsconfig/tsconfig.dom.json",
"include": ["types/*.d.ts", "src/**/*", "src/**/*.vue"],
...
}
新建·global.d.ts
/**
* 自定義全局類型
*/
declare module 'js-cookie'
type IfEquals<X, Y, A=X, B=never> =
(<T>() => T extends X ? 1 : 2) extends
(<T>() => T extends Y ? 1 : 2) ? A : B;
/** 獲取可編輯key */
export type WritableKeys<T> = {
[P in keyof T]-?: IfEquals<{
[Q in P]: T[P];
}, {
-readonly [Q in P]: T[P];
}, P>
}[keyof T];
新建·shims-vue.d.ts
,配置一些聲明文件,例如 .vue
格式文件、lodash
、各種文件格式等
declare module '*.vue' {
import type { DefineComponent } from 'vue';
const component: DefineComponent<{}, {}, any>;
export default component;
}
// declare module '*.vue' {
// import { DefineComponent } from 'vue'
// const component: DefineComponent<{}, {}, any>
// export default component
// }
/**
* window上屬性自定義
*/
interface Window {
[key: string]: string | number | Types.Noop | Types.PlainObject | unknown;
WeixinJSBridge: any;
}
declare module '*.svg';
declare module '*.png';
declare module '*.jpg';
declare module '*.jpeg';
declare module '*.gif';
declare module '*.bmp';
declare module '*.tiff';
declare module '*.mp3';
declare module 'lodash';
declare module '@/utils/pictureVerfy/pawebjs.min.js';
以上按照需求配置即可
E、配置css全局變量
我們這邊使用less處理,需要安裝以下依賴支持
npm i less -D
npm i less-loader -D
一般項目都會有很多重復(fù)使用的色系,布局位置信息等,這個時候咱可以使用使用less動態(tài)配置對應(yīng)變量進行使用功能
首先得新建個.less文件在./src/assets/
新建variables.less
文件,內(nèi)容如下
/**
* 定義全局使用的 less 變量
*/
@paddingDefault: 32px;
/**
* 顏色值
*/
@a94442: #a94442;
然后配置variables.less
文件到項目全局,不然沒法使用這些定義好的變量哦
import { fileURLToPath, URL } from 'node:url'
import { resolve } from 'path'
import { defineConfig} from 'vite'
// https://vitejs.dev/config/
export default defineConfig(({ command, mode, ssrBuild }) => {
...
return {
...
css: {
preprocessorOptions: {
// less: {
// javascriptEnabled: true,
// additionalData: `@import "${resolve(__dirname, 'src/assets/styles/base.less')}";`
// }
less: {
modifyVars: {
hack: `true; @import (reference) "${resolve('src/assets/styles/variables.less')}";`,
},
javascriptEnabled: true
}
}
},
build: {
...
}
}
})
使用方式,上述配置好之后直接在需要使用的地方使用即可
<div class="content"></div>
<style lang="less">
.content {
background: @a94442;
width: @paddingDefault;
height: @paddingDefault;
}
</style>
下圖可看到對應(yīng)值變成了variables.less
文件配置好的值
F、新建文件夾(不同功能內(nèi)容分開放)
良好的習(xí)慣和語義化的目錄名稱有助于項目維護和開發(fā)者閱讀的心情
目錄結(jié)構(gòu)是否合理也是當(dāng)今團隊項目開發(fā)中相當(dāng)重要的一部分
先在/src
目錄下新建以下目錄(詳細的結(jié)構(gòu)放在了最下方,有興趣可直接劃到底部查看)
- App
- composables
- const
- entity
- enum
- infrastructure
- interface
- lang
- server
- utils
G、引入axios
- 安裝axios
npm i axios -S
- 引入axios
1、在/src/server
文件夾內(nèi)新建 index.ts
和interface.ts
文件
在/src
文件夾內(nèi)新建 interface
文件,再在interface文件夾內(nèi)新建 index.ts
文件
有使用到了lodash-es
,所以需要安裝npm i lodash-es -D
/src/server/index.ts
如下(其他配置自定義即可)
import axios, { type AxiosRequestConfig } from 'axios'
import { assign } from 'lodash-es'
import { ElMessage as message } from 'element-plus'
const UN_SUPPORT_DIY_HEADER_REGEX = /^http(s)?:\/\//i
// 請求錯誤統(tǒng)一處理
import ERRORCODES from '@/enum/error-code'
import { resetInterfacePath } from '@/utils'
// 默認請求失效時間60s
export const AXIOS_TIMEOUT_LIMIT = 60000
axios.defaults.timeout = AXIOS_TIMEOUT_LIMIT;
import type { NUNBER_STRING as ERROR_CODES_TYPES } from '@/interface'
// 也可以直接使用 typeof 獲取 ERROR_CODES 的接口類型,這個時候需要ERROR_CODES 在同一文件內(nèi)才有效果
// type ERROR_CODES_TYPES = typeof ERROR_CODES
const ERROR_CODES = ERRORCODES as ERROR_CODES_TYPES
/**
* 后臺接口公共的返回格式
* 具體根據(jù)實際跟后臺約定的定義
*/
export interface ResCommonType<T = unknown> {
code: number
data: T
msg?: string
}
// 請求攔截
axios.interceptors.request.use(
(config) => {
/**
* Request header not allowed by Access-Control-Allow-Headers in preflight response
* 第三方接口不支持頭
*/
config.url = resetInterfacePath(config.url || '')
if (!UN_SUPPORT_DIY_HEADER_REGEX.test(config.url ?? '')) {
assign(config.headers, {
// 'X-RequestFrom': 'person',
})
}
// if (config?.url?.includes('/DownloadFile')) {
// assign(config.headers, {
// 'Accept': 'ext/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
// 'Content-Type': 'application/x-www-form-urlencoded',
// 'responseType': 'blob'
// })
// }
return config
},
(err) => Promise.reject(err),
)
// 響應(yīng)攔截
axios.interceptors.response.use(
(response) => {
if (typeof response.data === 'string') {
// location.href = '/signIn'
// return Promise.reject('登錄失效')
}
const data = response.data
const resCode: keyof ERROR_CODES_TYPES = data.status || data.code
console.log('ERROR_CODES[resCode]', ERROR_CODES[resCode])
if (ERROR_CODES[resCode]) {
return Promise.reject(data)
}
return Promise.resolve(data)
},
(err) => {
let errCode: keyof ERROR_CODES_TYPES = 500
let errMsg = err?.message || '連接到服務(wù)器失敗'
if (err?.response) {
const { code, status } = err.response
errCode = code || status || 500
errMsg = ERROR_CODES[errCode]
}
console.log('ERROR_CODES[]', errCode, ERROR_CODES[errCode])
message.error(errMsg)
return Promise.reject({
code: errCode,
msg: errMsg,
data: err || null,
})
},
)
/**
* 發(fā)起GET請求, 泛型 T 定義返回數(shù)據(jù) data 項類型, U 定義請求數(shù)據(jù)類型
* @param {string} url 請求鏈接
* @param {object} params 請求參數(shù)
* @param {object} config 配置
*/
export const get = <U = unknown, T = unknown>(
url: string,
params?: U,
config?: AxiosRequestConfig,
) => axios.get<T, T>(
url, { params: { ...params, t: Date.now() }, ...config },
)
/**
* 發(fā)起POST請求, 泛型 T 定義返回數(shù)據(jù) data 項類型, U 定義請求數(shù)據(jù)類型
* @param {string} url 請求鏈接
* @param {object} params 請求參數(shù)
* @param {object} config 配置
*/
export const post = <U = unknown, T = unknown>(
url: string,
params?: U,
config: AxiosRequestConfig = {},
) => {
if (Array.isArray(params)) {
return axios.post<T, T>(url, [...params], config)
}
return axios.post<T, T>(url, { ...params }, config)
}
/**
* 發(fā)起FormData請求, 泛型 T 定義返回數(shù)據(jù) data 項類型, U 定義請求數(shù)據(jù)類型
* @param {string} url 請求鏈接
* @param {object} params 請求參數(shù)
* @param {object} config 配置
*/
// export const postForm = <U = unknown, T = unknown>(
// url: string,
// params?: U,
// config: AxiosRequestConfig = {},
// ) => axios.post<T, ResCommonType<T>>(url, qs.stringify({ ...params }), config);
export const postForm = <U = unknown, T = unknown>(
url: string,
params?: U,
config: AxiosRequestConfig = {},
) => axios.post<T, T>(url, params, config)
/**
* 文件下載請求, 泛型 T 定義返回數(shù)據(jù) data 項類型, U 定義請求數(shù)據(jù)類型
* @param {string} url 請求鏈接
* @param {object} params 請求參數(shù)
* @param {object} config 配置
*/
// export const postFile = <U = unknown, T = unknown>(
// url: string,
// params?: U,
// config: AxiosRequestConfig = { responseType: 'blob' },
// ) => axios.post<T, ResCommonType<T>>(url, { ...params }, config);
export default {
get,
post,
// postForm,
// postFile,
}
/src/server/interface.ts
如下(用于接口的共有interface(接口)定義部分)
export interface BaseResType {
ResultCode: number
ResultDescription: string
}
export interface ApiResponseType<T> extends BaseResType{
result: T
}
2、在/src/enum
文件夾內(nèi)新建 error-code.ts
文件(后面配置成多語言)
// 請求錯誤統(tǒng)一處理
const ERROR_CODES = {
400: '錯誤請求,狀態(tài)碼:400',
401: '未授權(quán),請重新登錄,狀態(tài)碼:401',
403: '拒絕訪問,狀態(tài)碼:403',
404: '請求錯誤,未找到該資源,狀態(tài)碼:404',
405: '請求方法未允許,狀態(tài)碼:405',
408: '請求超時,狀態(tài)碼:408',
500: '服務(wù)器端出錯,狀態(tài)碼:500',
501: '網(wǎng)絡(luò)未實現(xiàn),狀態(tài)碼:501',
502: '網(wǎng)關(guān)錯誤,狀態(tài)碼:502',
503: '服務(wù)不可用,狀態(tài)碼:503',
504: '網(wǎng)絡(luò)超時,狀態(tài)碼:504',
505: 'HTTP版本不支持該請求,狀態(tài)碼:505',
}
export default ERROR_CODES
3、在/src/utils
文件夾內(nèi)新建 index.ts
文件(用于整個項目共有方法等)
import type { unKnow } from "@/interface";
/**
* 拼接接口路徑,例如代理接口的時候后臺接口前綴不統(tǒng)一等,可以自定義個前綴,有助于代理配置。
* @param url 接口路徑(一般只會配置相對路徑,也可直接配置絕對路徑)
* @returns
*/
export const resetInterfacePath = (url: string) => {
// return `/api/${url}`
return url
}
/**
* 對象轉(zhuǎn)formData
* @param data
*/
export const objectToFormData = (data: object): FormData => {
let formData = new FormData();
for (const [key, value] of Object.entries(data)) {
if (value !== null && value !== undefined) {
formData.append(key, value);
} else {
formData.append(key, '');
}
}
return formData;
};
/**
* 對象轉(zhuǎn)數(shù)組
* @param data
*/
export const objectToArray = (data: object): Array<Object> => {
let arr: Array<Object> = []
for (const [key, value] of Object.entries(data)) {
if (value !== null && value !== undefined) {
arr.push(value)
}
}
return arr;
};
/**
* 地址數(shù)據(jù)轉(zhuǎn)換
* @param data
* @returns
*/
export const translateAddress = (data: any) => {
if (data instanceof Object && !Array.isArray(data)) {
data = objectToArray(data)
data = data.map((item: any) => {
return {
value: item.code,
label: item.name,
children: translateAddress(item.node)
}
})
}
return data
}
interface Obj {
[key: string]: string | number;
}
/**
* 根據(jù)對象value獲取key
* @param obj
* @param value
* @returns
*/
export const objectGetKeyForValue = (obj: Obj | undefined, value: string | number): string => {
if (!obj) {
return ''
}
for (const key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
const element = obj[key];
if (value === element) {
return key
}
}
}
return ''
}
/**
* 根據(jù)數(shù)組中子對象value獲取對應(yīng)label
* @param arr
* @param value
* @returns
*/
export const arrayGetLabelForValue = (arr: Array<unKnow> | undefined, value: string | number): string => {
if (!arr?.length) {
return ''
}
let label = ''
arr.forEach(element => {
if (element.value === value) {
label = element.label
}
});
return label
}
4、在 /src/interface/index.ts
新增(整個項目的 interface
接口定義,即公共部分)
export interface unKnow {
[key: string]: any;
}
export interface Undefined {
[key: string]: undefined;
}
export interface NUNBER_STRING{
[key: number]: string;
}
export interface OBJ_STRING {
[key: string]: string;
}
配置好axios
之后就需要驗證配置是否可用呀。
以接口api為 /user/login
為例(其余接口根據(jù)此邏輯依次配置即可)
那咱就在/src/server
目錄新建個文件夾為user
,即API的前綴部分,然后在此新建兩個文件 index.ts
和 interface.ts
index.ts
文件
import { post, get, postForm } from '@/server/index';
import { objectToFormData } from '@/utils'
import type {
LoginParamsType, LoginResType
} from './interface';
/**
* 登錄
* @param params
*/
export const login = (params: LoginParamsType) => {
return post<LoginParamsType, LoginResType>('/user/login', params)
}
// export const login = (params: LoginParamsType) => {
// return get<LoginParamsType, LoginResType>('/user/login', params)
// }
// export const login = (params: LoginParamsType) => {
// return postForm<FormData, LoginResType>('/user/login', objectToFormData( params ))
// }
interface.ts
文件
/**
* 登錄參數(shù)
*/
export interface LoginParamsType {
}
/**
* 登錄返回結(jié)果
*/
export interface LoginResType {
}
再在需要調(diào)用此接口的地方直接使用即可,具體如下
import { login } from '@/server/user'
login({})
可看到接口調(diào)用起來了,由于還未開啟API所以是404,不過即使接口服務(wù)開啟了,也會出現(xiàn)跨域的情況,下一步詳細記錄
H、項目開發(fā)中難免遇到的跨域問題
跨域是瀏覽器調(diào)用Api不可避免的,所以咱之前也有一遍關(guān)于同源策略與跨域的解決方案
這里主要描述本地開發(fā)中vite代理解決跨越方案
在很多情況下,后臺Api都會存在不同的Api前綴,當(dāng)然有些后臺大佬會統(tǒng)一以 /api開頭(有助于代理配置),前端自己也可自動配置Api拼接 /api為前綴
由于咱不想看到vite.config.ts
文件超長,所以新建個build
文件夾,在這個文件夾下新增proxy.ts
文件
const fs = require('fs');
const path = require('path');
/**
* 自動添加代理
* 遍歷mock代理配置 START
*/
const getProxyConfig = () => {
// 讀取 ../src/server 下的目錄作為代理的根目錄
const mockFiles = fs.readdirSync(path.resolve(__dirname, '../src/server'));
const proxyMap = {};
mockFiles.forEach((fileName) => {
if (!fileName.includes('.ts')) {
proxyMap[`^/${fileName}`] = {
target: process.env.VITE_PROXY_API,
ws: true,
secure: true,
changeOrigin: true,
};
}
});
/**
* 統(tǒng)一以 /api 為前綴(也可為其他自定義,這里以 /api 為例)
* 如果后端Api沒有此前綴,前端可自行在調(diào)用接口路徑前拼接 /api 例如 后臺為 /login ,在實際調(diào)用接口改為 /api/login(即當(dāng)前構(gòu)建配置的resetInterfacePath方法處理)
* 不過不想手動拼接/api前綴,也可直接在server目錄下新建 api 文件夾,把對應(yīng)配置的接口放入即可,按照mockFiles的配置會直接配置上的,不過的注意 rewrite 問題哦
* 所以這里直接寫上 /api 的代理配置
*/
proxyMap[`^/api`] = {
target: process.env.VITE_PROXY_API, // 這個配置用到了上面env的環(huán)境配置字段
ws: true,
secure: true,
changeOrigin: true,
// 如果后端沒有這個前綴,需要重寫"/api"為 ""
rewrite: (path: string) => path.replace(/^\/api/, ""),
bypass(req: any, res: any, options: any) { // 查看代理前路徑
const proxyUrl = new URL( options.rewrite(req.url) || '', (options.target) as string)?.href || ''
res.setHeader("x-req.proxyUrl", proxyUrl)
}
};
console.log('proxyMap_proxyMap', proxyMap)
return proxyMap;
};
module.exports = getProxyConfig;
然后在vite.config.ts
文件配置proxy
即可
import { fileURLToPath, URL } from 'node:url'
import { defineConfig, loadEnv } from 'vite'
const getProxyConfig = require('./build/proxy');
// https://vitejs.dev/config/
export default defineConfig(({ command, mode }) => {
const env = loadEnv(mode, `${process.cwd()}/env`, '')
Object.assign(process.env, env, { NODE_ENV: mode })
return {
mode,
...
server: {
// host設(shè)置為true才可以使用network的形式,以ip訪問項目
host: true,
// 本地編譯后訪問的端口號
port: 8082,
// 編譯后是否自動打開瀏覽器訪問
open: true,
hmr: {
overlay: false
},
// 跨域設(shè)置允許
cors: false,
// 如果端口已占用直接退出
strictPort: false,
// 設(shè)置代理
proxy: getProxyConfig()
},
...
}
})
說到這里,咱同時提下本地開發(fā)編譯的時候,訪問端口默認5173
即http://localhost:5173/
可在上面server
中設(shè)置port
值即可,具體看上面?zhèn)渥?/p>
I、多語言配置
多語言系統(tǒng)網(wǎng)站常見的,那咱同時也配置個多語言系統(tǒng)的。
多語言常用的插件有 i18n
,適用vue框架的有vue-i18n
,先安裝好 npm i vue-i18n -S
一般在切換語言之后,頁面刷新等操作還得保留切換后的語言系統(tǒng),咱可使用緩存記錄,這邊使用了 js-cookie
做緩存設(shè)置,即使用cookie記錄當(dāng)前顯示語言,由于TS環(huán)境下使用得安裝兩個依賴 js-cookie
和@types/js-cookie
否則會出現(xiàn)報錯情況(當(dāng)然也可直接使用手寫設(shè)置cookie使用)
npm i @types/js-cookie -D
npm i js-cookie -D
如果只安裝 @types/js-cookie
會提示以下錯誤
然后在 /src/lang
文件夾下新建以下文件(夾)
- 新建
index.ts
文件 - 對應(yīng)的語言文件夾,以中英文為例,新建
zh-CN
和en-US
文件夾,且里面分別新建index.ts
文件 - 新建應(yīng)用的第三方資源庫(例如 新建
element-plus.ts
文件)
/src/lang/index.ts
文件(具體配置此處不細說哈,直接看代碼,包括了 elememt-plus
的多語言配置)
import { createI18n } from 'vue-i18n'
import Cookies from 'js-cookie'
import elementPlus from './element-plus'
import enLocale from './en-US/index'
import zhLocale from './zh-CN/index'
const messages = {
'zh-CN': zhLocale,
'en-US': enLocale
}
/**
* 設(shè)置語言環(huán)境
* @param lang 語言環(huán)境
*/
export function setLanguage(lang: string) {
Cookies.set('language', lang || 'zh-CN')
}
/**
* 獲取配置環(huán)境
* @returns
*/
export function getLanguage() {
// const bool = true
// if (bool) {
// return 'en'
// }
const chooseLanguage = Cookies.get('language')
if (chooseLanguage) return chooseLanguage
// 如果有需要也可以根據(jù)當(dāng)前用戶設(shè)備瀏覽器默認語言
// const language = navigator.language.toLowerCase() // IE10及IE10以下的版本 使用 navigator.browserLanguage
// const locales = Object.keys(messages)
// for (const locale of locales) {
// if (language.indexOf(locale.toLowerCase()) > -1) return locale
// }
return 'en-US'
}
const i18n = createI18n({
globalInjection: true, // 全局生效$t
locale: getLanguage(), // getLanguage()
messages,
legacy: false
})
export const elementPlusLocale = elementPlus
export const lang = () => {
const lang = getLanguage()
switch (lang) {
case 'zh-CN':
return messages['zh-CN']
case 'en-US':
return messages['en-US']
}
return messages['zh-CN']
}
export default i18n
/src/lang/zh-CN.index.ts
文件
export default {
ErrorPageTips: `抱歉!您訪問的頁面失聯(lián)啦`
}
/src/lang/en-US/index.ts
文件
export default {
ErrorPageTips: `I'm sorry! The page you visitedis lost`
}
/src/lang/element-plus.ts
文件(其他配置方法可直接去官網(wǎng)查閱)
import elementZhLocale from 'element-plus/lib/locale/lang/zh-CN'
import elementEnLocale from 'element-plus/lib/locale/lang/en'
import { getLanguage } from './index'
export default () => {
const lang = getLanguage()
switch (lang) {
case 'zh-CN':
return elementZhLocale
case 'en-US':
return elementEnLocale
}
}
新建并配置上上面內(nèi)容后,直接在 main.ts
文件應(yīng)用即可
import { createApp } from 'vue'
import ElementPlus from 'element-plus'
import 'element-plus/theme-chalk/index.css'
import App from '@/App/index.vue'
import i18n, { elementPlusLocale } from './lang'
...
// import VConsole from 'vconsole';
// 設(shè)置語言變量
// import { setLanguage } from '@/lang/index'
// setLanguage('zh-CN')
// const vConsole = new VConsole() as any
const app = createApp(App)
...
// .use(vConsole)
.use(ElementPlus, { locale: elementPlusLocale() })
.use(i18n)
.mount('#app')
配置并引用對語言配置之后,即可使用啦。
例如html
上直接使用
<div>{{$t('ErrorPageTips')}}</div>
或者是編寫邏輯的時候使用(注意需要在 setup 內(nèi)使用)
import { defineComponent } from 'vue'
import { useI18n } from 'vue-i18n'
export default defineComponent({
...
setup() {
const { t } = useI18n()
console.log(t('ErrorPageTips'))
return { ... }
}
}
按照上述操作,多語言配置即實現(xiàn)啦,至于切換語言,可以直接在需要切換的按鈕上觸發(fā)
setLanguage
方法即可
這里既然配置好了多語言,那上面在/src/enum
文件夾內(nèi) error-code.ts
文件需要改造下,在此處文件夾先新建 index.ts
import { getLanguage } from "@/lang"
/**
* 獲取枚舉
* @param ZHCN 中文枚舉
* @param EN 英文枚舉
* @returns
*/
export const getEnum = (ZHCN: unknown, EN: unknown) => {
const lang = getLanguage()
switch (lang) {
case 'zh-CN':
return ZHCN
case 'en-US':
return EN
default :
return {}
}
}
然后修改 error-code.ts
文件為,這樣就可以根據(jù)語言環(huán)境獲取對應(yīng)的枚舉值數(shù)據(jù)啦(其他枚舉同理)
import { getEnum } from "./index"
export const errorCodeEN = {
400: 'Error request, status code:400',
401: 'Unauthorized, please login again, status code:401',
403: 'Access denied, status code:403',
404: 'Request error, the resource was not found, status code:404',
405: 'Request method not allowed, status code:405',
408: 'Request timeout, status code:408',
500: 'Server side error, status code:500',
501: 'Network not implemented, status code:501',
502: 'Gateway error, status code:502',
503: 'Service unavailable, status code:503',
504: 'Network timeout, status code:504',
505: 'The HTTP version does not support this request, status code:505',
}
export const errorCodeCN = {
400: '錯誤請求,狀態(tài)碼:400',
401: '未授權(quán),請重新登錄,狀態(tài)碼:401',
403: '拒絕訪問,狀態(tài)碼:403',
404: '請求錯誤,未找到該資源,狀態(tài)碼:404',
405: '請求方法未允許,狀態(tài)碼:405',
408: '請求超時,狀態(tài)碼:408',
500: '服務(wù)器端出錯,狀態(tài)碼:500',
501: '網(wǎng)絡(luò)未實現(xiàn),狀態(tài)碼:501',
502: '網(wǎng)關(guān)錯誤,狀態(tài)碼:502',
503: '服務(wù)不可用,狀態(tài)碼:503',
504: '網(wǎng)絡(luò)超時,狀態(tài)碼:504',
505: 'HTTP版本不支持該請求,狀態(tài)碼:505',
}
export default getEnum(errorCodeCN, errorCodeEN)
K、狀態(tài)管理引入(pinia、Vuex)
因為官網(wǎng)推薦pinia, 那咱直接也用上
可先看下初始化出來的模版已經(jīng)使用了 pinia
且默認沒有使用緩存操作,具體可在某個頁面引入 /src/stores/counter.ts
且里面的寫法是組合式開發(fā)
然后修改對應(yīng)字段值,在跳頁到另一個路由,再次引入 /src/stores/counter.ts
獲取對應(yīng)字段,可以看到數(shù)據(jù)是被剛才修改過的值
例如路由 /a
/b
分別附上以下代碼
import { storeToRefs } from "pinia"
import { useCounterStore} from '@/stores/counter'
// 如果此代碼是抽離到了單獨ts文件內(nèi),那下面部分需要再 放置 setup 內(nèi)
// 變量需要使用 storeToRefs 轉(zhuǎn)為響應(yīng)式數(shù)據(jù)
const { count } = storeToRefs(useCounterStore())
// 方法直接使用即可
const { increment } = useCounterStore()
increment()
console.log('count', count)
如圖所示,路由 /a
/b
相互切換會發(fā)現(xiàn),count
的值一直在疊加,說明數(shù)據(jù)是共享的
如果有緩存需求的也可再安裝個 pinia-plugin-persistedstate
npm i pinia-plugin-persistedstate -S
在 main.ts
文件引入插件
import { createApp } from 'vue'
import App from '@/App/index.vue'
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
import './assets/styles/app.less'
...
const app = createApp(App)
app.use(
createPinia()
.use(piniaPluginPersistedstate)
)
...
.mount('#app')
使用緩存的時候,定義方式不太一樣,不能使用組合式開發(fā),例如 /src/stores
下新建 mapState.ts
文件,具體寫法直接查看代碼,如下
import { defineStore } from 'pinia'
export interface MapState {
address: string;
}
const { SIS_STORE_NAME } = import.meta.env
export const useMapStore = defineStore(SIS_STORE_NAME + 'map', {
state: (): MapState => ({
address: '',
}),
getters: {},
actions: {
setAdress(address: string) {
this.address = address;
},
clearMessage() {
this.address = '';
},
},
persist: {
/**
* 使用的存儲
* @default $store.id
*/
key: SIS_STORE_NAME + 'map',
/**
* 存儲位置
* @default localStorage
*/
storage: sessionStorage,
/**
* 需要設(shè)置緩存的state 如果未設(shè)置即存儲所有state
* @default undefined
*/
// paths: [],
/**
* 存儲之前
* @default null
*/
beforeRestore: () => {},
/**
* 存儲之后
* @default undefined
*/
afterRestore: () => {},
/**
* 啟用時在控制臺中記錄錯誤。
* @default false
*/
debug: true
},
});
使用方式如下
-
/a
路由設(shè)置值,然后看下緩存(當(dāng)前這是在sessionStorage
)
import { useMapStore} from '@/stores/mapState'
// 如果此代碼是抽離到了單獨ts文件內(nèi),那下面部分需要再 放置 setup 內(nèi)
// 變量需要使用 storeToRefs 轉(zhuǎn)為響應(yīng)式數(shù)據(jù)
const { setAdress } = useMapStore()
setAdress('驗證地區(qū)')
可看到已設(shè)置成功
-
/b
路由獲取值
import { useMapStore} from '@/stores/mapState'
// 如果此代碼是抽離到了單獨ts文件內(nèi),那下面部分需要再 放置 setup 內(nèi)
// 變量需要使用 storeToRefs 轉(zhuǎn)為響應(yīng)式數(shù)據(jù)
const { address } = useMapStore()
console.log('address', address)
可看到打印的數(shù)據(jù)是上面設(shè)置的值(不刷新頁面的情況下,在 mapState.ts
文件關(guān)閉緩存也是可以獲取值的)
J、按需引入資源
例如項目使用了Echarts
,如果全量引入,會增加構(gòu)建后的文件大小,且大部分項目不可能使用Echarts的大部分圖形的,這個時候咱就需要按需引入項目所用到的部分啦。
由于之前寫了篇Echarts的使用(優(yōu)化按需引入),有此需要的可移步閱讀哈
K、優(yōu)化構(gòu)建
優(yōu)化構(gòu)建其實也包含了按需引入資源、圖片壓縮、js壓縮等
這個優(yōu)化之前也單獨寫了篇打包優(yōu)化之vite構(gòu)建(視圖分析、CDN引入、依賴分包、gzip壓縮),有興趣的大佬可異步閱讀哈
三、項目結(jié)構(gòu)再次優(yōu)化
至此需要配置優(yōu)化的部分基本都已搞完
當(dāng)然除了上面講過的,還有一些文件夾定義規(guī)范,例如
-
App
抽離項目路由入口 -
composables
公用Hooks函數(shù) -
entity
實體(class類) -
interface
接口類型定義
…等等
根據(jù)本人項目習(xí)慣項目結(jié)構(gòu)具體如下(后面再花點時間構(gòu)以此結(jié)構(gòu)建個簡易的完整配置項目上傳,有興趣的小伙伴也可按照上述步驟一步步配置屬于自己的自定義模版)文章來源:http://www.zghlxwxcb.cn/news/detail-754252.html
└── build // 構(gòu)建抽離
├── proxy.js // 接口代理配置
└── docs // 項目文檔說明
├── user-url.json // 項目測試連接賬號
└── env // 環(huán)境變量相關(guān)配置
├── .env // 公用變量配置
├── .env.development // 開發(fā)環(huán)境變量配置
├── .env.production // 生產(chǎn)環(huán)境變量配置
├── .env.sit // 測試環(huán)境變量配置
├── .env.uat // 業(yè)務(wù)驗收環(huán)境變量配置
└── public // 靜態(tài)資源(無需經(jīng)過構(gòu)建內(nèi)容)
└── src
├── App // 項目路由入口
└── assets // 靜態(tài)文件
├── dic // 字段庫
├── images // 圖片
├── js // js庫
├── styles // 樣式庫
└── components // 公用組件
└── composables // 公用Hooks函數(shù)
└── const // 常量定義
└── entity // 實體(class類)-- 頁面級別
└── enum // 枚舉值
└── infrastructure // 基礎(chǔ)設(shè)施實體(class類) -- 項目級別(如配置數(shù)據(jù)、人臉、定位等功能相關(guān)實體)
└── interface // 類型定義
└── lang // 多語言
└── router // 路由配置
└── server // 接口
└── sotres // 狀態(tài)管理(緩存)
└── types // 自定義全局類型
└── utils // 常用方法
└── views // 頁面
└── main.js // Vue配置入口
├── .eslintrc.cjs // ESLint 規(guī)則配置
├── index.html // 項目單頁入口
├── package.json // 項目依賴
總結(jié)
由于之前幾乎都是使用Webpack 構(gòu)建項目,偶爾使用了gulp。最近兩年才開始使用Vue3開發(fā)項目,所以特意記錄下Vite構(gòu)建期間的使用,可以嘗試按照上述步驟配置下,歡迎各位大佬評論交流。后續(xù)也會花時間寫下webpack構(gòu)建項目的文章,期待后期的更新~文章來源地址http://www.zghlxwxcb.cn/news/detail-754252.html
到了這里,關(guān)于vite+vue3+ts項目構(gòu)建詳細步驟(配置多語言版本)的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!