目录
一、技术栈
- 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.xnpm init vite@latest my-vue-app --template vue-ts# npm 7+, 需要额外的双横线npm init vite@latest my-vue-app -- --template vue-ts# yarnyarn create vite my-vue-app --template vue-ts# pnpmpnpm create vite my-vue-app -- --template vue-ts
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
# 全局安装pnpmnpm i pnpm -g
- 1
- 2
2、引入element-plus
# -D安装到开发环境 -S安装到生产环境pnpm i element-plus -D
- 1
- 2
全局引入:main.ts
import { createApp } from 'vue'import App from './App.vue'// 引入element-plusimport element from 'element-plus'import 'element-plus/dist/index.css' // 不引入会导致ui样式不正常createApp(App).use(element).mount('#app')
- 1
- 2
- 3
- 4
- 5
- 6
- 7
3、引入vue-router
pnpm i vue-router@latest -D
- 1
配置别名:.config.ts
# 使用require需要安装@types/nodenpm i @types/node -D
- 1
- 2
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/, ''), }, } : {} }})
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
添加主路由文件:/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;
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
全局文件:/src/config/constant.ts
// 没有的vue文件自行创建引入即可export const Home = () => import('@/layout/index.vue')export const Login = () => import('@/views/login/Login.vue')
- 1
- 2
- 3
全局引入:main.ts
import { createApp } from 'vue'import App from './App.vue'import element from 'element-plus'import 'element-plus/dist/index.css'// 添加routerimport router from './router/index'// 全局引用createApp(App).use(element).use(router).mount('#app')
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
在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>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
4、引入axios
pnpm i axios -D
- 1
请求函数封装:/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// 请求超时时间60saxios.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 = falseaxios.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
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
- 106
- 107
- 108
- 109
- 110
- 111
- 112
- 113
- 114
- 115
- 116
- 117
- 118
- 119
- 120
- 121
- 122
- 123
- 124
- 125
- 126
- 127
- 128
- 129
添加全局配置文件:/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 }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
添加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 }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
添加url路径文件(根据后台接口定):/src/config/url.ts
// 用户urlconst userBaseUrl = '/user'export const userUrl = { add: userBaseUrl + '/add', get: userBaseUrl + '', edit: userBaseUrl + '/edit', delete: userBaseUrl + '/delete' }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
使用案例:/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>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
5、引入
pnpm i pinia -D
- 1
全局引入: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')
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
状态管理案例:/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 } }})
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
使用案例:/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>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
引入持久化插件:pinia-plugin-persist
pnpm i pinia-plugin-persist -D
- 1
在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')
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
编写persist配置文件piniaPersist.ts
export const piniaPluginPersist = (key: any) => { return { enabled: true, // 开启持久化存储 strategies: [ { // 修改存储中使用的键名称,默认为当前 Store的id key: key, // 修改为 sessionStorage,默认为 localStorage storage: localStorage, // []意味着没有状态被持久化(默认为undefined,持久化整个状态) // paths: [], } ] }}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
使用案例
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')})
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
三、运行与打包
运行命令
pnpm run dev
- 1
打包命令(环境自选)
pnpm run build:dev
- 1
配置不同的打包环境: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" }}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
由于使用到了vite作为打包工具,在实际使用过程中遇到了问题。webpack打包可以直接指定打包成zip或者其他格式的压缩包,但是在vite中是没有这个配置的,那么遇到流水线部署的时候我们应该怎么办呢?
方法:利用node插件compressing
引入compressing
pnpm i compressing -D
- 1
根目录创建: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); });
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
package.json中配置script命令
"build:dev": "vue-tsc --noEmit && vite build && node ./zip.js","build:prod": "vue-tsc --noEmit && vite build && node ./zip.js",
- 1
- 2
输入命令打包
pnpm run build:dev
- 1
命令执行完后在zip文件夹会生成dist.zip的压缩包