Vue CLI3从脚手架到毛坯房

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.png

前端配置好了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。

vuex持久化.png

这样是默认使用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都算是简单的实现了

当然了再做项目的时候,还有很多的配置项可以进行更改,大家也要灵活多变,合理配置,觉得有用的话帮忙点个赞吧

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,123评论 6 490
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,031评论 2 384
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,723评论 0 345
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,357评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,412评论 5 384
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,760评论 1 289
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,904评论 3 405
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,672评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,118评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,456评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,599评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,264评论 4 328
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,857评论 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,731评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,956评论 1 264
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,286评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,465评论 2 348

推荐阅读更多精彩内容