【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

<script setup lang="ts"></script>

<template>
  <router-view />
</template>

<style></style>

配置 keep-alive

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

文档地址

缓存 homesetting 组件

<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的良好替代品吗?

我把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 组件中使用

<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

  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 <T>(
  config: AxiosRequestConfig
): Promise<API.BaseResponseType<T>> => {
  try {
    const { data } = await axiosInterface(config);
    return data;
  } catch (error) {
    return Promise.reject(error);
  }
};
声明 API 的 namespcae
  1. 创建一个单独的 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, 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<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;
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容