(pageNo: number, pageSize: number, array: T[]): T[] {
const offset = (pageNo - 1) * Number(pageSize)
const ret =
offset + Number(pageSize) >= array.length
? array.slice(offset, array.length)
: array.slice(offset, offset + Number(pageSize))
return ret
}
export interface requestParams {
method: string
body: any
headers?: { authorization?: string }
query: any
}
/**
* @description 本函数用于从request数据中获取token,请根据项目的实际情况修改
*
*/
export function getRequestToken({ headers }: requestParams): string | undefined {
return headers?.authorization
}
```
```ts
// mock/sys/user
import { MockMethod } from 'vite-plugin-mock'
import { resultError, resultSuccess, getRequestToken, requestParams } from '../_util'
export default [
{
url: '/basic-api/getUserInfo',
method: 'get',
response: (request: requestParams) => {
console.log('----请求了getUserInfo---')
return resultSuccess({
name: '章三',
age: 40,
sex: '男'
})
}
}
] as MockMethod[]
```
### 3. 修改 vite.config.ts 配置
```ts
export default ({ mode, command }: ConfigEnv): UserConfigExport => {
const isBuild = command === 'build'
return defineConfig({
plugins: [
viteMockServe({
ignore: /^_/, // 正则匹配忽略的文件
mockPath: 'mock', // 设置mock.ts 文件的存储文件夹
localEnabled: true, // 设置是否启用本地 xxx.ts 文件,不要在生产环境中打开它.设置为 false 将禁用 mock 功能
prodEnabled: true, // 设置生产环境是否启用 mock 功能
watchFiles: true, // 设置是否监视mockPath对应的文件夹内文件中的更改
// 代码注入
injectCode: `
import { setupProdMockServer } from '../mock/_createProductionServer';
setupProdMockServer();
`
})
]
})
}
```
[▲ 回顶部](#top)
## ✅ 配置 proxy 跨域
```js
server: {
host: '0.0.0.0',
proxy: {
// 字符串简写写法
'/foo': 'http://localhost:4567',
// 选项写法
'/api': {
target: 'http://jsonplaceholder.typicode.com',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
},
// 正则表达式写法
'^/fallback/.*': {
target: 'http://jsonplaceholder.typicode.com',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/fallback/, '')
}
// 使用 proxy 实例
// "/api": {
// target: "http://jsonplaceholder.typicode.com",
// changeOrigin: true,
// configure: (proxy, options) => {
// // proxy 是 'http-proxy' 的实例
// },
// },
}
},
```
[▲ 回顶部](#top)
## ✅ Axios 封装及接口管理
`utils/request.js` 封装 axios ,开发者需要根据后台接口做修改。
- `service.interceptors.request.use` 里可以设置请求头,比如设置 `token`
- `config.hideloading` 是在 api 文件夹下的接口参数里设置,下文会讲
- `service.interceptors.response.use` 里可以对接口返回数据处理,比如 401 删除本地信息,重新登录
```ts
/**
* @description [ axios 请求封装]
*/
import store from '@/store'
import axios, { AxiosResponse, AxiosRequestConfig } from 'axios'
// import { Message, Modal } from 'view-design' // UI组件库
import { Dialog, Toast } from 'vant'
import router from '@/router'
// 根据环境不同引入不同api地址
import config from '@/config'
const service = axios.create({
baseURL: config.baseApi + '/api', // url = base url + request url
timeout: 5000,
withCredentials: false // send cookies when cross-domain requests
// headers: {
// // clear cors
// 'Cache-Control': 'no-cache',
// Pragma: 'no-cache'
// }
})
// Request interceptors
service.interceptors.request.use(
(config: AxiosRequestConfig) => {
// 加载动画
if (config.loading) {
Toast.loading({
message: '加载中...',
forbidClick: true
})
}
// 在此处添加请求头等,如添加 token
// if (store.state.token) {
// config.headers['Authorization'] = `Bearer ${store.state.token}`
// }
return config
},
(error: any) => {
Promise.reject(error)
}
)
// Response interceptors
service.interceptors.response.use(
async (response: AxiosResponse) => {
// await new Promise(resovle => setTimeout(resovle, 3000))
Toast.clear()
const res = response.data
if (res.code !== 0) {
// token 过期
if (res.code === 401) {
// 警告提示窗
return
}
if (res.code == 403) {
Dialog.alert({
title: '警告',
message: res.msg
}).then(() => {})
return
}
// 若后台返回错误值,此处返回对应错误对象,下面 error 就会接收
return Promise.reject(new Error(res.msg || 'Error'))
} else {
// 注意返回值
return response.data
}
},
(error: any) => {
Toast.clear()
if (error && error.response) {
switch (error.response.status) {
case 400:
error.message = '请求错误(400)'
break
case 401:
error.message = '未授权,请登录(401)'
break
case 403:
error.message = '拒绝访问(403)'
break
case 404:
error.message = `请求地址出错: ${error.response.config.url}`
break
case 405:
error.message = '请求方法未允许(405)'
break
case 408:
error.message = '请求超时(408)'
break
case 500:
error.message = '服务器内部错误(500)'
break
case 501:
error.message = '服务未实现(501)'
break
case 502:
error.message = '网络错误(502)'
break
case 503:
error.message = '服务不可用(503)'
break
case 504:
error.message = '网络超时(504)'
break
case 505:
error.message = 'HTTP版本不受支持(505)'
break
default:
error.message = `连接错误: ${error.message}`
}
} else {
if (error.message == 'Network Error') {
error.message == '网络异常,请检查后重试!'
}
error.message = '连接到服务器失败,请联系管理员'
}
Toast(error.message)
// store.auth.clearAuth()
store.dispatch('clearAuth')
return Promise.reject(error)
}
)
export default service
```
#### 接口管理
在`src/api` 文件夹下统一管理接口
- 你可以建立多个模块对接接口, 比如 `home.ts` 里是首页的接口这里讲解 `authController.ts`
- `url` 接口地址,请求的时候会拼接上 `config` 下的 `baseApi`
- `method` 请求方法
- `data` 请求参数 `qs.stringify(params)` 是对数据系列化操作
- `loading` 默认 `false`,设置为 `true` 后,显示 loading ui 交互中有些接口需要让用户感知
```ts
import request from '@/utils/request'
export interface IResponseType {
code: number
msg: string
data: P
}
interface IUserInfo {
id: string
avator: string
}
interface IError {
code: string
}
export const fetchUserInfo = () => {
return request>({
url: '/user/info',
method: 'get',
loading: true
})
}
```
#### 如何调用
由于`awaitWrap`类型推导很麻烦,所以还是采用 try catch 来捕获错误,既能捕获接口错误,也能捕获业务逻辑错误
```js
onMounted(async () => {
try {
let res = await fetchUserInfo()
console.log(res)
} catch (error) {
console.log(error)
}
})
```
[▲ 回顶部](#top)
## ✅ vue-request 管理接口
* 文档:https://cn.attojs.org/
* 使用 vue-request 可以更方便地管理接口
### 1. 安装依赖
```js
pnpm i vue-request
```
### 2. 使用axios来获取数据,vue-request进行管理
```js
// axios
export const fetchUserInfo = () => {
return request>({
url: '/user/info',
method: 'get',
loading: true
})
}
// vue-request
const { data: res, run } = useRequest(fetchUserInfo)
// 如果请求未完成,data为undefined。 使用 run 等待请求完成
await run()
console.log(res.value?.data)
```
### 3. 使用 vue-request 进行定时请求
```js
// axios
export const getTimingData = () => {
return request({
url: '/getTimingData',
method: 'GET'
})
}
// vue-request
const { data: resultData, run } = useRequest(getTimingData, {
pollingInterval: 5000,
onSuccess: (data) => {
console.log('onSuccess', data)
}
})
```
## ✅ unplugin-xxx 自动导入
* 参考资料:https://juejin.cn/post/7012446423367024676
* 自定义组件自动引入 [unplugin-vue-components](https://github.com/antfu/unplugin-vue-components#readme)
* vue3等插件 hooks 自动引入 [unplugin-auto-import/vite](https://github.com/antfu/unplugin-auto-import)
* message, notification 等引入样式自动引入 [vite-plugin-style-import](https://github.com/vbenjs/vite-plugin-style-import)
* eslint插件 [vue-global-api](https://github.com/antfu/vue-global-api)
### unplugin-vue-components
* 自动导入流行库组件和自定义组件
1. 安装依赖
```js
pnpm i -D unplugin-vue-components
```
2. 修改 vite.config.ts
```js
Components({
// 指定组件位置,默认是src/components
dirs: ['src/components'],
// ui库解析器
// resolvers: [ElementPlusResolver()],
extensions: ['vue', 'tsx'],
// 配置文件生成位置
dts: 'src/components.d.ts',
// 搜索子目录
deep: true,
// 允许子目录作为组件的命名空间前缀。
directoryAsNamespace: false
// include:[]
}),
```
### unplugin-auto-import
* 自动导入vue3相关api
1. 安装依赖
```js
pnpm i -D unplugin-auto-import
```
2. 配置 vite.config.ts
```js
AutoImport({
include: [
/\.[tj]sx?$/, // .ts, .tsx, .js, .jsx
/\.vue$/,
/\.vue\?vue/, // .vue
/\.md$/ // .md
],
imports: ['vue', 'vue-router', '@vueuse/core'],
// 可以选择auto-import.d.ts生成的位置,使用ts建议设置为'src/auto-import.d.ts'
dts: 'src/auto-import.d.ts',
// eslint globals Docs - https://eslint.org/docs/user-guide/configuring/language-options#specifying-globals
// 生成全局声明文件,给eslint用
eslintrc: {
enabled: true, // Default `false`
filepath: './.eslintrc-auto-import.json', // Default `./.eslintrc-auto-import.json`
globalsPropValue: true // Default `true`, (true | false | 'readonly' | 'readable' | 'writable' | 'writeable')
}
})
```
3. 配置 eslintrc
```js
// .eslintrc.js
module.exports = {
/* ... */
extends: [
// ...
'./.eslintrc-auto-import.json',
],
}
```
### vue-global-api
* 在页面没有引入的情况下,使用unplugin-auto-import/vite来自动引入hooks,在项目中肯定会报错的,这时候需要在eslintrc.js中的extends引入vue-global-api,这个插件是vue3hooks的,其他自己找找,找不到的话可以手动配置一下globals
1. 安装依赖
```js
pnpm i -D vue-global-api
```
2. 配置 eslintrc
```js
// .eslintrc.js
module.exports = {
extends: [
'vue-global-api'
]
};
```
## ✅ VantUI 组件按需加载
- 文档:https://vant-contrib.gitee.io/vant/v3/#/zh-CN/quickstart
### 1. 安装依赖
```js
pnpm add vant@3
pnpm add vite-plugin-style-import -D
```
### 2. 按需引入配置
- vite.config.ts
```js
import vue from '@vitejs/plugin-vue'
import styleImport, { VantResolve } from 'vite-plugin-style-import'
export default {
plugins: [
vue(),
styleImport({
resolves: [VantResolve()]
})
]
}
```
- plugins/vant.ts
```ts
import { App as VM } from 'vue'
import { Button, Cell, CellGroup, Icon, Tabbar, TabbarItem, Image as VanImage } from 'vant'
const plugins = [Button, Icon, Cell, CellGroup, Tabbar, TabbarItem, VanImage]
export const vantPlugins = {
install: function (vm: VM) {
plugins.forEach((item) => {
vm.component(item.name, item)
})
}
}
```
- main.ts
```ts
// 全局引入按需引入UI库 vant
import { vantPlugins } from './plugins/vant'
app.use(vantPlugins)
```
### 3. 在
```
### 4. 在 JSX 和 TSX 中可以直接使用 Vant 组件,不需要进行组件注册。
- 如果使用这种方式,就不需要注册上面的 `plugins/vant.ts` 了
```ts
import { Button } from 'vant'
export default {
render() {
return
}
}
```
[▲ 回顶部](#top)
## ✅ viewport 适配方案
- 看到 `lib-flexible` 仓库说,由于 viewport 单位得到众多浏览器的兼容,lib-flexible 这个过渡方案已经可以放弃使用,建议大家开始使用 viewport 来替代此方,所以就踩坑用用 viewport
- 参考文档:https://blog.csdn.net/weixin_46429258/article/details/115537383
- vant 官方文档有说怎么配,先按着官方文档配一下
- postcss-px-to-viewport 文档: https://github.com/evrone/postcss-px-to-viewport/blob/master/README_CN.md
### 1. 安装依赖
```js
pnpm i -D postcss-px-to-viewport autoprefixer
```
### 2. 添加 .postcssrc.js
```js
module.exports = {
plugins: {
// 用来给不同的浏览器自动添加相应前缀,如-webkit-,-moz-等等
autoprefixer: {
overrideBrowserslist: ['Android 4.1', 'iOS 7.1', 'Chrome > 31', 'ff > 31', 'ie >= 8']
},
'postcss-px-to-viewport': {
unitToConvert: 'px', // 要转化的单位
viewportWidth: 375, // UI设计稿的宽度
unitPrecision: 6, // 转换后的精度,即小数点位数
propList: ['*'], // 指定转换的css属性的单位,*代表全部css属性的单位都进行转换
viewportUnit: 'vw', // 指定需要转换成的视窗单位,默认vw
fontViewportUnit: 'vw', // 指定字体需要转换成的视窗单位,默认vw
selectorBlackList: ['wrap'], // 指定不转换为视窗单位的类名,
minPixelValue: 1, // 默认值1,小于或等于1px则不进行转换
mediaQuery: true, // 是否在媒体查询的css代码中也进行转换,默认false
replace: true, // 是否转换后直接更换属性值
exclude: [/node_modules/], // 设置忽略文件,用正则做目录名匹配
landscape: false // 是否处理横屏情况
}
}
}
```
[▲ 回顶部](#top)
## ✅ 适配苹果底部安全距离
- index.html 的 meta 指定了 viewport-fit=cover
- [vant 中自带底部安全距离参数](https://vant-contrib.gitee.io/vant/v3/#/zh-CN/advanced-usage#di-bu-an-quan-qu-gua-pei)
```js
```
如果不用 vant 中的适配,也可以自己写,我在 scss 中写了通用样式
```scss
.fixIphonex {
padding-bottom: $safe-bottom !important;
&::after {
content: '';
position: fixed;
bottom: 0 !important;
left: 0;
height: calc(#{$safe-bottom} + 1px);
width: 100%;
background: #ffffff;
}
}
```
[▲ 回顶部](#top)
## ✅ 动态设置 title
```js
// utils/index.ts
import { config } from '@/config'
/**
* 动态设置浏览器标题
* @param title
*/
export const setDocumentTitle = (title?: string) => {
document.title = title || config.title
}
```
router/index.ts 使用
```ts
router.beforeEach((to, from, next) => {
setDocumentTitle(to.meta.title as string)
next()
})
```
[▲ 回顶部](#top)
## ✅ 配置 Jssdk
1. 安装:
```bash
yarn add weixin-js-sdk
```
类型声明写在了 model/weixin-js-sdk.d.ts
由于苹果浏览器只识别第一次进入的路由,所以需要先处理下配置使用的 url
- router.ts
此处的jssdk配置仅供演示,正常业务逻辑需要配合后端去写
```ts
```
```ts
import { defineStore } from 'pinia'
export interface ILinkState {
initLink: string
}
export const useAuthStore = defineStore({
id: 'auth', // id 必须唯一
state: () =>
({
initLink: ''
} as ILinkState),
actions: {
setInitLink(data: any) {
this.$state.initLink = data
},
setIsAuth(data) {
this.$state.isAuth = data
},
setCode(code) {
this.$state.code = code
}
},
// 开启数据缓存
persist: {
key: 'auth',
storage: window.localStorage,
// paths: ['name'],
overwrite: true
}
}
```
由于window没有entryUrl变量,需要声明文件进行声明
```ts
// typings/index.d.ts
declare interface Window {
entryUrl: any
}
```
创建 hooks 函数
hooks/useWxJsSdk.ts
每个页面使用jssdk,都需要调用一次useWxJsSdk,然后再使用其他封装的函数
调用:
```ts
```
[▲ 回顶部](#top)
## ✅ Eslint + Prettier 统一开发规范
- 初始化项目集成了 eslint + prettier,我们这里只做配置
- .eslintrc.js
```js
/* eslint-env node */
require('@rushstack/eslint-patch/modern-module-resolution')
module.exports = {
root: true,
extends: [
'plugin:vue/vue3-essential',
'eslint:recommended',
'@vue/eslint-config-typescript/recommended',
'@vue/eslint-config-prettier'
],
env: {
'vue/setup-compiler-macros': true
},
rules: {
'prettier/prettier': 'warn',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-unused-vars': 'off',
'vue/multi-word-component-names': 'off'
}
}
```
- .prettier.js
```js
module.exports = {
// 定制格式化要求
overrides: [
{
files: '.prettierrc',
options: {
parser: 'json'
}
}
],
printWidth: 100, // 一行最多 100 字符
tabWidth: 2, // 使用 4 个空格缩进
semi: false, // 行尾需要有分号
singleQuote: true, // 使用单引号而不是双引号
useTabs: false, // 用制表符而不是空格缩进行
quoteProps: 'as-needed', // 仅在需要时在对象属性两边添加引号
jsxSingleQuote: false, // 在 JSX 中使用单引号而不是双引号
trailingComma: 'none', // 末尾不需要逗号
bracketSpacing: true, // 大括号内的首尾需要空格
bracketSameLine: false, // 将多行 HTML(HTML、JSX、Vue、Angular)元素反尖括号需要换行
arrowParens: 'always', // 箭头函数,只有一个参数的时候,也需要括号 avoid
rangeStart: 0, // 每个文件格式化的范围是开头-结束
rangeEnd: Infinity, // 每个文件格式化的范围是文件的全部内容
requirePragma: false, // 不需要写文件开头的 @prettier
insertPragma: false, // 不需要自动在文件开头插入 @prettier
proseWrap: 'preserve', // 使用默认的折行标准 always
htmlWhitespaceSensitivity: 'css', // 根据显示样式决定 html 要不要折行
vueIndentScriptAndStyle: false, //(默认值)对于 .vue 文件,不缩进