Vite+Vue3+axios+vueuse+Vue Router+Pinia+echarts+国际化+env变量

1,181 阅读8分钟

Vite+Vue3+axios+vueuse+Vue Router+Pinia+echarts+国际化+env变量+更换主题颜色

利用脚手架创建

npm create vite@latest 
yarn create vite 
pnpm create vite
复制代码

项目目录结构

│  ├─public # 静态资源目录
│  │      favicon.ico
│  │
│  ├─src
│  │  │  App.vue # 入口vue文件
│  │  │  main.js # 入口文件
│  │  │  vite-env.d.ts # vite环境变量声明文件
│  │  │
│  │  ├─assets # 资源文件目录
│  │  │      logo.png
│  │  │
│  │  └─components # 组件文件目录
│  │         HelloWorld.vue
│  │
│  │ .gitignore
│  │ index.html # Vite项目的入口文件
│  │ package.json
│  │ README.md
│  │ vite.config.js # vite配置文件

复制代码

配置别名 

在开发中,我们经常会通过 @/view/xxx.vue 的方式去书写我们的文件路径,我们可以通过配置 vite.config.ts 和 tsconfig.json 来实现别名。 vite.config.js

import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import * as path from "path";

// https://vitejs.dev/config/
export default defineConfig({
  resolve: {
    //设置别名
    alias: {
      "@": path.resolve(__dirname, "src"),
    },
  },
  plugins: [vue()],
});
复制代码

此时,我们会发现这行代码会报错。 import * as path from "path"; 找不到模块“path”或其相应的类型声明。ts(2307) 这是因为我们的配置文件是 ts 类型。我们只需要安装 Node.js 类型检查包就好

install -D @types/node
复制代码

tsconfig.json

{
  "compilerOptions": {
    "target": "esnext",
    "useDefineForClassFields": true,
    "module": "esnext",
    "lib": ["esnext", "DOM", "DOM.Iterable"],
    /** 路径别名 */
    "baseUrl": "./",
    "paths": {
      "@": ["src"],
      "@/*": ["src/*"]
    },
    /* Bundler mode */
    "moduleResolution": "node",
    "allowImportingTsExtensions": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "preserve",
    "strict": true,
    "noUnusedLocals": false,
    "noUnusedParameters": false,
    "noFallthroughCasesInSwitch": false
  },
  "include": [
    "src/**/*.ts",
    "src/**/*.d.ts",
    "src/**/*.tsx",
    "src/**/*.vue",
    "src/api/xhr/config.js"
  ],
  "references": [
    {
      "path": "./tsconfig.node.json"
    }
  ]
}

复制代码

集成 Vue Router

安装路由

pnpm i vue-router@4

npm i -D unplugin-vue-router // 自动配置路由插件

使用 vue-router

在 src 目录下创建 store 文件夹, 目录结构

│  ├─src
│  │  ├─router # 路由资源文件目录
│  │  │    │  index.ts
│  │  ├─views # 视图文件目录

// vite.config.ts
import VueRouter from 'unplugin-vue-router/vite'

export default defineConfig({
  plugins: [
    VueRouter({
      routesFolder: 'src/views', // 指定路由文件所在的目录
      exclude: ['**/components/*.vue'],
      extensions: ['.vue'], // 指定路由文件的后缀名
    }),
    // ⚠️ 必须要放到Vue()之前
    Vue(),
  ],
})

src/router/index.ts

import { RouteRecordRaw, createRouter, createWebHistory } from "vue-router";

import { routes } from 'vue-router/auto-routes';
// view文件下的index为根目录,即 "/"

// const routes: Array<RouteRecordRaw> = [
//  { path: "/", redirect: "/home" },
//  { path: "/home", name: "Home", component: () => import("@/views/Home.vue") },
//  { path: "/demo", name: "Demo", component: () => import("@/views/Demo.vue") },
//];

console.log("routes",routes);

//创建路由
const router = createRouter({
  // history: createWebHashHistory(), //hash模式
  history: createWebHistory(), //History模式
  routes, // `routes: routes` 的缩写
  scrollBehavior(to, from, savedPosition) {
    //滚动行为
    if (savedPosition) {
      return savedPosition;
    } else {
      if (to.meta.savedPosition) {
        // 如果meta信息中存在savedPosition,页面就滚动到上次浏览的位置(savedPosition)
        return { x: 0, y: to.meta.savedPosition };
      }
      return { x: 0, y: 0 };
    }
  },
});

//全局前置守卫
// router.beforeEach(async (to, from,next) => {
//   if (to.name !== 'Login' && !isAuthenticated) next({ name: 'Login' })
//   // 如果用户未能验证身份,则 `next` 会被调用两次
//   else  next()
// })

// 全局后置钩子
router.afterEach((to, from) => {
  const toDepth = to.path.split("/").length;
  const fromDepth = from.path.split("/").length;
  to.meta.transition = toDepth < fromDepth ? "slide-right" : "slide-left";
});

export default router;

复制代码
// views/Home.vue
<template>
  <div>HOME</div>
</template>

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

// views/Demo.vue
<template>
  <div>DEMO</div>
</template>

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

// App.vue
<script setup lang="ts"></script>

<template>
  <RouterView />
</template>

<style scoped>
  #app {
    width: 100vw;
    height: 100vh;
  }
</style>
复制代码

在 vue 中使用路由。 main.ts

import { createApp } from "vue";
import "./style.css";
import App from "./App.vue";
import router from "./router";

const app = createApp(App);

app.use(router);

app.mount("#app");

复制代码

集成 Pinia

安装 Pinia

pnpm i pinia
复制代码

使用 Pinia

与 router 一样,先在 src 目录下创建 store 文件夹,来存放相关的数据状态文件。 目录结构

│  ├─src
│  │  ├─store # 数据状态文件目录
│  │  │    │  index.ts
│  │  │    ├─ modules
│  │  │    │     │   user.ts

复制代码
// 创建pinia实例
// store/index.ts
import { createPinia } from "pinia";

const store = createPinia();

export default store;

// 定义user数据
// store/modules/user.ts
import { defineStore } from 'pinia'

// 第一个参数是应用程序中 store 的唯一 id
const useUserStore = defineStore('user', {
    state: () => {
        return {
            name: 'hps',
        }
    },
    // other options...
})

export default useUserStore

// 在vue中使用pinia
// main.ts

import store from "./store";

app.use(store);

// 使用user数据
// App.vue
<script setup lang="ts">
import useUserStore from "@/store/modules/user";

const userStore = useUserStore();
</script>

<template>
  <div>{{ userStore.name }}</div>
  <RouterView />
</template>

<style scoped>
#app {
  width: 100vw;
  height: 100vh;
}
</style>

复制代码

集成vueuse

安装vueuse

npm i @vueuse/core
复制代码

使用vueuse

// 直接使用即可
<script setup lang="ts">
import { useMouse, useCounter } from '@vueuse/core'

const { x, y } = useMouse()
const { count, inc, dec } = useCounter()
</script>
复制代码

集成axios

安装axios

npm i axios
复制代码

使用axios

在 src 目录下创建 api 文件夹,来存放相关的数据状态文件。 目录结构

│ ├─src 
│ │ ├─api # api文件目录 
│ │ │ │ api.js      //api接口
│ │ │ │ axios.js   //axios封装
│ │ │ │ status.js //状态管理
│ │ ├─views # 视图文件目录
复制代码
// api,js
import { request } from './axios'
export class Logins {       // 登录模块
    static async register(params) {  //注册
        return request('/api/users/register',params, 'post')
    }
    static async login(params) {  //登录
        return request('/api/users/login',params, 'post')
    }
    static async changePassword(params) {  //修改密码
        return request('/api/users/',params, 'patch')
    }
    static async wxLogin(params) {  //微信扫码登录
        return request('/api/wxLogin/tempUserId',params, 'get')
    }
}

//在Demo.vue 使用
<script setup>
import { Logins } from '@/api/api'
let { login, } = Logins

const login = async () => {
   const pram = {}
   const { data: res } = await login(pram)
}
</script>
复制代码
//axios.js
import axios from "axios";
import { showMessage } from "./status"; // 引入状态码文件
import { ElMessage } from "element-plus"; // 引入el 提示框,这个项目里用什么组件库这里引什么
import tokenStore from "@/store/token.js";

// 设置接口超时时间
axios.defaults.timeout = 60000;

// 请求地址,这里是动态赋值的的环境变量,下一篇会细讲,这里跳过
// @ts-ignore
axios.defaults.baseURL = "";

//http request 拦截器
axios.interceptors.request.use(
  (config) => {
    // 配置请求头
    let ContentType = "application/json;charset=UTF-8";
    if (config.url == "/api/goods/upload") {
      ContentType = "multipart/form-data"; //图片上传
    }
    config.headers = {
      "Content-Type": ContentType, // 传参方式json
      Authorization: `Bearer ${tokenStore().token}`, // 这里自定义配置,这里传的是token
    };
    return config;
  },
  (error) => {
    console.log("error");
    return Promise.reject(error);
  }
);

// 定义一个变量,用于标记 token 刷新的状态
let isRefreshing = false;
let refreshSubscribers = [];
//刷新token
function refreshToken() {
  // instance是当前request.js中已创建的axios实例
  return axios.post("/refreshtoken").then((res) => res.data);
}

//http response 拦截器
axios.interceptors.response.use(
  (response) => {
    // 对响应数据做一些处理
    return response;
  },
  (error) => {
    const { response } = error;
    const originalRequest = error.config;
    if (response) {
      if (response.status === 401) {
        if (!isRefreshing) {
          isRefreshing = true;
          // 发起刷新 token 的请求
          return refreshToken()
            .then((res) => {
              const newToken = res.data.token;
              tokenStore().token = newToken;
              // 刷新 token 完成后,重新发送之前失败的请求
              refreshSubscribers.forEach((subscriber) => subscriber(newToken));
              refreshSubscribers = [];

              return axios(originalRequest);
            })
            .catch((res) => {
              console.error("refreshtoken error =>", res);
              //去登录页
            })
            .finally(() => {
              isRefreshing = false;
            });
        } else {
          // 正在刷新 token,将当前请求加入队列,等待刷新完成后再重新发送
          return new Promise((resolve) => {
            refreshSubscribers.push((newToken) => {
              originalRequest.headers.Authorization = `Bearer ${newToken}`;
              resolve(axios(originalRequest));
            });
          });
        }
      }
      let show = showMessage(response.status); // 传入响应码,匹配响应码对应信息
      ElMessage.warning(response.data.message || show);
      return Promise.reject(response.data);
    } else {
      ElMessage.warning("网络连接异常,请稍后再试!");
    }
  }
);

// 封装 GET POST 请求并导出
export function request(url = "", params = {}, type = "POST") {
  //设置 url params type 的默认值
  return new Promise((resolve, reject) => {
    let promise;
    if (type.toUpperCase() === "GET") {
      promise = axios({
        url,
        params,
      });
    } else if (type.toUpperCase() === "POST") {
      promise = axios({
        method: "POST",
        url,
        data: params,
      });
    } else if (type.toUpperCase() === "PUT") {
      promise = axios({
        method: "PUT",
        url,
        data: params,
      });
    } else if (type.toUpperCase() === "DELETE") {
      promise = axios({
        method: "delete",
        url,
        params,
      });
    } else if (type.toUpperCase() === "PATCH") {
      promise = axios({
        method: "patch",
        url,
        params,
      });
    }
    //处理返回
    promise
      .then((res) => {
        resolve(res);
      })
      .catch((err) => {
        reject(err);
      });
  });
}
复制代码
//status.js
export const showMessage = (status)  => {
    let message = "";
    switch (status) {
        case 400:
            message = "请求错误(400)";
            break;
        case 401:
            message = "未授权,请重新登录(401)";
            break;
        case 403:
            message = "拒绝访问(403)";
            break;
        case 404:
            message = "请求出错(404)";
            break;
        case 408:
            message = "请求超时(408)";
            break;
        case 500:
            message = "服务器错误(500)";
            break;
        case 501:
            message = "服务未实现(501)";
            break;
        case 502:
            message = "网络错误(502)";
            break;
        case 503:
            message = "服务不可用(503)";
            break;
        case 504:
            message = "网络超时(504)";
            break;
        case 505:
            message = "HTTP版本不受支持(505)";
            break;
        default:
            message = `连接出错(${status})!`;
    }
    return `${message},请检查网络或联系管理员!`;
};
复制代码

集成echarts

安装echarts

npm i echarts
复制代码

使用echarts

全局引用

在main.js中

import * as echarts from 'echarts';

// 持久化插件
pinia.use(piniaPluginPersistedstate);
// 创建app
const app = createApp(App)
// 注入
app.config.globalProperties.$echarts = echarts; // 全局挂载echarts

// 挂载实例
app.mount('#app')

在Dome.vue中 利用getCurrentInstance()获取

<template>
  <div class="right-content">
    <div ref="Chart" style="width: 800px; height: 500px"></div>
  </div>
</template>

<script setup lang="ts">
import { onMounted, getCurrentInstance, ref } from 'vue'
let internalInstance = getCurrentInstance(); //获取当前实例
let echarts = internalInstance.appContext.config.globalProperties.$echarts; //获取echarts实例


//通过ref获取html元素
const Chart = ref();

const init = () => {
  // 渲染echarts的父元素
  var infoEl = Chart.value;

  //  light dark
  var myChart = echarts.init(infoEl, "light"); //初始化echarts实例

  // 指定图表的配置项和数据 树图
  var option = {
    title: {
      text: 'ECharts 入门示例'
    },
    tooltip: {},
    xAxis: {
      data: ['衬衫', '羊毛衫', '雪纺衫', '裤子', '高跟鞋', '袜子']
    },
    yAxis: {},
    series: [
      {
        name: '销量',
        type: 'bar',
        data: [5, 20, 36, 10, 10, 20]
      }
    ]
  }

  // 使用刚指定的配置项和数据显示图表。
  myChart.setOption(option);
  window.onresize = function () {
    myChart.resize()
  }
}

onMounted(() => {
  init()
});

</script>

<style scope lang="scss"></style>

通过provide提供echarts

在App.vue导入echarts,

//App,vue
<script setup>
import { onMounted, ref ,provide} from 'vue'
import * as echarts from "echarts";
//通过provide提供echarts
provide("echarts", echarts);
</script>
复制代码

在Dome.vue使用rcharts

<script setup>
import { onMounted, ref, inject } from "vue";

//通过inject使用echarts
const echarts = inject("echarts");

//通过ref获取html元素
const info = ref();

const init = () => {
    // 渲染echarts的父元素
    var infoEl = info.value;

    //  light dark
    var userEc = echarts.init(infoEl, "light");

    // 指定图表的配置项和数据 树图
    var option = {
        tooltip: {
            trigger: 'item',
            triggerOn: 'mousemove'
        },
        series: [
            {
                type: 'pie', // 饼图
                data: [1, 2, 3, 4, 5, 6, 7], // 数据
                top: '1%',
                left: '10%',
                bottom: '1%',
                right: '20%',
                symbolSize: 7,
                // symbol: 'emptyCircle',
                // orient: 'vertical',
                // expandAndCollapse: true,
                label: {
                    position: 'left',
                    // position: 'top',
                    // rotate: -90,
                    verticalAlign: 'middle',
                    align: 'right',
                    fontSize: 16

                },
                leaves: {
                    label: {
                        position: 'right',
                        // position: 'bottom',
                        // rotate: -90,
                        verticalAlign: 'middle',
                        align: 'left'
                    }
                },
                emphasis: {
                    focus: 'descendant'
                },
                expandAndCollapse: true,
                animationDuration: 550,
                animationDurationUpdate: 750
            }
        ]
    };

    // 使用刚指定的配置项和数据显示图表。
    userEc.setOption(option);
    window.onresize = function () {
        userEc.resize()
    }
}

onMounted(() => {
    init()
});

</script>

<template>
    <div>
        <!--  通过ref获取html元素 宽高必须设置 -->
        <div ref="info" style="width: 800px; height: 500px"></div>
    </div>
</template>
复制代码

国际化

安装vue-i18n

npm i vue-i18n
复制代码

创建语言包文件

src 目录下新建 langurage 目录,在其中新建 zh.js 和 en.js 文件,

│  ├─src
│  │  ├─langurage # 语言文件目录
│  │  │    │  zh.js // 中文
│  │  │    │  en.js // 英文
│  │  │    │  index.js
│  │  ├─views # 视图文件目录
复制代码

导出语言包对象

// zh.js 创建中文语言包对象
export default{
    table: {
      username: '用户名',
      email: '邮箱',
      mobile: '手机'
    },
    menus: {
      home: '首页',
      download: '下载'
    },
    tabs: {
      info: '商品描述',
      comment: '评价'
    }
  }

复制代码
// en.js 创建英文语言包对象
export default {
    table: {
        username: 'Username',
        email: 'Email',
        mobile: 'Mobile'
    },
    menus: {
        home: 'Home',
        download: 'DownLoad'
    },
    tabs: {
        info: 'Describe',
        comment: 'Comment On'
    }
}

复制代码

langurage 目录中新建 index.js,代码如下

//index.js
import {createI18n}  from 'vue-i18n'
// 从语言包文件中导入语言包对象
import zh from './zh'
import en from './en'

const messages = {
    'zh-cn': zh,
    'en-us': en
}

const language = (navigator.language || 'en').toLocaleLowerCase() // 这是获取浏览器的语言
// 获取浏览器当前使用的语言,并进行处理 
const i18n = createI18n({ 
    legacy: false,
    locale: localStorage.getItem('lang') || language.split('-')[0] || 'en', // 首先从缓存里拿,没有的话就用浏览器语言,
    fallbackLocale: 'zh-cn', // 设置备用语言
    messages,
})

export default i18n
复制代码

在main.js导入

import Vue from 'vue'
import App from './App.vue'
import i18n from './langurage'

// 注入
app.use(i18n)

// 挂载实例
app.mount('#app')
复制代码

点击按钮切换

// App.vue
<template>
    <div>
        <p>{{ $t('table.username') }}</p>
    </div>
    <button @click="translate('zh-cn')">切换为中文</button>
    <button @click="translate('en-us')">切换为英文</button>
</template>

<script setup>
// 国际化 
import { useI18n } from 'vue-i18n' 
const I18n = useI18n() 
const { locale } = useI18n() 
// 切换语言更改locale.value的值即可但要跟你index.js中message设置的值一致!

const translate = (lang) => {
  locale.value = lang
  localStorage.setItem('lang', lang)
}

</script>

复制代码

env变量

在src同级目录建立文件

image.png

# 开发.env.development
VITE_MODE_NAME=development

VITE_RES_URL=http://127.0.0.1:8000

复制代码
# 生产.env.production
VITE_MODE_NAME=production

VITE_RES_URL=http://119.188.247.121:8000
复制代码
# 测试环境.env.test
VITE_MODE_NAME=test

VITE_RES_URL=https://www.baidu.com
复制代码

package.json添加

  "scripts": {
    "dev": "vite --mode development",
    "prod": "vite --mode production",
    "test": "vite --mode test",
    "build": "vite build --mode development",
    "build:prod": "vite build --mode production",
    "build:test": "vite build --mode test",
    "preview": "vite preview"
  },
复制代码

vite.config.js 使用

import { defineConfig, loadEnv } from "vite";

export default defineConfig(({ command, mode }) => {
  // 根据当前工作目录中的 `mode` 加载 .env 文件
  // 设置第三个参数为 '' 来加载所有环境变量,而不管是否有 `VITE_` 前缀。
  const env = loadEnv(mode, process.cwd(), "");
  console.log('proxy.target',env.VITE_RES_URL);
});

更换主题颜色

在src下创建Theme文件夹,在Theme下创建index.scss

// index.scss
:root[theme-mode='light'] {
    --bg-color:  #fff;
    --text-color: #000
}

:root[theme-mode='dark'] {
    --bg-color: #2c2c2c;
    --text-color: #fff
}

:root[theme-mode='red'] {
    --bg-color: rgb(0, 128, 255);
    --text-color: red;
}

在样式文件中使用scss变量

// style.css
:root {
  color: var(--text-color);
  background-color: var(--bg-color);
}

在main.js导入

import './style.css'
import './theme/index.scss' 
import { createApp } from 'vue'

利用setAttribute改变主题,我是在App.vue中使用的

//App.vue 
<script>
const type = ref('light')
const onChange = (e) => {
  document.documentElement.setAttribute('theme-mode', type.value)
}
</script>


<template> 
      <el-select style="width: 80px;margin: 10px;" v-model="type" @change="onChange">
        <el-option label="light" value="light" />
        <el-option label="dark" value="dark" />
        <el-option label="red" value="red" />
      </el-select>
</template>