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同级目录建立文件
# 开发.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>