一、技术栈

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

四、参考

https://juejin.cn/post/7080857426123030559

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐