文章目录
- 前言
- 一、构建基础项目模板
- 二、根据基础模版搭建优化项目结构(持续更新中)
- A、环境配置优化
- B、优化生产构建后 console和debugger关闭
- C、优化文件引用路径
- D、优化项目全局文件类型声明配置(ts的泛型)
- E、配置css全局变量
- F、新建文件夹(不同功能内容分开放)
- G、引入axios
- H、项目开发中难免遇到的跨域问题
- I、多语言配置
- K、状态管理引入(pinia、Vuex)
- J、按需引入资源
- K、优化构建
- 三、项目结构再次优化
- 总结
前言
- 时过境迁,我们见证了诸如 webpack、Rollup 和 Parcel 等工具的变迁,它们极大地改善了前端开发者的开发体验。
- 然而,当我们开始构建越来越大型的应用时,需要处理的 JavaScript 代码量也呈指数级增长。包含数千个模块的大型项目相当普遍。基于 JavaScript 开发的工具就会开始遇到性能瓶颈:通常需要很长时间(甚至是几分钟!)才能启动开发服务器,即使使用模块热替换(HMR),文件修改后的效果也需要几秒钟才能在浏览器中反映出来。如此循环往复,迟钝的反馈会极大地影响开发者的开发效率和幸福感。
- Vite 旨在利用生态系统中的新进展解决上述问题:浏览器开始原生支持 ES 模块,且越来越多 JavaScript 工具使用编译型语言编写。
以上引用了Vite官网的描述
为什么要记录这篇文章而且还很详细呢?那是因为进入当前这家公司的时候,这边连基本的前端架构都是没有的,每次做项目都是复用网上那些很臃肿的代码模版(做一套扔一套那种,没有保留通用前端模板框架的)。然后这个项目组之前都是vue2做开发、最近领导说以后项目考虑使用vue3来开发了,那我作为这项目组唯一的纯前端人员(其他都是C#大佬,而且很多项目都是前后端不分离那种)就得考虑从头搭建一套自定义的vue3架构啦,结合之前项目的经验,从零开始搭建了这套前端架构(由于没有大佬带,只能自个一点点查看资料且一步步摸索,期间也遇到了不少问题),如果有优化的地方,还请各位大佬多多评论交流哈。抱拳!
那么接下来咱们一步步来构建vite
+vue3
+ts
吧~
Tips:本篇文章是按照配置一步步配置的,所以稍微较长,如果是新手建议往下一步步配置,花费的时间也不会很长
一、构建基础项目模板
- 首先需要安装Node
先看下安装node后版本
node -v
Tips 由于vite在node低版本不支持,所以建议安装最新的稳定版本,或者是 14.18+
- 安装Vue最新版本基础模板
当前基础项目模板是按照vue最新版本构建的(其他安装方式可异步vite官网查看更多)
安装命令如下:
npm init vue@latest
执行以上会有部分配置需要选择,本项目具体选择如下图:
执行完后生成目录如图:
- 安装依赖
npm i
- 项目执行
npm run dev
- 访问页面
按照上述操作后访问 http://localhost:5173/ 即显示下面内容啦
因为当前demo是基于element-plus
,先安装npm i element-plus -S
并在应用
import ElementPlus from 'element-plus'
import 'element-plus/theme-chalk/'
...
const app = createApp(App)
...
app.use(ElementPlus)
二、根据基础模版搭建优化项目结构(持续更新中)
由于初始化的基础项目结构比较简单,所以一般都需要根据个人习惯,优化结构及配置相关信息
A、环境配置优化
上面的基础模板可看到,并没看到不同环境变量,其实是有测试环境和生产环境的,具体可以 使用打印出看下。
在输出
然后执行npm run dev
,查看页面控制台可看到MODE
的值为"development"
执行npm run build-only
,把构建的dist内的文件放置服务器。
- 本地开发推荐使用
nginx
(推荐使用),可参考Nginx在Window与Mac环境的使用及配置详情 - 还可使用
express-generator
开启服务 - 当然也可以直接在开发测试访问
http://localhost:5173/dist/
,但这种需要修改对应文件引入路径,想了解的可自行研究下哈
然后访问可看到控制台打印出MODE
的值为"production"
但由于项目开发到上线,可能都存在有不同环境需求,所以咱们就需要配置环境变量以对应不同环境的Api,而且配置好之后,如无特殊修改,并不需要再修改环境配置相关的,这样就可避免部署构建后环境错误的情况啦
正常流程一般分为
- 开发环境(可根据实际开发情况链接对应环境的Api)
- sit环境(测试)
- uat环境(业务验收)
- prod环境(线上环境)
先把环境变量这些配置好,在根目录新建不同环境的文件(具体配置的key默认VITE_
开头,也可配置envPrefix'
值自定义)
- .env(这个是公共环境配置,都会获取这里面的配置信息)
- .
- .
- .
- .
.env文件是共有配置,所以即使配置配置对应执行脚本环境也会获取
# 公有全局环境配置
VITE_STORE_NAME = invade-
想要执行不同命令读取对应环境配置文件,还需要再在配置对应脚本,执行脚本后vite会根据编译的命令读取对应环境文件的,新增具体如下
{
...
"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
对应的是本地开发联调测试用的,可能不同环境都得在本地联调测试问题,所以存在dev-sit
这些,如果不指定--mode
后面的值,默认为development
-
vite build
是构建打包执行的,如果不指定--mode
后面的值,默认为production
-
--mode
后面跟着的就是配置的环境变量值,即对应上面截图控制台看到的MODE
的值
不过咱感觉项目结构看起来没那么优雅(主要不想更目录文件太多,影响阅读)。
根据官网提供配置,可自定义个文件夹,放置这些环境配置文件,那咱们就在根目录新建文件夹env(这个文件名称可自定义,对应下面envDir配置的值),把环境配置的文件都移进去
且需要修改配置,指定
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'
// /config/
export default defineConfig({
// env配置文件变量前缀, 默认 VITE_,可自行定义
envPrefix: 'VITE_',
// env配置文件夹位置
envDir: 'env',
plugins: [vue(), vueJsx()],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
}
})
例如 .
配置
# 本地开发环境的代理配置配置, 任何代理请写好注释
# 接口地址
VITE_PROXY_API=http://localhost:8000
# 构建后文件引用 相对路径
VITE_PUBLIC_PATH=/
# 输出路径
VITE_OUT_DIR=/dist/
执行 npm run dev
控制台打印,可看到
.env
和.
配置的信息,如下
例如 .
配置
# 本地开发环境的代理配置配置, 任何代理请写好注释
# 接口地址
VITE_PROXY_API=http://localhost:8001
# 构建后文件引用 相对路径
VITE_PUBLIC_PATH=/
# 输出路径
VITE_OUT_DIR=/dist/
执行 npm run dev-sit
(当然执行npm run sit
构建后打印的也是一致的,其他环境下的也是如此)
控制台打印,可看到
.env
和.
配置的信息,且VITE_PROXY_API
的值同dev环境的不一样,如下
B、优化生产构建后 console和debugger关闭
由于生产一般避免控制台打印相关信息,vite也有这种配置优化,话不多说,直接上配置
import { defineConfig } from 'vite'
// /config/
export default defineConfig({
...
build: {
minify: "terser", // 必须开启:使用 terserOptions 才有效果
terserOptions: {
compress: {
drop_console: true,
drop_debugger: true,
},
},
}
})
如果这样配置后直接构建会报错,提示terser not found.
提示需要安装这个terser
,可执行npm i terser -D
安装
安装完terser后再次构建成功。然后访问构建的内容发现()
没打印出来,说明配置生效了
不过这样配置发现如果测试环境想要看到打印信息判断错误等,还得修改配置再次构建下,明显不友好,那么我们稍微优化下只在prod
环境下生效,即在defineConfig
使用回调函数的回参来配置(查看源码可看到defineConfig入参有两种),如下
import { defineConfig, loadEnv } from 'vite'
// /config/
export default defineConfig(({ command, mode, ssrBuild }) => {
const env = loadEnv(mode, `${process.cwd()}/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,
},
},
}
}
})
可看到有三个参数,ssrBuild
为可选
command: 'build' | 'serve'; // 二选一,默认为serve ,可在执行脚本配置(配置在vite 后面) 例如 "dev": "vite serve --mode development",
mode: string; // 自定义 一般会设置为 development | sit | uat | production
/**
* @experimental
*/
ssrBuild?: boolean; // 看起来应该是配置SSR模式使用的,后面有机会再ssr模式下详细记录下
结合上面配置,在构建sit | uat
的时候打印还在,而构建production
没有了,说明咱们配置成功啦。
那么为什么不直接使用.NODE_ENV
直接判断呢?因为在开发环境.NODE_ENV
默认是development
,即使跑的是dev-sit
也还是一样为development
。我们看个截图,所以需要使用回调函数根据配置的--mode
进行判断
C、优化文件引用路径
在很多时候,不同页面需要使用同一个组件,且引入的路径可能不一致,那么怎样才能统一呢?其实在初始化构建项目之后,框架本身已经生成了最基本的文件指向了,看下配置
import { fileURLToPath, URL } from 'node:url'
...
import { defineConfig } from 'vite'
// /config/
export default defineConfig({
...
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
},
...
})
即在defineConfig
配置resolve
引用相关配置指向,如果指向 ./src
不能满足需要,可以自行自定义下(咱得习惯是使用一个@即可,前提需要把项目目录结构合理化,即不要过于深入嵌套且按模块分好目录)
使用方式,在所需要的文件引入,例如在/src/components/index/引入/src/views/文件
以下代码可看出多层 ../
引入的文件使用了alias
配置@
指向./src
,即可避免文件引入错误等情况
// import HomeView from '../../views/'
import HomeView from '@/views/'
D、优化项目全局文件类型声明配置(ts的泛型)
-
.
文件,它是用来做类型声明(declare)。它仅仅用来做类型检测,告知TypeScript我们有哪些类型; -
d
是 (declare,声明) -
declare module
** 声明文件 -
declare namespace
** 声明命名空间
初始化构建出来的根目录有个
文件,会在
文件引入(如果未配置,有些文件引入会报错,因为没有在任何地方声明。而有些是会自带声明,例如
document
在文件中进行了声明了,具体可按下
ctrl
键盘并鼠标左键点击查看即可)
如果有多个这种配置文件,就得在文件手动一个个引入进来,例如新增个
文件配置
weixin-js-sdk
全局。
{
"extends": "@vue/tsconfig/",
"include": ["", "src/**/*", "src/**/*.vue", ""],
...
}
其实咱们也可以直接把此配置直接配置在文件内,可是如果有更多配置咋办呢?不就出现了很多配置在一起,可能修改或新增某个配置的时候,不小心删除了其他的配置,那怎样避免这种情况呢?
/// <reference types="vite/client" />
declare module 'weixin-js-sdk'
可以看到配置文件以
.
结尾,那咱就优化下上面配置,先在根目录新建个types
文件夹,然后把文件放进来,在修改下
文件(一般修改了
会自动修改在引入的位置),即
""
修改成"types/"
了
{
"extends": "@vue/tsconfig/",
"include": ["types/", "src/**/*", "src/**/*.vue"],
...
}
然后优化配置
"types/"
改为"types/*."
即动态引入types文件夹下所有.
结尾的文件,如下
{
"extends": "@vue/tsconfig/",
"include": ["types/*.", "src/**/*", "src/**/*.vue"],
...
}
新建·
/**
* 自定义全局类型
*/
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];
新建·,配置一些声明文件,例如
.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/';
以上按照需求配置即可
E、配置css全局变量
我们这边使用less处理,需要安装以下依赖支持
npm i less -D
npm i less-loader -D
一般项目都会有很多重复使用的色系,布局位置信息等,这个时候咱可以使用使用less动态配置对应变量进行使用功能
首先得新建个.less文件在./src/assets/
新建文件,内容如下
/**
* 定义全局使用的 less 变量
*/
@paddingDefault: 32px;
/**
* 颜色值
*/
@a94442: #a94442;
然后配置文件到项目全局,不然没法使用这些定义好的变量哦
import { fileURLToPath, URL } from 'node:url'
import { resolve } from 'path'
import { defineConfig} from 'vite'
// /config/
export default defineConfig(({ command, mode, ssrBuild }) => {
...
return {
...
css: {
preprocessorOptions: {
// less: {
// javascriptEnabled: true,
// additionalData: `@import "${resolve(__dirname, 'src/assets/styles/')}";`
// }
less: {
modifyVars: {
hack: `true; @import (reference) "${resolve('src/assets/styles/')}";`,
},
javascriptEnabled: true
}
}
},
build: {
...
}
}
})
使用方式,上述配置好之后直接在需要使用的地方使用即可
<div class="content"></div>
<style lang="less">
.content {
background: @a94442;
width: @paddingDefault;
height: @paddingDefault;
}
</style>
下图可看到对应值变成了文件配置好的值
F、新建文件夹(不同功能内容分开放)
良好的习惯和语义化的目录名称有助于项目维护和开发者阅读的心情
目录结构是否合理也是当今团队项目开发中相当重要的一部分
先在/src
目录下新建以下目录(详细的结构放在了最下方,有兴趣可直接划到底部查看)
- App
- composables
- const
- entity
- enum
- infrastructure
- interface
- lang
- server
- utils
G、引入axios
- 安装axios
npm i axios -S
- 引入axios
1、在/src/server
文件夹内新建 和
文件
在/src
文件夹内新建 interface
文件,再在interface文件夹内新建 文件
有使用到了lodash-es
,所以需要安装npm i lodash-es -D
/src/server/
如下(其他配置自定义即可)
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
// 请求错误统一处理
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 在同一文件内才有效果
// type ERROR_CODES_TYPES = typeof ERROR_CODES
const ERROR_CODES = ERRORCODES as ERROR_CODES_TYPES
/**
* 后台接口公共的返回格式
* 具体根据实际跟后台约定的定义
*/
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(, {
// '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),
)
// 响应拦截
axios.interceptors.response.use(
(response) => {
if (typeof response.data === 'string') {
// = '/signIn'
// return ('登录失效')
}
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 || '连接到服务器失败'
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,
})
},
)
/**
* 发起GET请求, 泛型 T 定义返回数据 data 项类型, U 定义请求数据类型
* @param {string} url 请求链接
* @param {object} params 请求参数
* @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 },
)
/**
* 发起POST请求, 泛型 T 定义返回数据 data 项类型, U 定义请求数据类型
* @param {string} url 请求链接
* @param {object} params 请求参数
* @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)
}
/**
* 发起FormData请求, 泛型 T 定义返回数据 data 项类型, U 定义请求数据类型
* @param {string} url 请求链接
* @param {object} params 请求参数
* @param {object} config 配置
*/
// export const postForm = <U = unknown, T = unknown>(
// url: string,
// params?: U,
// config: AxiosRequestConfig = {},
// ) => <T, ResCommonType<T>>(url, ({ ...params }), config);
export const postForm = <U = unknown, T = unknown>(
url: string,
params?: U,
config: AxiosRequestConfig = {},
) => axios.post<T, T>(url, params, config)
/**
* 文件下载请求, 泛型 T 定义返回数据 data 项类型, U 定义请求数据类型
* @param {string} url 请求链接
* @param {object} params 请求参数
* @param {object} config 配置
*/
// export const postFile = <U = unknown, T = unknown>(
// url: string,
// params?: U,
// config: AxiosRequestConfig = { responseType: 'blob' },
// ) => <T, ResCommonType<T>>(url, { ...params }, config);
export default {
get,
post,
// postForm,
// postFile,
}
/src/server/
如下(用于接口的共有interface(接口)定义部分)
export interface BaseResType {
ResultCode: number
ResultDescription: string
}
export interface ApiResponseType<T> extends BaseResType{
result: T
}
2、在/src/enum
文件夹内新建 文件(后面配置成多语言)
// 请求错误统一处理
const ERROR_CODES = {
400: '错误请求,状态码:400',
401: '未授权,请重新登录,状态码:401',
403: '拒绝访问,状态码:403',
404: '请求错误,未找到该资源,状态码:404',
405: '请求方法未允许,状态码:405',
408: '请求超时,状态码:408',
500: '服务器端出错,状态码:500',
501: '网络未实现,状态码:501',
502: '网关错误,状态码:502',
503: '服务不可用,状态码:503',
504: '网络超时,状态码:504',
505: 'HTTP版本不支持该请求,状态码:505',
}
export default ERROR_CODES
3、在/src/utils
文件夹内新建 文件(用于整个项目共有方法等)
import type { unKnow } from "@/interface";
/**
* 拼接接口路径,例如代理接口的时候后台接口前缀不统一等,可以自定义个前缀,有助于代理配置。
* @param url 接口路径(一般只会配置相对路径,也可直接配置绝对路径)
* @returns
*/
export const resetInterfacePath = (url: string) => {
// return `/api/${url}`
return url
}
/**
* 对象转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;
};
/**
* 对象转数组
* @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;
};
/**
* 地址数据转换
* @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;
}
/**
* 根据对象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 ''
}
/**
* 根据数组中子对象value获取对应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/
新增(整个项目的 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
为例(其余接口根据此逻辑依次配置即可)
那咱就在/src/server
目录新建个文件夹为user
,即API的前缀部分,然后在此新建两个文件 和
文件
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 ))
// }
文件
/**
* 登录参数
*/
export interface LoginParamsType {
}
/**
* 登录返回结果
*/
export interface LoginResType {
}
再在需要调用此接口的地方直接使用即可,具体如下
import { login } from '@/server/user'
login({})
可看到接口调用起来了,由于还未开启API所以是404,不过即使接口服务开启了,也会出现跨域的情况,下一步详细记录
H、项目开发中难免遇到的跨域问题
跨域是浏览器调用Api不可避免的,所以咱之前也有一遍关于同源策略与跨域的解决方案
这里主要描述本地开发中vite代理解决跨越方案
在很多情况下,后台Api都会存在不同的Api前缀,当然有些后台大佬会统一以 /api开头(有助于代理配置),前端自己也可自动配置Api拼接 /api为前缀
由于咱不想看到文件超长,所以新建个
build
文件夹,在这个文件夹下新增文件
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,
};
}
});
/**
* 统一以 /api 为前缀(也可为其他自定义,这里以 /api 为例)
* 如果后端Api没有此前缀,前端可自行在调用接口路径前拼接 /api 例如 后台为 /login ,在实际调用接口改为 /api/login(即当前构建配置的resetInterfacePath方法处理)
* 不过不想手动拼接/api前缀,也可直接在server目录下新建 api 文件夹,把对应配置的接口放入即可,按照mockFiles的配置会直接配置上的,不过的注意 rewrite 问题哦
* 所以这里直接写上 /api 的代理配置
*/
proxyMap[`^/api`] = {
target: process.env.VITE_PROXY_API, // 这个配置用到了上面env的环境配置字段
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("", proxyUrl)
}
};
console.log('proxyMap_proxyMap', proxyMap)
return proxyMap;
};
module.exports = getProxyConfig;
然后在文件配置
proxy
即可
import { fileURLToPath, URL } from 'node:url'
import { defineConfig, loadEnv } from 'vite'
const getProxyConfig = require('./build/proxy');
// /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设置为true才可以使用network的形式,以ip访问项目
host: true,
// 本地编译后访问的端口号
port: 8082,
// 编译后是否自动打开浏览器访问
open: true,
hmr: {
overlay: false
},
// 跨域设置允许
cors: false,
// 如果端口已占用直接退出
strictPort: false,
// 设置代理
proxy: getProxyConfig()
},
...
}
})
说到这里,咱同时提下本地开发编译的时候,访问端口默认5173
即http://localhost:5173/
可在上面server
中设置port
值即可,具体看上面备注
I、多语言配置
多语言系统网站常见的,那咱同时也配置个多语言系统的。
多语言常用的插件有 i18n
,适用vue框架的有vue-i18n
,先安装好 npm i vue-i18n -S
一般在切换语言之后,页面刷新等操作还得保留切换后的语言系统,咱可使用缓存记录,这边使用了 js-cookie
做缓存设置,即使用cookie记录当前显示语言,由于TS环境下使用得安装两个依赖 js-cookie
和@types/js-cookie
否则会出现报错情况(当然也可直接使用手写设置cookie使用)
npm i @types/js-cookie -D
npm i js-cookie -D
如果只安装 @types/js-cookie
会提示以下错误
然后在 /src/lang
文件夹下新建以下文件(夹)
- 新建
文件
- 对应的语言文件夹,以中英文为例,新建
zh-CN
和en-US
文件夹,且里面分别新建文件
- 新建应用的第三方资源库(例如 新建
文件)
/src/lang/
文件(具体配置此处不细说哈,直接看代码,包括了 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
}
/**
* 设置语言环境
* @param lang 语言环境
*/
export function setLanguage(lang: string) {
Cookies.set('language', lang || 'zh-CN')
}
/**
* 获取配置环境
* @returns
*/
export function getLanguage() {
// const bool = true
// if (bool) {
// return 'en'
// }
const chooseLanguage = Cookies.get('language')
if (chooseLanguage) return chooseLanguage
// 如果有需要也可以根据当前用户设备浏览器默认语言
// const language = () // IE10及IE10以下的版本 使用
// const locales = (messages)
// for (const locale of locales) {
// if ((()) > -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/
文件
export default {
ErrorPageTips: `抱歉!您访问的页面失联啦`
}
/src/lang/en-US/
文件
export default {
ErrorPageTips: `I'm sorry! The page you visitedis lost`
}
/src/lang/
文件(其他配置方法可直接去官网查阅)
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
}
}
新建并配置上上面内容后,直接在 文件应用即可
import { createApp } from 'vue'
import ElementPlus from 'element-plus'
import 'element-plus/theme-chalk/'
import App from '@/App/'
import i18n, { elementPlusLocale } from './lang'
...
// import VConsole from 'vconsole';
// 设置语言变量
// 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 内使用)
import { defineComponent } from 'vue'
import { useI18n } from 'vue-i18n'
export default defineComponent({
...
setup() {
const { t } = useI18n()
console.log(t('ErrorPageTips'))
return { ... }
}
}
按照上述操作,多语言配置即实现啦,至于切换语言,可以直接在需要切换的按钮上触发
setLanguage
方法即可
这里既然配置好了多语言,那上面在/src/enum
文件夹内 文件需要改造下,在此处文件夹先新建
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 {}
}
}
然后修改 文件为,这样就可以根据语言环境获取对应的枚举值数据啦(其他枚举同理)
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: '错误请求,状态码:400',
401: '未授权,请重新登录,状态码:401',
403: '拒绝访问,状态码:403',
404: '请求错误,未找到该资源,状态码:404',
405: '请求方法未允许,状态码:405',
408: '请求超时,状态码:408',
500: '服务器端出错,状态码:500',
501: '网络未实现,状态码:501',
502: '网关错误,状态码:502',
503: '服务不可用,状态码:503',
504: '网络超时,状态码:504',
505: 'HTTP版本不支持该请求,状态码:505',
}
export default getEnum(errorCodeCN, errorCodeEN)
K、状态管理引入(pinia、Vuex)
因为官网推荐pinia, 那咱直接也用上
可先看下初始化出来的模版已经使用了 pinia
且默认没有使用缓存操作,具体可在某个页面引入 /src/stores/
且里面的写法是组合式开发
然后修改对应字段值,在跳页到另一个路由,再次引入 /src/stores/
获取对应字段,可以看到数据是被刚才修改过的值
例如路由 /a
/b
分别附上以下代码
import { storeToRefs } from "pinia"
import { useCounterStore} from '@/stores/counter'
// 如果此代码是抽离到了单独ts文件内,那下面部分需要再 放置 setup 内
// 变量需要使用 storeToRefs 转为响应式数据
const { count } = storeToRefs(useCounterStore())
// 方法直接使用即可
const { increment } = useCounterStore()
increment()
console.log('count', count)
如图所示,路由 /a
/b
相互切换会发现,count
的值一直在叠加,说明数据是共享的
如果有缓存需求的也可再安装个 pinia-plugin-persistedstate
npm i pinia-plugin-persistedstate -S
在 文件引入插件
import { createApp } from 'vue'
import App from '@/App/'
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
import './assets/styles/'
...
const app = createApp(App)
app.use(
createPinia()
.use(piniaPluginPersistedstate)
)
...
.mount('#app')
使用缓存的时候,定义方式不太一样,不能使用组合式开发,例如 /src/stores
下新建 文件,具体写法直接查看代码,如下
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 $
*/
key: SIS_STORE_NAME + 'map',
/**
* 存储位置
* @default localStorage
*/
storage: sessionStorage,
/**
* 需要设置缓存的state 如果未设置即存储所有state
* @default undefined
*/
// paths: [],
/**
* 存储之前
* @default null
*/
beforeRestore: () => {},
/**
* 存储之后
* @default undefined
*/
afterRestore: () => {},
/**
* 启用时在控制台中记录错误。
* @default false
*/
debug: true
},
});
使用方式如下
-
/a
路由设置值,然后看下缓存(当前这是在sessionStorage
)
import { useMapStore} from '@/stores/mapState'
// 如果此代码是抽离到了单独ts文件内,那下面部分需要再 放置 setup 内
// 变量需要使用 storeToRefs 转为响应式数据
const { setAdress } = useMapStore()
setAdress('验证地区')
可看到已设置成功
-
/b
路由获取值
import { useMapStore} from '@/stores/mapState'
// 如果此代码是抽离到了单独ts文件内,那下面部分需要再 放置 setup 内
// 变量需要使用 storeToRefs 转为响应式数据
const { address } = useMapStore()
console.log('address', address)
可看到打印的数据是上面设置的值(不刷新页面的情况下,在 文件关闭缓存也是可以获取值的)
J、按需引入资源
例如项目使用了Echarts
,如果全量引入,会增加构建后的文件大小,且大部分项目不可能使用Echarts的大部分图形的,这个时候咱就需要按需引入项目所用到的部分啦。
由于之前写了篇Echarts的使用(优化按需引入),有此需要的可移步阅读哈
K、优化构建
优化构建其实也包含了按需引入资源、图片压缩、js压缩等
这个优化之前也单独写了篇打包优化之vite构建(视图分析、CDN引入、依赖分包、gzip压缩),有兴趣的大佬可异步阅读哈
三、项目结构再次优化
至此需要配置优化的部分基本都已搞完
当然除了上面讲过的,还有一些文件夹定义规范,例如
-
App
抽离项目路由入口 -
composables
公用Hooks函数 -
entity
实体(class类) -
interface
接口类型定义
…等等
根据本人项目习惯项目结构具体如下(后面再花点时间构以此结构建个简易的完整配置项目上传,有兴趣的小伙伴也可按照上述步骤一步步配置属于自己的自定义模版)
└── build // 构建抽离
├── // 接口代理配置
└── docs // 项目文档说明
├── // 项目测试连接账号
└── env // 环境变量相关配置
├── .env // 公用变量配置
├── . // 开发环境变量配置
├── . // 生产环境变量配置
├── . // 测试环境变量配置
├── . // 业务验收环境变量配置
└── public // 静态资源(无需经过构建内容)
└── src
├── App // 项目路由入口
└── assets // 静态文件
├── dic // 字段库
├── images // 图片
├── js // js库
├── styles // 样式库
└── components // 公用组件
└── composables // 公用Hooks函数
└── const // 常量定义
└── entity // 实体(class类)-- 页面级别
└── enum // 枚举值
└── infrastructure // 基础设施实体(class类) -- 项目级别(如配置数据、人脸、定位等功能相关实体)
└── interface // 类型定义
└── lang // 多语言
└── router // 路由配置
└── server // 接口
└── sotres // 状态管理(缓存)
└── types // 自定义全局类型
└── utils // 常用方法
└── views // 页面
└── // Vue配置入口
├── . // ESLint 规则配置
├── // 项目单页入口
├── // 项目依赖
总结
由于之前几乎都是使用Webpack 构建项目,偶尔使用了gulp。最近两年才开始使用Vue3开发项目,所以特意记录下Vite构建期间的使用,可以尝试按照上述步骤配置下,欢迎各位大佬评论交流。后续也会花时间写下webpack构建项目的文章,期待后期的更新~