【Midway+Vue3】初始化一个 Vue 项目 (前端篇 01)


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

因为 eslintprettier 都可以去做格式化代码,这就造成两者在使用上会出现冲突,它主要负责两者的冲突

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






配置 keep-alive

keep-alive 和 vue2 写法还不太一样

文档地址

缓存 homesetting 组件


  
    
  

指定组件 name

对于 script setup 语法,我们可以安装 vite-plugin-vue-setup-extend插件,让其支持 name 属性


配置 Pinia

中文文档 官方文档

安装

pnpm install pinia

import { createPinia } from "pinia";

app.use(createPinia());

关于 Vuex 和 Pinia 的对比,可以看以下几篇文章

Pinia与Vuex的对比:Pinia是Vuex的良好替代品吗?

我把vue3项目中的vuex去除了,改用 pinia

Pinia 官方文档

定义 store

  1. defineStore 定义 store
  2. state 定义状态
  3. 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 组件中使用


配置 Tailwind CSS

  1. 如果不想花太多时间去写 css,那么其实可以尝试使用下 tailwind css 这种原子化 css
    1. 当然目前社区中原子化 css 的方案还有很多,大家根据自己喜好选择
  1. 虽然要记那么多的 classname,但是有 vscode 插件啊,用起来之后你就会觉得其实还挺香的

安装和初始化

这里官方文档已经说的够详细了,就直接贴文档了

VS Code 类名提示

安装插件 Tailwind CSS IntelliSense

tailwind.png

处理编辑器警告

@tailwind 报警告

解决方法:装一个 postcss vscode 插件

PostCSS Language Support

参考链接

插件提示不生效

设置中输入 quickSuggestions ,将 strings 置为 on

quickSuggestions.png

tailwindcss 使用 @apply 时 报 warning

解决方案: https://github.com/tailwindlabs/tailwindcss/discussions/5258

  1. 下载 vscode 插件 PostCSS Language Support
  2. css.lint.unknownAtRules: ignore
    1. 如果你在项目中使用的是 scss,那么把 css 改成 scss 即可

css.lint.unknow 设置为 ignore

css.lint.png

配置 axios

axios + ts 案例

axios 封装每个团队有每个团队的习惯和规范,没有最好的,用起来爽就行;

request 函数

  1. 接收 axios request config 配置对象,并返回一个 Promise 类型的 API.BaseResponseType
  2. API.BaseResponseType 接受一个 泛型 T,用于约束后端返回的数据 data 的类型
const request = async (
  config: AxiosRequestConfig
): Promise> => {
  try {
    const { data } = await axiosInterface(config);
    return data;
  } catch (error) {
    return Promise.reject(error);
  }
};
声明 API 的 namespcae
  1. 创建一个单独的 namespcae API,用于约束与后端交互的数据类型
declare namespace API {
  type BaseResponseType = {
    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, accessTokenrefreshToken,有效时间分别为 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 = {
  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>) => {
    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 (
  config: AxiosRequestConfig
): Promise> => {
  try {
    const { data } = await axiosInterface(config);
    return data;
  } catch (error) {
    return Promise.reject(error);
  }
};

export default request;

你可能感兴趣的:(【Midway+Vue3】初始化一个 Vue 项目 (前端篇 01))