当前位置:   article > 正文

vue3+ts+vite+element plus+axios+pinia框架搭建_vue3 vite elementplus 框架

vue3 vite elementplus 框架

一、技术栈

  • 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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
# 全局安装pnpm
npm 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-plus
import 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

配置别名:vite.config.ts

# 使用require需要安装@types/node
npm 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'
// 添加router
import 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
// 请求超时时间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
  • 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

// 用户url
const 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、引入pinia

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的压缩包

四、参考

https://juejin.cn/post/7080857426123030559

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/凡人多烦事01/article/detail/225651
推荐阅读
相关标签
  

闽ICP备14008679号