Vue CLI 是一个基于 Vue.js 进行快速开发的完整系统,是个初始的vue脚手架,有关Vue CLI的更多信息,可以参考vue_cli官网
但是仅仅只有vue_cli脚手架的话还是不够的,于是乎就需要给vue_cli添砖添瓦,大概需要添加的砖瓦大体可以考虑以下方面
-
GZIP
-
打包自动添加版本号
-
vuex模块化(与持久化)
-
路由导航守卫
-
rem
-
请求的封装与配置
-
loading
接下来我们一步一步,把毛坯房给建出来
首先是GZIP,为什么我把这个放在了首位呢,因为它真的很重要,尽量减少文件的大小,提升响应速度,强烈推荐配置GZIP
GZIP
我们需要用到的插件compression-webpack-plugin
npm install compression-webpack-plugin --save-dev
然后在你的vue.config.js中进行gzip的配置,在打正式环境包的时候开启gzip
// vue.config.js
const IS_PROD = ['production'].includes(process.env.NODE_ENV) // 是否是生产环境
const CompressionWebpackPlugin = require('compression-webpack-plugin') // 引入compression-webpack-plugin
const productionGzipExtensions = ['js', 'css'] // 需要gzip的文件
module.exports = {
configureWebpack: config => {
if (IS_PROD) {
config.plugins.push(new CompressionWebpackPlugin({
algorithm: 'gzip',
test: new RegExp('\\.(' + productionGzipExtensions.join('|') + ')$'),
threshold: 10240,
minRatio: 0.8
}))
}
}
}
配置好了,打个包看看效果,css和js超过配置的大小就会生成一份gzip文件,大小减少了很多
前端配置好了GZIP,就需要服务端配合了,服务器开启GZIP,以nginx为例
在nginx.config配置文件中
gzip on;
gzip_types text/plain application/x-javascript application/javascript text/css application/xml text/javascript application/x-httpd-php image/jpeg image/gif image/png;
nginx -s reload
重启nginx看看效果,这个时候请求的时候的就默认先读取gzip文件
打包自动添加版本号
这个功能的话,还是有必要加上的,防止浏览器缓存,一般防止js缓存的话很多项目都会做,我接下来配置js和css的打包自动添加版本号
css和js的话,处理起来是用的不同方法,这里的版本号我取的是当前的时间戳
npm install mini-css-extract-plugin --save-dev
// vue.config.js
const MiniCssExtractPlugin = require('mini-css-extract-plugin') // css增加版本号需要的插件
const Timestamp = new Date().getTime()
module.exports = {
configureWebpack: config => {
// js 文件打包生产版本号,防止浏览器缓存
config.output.filename = `js/[name].[hash:6].${Timestamp}.js`
config.output.chunkFilename = `js/[name].[hash:6].${Timestamp}.js`
config.plugins.push(new MiniCssExtractPlugin({
filename: `css/[name].[hash:6].${Timestamp}.css`,
chunkFilename: `css/[name].[hash:6].${Timestamp}.css`
}))
}
}
vuex模块化与持久化
vuex模块化我之前有过单独的一篇进行介绍,可以参考vuex模块化,持久化的功能是看情况下的,当你的业务场景有刷新,并且不想直接使用localStorage的时候,可以配置下vuex的持久化
npm i -S vuex-persistedstate
vuex-persistedstate会同步vuex状态到本地存储localStorage、 sessionStorage或者cookie。
这样是默认使用localStorage来同步数据,也可以去vuex-persistedstate
查看其他配置选项
rem
rem的话,直接上代码,在src目录下新建一个util文件夹,在util下新建rem.js
//rem.js
// 设置 rem 函数
function setRem() {
// 320 默认大小16px; 320px = 20rem ;每个元素px基础上/16
const htmlWidth = document.documentElement.clientWidth || document.body.clientWidth
// 得到html的Dom元素
const htmlDom = document.getElementsByTagName('html')[0]
// 设置根元素字体大小
htmlDom.style.fontSize = htmlWidth / 20 + 'px'
}
// 初始化
setRem()
// 改变窗口大小时重新设置 rem
window.onresize = function() {
setRem()
}
// main.js
import './util/rem' // 引入rem
借助插件postcss-pxtorem来自动换算rem
module.exports = {
css: {
// 是否开启支持 foo.module.css 样式
requireModuleExtension: true,
// css预设器配置项
loaderOptions: {
css: {
// options here will be passed to css-loader
},
postcss: {
// options here will be passed to postcss-loader
plugins: [
require('postcss-pxtorem')({
rootValue: 18.75, // 换算的基数
propList: ['*']
})
]
}
}
}
}
请求的封装与配置
在各个环境的环境变量文件中,有
VUE_APP_BASE_API = '/api'
VUE_APP_BASE_URL = '服务器地址'
配置跨域代理process.env.环境变量名可以拿到该环境变量
// vue.config.js
module.exports = {
devServer: {
proxy: {
[process.env.VUE_APP_BASE_API]: {
target: process.env.VUE_APP_BASE_URL,
pathRewrite: { // 重写路径: 去掉路径中开头的'/api'
'^/api': ''
},
changeOrigin: true
}
}
}
}
这里的VUE_APP_BASE_API = '/api',/api是我们使用的统一前缀,服务端微服务可能前缀有很多,所以推荐使用一个统一的接口前缀
在根目录下新建一个config.js用来存放微服务的接口前缀
module.exports = {
partner: '/mtourists-partner' // API接口前缀
}
接下来封装request了,在util文件夹下新建request.js文件
import axios from 'axios'
import { Toast } from 'vant'
const defaultToken = '06764f6f3f9098c31979ab6e6a837267'
const service = axios.create({
baseURL: process.env.VUE_APP_BASE_API,
timeout: 5000
})
service.interceptors.request.use(
config => {
const localToken = localStorage.getItem('Token')
const configToken = localToken || defaultToken
// 请求头中增加token
config.headers['X-Authorization'] = `Bearer ${configToken}`
return config
},
error => {
return Promise.reject(error)
}
)
service.interceptors.response.use(
response => {
// response的headers中返回token,存储下来,放在本地
const authorization = response.headers['x-authorization']
if (authorization) {
const token = authorization.replace(/Bearer\s/, '')
const locToken = localStorage.getItem('Token')
if (token && token !== locToken) {
localStorage.setItem('Token', token)
}
}
const res = response.data
const code = 200
if (res.code !== 20000 && code !== 200) {
// handle error
} else {
return res
}
},
error => {
console.log('err' + error)
// handle error
return Promise.reject(error)
}
)
export default service
在src下新建api文件夹,用来存放我们的请求,新建一个user.js,是我们user模块的请求
import request from '../util/request'
const apiConfig = require('../../config')
// 登录
export function userLogin(data) {
return request({
url: `${apiConfig.partner}/index/login`,
method: 'post',
data: data
})
}
// 登出
export function userLoginOut() {
return request({
url: `${apiConfig.partner}/index/logout`,
method: 'post'
})
}
调用的时候先import引入
// login.vue
import { userLogin } from '../api/user'
//发起请求
userLogin(loginParams).then(res => {
if (res.state === 1) {
// 登录成功
this.$store.commit('RECEIVE_USER_INFO', res.data)
this.$store.commit('IS_LOGIN', true)
this.$router.replace('/home')
} else {
Toast.fail('账号或密码不正确')
}
})
loading
loading的实现的话可以借助插件,这里我们手写一个loading,然后挂载在vuex上
启动loading先解决,在index.html里加上loading,然后在App.vue的mounted中隐藏loading
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
<link rel="icon" href="<%= BASE_URL %>favicon.ico" />
<title>项目</title>
<style>
#loading {
position: fixed;
text-align: center;
padding-top: 50%;
width: 100%;
height: 100%;
z-index: 1000;
background-color: #ffffff;
}
</style>
</head>
<body>
<noscript>
<strong
>We're sorry but pdd-partner doesn't work properly without JavaScript
enabled. Please enable it to continue.</strong
>
</noscript>
<div id="loading">
<img src="./loading.gif" alt="loading" />
</div>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>
// App.vue
mounted() {
document.getElementById('loading').style.display = 'none'
}
这样项目启动loading就完成了
接下来是全局loading的配置了
新建一个Loading.vue
<template>
<div class="loading">
<img src="../../public/loading.gif" alt="loading">
</div>
</template>
<script>
export default {
name: 'Loading'
}
</script>
<style scoped>
.loading {
position: fixed;
text-align: center;
padding-top: 50%;
width: 100%;
height: 100%;
z-index: 1000;
background-color: rgba(0,0,0,0.4)
}
</style>
在App.vue中引入
<template>
<div id="app">
<Loading v-show="loading" />
<keep-alive>
<router-view />
</keep-alive>
</div>
</template>
<script>
import Loading from './components/Loading.vue'
export default {
name: 'App',
components: {
Loading
},
computed: {
loading() {
return this.$store.state.status.loading
}
},
mounted() {
document.getElementById('loading').style.display = 'none'
}
}
</script>
<style>
</style>
在vuex中新建一个status模块,用于存放全局状态的,如loading之类的
// status.js
import * as types from '../mutation-types'
// initial state
const state = () => ({
loading: false
})
// getters
const getters = {
getLoading: store => store.loading
}
// mutations
const mutations = {
[types.showLoading](store) {
store.loading = true
},
[types.hideLoading](store) {
store.loading = false
}
}
// actions
const actions = {
getLoading({ commit }) {}
}
export default {
state,
getters,
actions,
mutations
}
// mutation-types.js
// status状态模块
export const showLoading = 'showLoading'
export const hideLoading = 'hideLoading'
这样
this.$store.commit('showLoading') // 显示loading
this.$store.commit('hideLoading') // 隐藏loading
导航守卫
为什么我把导航守卫也当做了一块需要完善的砖瓦呢?因为我们做项目的话,很容易就遇到权限相关的需求,这个时候使用导航守卫进行处理那肯定是很方便的,vue-router 提供的导航守卫主要用来通过跳转或取消的方式守卫导航。有多种机会植入路由导航过程中:全局的, 单个路由独享的, 或者组件级的
注意点:参数或查询的改变并不会触发进入/离开的导航守卫。你可以通过观察 $route
对象来应对这些变化,或使用 beforeRouteUpdate
的组件内守卫。
我们来完成一个登陆权限的导航守卫,需求1:没有登陆信息,访问除登录页面的其他路由重定向登录页;需求2:有登录信息访问登录页的话重定向首页
// router
const router = new VueRouter({
mode: 'history',
routes
})
router.beforeEach((to, from, next) => {
if (to.name !== 'login') {
if (
router.app.$options.store.state.user.userInfo &&
router.app.$options.store.state.user.islogin
) {
// 有登录状态
next()
} else {
next({ path: '/', replace: true })
}
} else {
next()
}
})
export default router
因为我使用的持久化vuex,用户信息固化在localStorage里,在router里面使用vuex,是router.app.$options.store
记住判断用户信息的话,一定要除去login路由,不然的话会导致栈溢出(想一想就明白了)
这样我们就完成了需求1
我们再来看看需求2,需求2是单个路由独享的导航守卫
const routes = [
// 登录页
{
path: '/',
name: 'login',
component: Login,
beforeEnter: (to, from, next) => {
if (router.app.$options.store.state.user.userInfo && router.app.$options.store.state.user.islogin) {
next({ path: '/home', replace: true })
} else {
next()
}
}
},
// 首页
{
path: '/home',
name: 'Home',
component: Home
}
{ path: '*', redirect: '/' } // 所有未匹配到的路由,都跳转登录页
]
到这里我们的需求1和2都算是简单的实现了
当然了再做项目的时候,还有很多的配置项可以进行更改,大家也要灵活多变,合理配置,觉得有用的话帮忙点个赞吧