一、技术栈
- vue3:组件封装和拆分比Vue2更加细化和合理。
- typescript:比js更加严格的类型检查,能够在编译期就能发现错误。
- vite:下一代前端开发和构建工具。
- element plus:ui组件库,比较热门的vue组件库之一。
- axios:基于promise的网络请求库。
- vue-router:路由控制。
- pinia:状态管理类库,比vuex更小,对ts的支持更友好。
- volar插件:代码补全和检测工具,可以尝试替换vetur,如果不替换的话,用ts的语法糖的时候会出现找不到默认的default的错误。
- pnpm:比npm和yarn更强大的包管理工具,包安装速度极快,磁盘空间利用效率高。
二、搭建过程
1、创建项目
# npm 6.x
npm init vite@latest my-vue-app --template vue-ts
# npm 7+, 需要额外的双横线
npm init vite@latest my-vue-app -- --template vue-ts
# yarn
yarn create vite my-vue-app --template vue-ts
# pnpm
pnpm create vite my-vue-app -- --template vue-ts
# 全局安装pnpm
npm i pnpm -g
2、引入element-plus
# -D安装到开发环境 -S安装到生产环境
pnpm i element-plus -D
全局引入:main.ts
import { createApp } from 'vue'
import App from './App.vue'
// 引入element-plus
import element from 'element-plus'
import 'element-plus/dist/index.css' // 不引入会导致ui样式不正常
createApp(App).use(element).mount('#app')
3、引入vue-router
pnpm i vue-router@latest -D
配置别名:vite.config.ts
# 使用require需要安装@types/node
npm i @types/node -D
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import * as path from 'path'
import { settings } from './src/config/index'
export default defineConfig({
plugins: [vue()],
base: settings.base, // 生产环境路径
resolve: {
alias: { // 配置别名
'@': path.resolve(__dirname, 'src'),
'assets': path.resolve(__dirname, 'src/assets'),
'components': path.resolve(__dirname, 'src/components'),
'config': path.resolve(__dirname, 'src/config'),
'router': path.resolve(__dirname, 'src/router'),
'tools': path.resolve(__dirname, 'src/tools'),
'views': path.resolve(__dirname, 'src/views'),
'plugins': path.resolve(__dirname, 'src/plugins'),
'store': path.resolve(__dirname, 'src/store'),
}
},
build: {
target: 'modules',
outDir: 'dist', // 指定输出路径
assetsDir: 'static', // 指定生成静态资源的存放路径
minify: 'terser', // 混淆器,terser构建后文件体积更小
sourcemap: false, // 输出.map文件
terserOptions: {
compress: {
drop_console: true, // 生产环境移除console
drop_debugger: true // 生产环境移除debugger
}
},
},
server: {
// 是否主动唤醒浏览器
open: true,
// 占用端口
port: settings.port,
// 是否使用https请求
https: settings.https,
// 扩展访问端口
// host: settings.host,
proxy: settings.proxyFlag ? {
'/api': {
target: 'http://127.0.0.1:8080', // 后台接口
changeOrigin: true, // 是否允许跨域
// secure: false, // 如果是https接口,需要配置这个参数
rewrite: (path: any) => path.replace(/^\/api/, ''),
},
} : {}
}
})
添加主路由文件:/src/router/index.ts
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'
import { Home } from '../config/constant';
const routes: Array<RouteRecordRaw> = [
{
path: '',
name: 'index',
redirect: '/home',
},
{
path: '/home',
name: 'home',
component: Home,
meta: {
title: '首页'
}
},
]
const router = createRouter({
history: createWebHistory(),
routes
})
export default router;
全局路由懒加载文件:/src/config/constant.ts
// 没有的vue文件自行创建引入即可
export const Home = () => import('@/layout/index.vue')
export const Login = () => import('@/views/login/Login.vue')
全局引入:main.ts
import { createApp } from 'vue'
import App from './App.vue'
import element from 'element-plus'
import 'element-plus/dist/index.css'
// 添加router
import router from './router/index'
// 全局引用
createApp(App).use(element).use(router).mount('#app')
在App.vue添加路由渲染
<script setup lang="ts">
</script>
<template>
<!-- router组件渲染的地方 -->
<router-view></router-view>
</template>
<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>
4、引入axios
pnpm i axios -D
请求函数封装:/src/plugins/request.ts
import axios from 'axios'
import cookieService from 'tools/cookie'
import { ElMessage } from 'element-plus'
import { settings } from 'config/index'
axios.defaults.withCredentials = true
// 请求超时时间60s
axios.defaults.timeout = 1 * 60 * 1000
// get请求头
axios.defaults.headers.get['Content-Type'] = 'application/json'
// post请求头
axios.defaults.headers.post['Content-Type'] = 'application/json'
// 根请求路径
axios.defaults.baseURL = settings.baseUrl
// 请求拦截器
axios.interceptors.request.use(
config => {
// 每次发送请求之前判断是否存在token,如果存在,则统一在http请求的header都加上token,不用每次请求都手动添加了
// 即使本地存在token,也有可能token是过期的,所以在响应拦截器中要对返回状态进行判断
// 增加接口时间戳
config.params = { _t: 1000, ...config.params }
config.headers = { 'x-csrf-token': "xxx" }
return config
},
error => {
return Promise.reject(error)
}
)
// 响应拦截器
let timer: any = false
axios.interceptors.response.use(
response => {
cookieService.set('xxx', response.headers['csrftoken'])
if (response.status === 200) {
return Promise.resolve(response)
} else {
return Promise.reject(response)
}
},
error => {
if (error.response && error.response.status) {
const path = window.location.href
switch (error.response.status) {
case 302:
window.location.href =
'' + path
break
case 401:
window.location.href =
'' + path
break
case 403:
// 清除token
if (!timer) {
timer = setTimeout(() => {
ElMessage({
message: '登录信息已过期,请重新登录!',
type: 'error',
})
setTimeout(() => {
window.location.href = 'xxx' + path
cookieService.set('loginCookie', false, 1)
}, 2000)
}, 0)
}
break
// 404请求不存在
case 404:
ElMessage({
message: '请求不存在',
type: 'error',
})
break
case 500:
ElMessage({
message: error.response.statusText,
type: 'error',
})
break
default:
ElMessage({
message: error.response.data.message,
type: 'error',
})
}
return Promise.reject(error.response)
}
}
)
/**
* get方法,对应get请求
* @param {String} url [请求的url地址]
* @param {Object} params [请求时携带的参数]
*/
export function get(url: string, params: any) {
return new Promise((resolve, reject) => {
axios
.get(url, { params: params })
.then(res => {
resolve(res.data)
})
.catch(err => {
reject(err.data)
})
})
}
/**
* post方法,对应post请求
* @param {String} url [请求的url地址]
* @param {Object} params [请求时携带的参数]
*/
export function post(url: string, params: any) {
return new Promise((resolve, reject) => {
axios
.post(url, params)
.then(res => {
resolve(res.data)
})
.catch(err => {
reject(err.data)
})
})
}
export default axios
添加全局配置文件:/src/config/index.ts
const BASE_URL = process.env.NODE_ENV === 'development' ? '/api' : 'http://localhost:8080'
const settings = {
// 请求根路径
baseUrl: BASE_URL,
// 是否开启代理,本地需要开,线上环境关闭
proxyFlag: true,
// 端口
port: 8081,
// 是否开启https
https: false,
// 扩展端口
// host: 'localhost',
// 公共路径
base: './'
}
export { settings }
添加api请求文件:/src/config/api.ts
import { get, post } from 'plugins/request'
// 用户请求
const user = () => {
const getUser = (url: string, params: any) => {
return get(url, params)
}
return {
getUser
}
}
// 权限请求
const permission = () => {
const login = (url: string, params: any) => {
return get(url, params)
}
return {
login
}
}
const userService = user()
const permissionService = permission()
export { userService, permissionService }
添加url路径文件(根据后台接口定):/src/config/url.ts
// 用户url
const userBaseUrl = '/user'
export const userUrl = {
add: userBaseUrl + '/add',
get: userBaseUrl + '',
edit: userBaseUrl + '/edit',
delete: userBaseUrl + '/delete'
}
使用案例:/src/views/Home.vue
<template>
<div>
{{ state.userName }}
</div>
</template>
<script lang='ts' setup>
import { reactive } from 'vue';
import { userService } from 'config/api';
import { userUrl } from 'config/url';
const state = reactive({
userName: ''
})
getUser()
function getUser() {
userService.getUser(userUrl.get, '').then((resp: any) => {
console.log(resp)
state.userName = resp.data;
})
}
</script>
<style scoped>
</style>
5、引入pinia
pnpm i pinia -D
全局引入:main.ts
import { createApp } from 'vue'
import App from './App.vue'
import element from 'element-plus'
import 'element-plus/dist/index.css'
import router from '@/router'
import { createPinia } from 'pinia'
const pinia = createPinia()
createApp(App).use(element).use(router).use(pinia).mount('#app')
状态管理案例:/src/store/index.ts
import { defineStore } from 'pinia'
/*
* 传入2个参数,定义仓库并导出
* 第一个参数唯一不可重复,字符串类型,作为仓库ID以区分仓库
* 第二个参数,以对象形式配置仓库的state、getters、actions
* 配置 state getters actions
*/
export const mainStore = defineStore('main', {
/*
* 类似于组件的data数据,用来存储全局状态的
* 1、必须是箭头函数
*/
state: () => {
return {
msg: 'hello world!',
counter: 0
}
},
/*
* 类似于组件的计算属性computed的get方法,有缓存的功能
* 不同的是,这里的getters是一个函数,不是一个对象
*/
getters: {
count10(state) {
console.log('count10被调用了')
return state.counter + 10
}
},
/*
* 类似于组件的methods的方法,用来操作state的
* 封装处理数据的函数(业务逻辑):初始化数据、修改数据
*/
actions: {
updateCounter(value: number) {
console.log('updateCounter被调用了')
this.counter = value * 1000
}
}
})
使用案例:/src/views/Home.vue
<template>
<div>
{{ state.userName }}
</div>
<el-button @click="handleClick">增加</el-button>
<div>
{{ counter }}
</div>
</template>
<script lang='ts' setup>
import { reactive } from 'vue';
import { userService } from 'config/api';
import { userUrl } from 'config/url';
// 定义一个状态对象
import { mainStore } from 'store/index';
import { storeToRefs } from 'pinia';
// 创建一个该组件的状态对象
const state = reactive({
userName: ''
})
// 实例化一个状态对象
const store = mainStore();
// 解构并使数据具有响应式
const { counter } = storeToRefs(store);
getUser()
function getUser() {
userService.getUser(userUrl.get, '').then((resp: any) => {
console.log(resp)
state.userName = resp.data;
})
}
function handleClick() {
counter.value++;
store.updateCounter(counter.value)
}
</script>
<style scoped>
</style>
引入持久化插件:pinia-plugin-persist
pnpm i pinia-plugin-persist -D
在main.ts全局引入
import { createApp } from 'vue'
import App from './App.vue'
import element from 'element-plus'
import 'element-plus/dist/index.css'
import router from '@/router'
import { createPinia } from 'pinia'
import piniaPluginPersist from 'pinia-plugin-persist'
const pinia = createPinia()
pinia.use(piniaPluginPersist)
createApp(App).use(element).use(router).use(pinia).mount('#app')
编写persist配置文件piniaPersist.ts
export const piniaPluginPersist = (key: any) => {
return {
enabled: true, // 开启持久化存储
strategies: [
{
// 修改存储中使用的键名称,默认为当前 Store的id
key: key,
// 修改为 sessionStorage,默认为 localStorage
storage: localStorage,
// []意味着没有状态被持久化(默认为undefined,持久化整个状态)
// paths: [],
}
]
}
}
使用案例
import { defineStore } from 'pinia'
import { piniaPluginPersist } from 'plugins/piniaPersist'
/*
* 传入2个参数,定义仓库并导出
* 第一个参数唯一不可重复,字符串类型,作为仓库ID以区分仓库
* 第二个参数,以对象形式配置仓库的state、getters、actions
* 配置 state getters actions
*/
export const mainStore = defineStore('mainStore', {
/*
* 类似于组件的data,用来存储全局状态的
* 1、必须是箭头函数
*/
state: () => {
return {
msg: 'hello world!',
counter: 0
}
},
/*
* 类似于组件的计算属性computed,有缓存的功能
* 不同的是,这里的getters是一个函数,不是一个对象
*/
getters: {
count10(state) {
console.log('count10被调用了')
return state.counter + 10
}
},
/*
* 类似于组件的methods,用来操作state的
* 封装处理数据的函数(业务逻辑):同步异步请求,更新数据
*/
actions: {
updateCounter(value: number) {
console.log('updateCounter被调用了')
this.counter = value * 1000
}
},
/*
* 持久化,可选用localStorage或者sessionStorage
*
*/
persist: piniaPluginPersist('mainStore')
})
三、运行与打包
运行命令
pnpm run dev
打包命令(环境自选)
pnpm run build:dev
配置不同的打包环境:package.json
{
"name": "vite-study",
"private": true,
"version": "0.0.0",
"scripts": {
"dev": "vite",
"build": "vue-tsc --noEmit && vite build",
"build:dev": "vue-tsc --noEmit && vite build", // 开发环境
"build:prod": "vue-tsc --noEmit && vite build", // 生产环境
"preview": "vite preview"
},
"dependencies": {
"vue": "^3.2.37"
},
"devDependencies": {
"@types/node": "^18.0.0",
"@vitejs/plugin-vue": "^2.3.3",
"axios": "^0.27.2",
"element-plus": "^2.2.6",
"pinia": "^2.0.14",
"typescript": "^4.7.4",
"vite": "^2.9.12",
"vue-router": "^4.0.16",
"vue-tsc": "^0.34.17"
}
}
由于使用到了vite作为打包工具,在实际使用过程中遇到了问题。webpack打包可以直接指定打包成zip或者其他格式的压缩包,但是在vite中是没有这个配置的,那么遇到流水线部署的时候我们应该怎么办呢?
方法:利用node插件compressing
引入compressing
pnpm i compressing -D
根目录创建:zip.js
const path = require("path");
const { resolve } = require("path");
const fs = require("fs");
const compressing = require("compressing");
const zipPath = resolve("zip");
const zipName = (() => `zip/dist.zip`)();
// 判断是否存在当前zip路径,没有就新增
if (!fs.existsSync(zipPath)) {
fs.mkdirSync(zipPath);
}
// 清空zip目录
const zipDirs = fs.readdirSync("./zip");
if (zipDirs && zipDirs.length > 0) {
for (let index = 0; index < zipDirs.length; index++) {
const dir = zipDirs[index];
const dirPath = resolve(__dirname, "zip/" + dir)
console.log("del ===", dirPath);
fs.unlinkSync(dirPath)
}
}
// 文件压缩
compressing.zip
.compressDir(resolve("dist/"), resolve(zipName))
.then(() => {
console.log(`Tip: 文件压缩成功,已压缩至【${resolve(zipName)}】`);
})
.catch(err => {
console.log("Tip: 压缩报错");
console.error(err);
});
package.json中配置script命令
"build:dev": "vue-tsc --noEmit && vite build && node ./zip.js",
"build:prod": "vue-tsc --noEmit && vite build && node ./zip.js",
输入命令打包
pnpm run build:dev
命令执行完后在zip文件夹会生成dist.zip的压缩包