Vue3+Vite系统基础模板(很基础,不喜勿喷)

2,307 阅读7分钟

日常前言

好久没有写文章了,其实Vue3/Vite的文章已经很多了(很多大佬都有完整的项目),这里只是随笔记录一下我自己搭建最最最基础的系统模板(就几个基础页面)以及搭建过程踩的几个坑。(不喜勿喷哈)

前置条件

  • Node版本需要大于12.0.0(官方说的,最好去更新一下,不要问为什么,我在10.X.X版本的时候,vite的一些配置一直报错,更新后就不会了,window我尝试命令行更新后失败了,所以我选择直接去Nodejs官网下载最新的稳定版本,然后直接傻瓜式下一步就可以,node会帮我们设置好环境变量的)
  • Vscode(好像有点废话了,但有些同学可能是用IDEA的)

用到的插件

  • sass/sass-loader(在配置里面可以不用配置,因为Vite已经集成了,但是依赖还是要安装的)

  • element-plus/vite-plugin-style-import(饿了么组件库Vue3.0版本以及对应的style处理)

  • vue-router(4.0以上的版本,我用的是4.0.6)/vuex(4.0以上的版本,我用的是4.0.0)(路由管理与状态管理)

  • axios(http请求)

  • crypto-js/encryptlong(这个是我自己用来测试加解密的,不想用可以不用)

搭建开始

安装/启动项目

让我们先看看看官方说的(都是以npm为例,yarn其实同理)

npm init @vitejs/app

等等,不要急,vite很友好,它还提供了一些模板给我们选择(具体有哪些模板请移步vite官网查看)

# npm 6.x
npm init @vitejs/app my-vue-app --template vue
# npm 7+, 需要额外的双横线:
npm init @vitejs/app my-vue-app -- --template vue

这里我们选择npm7+的,在vscode控制台执行命令

npm init @vitejs/app vue3-vite-test -- --template vue

然后会出现像我们初始化vue项目那样的选项,我们选

第一个选择vue
第二个选择javascript(当然你可以选择ts)

然后进入对应文件就可以执行启动命令

npm install (先安装基础依赖)
npm run dev

怎么样,是不是速度很快,然后对应浏览器打开控制台的链接,然后你会发现打开链接之后,控制台输出了两行警示

image.png

这个不影响我们继续进行,不过在这里我们可以发现vite的一个特性-按需编译,因为我们是在浏览器打开链接之后才有提示,这就表明在没有打开对应页面时,是没有进行编译的。

依赖引入

安装一下我们本次需要用到的依赖,安装完成之后记得重启一下项目

npm install sass sass-loader -D -S
npm install element-plus vite-plugin-style-import -D -S
npm install vue-router@4 vuex@4 -D -S
npm install axios -D -S
npm install crypto-js encryptlong -D -S(这个可以不安装)

我们先来看看入口文件main.js

import { createApp } from 'vue'
import App from './App.vue'

createApp(App).mount('#app')

这是最初的样子,我们稍微改动一下,定义一个变量去接受vue实例

import { createApp } from 'vue'
import App from './App.vue'

const app = createApp(App)
app.mount('#app')

然后引入element-ui,并在app上挂载

import ElementPlus from 'element-plus';
import 'element-plus/lib/theme-chalk/index.css';

app.use(ElementPlus)
    .mount('#app')

稍微修改一下vite.config.js

import { defineConfig } from 'vite'
import { resolve } from 'path'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
    server: {
        // 代理,最重要,其他的都可以有默认配置
        proxy: {
            '/api': {
                target: 'http://localhost:8888/',
                changeOrigin: true,
            }
        },
        // 端口
        port: 8888,
        // 是否自动开启浏览器
        open: false
    },
    //起别名
    resolve: {
        alias: {
            '@': resolve(__dirname, 'src')
        }
    },
    plugins: [vue()]
})

封装vue-router

在src目录下新建一个router的文件夹,然后再router文件夹下新建一个module文件夹(用来存放不同模块的页面路由),具体如下图

image.png 然后在index.js中添加以下代码

// 引入对应创建函数
import { createRouter, createWebHashHistory } from 'vue-router'//需要4.0以上版本的

const routes = []
// import.meta.globEager是vite特有的写法,
// 意思是获取module文件夹下所有以.router.js结尾的文件(会递归遍历子文件夹),等同于下面的代码
// const context = require.context('../modules', true, /\.router.js$/)
const context = import.meta.globEager('./**/*.router.js')
Object.keys(context).forEach(key => {
    let _arr = context[key].default || []
    routes.push(..._arr)
})

const router = createRouter({
    history: createWebHashHistory(),
    routes
})
//导出路由
export default router

然后我们新建一些页面,并在module下对应文件夹创建对应路由文件 首先修改一下App.vue,东西都不要了,只要一个router-view

<template>
    <router-view></router-view>
</template>

创建两个页面,一个首页,一个文章页,页面自己随便写点东西,顺便放个router-link方便验证

image.png

两种写法 第一种(旧写法)

<template>
    <div>
        <el-result icon="info" title="我是文章首页">
        </el-result>
        <el-button @click="backToIndex">返回首页</el-button>
    </div>
</template>
<script>
export default{
    data(){
        return{}
    },
    methods:{
        backToIndex(){
            this.$router.push('/')
        }
    }
}
</script>

这里也说一下使用setup中router的写法吧 第二种(这种需要vue-router4.0以上版本的)

<template>
    <div>
        <el-result icon="info" title="我是文章首页">
        </el-result>
        <el-button @click="backToIndex">返回首页</el-button>
    </div>
</template>
<script setup>
 import { useRouter } from 'vue-router'
 const router = useRouter()
 let backToIndex = () => {
     router.push('/')
 }
</script>

然后router重点来了,在对应xx.router.js定义并导出 我下面的@符号需要在vite.config.js中进行配置才可以,不然必须把路径名称写全 例如我的module/base.router.js

import Home from '@/views/Index.vue'

export default [
    { path: '/', component: Home }
]

我的module/article/index.router.js

export default [
    { path: '/articleIndex', component: import('@/views/article/ArticleIndex.vue') }
]

最后在main.js中使用

import router from '@/router'
app.use(ElementPlus)
    .use(router)
    .mount('#app')

大概效果就是这样

2.gif

封装vuex

开始的流程和创建路由文件差不多,只是多了一个getters.js文件,这里直接贴创建后的目录

image.png

首先是index.js文件(其实和路由的封装差不多,看懂路由的话,这个也是同理的)

import { createStore } from 'vuex'
import getters from './getters'

let modules = {}
const context = import.meta.globEager('./**/*.vuex.js')
// const context = require.context('../modules', true, /\.vuex.js$/)
Object.keys(context).forEach(key => {
    // 对名称进行提取
    let name = key.match(/.\/(\S*)\/(\S*).vuex.js$/)
    modules[name[2]] = context[key].default
})
export default createStore({
    modules,
    getters
})

getters.js(这里定义一些我们经常需要用到的)

const getters = {
    userInfo: state => state.user.userInfo,
}

export default getters

然后是user.vuex.js

const user = {
    state: {
        userInfo: {
            name: 'cybtls'
        }
    },
    mutations: {
        UPDATA_USERINFO (state, data) {
            state.userInfo = data
        }
    },
    actions: {
        changeUserInfo (context, data) {
            context.commit('UPDATA_USERINFO', data)
        }
    }
}
export default user

在main.js也是同样操作

import store from '@/store/index'
app.use(ElementPlus)
    .use(router)
    .use(store)
    .mount('#app')

敲黑板....,这里是重点,要求全文背诵 写法同样有两种,和路由一样 第一种(旧写法)

<script>
export default {
    data () {
        return {}
    },
    mounted(){
        console.log(this.$store.getters.userInfo)
    }
}
</script>

第二种(需要vuex4.0以上)

<script setup>
import {useStore } from 'vuex'
const store = useStore()
console.log(store.getters.userInfo)
</script>

返回结果 image.png

封装axios

上mock

因为我没有在本地起服务器,所以找了个mock网站,在这个网站先自定义一下自己的请求,具体怎么操作这里就不说了

开始封装

同样创建对应文件

image.png

user.api.js

const userList = [
    {
        apiName: 'getUserInfo',
        url: '/user/getUserInfo',
        method: 'get'
    }
]
export default userList

index.js(这里还可以进行其他处理,暂时还没有写)

import axios from 'axios'
import env from '../config/config'
import { ElMessage } from 'element-plus'

const context = import.meta.globEager('./**/*.api.js')
// const context = require.context('../modules', true, /\.api.js$/)
let apiList = []
Object.keys(context).forEach(key => {
    apiList = apiList.concat(context[key].default)
})

class HttpInstance {
    _apiList = []

    constructor(apiList = []) {
        this._instance = axios.create({
            baseURL: env.baseUrl,
            withCredentials: true
        })
        // 请求拦截
        this._instance.interceptors.request.use(this._requestIntercept.bind(this), this._errorIntercept)
        // 响应拦截
        this._instance.interceptors.response.use(this._responseIntercept.bind(this), this._errorIntercept)

        // 一个个创建对应请求方法
        apiList.forEach(item => {
            let apiName = this._getApiName(item)
            this[apiName] = function (value = {}, options = {}) {
                return this._getApiFunction(value, options, item)
            }
        })
    }

    // 请求拦截封装
    _requestIntercept (config) {
        return config
    }

    // 响应拦截封装
    _responseIntercept (response) {
        if (response.data.code === 200) {
            return response
        } else {
            ElMessage('服务异常')
            return Promise.reject(response)
        }
    }

    // 错误拦截封装
    _errorIntercept (err) {
        return Promise.reject(err)
    }

    // 获取名称
    _getApiName (item) {
        let apiName = '',
            arr = item.url.split('/')
        if (item.url) {
            apiName = item.apiName ? item.apiName : arr[arr.length - 1]
        } else {
            throw new Error('api对象必须要有url', item)
        }
        return apiName
    }

    // 获取配置
    _getConfig (value, options, item) {
        let config = {
            url: item.url,
            method: item.method ? item.method : 'post',
        }
        if (config.method === 'post') {
            config.data = value
        } else {
            config.params = value
        }
        Object.keys(options).forEach(key => {
            config[key] = options[key]
        })
        return config
    }

    // 发送请求
    _getApiFunction (value = {}, options = {}, item = {}) {
        let config = this._getConfig(value, options, item)
        return this._instance(config).then(res => {
            return res
        }).catch((err) => {
            // ElMessage({
            //     type: 'error',
            //     message: '异常'
            // })
            return Promise.reject(err)
        })
    }
}
export default new HttpInstance(apiList)

在main.js中引入为全局变量,其实在vue3中,就不支持将变量挂载在原型上,因为我们现在是创建的一个个vue实例,网上找了一种写法,是这样的

//定义
app.config.globalProperties.$message = message


//使用方法
import { reactive, getCurrentInstance } from 'vue';
setup() {
   const { ctx } = getCurrentInstance();
   const login= () => {
      ctx.$message.info('登录成功')
   };
   return {
     login
   }
}

但是这种写法好像在打包上线的时候会报ctx为undefined(这个我自己也没有试过),所以我们使用provide/inject这种写法来进行全局绑定 看看main.js怎么写

import http from '@/http/index'
app.use(ElementPlus)
    .provide('$http', http)
    .use(router)
    .use(store)
    .mount('#app')

具体用法(同样有两种写法,累了,就不写了)

<script setup>
import { inject } from 'vue'
const http = inject('$http')
console.log(http.getUserInfo())
</script>

看看结果 这是我在mock定义的数据

image.png

请求返回的结果

image.png

非对称/对称加解密方法封装(这个可以不要,我也不会细讲)

定义一个加密文件并导出加解密方法

// 非对称加密
import env from '@/config/env.js'
// const { JSEncrypt } = require('encryptlong')
// // 加密
// export const encryption = (data) => {
//     let decrypt = new JSEncrypt()
//     decrypt.setPublicKey(env.serverRsaPublicKey)
//     return decrypt.encryptLong(encodeURI(data))
// }
// // 解密
// export const decrypt = (data) => {
//     let decrypt = new JSEncrypt()
//     decrypt.setPrivateKey(env.clientRsaPrivateKey)
//     return decodeURIComponent(decrypt.decryptLong(data))
// }

// 对称加密
import CryptoJS from 'crypto-js'//引用AES源码js
const key = CryptoJS.enc.Utf8.parse("0102030405060708")  //十六位十六进制数作为密钥
const iv = CryptoJS.enc.Utf8.parse('0102030405060708')  //十六位十六进制数作为密钥偏移量
// 加密
export const encryption = (data) => {
    let srcs = CryptoJS.enc.Utf8.parse(encodeURI(data))
    let encrypted = CryptoJS.AES.encrypt(srcs, key, { iv: iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 })
    return encrypted.ciphertext.toString().toUpperCase()
}
// 解密
export const decrypt = (data) => {
    let encryptedHexStr = CryptoJS.enc.Hex.parse(data)
    let srcs = CryptoJS.enc.Base64.stringify(encryptedHexStr)
    let decrypt = CryptoJS.AES.decrypt(srcs, key, { iv: iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 })
    let decryptedStr = decrypt.toString(CryptoJS.enc.Utf8)
    return decodeURIComponent(decryptedStr.toString())
}

然后修改一下http中的index.js文件

    // 请求拦截封装
    _requestIntercept (config) {
        let needEncryption = false
        this._apiList.some(item => {
            if (config.url.indexOf(item.url) !== -1) {
                needEncryption = item.needEncryption
                return true
            }
        })
        // 需要加密
        if (needEncryption) {
            config.headers['Content-Type'] = 'application/json'
            config.data = encryption(JSON.stringify(config.data))
        }
        return config
    }

    // 响应拦截封装
    _responseIntercept (response) {
        let needDecrypt = false,
            responseUrl = response.config.url.split(env.baseUrl)[1]
        this._apiList.some(item => {
            if (item.url === responseUrl) {
                needDecrypt = item.needDecrypt
                return true
            }
        })
        // 需要解密
        if (needDecrypt) {
            response.data = JSON.parse(decrypt(response.data))
        }
        if (response.data.code === 200) {
            return response
        } else {
            ElMessage('服务异常')
            return Promise.reject(response)
        }
    }

关于一些打包配置文件

env

在src下创建config文件夹并创建config.js

let baseUrl = 'https://www.fastmock.site/mock/e4d5c5b159b6ac1254145b8735b5db49/vite',
    serverRsaPublicKey = "",
    clientRsaPrivateKey = ""

//import.meta.env是vite提供的一方特性,可以进行特殊配置
console.log(import.meta.env)
switch (import.meta.env.VITE_ENVIRONMENT) {
    // 开发环境
    case 'development':
        baseUrl = 'https://www.fastmock.site/mock/e4d5c5b159b6ac1254145b8735b5db49/vite'
        break
    // 生产环境
    case 'production':
        baseUrl = ''
        break
    default:
        break
}

export default {
    baseUrl, serverRsaPublicKey, clientRsaPrivateKey
}

在src同级下创建env.dev与env.prod文件 env.dev

wuwuwu="production"(这个是为了后面的说明用的)
VITE_ENVIRONMENT = 'development'

env.prod

wuwuwu="production"
VITE_ENVIRONMENT = 'production'

然后我们在package.json定义一条新脚本试试 "test":"vite --mode dev",

  "scripts": {
    "test":"vite --mode dev",
    "dev": "vite",
    "build": "vite build",
    "serve": "vite preview"
  },

最后npm run test启动项目,看看控制台

image.png

输出了我们定义的VITE_ENVIRONMENT,但是我们定义的wuwuwu却没有输出,为什么?详情可以看看官网的介绍

只有 VITE_SOME_KEY 会被暴露为 import.meta.env.VITE_SOME_KEY 提供给客户端源码,而 DB_PASSWORD 则不会

现在我们可以通过控制脚本来定义我们开发环境中一些常量,例如接口前缀什么的,就像我前面在config,js里面的判断。

github地址

vue3/vite

一些官网

Vite中文文档

Vue3中文文档

Element-plus中文文档

Nodejs

一个mock网站(之前是用easy-mock,但是好像挂了)

吐槽一下

原来掘金现在还可以选择Markdown主题,难怪我说别人的为什么那么好看,我之前写的样式怎么那么丑... 都看到这了,给个👍吧 祝大家基金都起飞起飞🚀🚀(写得有点赶,如果里面有什么错误欢迎指出)