美文网首页vue实战Vue
Vue CLI3从脚手架到毛坯房

Vue CLI3从脚手架到毛坯房

作者: 胡先森很忙 | 来源:发表于2019-11-28 19:55 被阅读0次

    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都算是简单的实现了

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

    相关文章

      网友评论

        本文标题:Vue CLI3从脚手架到毛坯房

        本文链接:https://www.haomeiwen.com/subject/ijlwmctx.html