[学习笔记]Vue3+TS项目初始化配置

1,438 阅读8分钟

全局环境

  1. Node.js 版本v14.15.4
  2. Vue CLI版本@vue/cli 4.5.13
  3. TypeScript版本 Version 4.2.3

项目创建

  1. 执行命令
vue create project_name

2.选择初始化配置项 image.png 选择 vue3.x 版本

总体项目结构

ts_project
├─ .browserslistrc
├─ .env.development // 开发环境配置文件
├─ .env.production // 生产环境配置文件
├─ .env.test // 测试环境配置文件
├─ .eslintrc.js
├─ .gitignore
├─ babel.config.js
├─ package-lock.json
├─ package.json
├─ public
│  ├─ favicon.ico
│  └─ index.html
├─ README.md
├─ src
│  ├─ api
│  │  ├─ ajaxUrl.config.ts // 统一管理项目请求URL
│  │  ├─ login.ts // 退出和登录方法封装,目前仅封装了退出登录
│  │  └─ manage.ts // 封装请求方法
│  ├─ App.vue
│  ├─ assets
│  │  ├─ scss // 全局css样式
│  |  │  └─ index.vue
│  │  └─ logo.png
│  ├─ components
│  │  └─ NoFind.vue // 定义 404 页面
│  ├─ main.ts // 入口
│  ├─ router // 配置路由
│  │  └─ index.ts
│  ├─ shims-vue.d.ts
│  ├─ store
│  │  └─ index.ts // Vuex 存储token 和用户信息等
│  ├─ utils // 工具函数等
│  │  ├─ request.ts // 请求拦截
│  │  └─ storage.ts // 封装 sessionStorage、localStorage、cookie 存储删除等
│  └─ views
│     ├─ Home // 内容页
│     │  └─ index.vue
│     ├─ Layout // 项目 Layout 布局
│     │  └─ index.vue
│     └─ Login // 登录页
│        └─ index.vue
├─ tsconfig.json
├─ vue.config.js //可选配置项
└─ yarn.lock

UI库的安装和引入

安装 yarn add ant-design-vue ant-design-vue官网提供了两种引入方式一种是按需加载一种是全局引入,这里采用全局引入。安装完成后在main.ts 文件添加如下代码

import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
// 引入ant-design-vue
import Antd from 'ant-design-vue'
import "ant-design-vue/dist/antd.css";

createApp(App).use(store).use(Antd).use(router).mount('#app')

Axios请求封装

安装

yarn add axios

请求拦截

注意:涉及到token 的存储、vuex 存储 token 在对应的章节进行描述。request.ts文件只是使用 在项目src文件夹下创建文件夹 utils文件夹 ,并创建 request.ts 文件,文件内容如下:

import axios,{AxiosResponse, AxiosRequestConfig} from "axios";
import { notification } from 'ant-design-vue' // 结合 ant-design-vue 做全局提示
import store from '@/store' // 获取 token
import { localCache } from '@/utils/storage' // 引入读取token
import { logout } from "@/api/login"; // 引入退出登录方法
// 初始化
const instance = axios.create({
  baseURL: process.env.VUE_APP_API_BASE_URL,
  timeout: 120 * 1000,
  withCredentials: true
})

// 请求错误
const err = (error: { message: string | string[]; response: { data: any; status: number; }; }) => {
  if (error.message.includes('timeout')) {
    notification.error({ message: '系统提示', description: '请求超时', duration: 4 });
  }
  if (error.response) {
    const data = error.response.data;
    const token = localCache.getCache('token')
    if (error.response.status == 403) {
      notification.error({ message: '系统提示', description: '请求资源失败', duration: 4 });
    }
    if (error.response.status === 401 && !(data.result && data.result.isLogin)) {
      notification.error({ message: '系统提示', description: '没有访问权限', duration: 4 });
      // token 存在但是没有访问权限,退出登录
      if (token) {
        logout();
      }
    }
  }
  return Promise.reject(error);
};

// 请求
instance.interceptors.request.use((config: AxiosRequestConfig) => {
  // 获取系统token
  const token:string = store.state.token;
  if (token) {
    config.headers['X-Access-Token'] = token // 让每个请求携带自定义 token 请根据实际情况自行修改
  }
  // 配置 get 接口默认参数携带时间戳请求
  if (config.method == 'get') {
      config.params = {
        _t: new Date().getTime(),
        ...config.params
      }
  }
  return config;
}, err)


// 拦截成功请求
instance.interceptors.response.use((response: AxiosResponse) => {
  const config: AxiosRequestConfig = response.config || '';
  const code = Number(response.data.code);
  // code 状态根据前后端协定接口成功状态修改
  if (code == 200 || code == 0) {
    if (config) {
      console.log('请求成功')
    }
    return response.data;
  } else {
    const errorCode = [402, 403, 500];
    if (errorCode.includes(response.data.code)) {
      notification.error({ message: '系统提示', description: '没有权限', duration: 4 });
      setTimeout(() => {
        window.location.reload();
      }, 500)
    }
  }
},err)

export default instance;

说明:初始化里面的baseURL的值直接读取 process.env.VUE_APP_API_BASE_URL,值的配置项来源于.env环境变量。其中.env所有环境都会被载入,.env.test测试环境被载入,.env.development开发环境被载入,.env.production生产环境被载入。文件的创建在项目的根目录和vue.config.js同级。具体配置项可参考Vue CLI 模式和环境变量

封装

在项目src文件夹下创建文件夹 api文件夹 ,并创建 manage.ts 文件,文件内容如下:

import axios from '@/utils/request'

/**
 * @desc post请求
 * @param url 请求路径
 * @param parameter 请求参数
 *  */
export function postAction(url: string, parameter: any) {
  return axios({
    url: url,
    method: 'post',
    data: parameter
  })
}

/**
 * @desc http请求
 * @param url 请求路径
 * @param parameter 请求参数
 * @param method= {post | put} 
 *  */
export function httpAction(url: string, parameter: any, method: any) {
  return axios({
    url: url,
    method: method,
    data: parameter
  })
}

/**
 * @desc put请求
 * @param url 请求路径
 * @param parameter 请求参数
 *  */
export function putAction(url: string, parameter: any) {
  return axios({
    url: url,
    method: 'put',
    data: parameter
  })
}

/**
 * @desc get请求
 * @param url 请求路径
 * @param parameter 请求参数
 *  */
export function getAction(url: string, parameter: any) {
  return axios({
    url: url,
    method: 'get',
    params: parameter
  })
}

/**
 * @desc delete请求
 * @param url 请求路径
 * @param parameter 请求参数
 *  */
export function deleteAction(url: string, parameter: any) {
  return axios({
    url: url,
    method: 'delete',
    params: parameter
  })
}

vue.config.js配置代理

  1. 在项目根目录创建vue.config.js 配置服务器请求代理
module.exports = {
  devServer: {
    port: 3082,
    proxy: {
      "/pr-api": {
        target: "http://localhost:3085",
        ws: false,
        changeOrigin: true,
      },
    },
  },

  lintOnSave: undefined,
};

统一管理项目请求API

  1. 在项目src文件夹下创建文件夹 api文件夹 ,并创建 ajaxUrl.config.ts 文件,用于统一管理项目请求API。示例如下
const Login = {
  login: '/sys/login'
}
export {
  Login
}

登录、退出

退出登录公共方法

在项目src文件夹下创建文件夹 api文件夹 ,并创建 login.ts 文件,管理退出登录。

import { createVNode } from "vue";
import { ExclamationCircleOutlined } from '@ant-design/icons-vue';
// import { useRouter } from "vue-router";
import { postAction } from "@/api/manage";
import {cookies, localCache} from '@/utils/storage'
import { Modal, message } from 'ant-design-vue'
/* 
退出
*/
export function logout() {
  // const router = useRouter();
  Modal.confirm({
    title: '退出登录?',
    icon: createVNode(ExclamationCircleOutlined),
    okText: '确认',
    cancelText: '取消',
    onOk() {
      postAction("/sys/logout", {}).then((res: any) => {
        if (res.success) {
          // 清空浏览器存储的所有数据
          cookies.removeCookie('vuex')
          localCache.clearCache()
          // 提示退出成功
          message.success(res.message);
          // 跳转到登录页
          //  router.push({ name: "Login" });
          // 刷新整个浏览器
          setTimeout(() => {
            window.location.reload();
          }, 100)
        }
      });
    },
    onCancel() {
      message.info('取消退出登录');
    },
    class: 'test',
  });
}

缓存存储读取封装

注意:js-cookie的安装查看vuex-persistedstate 使用步骤章节的3.安装js-cookie
在项目src文件夹下创建文件夹 utils文件夹 ,并创建 storage.ts 文件,文件内容如下:

import Cookies from 'js-cookie'
/* 
* localStorage 封装
*/
const localCache = {
  // 设置
  setCache(key: string, value: any) {
    window.localStorage.setItem(key, JSON.stringify(value))
  },
  // 获取
  getCache(key: string) {
    const value = window.localStorage.getItem(key)
    if (value) {
      return JSON.parse(value)
    }
  },
  // 清空某一个
  deleteCache(key: string) {
    window.localStorage.removeItem(key)
  },
  // 清空全部
  clearCache() {
    window.localStorage.clear()
  },
}

/**
 * sessionStorage 分装
 */
const sessionStorage = {
  //存储
  set(key: string, value: any) {
    window.sessionStorage.setItem(key, JSON.stringify(value))
  },
  //取出数据
  get<T>(key: string) {
    const value = window.sessionStorage.getItem(key)
    if (value && value != "undefined" && value != "null") {
      return JSON.parse(value)
    }
    return null
  },
  // 删除数据
  remove(key: string) {
    window.sessionStorage.removeItem(key)
  }
}

const cookies = {
  getCookie(key:string) {
    return Cookies.get(key)
  },
  setCookie(key: string, value:any) {
    Cookies.set(key, value)
  },
  removeCookie(key:string) {
    Cookies.remove(key);
    return
  }
  
}

export {
  sessionStorage,
  localCache,
  cookies
}

登录

注意:涉及到路由、vuex 存储 token 在对应的章节进行描述。

<template>
  <a-form layout="horizontal" :model="formState">
    <a-form-item label="账号">
      <a-input v-model:value="formState.username" placeholder="请输入账号" />
    </a-form-item>
    <a-form-item label="密码">
      <a-input
        v-model:value="formState.password"
        type="password"
        placeholder="请输入密码"
      />
    </a-form-item>
    <a-button type="primary" @click="onSubmit">登录</a-button>
  </a-form>
</template>
<script lang="ts">
import { defineComponent, reactive, UnwrapRef, toRaw } from "vue";
import { useStore } from "vuex";
import { useRouter } from "vue-router";
import { message } from "ant-design-vue";
// 引入 封装的请求方法
import { postAction } from "@/api/manage";
// 引入 URL 统一管理文件
import { Login } from "@/api/ajaxUrl.config";
// 声明接口
interface FormState {
  username: string;
  password: string | number;
}
export default defineComponent({
  setup() {
    const formState: UnwrapRef<FormState> = reactive({
      username: "",
      password: "",
    });
    // Vuex
    const store = useStore();
    // Vue Router
    const router = useRouter();

    // 提交表单
    const onSubmit = () => {
      let params = toRaw(formState);
      postAction(Login.login, params).then((res: any) => {
        if (res.success) {
          // 将token数据设置到Vuex
          store.commit("setToken", res.result.token);
          // 设置用户信息到Vuex
          store.commit("setUserInfo", res.result.userInfo);
          // 登录成功
          router.push({ name: "Layout" });
        } else {
          message.error(res.message);
        }
      });
    };
    return {
      formState,
      onSubmit,
    };
  },
});
</script>

退出

<template>
  <div>
    <a-button type="primary" @click="outLogin">退出登录</a-button>
  </div>
</template>

<script lang="ts">
import { defineComponent } from "vue";
import { logout } from "@/api/login";

export default defineComponent({
  setup() {
    // 退出登录
    const outLogin = () => {
      logout();
    };

    return {
      outLogin,
    };
  },
});
</script>

Layout布局

Layout 布局

  1. 修改 App.vue 文件
<template>
    <!-- 设置路由出口 -->
  <router-view />
</template>

<style lang="scss"></style>
  1. 路由跳转和 layout 布局内容渲染
<template>
  <a-layout>
    <!-- 头部导航 -->
    <a-layout-header class="header">
      <div class="logo" />
      <a-menu
        theme="dark"
        mode="horizontal"
        v-model:selectedKeys="selectedKeys1"
        :style="{ lineHeight: '64px' }"
      >
        <a-menu-item key="1">nav 1</a-menu-item>
      </a-menu>
    </a-layout-header>
    <a-layout>
      <!-- 左侧菜单 -->
      <a-layout-sider width="200" style="background: #fff">
        <a-menu
          mode="inline"
          v-model:selectedKeys="selectedKeys2"
          v-model:openKeys="openKeys"
          :style="{ height: '100%', borderRight: 0 }"
        >
          <a-sub-menu key="sub1">
            <template #title>
              <span>
                <user-outlined />
                用户信息
              </span>
            </template>
            <a-menu-item key="/home">
              <router-link :to="{ path: 'home' }">
                <span>home</span>
              </router-link>
            </a-menu-item>
            <a-menu-item key="2">option2</a-menu-item>
          </a-sub-menu>
        </a-menu>
      </a-layout-sider>
      <!-- 中心区域 -->
      <a-layout style="padding: 0 24px 24px">
        <!-- 面包屑 -->
        <a-breadcrumb style="margin: 16px 0">
          <a-breadcrumb-item>Home</a-breadcrumb-item>
        </a-breadcrumb>
        <a-layout-content
          :style="{
            background: '#fff',
            padding: '24px',
            margin: 0,
            minHeight: '280px',
          }"
        >
          <!-- 设置路由出口 -->
          <router-view />
        </a-layout-content>
      </a-layout>
    </a-layout>
  </a-layout>
</template>
<script lang="ts">
import { UserOutlined } from "@ant-design/icons-vue";
import { defineComponent, ref } from "vue";
export default defineComponent({
  components: {
    UserOutlined,
  },
  setup() {
    return {
      selectedKeys1: ref<string[]>(["2"]),
      selectedKeys2: ref<string[]>(["1"]),
      collapsed: ref<boolean>(false),
      openKeys: ref<string[]>(["sub1"]),
    };
  },
});
</script>
<style>
#components-layout-demo-top-side-2 .logo {
  float: left;
  width: 120px;
  height: 31px;
  margin: 16px 24px 16px 0;
  background: rgba(255, 255, 255, 0.3);
}

.ant-row-rtl #components-layout-demo-top-side-2 .logo {
  float: right;
  margin: 16px 0 16px 24px;
}

.site-layout-background {
  background: #fff;
}
</style>

VueX 共享数据

由于 Vuex 需要存储全局的 token 和用户信息,由于 Vuex 是运行在内存中,数据也存储在内存中,用户刷新页面操作时,内存数据会重新进行初始化 通过 sessionStorage / localStorage / cookie 来进行数据的持久化存储。这里我们使用了两个插件实现Vuex数据的持久化。
Vuex 数据持久化插件vuex-persistedstate结合js-cookie 进行 Vuex 状态持久化的设置获取移除。
注:ts 项目使用 js-cookie 需要安装 "js-cookie": "^3.0.1", 和 "@types/js-cookie": "^2.2.7", 两个依赖包

vuex-persistedstate 使用步骤

1.安装插件

yarn add vuex-persistedstate -S

2.使用插件

export default createStore({
  getters,
  // 使用插件
  plugins: [createPersistedState()]
})

3.安装js-cookie

yarn add js-cookie@3.0.1 @types/js-cookie@2.2.7

具体使用

import { createStore } from 'vuex'
import createPersistedState from 'vuex-persistedstate'
import Cookies from 'js-cookie'
import {localCache} from '@/utils/storage'

// 定义用户状态接口
export interface UserState {
  count: number
  token: string,
  userInfo: any,
}

// vuex-persistedstate提供有一个reducer函数,可以自定义存储Key,或者使用paths参数,建议使用paths参数比较简单
// 非Module格式:xxxx
// 使用了Module的格式:ModuleName.xxxx,这里持久化的是Theme Module下面的persistData数据

const PERSIST_PATHS = ['token', 'userInfo']

// 初始化Vuex
export default createStore({
  state:<UserState> {
    count: 0,
    token: '',
    userInfo: {}
  },
  mutations: {
    add(state){
      state.count ++ 
    },
    // 设置token
    setToken(state, token) {
      state.token = token;
      localCache.setCache('token', token)
    },
    // 设置用户信息
    setUserInfo(state, userInfo) {
      state.userInfo = userInfo;
      localCache.setCache('userInfo', userInfo)
    }
  },
  
  actions: {
  },
  modules: {
  },
  getters: {
  },
  // 使用Vuex 数据持久化插件
  plugins: [createPersistedState({
    /* 
    storage 默认存储到 localStorage
    存储到 sessionStorage 配置 storage: window.sessionStorage
     */
    // reducer: function 返回需要储存的state对象
    // reducer(val) {
    //   return {
    //     // 持久化存储 state 中的 token
    //     token: val.token,
    //     userInfo: val.userInfo
    //   }
    // },
    /* 
      paths 设置保留持久化的数据,不设置则持久化全部数据
    */
    paths: PERSIST_PATHS,
    storage: {
      getItem: (key) => Cookies.get(key),
      // Please see https://github.com/js-cookie/js-cookie#json, on how to handle JSON.
      setItem: (key, value) =>  Cookies.set(key, value, { expires: 3, secure: true }),
      removeItem: (key) => Cookies.remove(key),
    },
  })]
})

VueRouter 使用

嵌套路由Layout布局

import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'
// 路由懒加载
const Login = () => import('@/views/Login/index.vue')

const routes: Array<RouteRecordRaw> = [
  {
    path: '/',
    name: 'Login',
    component: Login,
  },
  {
    path: '/layout',
    name: 'Layout',
    component: () => import("@/views/Layout/index.vue"),
    // 定义嵌套路由 实现 Layout 布局
    children: [
      {
        path: '/home',
        name: 'Home',
        component: () => import('@/views/Home/index.vue')
      },
      {
        path: '/about',
        name: 'About',
        component: () => import('@/views/About.vue')
      }
    ]
  },
  {
    // 匹配所有路径  vue2使用*   vue3使用/:pathMatch(.*)*或/:pathMatch(.*)或/:catchAll(.*)
    path: '/:pathMatch(.*)*',
    name: "404",
    component: ()=> import("@/components/NoFind.vue")

  }
]

// 初始化路由
const router = createRouter({
  history: createWebHistory(process.env.BASE_URL),
  routes
})

export default router

路由拦截

import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'
// 引入获取token 方法
import { localCache } from '@/utils/storage'

// 路由懒加载
const Login = () => import('@/views/Login/index.vue')

// 配置路由
const routes: Array<RouteRecordRaw> = []

// 初始化路由
const router = createRouter({
  history: createWebHistory(process.env.BASE_URL),
  routes
})

// 设置路由守卫
router.beforeEach((to, from, next) => {
  // to表示将要访问的路径,form表示从那个页面跳转而来,next表示允许跳转到指定位置
  if (to.path == '/') { // 当前访问为登陆页,直接进入
    next()
  } else {
    // 获取用户本地的token, 如果token不存在则跳转到登录页
    const token = localCache.getCache('token')
    if (!token) {
      next('/')
    } else { // 如果登录了,则直接跳转
      next()
    }
  }
})

export default router

整体代码

import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'
// 引入获取token 方法
import { localCache } from '@/utils/storage'

// 路由懒加载
const Login = () => import('@/views/Login/index.vue')

const routes: Array<RouteRecordRaw> = [
  {
    path: '/',
    name: 'Login',
    component: Login,
  },
  {
    path: '/layout',
    name: 'Layout',
    component: () => import("@/views/Layout/index.vue"),
    // 定义嵌套路由 实现 Layout 布局
    children: [
      {
        path: '/home',
        name: 'Home',
        component: () => import('@/views/Home/index.vue')
      },
      {
        path: '/about',
        name: 'About',
        component: () => import('@/views/About.vue')
      }
    ]
  },
  {
    // 匹配所有路径  vue2使用*   vue3使用/:pathMatch(.*)*或/:pathMatch(.*)或/:catchAll(.*)
    path: '/:pathMatch(.*)*',
    name: "404",
    component: ()=> import("@/components/NoFind.vue")

  }
]

// 初始化路由
const router = createRouter({
  history: createWebHistory(process.env.BASE_URL),
  routes
})

// 设置路由守卫
router.beforeEach((to, from, next) => {
  // to表示将要访问的路径,form表示从那个页面跳转而来,next表示允许跳转到指定位置
  if (to.path == '/') { // 当前访问为登陆页,直接进入
    next()
  } else {
    // 获取用户本地的token, 如果token不存在则跳转到登录页
    const token = localCache.getCache('token')
    if (!token) {
      next('/')
    } else { // 如果登录了,则直接跳转
      next()
    }
  }
})

export default router

配置全局样式

结合 scss 和 vue.config.js 进行全局样式配置,scss预处理在项目创建初始化配置项已经安装。 在src文件夹下的assets文件夹下建scss文件夹并创建index.scss文件,在vue.config.js中配置。参考项目vue.config配置中配置scss

项目vue.config配置

在vue.config.js进行配置

module.exports = {
  configureWebpack: (config) => {
    //生产环境取消 console.log
    if (process.env.NODE_ENV === "production") {
      config.optimization.minimizer[0].options.terserOptions.compress.drop_console = true;
    } else {
      // 开发环境
    }
  },
  // 配置 scss
  css: {
    loaderOptions: {
      scss: {
        prependData: `@import "@/assets/scss/index.scss";`,
      },
    },
  },
  // 配置服务端代理
  devServer: {
    port: 3082,
    proxy: {
      "/pr-api": {
        target: "http://localhost:3085",
        ws: false,
        changeOrigin: true,
      },
    },
  },

  lintOnSave: undefined,
};