theme: nico
初始化 vue-ts 项目
pnpm create vite tutulist-web-app --template vue-ts
安装 vscode 插件
Volar
vue3
语法支持,此插件并不兼容 vue2
,使用时需要将 vue2
插件禁用
vue3-snippets-for-vscode
可根据关键词快速键入 vue3 相关代码
配置 lint
eslint
和 vue-cli
创建的项目不一样,vite
创建的项目是不带 eslint
,所以需要手动去配
# eslint 和 eslint vue 插件
pnpm install --save-dev eslint eslint-plugin-vue
# vite 接入 eslint
pnpm install vite-plugin-eslint --save-dev
pnpm i @typescript-eslint/parser @typescript-eslint/eslint-plugin --save-dev
@typescript-eslint/parser
增加 eslint 解析 typescript 的能力
@typescript-eslint/eslint-plugin
eslint 插件,为 typescript 代码提供 lint 规则
prettier
eslint-plugin-prettier
用于将 prettier
的 错误报错给 eslint
eslint-config-prettier
因为 eslint
和 prettier
都可以去做格式化代码,这就造成两者在使用上会出现冲突,它主要负责两者的冲突
pnpm install prettier eslint-plugin-prettier eslint-config-prettier --save-dev
创建 .eslintrc.json
文件
{
"env": {
"browser": true,
"node": true
},
"extends": [
"eslint:recommended",
"plugin:vue/vue3-recommended",
"prettier",
"plugin:prettier/recommended"
],
"plugins": ["vue", "@typescript-eslint"],
"parserOptions": {
"ecmaVersion": 12,
"parser": "@typescript-eslint/parser",
"sourceType": "module"
},
"rules": {
"vue/multi-word-component-names": "off",
"no-unused-vars": [
"error",
{
"varsIgnorePattern": ".*",
"args": "none",
"vars": "all",
"ignoreRestSiblings": true,
"argsIgnorePattern": "^_"
}
]
}
}
自动格式化代码
借助 vscode
Prettier
插件 格式化代码
下载 Prettier
vscode 插件,然后在设置中搜索 editor.default formatter
使用 Prettier
Prettier 这里我们就不配了,使用官方插件默认格式化就好。
但如果在团队中,尤其成员之前使用不同的编辑器,那么就需要配置一下 Prettier
统一代码风格了。
开启保存时格式化文件
设置中搜索 formatOnSave
配置 rules
vue/multi-word-component-names
创建 vue 组件时,可以使用单个单词
no-unused-vars
声明但未使用的变量,当变量名以 _ 为前缀时,可忽略错误
"rules": {
"vue/multi-word-component-names": "off",
"no-unused-vars": [
"error",
{
"varsIgnorePattern": ".*",
"args": "none",
"vars": "all",
"ignoreRestSiblings": true,
"argsIgnorePattern": "^_"
}
]
}
vite 接入 eslint
使用此 vite 插件可以将 eslint 的错误信息展示到浏览器上
代码配置
vite.config.ts 中引入 eslintPlugin
import eslintPlugin from 'vite-plugin-eslint';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [eslintPlugin()]
})
配置路径别名
导入 path 时,可能会报类型错误,需安装 @types/node
pnpm install --save-dev @types/node
vite.config.js
import { resolve } from "path";
export default defineConfig({
plugins: [eslintPlugin(), vue()],
resolve: {
alias: {
"@": resolve(__dirname, "/src"),
},
},
});
tsconfig.json
在 compilerOptions
处添加
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
}
}
配置 vue-router
pnpm install vue-router@4
配置 router
import { createRouter, createWebHashHistory } from "vue-router";
import type { RouteRecordRaw } from "vue-router";
import Home from "@/pages/home/index.vue";
import Calendar from "@/pages/calendar/index.vue";
import Setting from "@/pages/setting/index.vue";
const routes: RouteRecordRaw[] = [
{
path: "/",
name: "homePage",
component: Home,
meta: {
title: "首页",
},
},
{
path: "/calendar",
name: "calendar",
component: Calendar,
meta: {
title: "日历",
},
},
{
path: "/setting",
name: "setting",
component: Setting,
meta: {
title: "设置",
},
},
];
const router = createRouter({
history: createWebHashHistory(), // hash 模式
routes,
});
export default router;
让应用支持 router
import { createApp } from "vue";
import router from "./routes";
import App from "./App.vue";
import "./index.css";
const app = createApp(App);
app.use(router);
app.mount("#app");
App.vue 中 添加 router-view
<script setup lang="ts"></script>
<template>
<router-view />
</template>
<style></style>
配置 keep-alive
keep-alive 和 vue2 写法还不太一样
缓存 home
和 setting
组件
<router-view v-slot="{ Component }">
<keep-alive :include="['home', 'setting']">
<component :is="Component" />
</keep-alive>
</router-view>
指定组件 name
对于 script setup
语法,我们可以安装 vite-plugin-vue-setup-extend
插件,让其支持 name
属性
<script setup lang="ts" name="setting">
import { onMounted } from "vue";
onMounted(() => {
console.log("setting");
});
</script>
配置 Pinia
安装
pnpm install pinia
import { createPinia } from "pinia";
app.use(createPinia());
关于 Vuex 和 Pinia 的对比,可以看以下几篇文章
Pinia与Vuex的对比:Pinia是Vuex的良好替代品吗?
定义 store
- defineStore 定义 store
- state 定义状态
- actions 定义方法,既可以定义同步也也可以定义异步方法
import { defineStore } from "pinia";
// calendar 定义唯一key
const useCalendarStore = defineStore("calendar", {
state: () => ({
isStartSunday: false,
}),
actions: {
setStartSundaySync(value: boolean) {
this.isStartSunday = value;
},
async setStartSunday() {
const data = await getInfo();
this.isStartSunday = data;
},
},
});
export default useCalendarStore;
在 Vue 组件中使用
<script setup lang="ts">
import useCalendarStore from "@/stores/calendar";
const caledarStore = useCalendarStore();
onMounted(() => {
// 获取 state
const isStartSunday = caledarStore.isStartSunday;
// 触发 actions
caledarStore.setStartSunday(false);
});
</script>
配置 Tailwind CSS
- 如果不想花太多时间去写
css
,那么其实可以尝试使用下tailwind css
这种原子化css
- 当然目前社区中原子化
css
的方案还有很多,大家根据自己喜好选择
- 当然目前社区中原子化
- 虽然要记那么多的
classname
,但是有vscode
插件啊,用起来之后你就会觉得其实还挺香的
安装和初始化
这里官方文档已经说的够详细了,就直接贴文档了
VS Code 类名提示
安装插件 Tailwind CSS IntelliSense
处理编辑器警告
@tailwind 报警告
解决方法:装一个 postcss vscode
插件
插件提示不生效
设置中输入 quickSuggestions
,将 strings
置为 on
tailwindcss 使用 @apply 时 报 warning
解决方案: https://github.com/tailwindlabs/tailwindcss/discussions/5258
- 下载 vscode 插件 PostCSS Language Support
css.lint.unknownAtRules: ignore
- 如果你在项目中使用的是
scss
,那么把css
改成scss
即可
- 如果你在项目中使用的是
css.lint.unknow 设置为 ignore
配置 axios
axios + ts 案例
axios
封装每个团队有每个团队的习惯和规范,没有最好的,用起来爽就行;
request 函数
- 接收 axios request config 配置对象,并返回一个 Promise 类型的
API.BaseResponseType
-
API.BaseResponseType
接受一个 泛型 T,用于约束后端返回的数据data
的类型
const request = async <T>(
config: AxiosRequestConfig
): Promise<API.BaseResponseType<T>> => {
try {
const { data } = await axiosInterface(config);
return data;
} catch (error) {
return Promise.reject(error);
}
};
声明 API 的 namespcae
- 创建一个单独的 namespcae API,用于约束与后端交互的数据类型
declare namespace API {
type BaseResponseType<T> = {
code: number;
message: string;
data: T;
};
}
需要在 .eslintrc.json
文件中添加配件
"globals": {
"API": "readonly"
},
API.BaseResponseType
后端返回最基本的 响应数据 结构
通过在 request 函数中传入泛型约束后端返回的具体数据结构
export const loginByPassword = async (loginInfo: LoginByPassword) => {
return await request<{
accessToken: string;
refreshToken: string;
}>({
url: "/user/loginByPassword",
method: "post",
data: loginInfo,
});
};
配置 Refresh Token
登录成功后,后端会返回两个 token, accessToken
和 refreshToken
,有效时间分别为 2天 和 4天;
用户在使用过程中,如果后端返回 401 状态码,就代表 accessToken
过期了。这时候要缓存过期后的请求函数,同时发送一个新的请求并携带 refreshToken
去从后端获取新的 token
,获取新的 token
成功后,再执行之前缓存过的请求函数
如果获取新token 的请求返回的状态码非 200,那么代表 refreshToken
也过期了,这时候需要跳转到登录页,重新登录
const handleRefreshToken = async () => {
const { code, data } = await request<{
accessToken: string;
refreshToken: string;
}>({
url: "/user/refreshToken",
method: "post",
data: {
refreshToken: window.localStorage.getItem(UserTokenEnum.REFRESH_TOKEN),
},
});
if (Number(code) === 200) {
localStorage.setItem(UserTokenEnum.ASSET_TOKEN, data.accessToken);
localStorage.setItem(UserTokenEnum.REFRESH_TOKEN, data.refreshToken);
axiosInterface.defaults.headers.common[
"Authorization"
] = `Bearer ${data.accessToken}`;
// 执行 token 失效后缓存的请求函数
catchRequestFunc.forEach(async (catchFunc) => {
await catchFunc();
});
} else {
// refreshtoken 也过期了,那么跳登录页,重新登录
const globalStore = useGlobalStore();
globalStore.handleLogout();
catchRequestFunc = [];
router.push({
name: "homePage",
});
window.$message.warning("请重新登录");
}
};
完整代码
import axios from "axios";
import router from "@/router";
import useGlobalStore from "@/stores/global";
import { UserTokenEnum } from "@/types/user";
import type { AxiosRequestConfig, AxiosResponse } from "axios"
const netWorkCodeMaps: Record<number, string> = {
404: "404 Not Found",
405: "Method Not Allowed",
504: "网关错误",
500: "服务器错误",
} as const;
const axiosInterface = axios.create({
baseURL: `/api`,
timeout: 10000,
headers: {
"content-type": "application/json",
},
});
// 缓存 token 过期后的请求函数
let catchRequestFunc: Array<() => void> = [];
// 请求拦截
axiosInterface.interceptors.request.use((config: AxiosRequestConfig) => {
const token = localStorage.getItem(UserTokenEnum.ASSET_TOKEN);
if (token) {
const { headers } = config;
headers!.Authorization = `Bearer ${token}`;
}
return config;
});
// 响应拦截
axiosInterface.interceptors.response.use(
async (response: AxiosResponse<API.BaseResponseType<any>>) => {
const { status, data } = response;
if (status === 200) {
const { code, message } = data;
const responseCode = Number(code);
// token 过期
if (responseCode == 401) {
// 缓存过期后的请求函数
new Promise((resolve) => {
catchRequestFunc.push(() => {
resolve(request(response.config));
});
});
// 通过 reference token 获取新 token
await handleRefreshToken();
} else if (responseCode === 403) {
router.push({
name: "homePage",
});
} else if (responseCode !== 200) {
// 业务中非 200 的状态码一律弹出
window.$message.error(message);
}
}
return response;
},
({ response }) => {
// 请求失败,也弹出状态码
window.$message.error(netWorkCodeMaps[response.status] || "服务器错误");
}
);
const handleRefreshToken = async () => {
const { code, data } = await request<{
accessToken: string;
refreshToken: string;
}>({
url: "/user/refreshToken",
method: "post",
data: {
refreshToken: window.localStorage.getItem(UserTokenEnum.REFRESH_TOKEN),
},
});
if (Number(code) === 200) {
localStorage.setItem(UserTokenEnum.ASSET_TOKEN, data.accessToken);
localStorage.setItem(UserTokenEnum.REFRESH_TOKEN, data.refreshToken);
axiosInterface.defaults.headers.common[
"Authorization"
] = `Bearer ${data.accessToken}`;
// 执行 token 失效后缓存的请求函数
catchRequestFunc.forEach(async (catchFunc) => {
await catchFunc();
});
} else {
// refreshtoken 也过期了,那么跳登录页,重新登录
const globalStore = useGlobalStore();
globalStore.handleLogout();
catchRequestFunc = [];
router.push({
name: "homePage",
});
window.$message.warning("请重新登录");
}
};
// 对外暴露 request 请求函数
const request = async <T>(
config: AxiosRequestConfig
): Promise<API.BaseResponseType<T>> => {
try {
const { data } = await axiosInterface(config);
return data;
} catch (error) {
return Promise.reject(error);
}
};
export default request;