日常前言
好久没有写文章了,其实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
怎么样,是不是速度很快,然后对应浏览器打开控制台的链接,然后你会发现打开链接之后,控制台输出了两行警示
这个不影响我们继续进行,不过在这里我们可以发现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文件夹(用来存放不同模块的页面路由),具体如下图
然后在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方便验证
两种写法 第一种(旧写法)
<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')
大概效果就是这样
封装vuex
开始的流程和创建路由文件差不多,只是多了一个getters.js文件,这里直接贴创建后的目录
首先是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>
返回结果
封装axios
上mock
因为我没有在本地起服务器,所以找了个mock网站,在这个网站先自定义一下自己的请求,具体怎么操作这里就不说了
开始封装
同样创建对应文件
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定义的数据
请求返回的结果
非对称/对称加解密方法封装(这个可以不要,我也不会细讲)
定义一个加密文件并导出加解密方法
// 非对称加密
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启动项目,看看控制台
输出了我们定义的VITE_ENVIRONMENT,但是我们定义的wuwuwu却没有输出,为什么?详情可以看看官网的介绍
只有 VITE_SOME_KEY 会被暴露为 import.meta.env.VITE_SOME_KEY 提供给客户端源码,而 DB_PASSWORD 则不会
现在我们可以通过控制脚本来定义我们开发环境中一些常量,例如接口前缀什么的,就像我前面在config,js里面的判断。
github地址
一些官网
一个mock网站(之前是用easy-mock,但是好像挂了)
吐槽一下
原来掘金现在还可以选择Markdown主题,难怪我说别人的为什么那么好看,我之前写的样式怎么那么丑... 都看到这了,给个👍吧 祝大家基金都起飞起飞🚀🚀(写得有点赶,如果里面有什么错误欢迎指出)