分类 Vue 下的文章

前言

此文章用于记录自己实现一个后台的历程,仅记录前端,数据暂时以mock方式。

环境

  • node:v8.16.1
  • npm: 6.11.3
  • ElementUI

约定

  1. 每个页面的第一个div容器,必须有class,且该class是其名字-功能组合而成,如login页面为 “login-page”
  2. 命名规则:

    2.1. class 中只能出现小写字符和连字符(Hyphenate 命名)
    2.2. 尽量使用英文命名
    2.3. 尽量不缩写
    

创建Vue项目

vue create itcys // 注意 项目名不能大写

配置

多环境
Webpack配置
1、在项目根目录创建.env文件,作为环境的变量配置
.env 所有环境下都会载入的配置
.env.[环境] #指定的环境下才会载入的配置

2、创建vue.config.js,进行项目配置,包括webpack配置、代理(mock)等
通过 process.env.NODE_ENV的方式获取环境

 'use strict'
const path = require('path')
const resolve = (dir) => { // 相对目录
  return path.join(__dirname, dir) // __dirname 就是当前目录
}
const isDevelopment = process.env.NODE_ENV === 'development' // 是否为开发环境
const port = 8080
module.exports = {
  publicPath: '/', // 部署应用包时的基本 URL
  outputDir: 'dist', //  当运行 vue-cli-service build 时生成的生产环境构建文件的目录,注意目标目录在构建之前会被清除 (构建时传入 --no-clean 可关闭该行为)
  assetsDir: 'static', // 放置生成的静态资源 (js、css、img、fonts) 的 (相对于 outputDir 的) 目录
  filenameHashing: true, // 静态资源的文件名包含hash,以便缓存
  lintOnSave: isDevelopment, // 每次保存时,出错后,输出Lint警告(true),
  runtimeCompiler: isDevelopment, // 是否包含运行时编译器的Vue构建版本,true会导致容量增加
  productionSourceMap: isDevelopment, // 是否需要生产环境的 source map
  configureWebpack: { // 可以在此配置更多的loader,
    resolve: { // 相对目录配置
      alias: { // 别名
        '@': resolve('src') // 这里把 @符号作为根目录的意思
      }
    }
  },
  chainWebpack(config) { // 这个是webpack的一个链式函数,适合通过函数的方式配置
    // 加载src/icons里所有的svg文件
    config.module.rule('svg').exclude.add(resolve('src/icons')).end()
  },
  css: {
    extract: process.env.NODE_ENV === 'production', // 是否将组件中的css 提取到一个独立的css中
    sourceMap: isDevelopment // 是否为css开启 source map,设置为 true 之后可能会影响构建的性能
    // loaderOptions: {
    //     css: {
    //         // 这里的选项会传递给 css-loader
    //         localIdentName: '[name]-[hash]',
    //         camelCase: 'only'
    //     },
    //     postcss: {
    //         // 这里的选项会传递给 postcss-loader
    //     }
    // }
  },
  devServer: { // 开发时的一些服务,(启动一个模拟数据服务)

    // 等同 webpack-dev-server,有些值像 host、port 和 https 可能会被命令行参数覆写  https://webpack.js.org/configuration/dev-server/
    host: '0.0.0.0', // host,默认localhost,如果希望外部访问,就是0.0.0.0
    port: port, // 监听请求的端口
    open: true, // 当open启用时,将直接打开浏览器
    // https: true // 提供https服务,这里使用自签名证书,下列为手动配置证书
    // https: {
    //     key: fs.readFileSync("/path/to/server.key"),
    //     cert: fs.readFileSync("/path/to/server.crt"),
    //     ca: fs.readFileSync("/path/to/ca.pem"),
    // }
    overlay: { // 显示警告和错误
      warnings: true,
      errors: true
    },
    proxy: {
      // 代理将 接口路径代理到mock,这样就可以模拟数据了
      [process.env.VUE_APP_BASE_API]: {
        target: `http://localhost:${port}/mock`,
        changeOrigin: true,
        pathRewrite: {
          ['^' + process.env.VUE_APP_BASE_API]: ''
        }
      }
    }
  }

}

初始工作

1. 安装Element UI前端框架

npm i element-ui -S

2. 引入Element UI

在src/main.js中引入'element-ui'、'element-ui/lib/locale/lang/zh-CN'

import Element from 'element-ui' 
import locale from 'element-ui/lib/locale/lang/zh-CN' // 本地化环境
Vue.use(Element, { locale })

3. 引入样式文件

    3.1 创建element-variables.scss文件,以此文件为Element-Ui的样式文件,有针对Element的自定义的修改,也在此文件。
在此文件中导入Element-ui的默认主题样式:(**注意 $--font-path 是必须的**)

$--font-path: '~element-ui/lib/theme-chalk/fonts';
@import '~element-ui/packages/theme-chalk/src/index';

    3.2 创建styles/index.scss文件,以此文件为全局样式文件。
    在此文件中导入所有的样式文件,这样就只需要在main.js中引用这一个文件即可
// index.scss
@import './element-variables.scss';    
  *{
      margin: 0;
  }

全局样式

在main.js中引用index.scss
// main.js

import './styles/index.scss' 

路由 Vue-Router

路由,首先需要声明不变的、固定路由,如login、404等,然后需要考虑因为权限,环境等因素产生的不同路由。

一、引入路由
在src中创建router/index.js文件,然后在main.js中添加下列代码

import router from './router'
new Vue({
  router,
  store,
  render: h => h(App)
}).$mount('#app')

修改App.vue,引入路由视图

<template>
  <div id="app">
    <router-view />
  </div>
</template>

<script>
export default {
  name: 'App'
}
</script>

二、声明路由
router/index.js

import Vue from 'vue'
import VueRouter from 'vue-router'

Vue.use(VueRouter)

const constantRoutes = [{
  path: '/login',
  component: () => import('@/views/login/login'),
  hidden: true
}, {

path: '/', // 首页
component: () => import('@/views/home/index'),
hidden: true
}]


const createRouter = () => new VueRouter({
  mode: 'history',
  // base: process.env.BASE_URL,
  scrollBehavior: () => ({ y: 0 }),
  routes: constantRoutes
})

const router = createRouter()

// 重置路由
export function resetRouter() {
  const newRouter = createRouter()
  router.matcher = newRouter.matcher
}
export default router

由于需要考虑不同权限,路由不同时,需要将重置router也暴露出去,所以实现了resetRouter函数,
目前 固定路由只有 '/login' 和'/'


权限

登录也算是一种权限,已登录和没有登录的两种权限。
需要实现一个全局钩子,对路由进行权限控制,使每个路由进入前都会进行验证权限,然后根据结果做出相应的处理

  1. 创建permission.js,并在main.js引入
  import router from './router'
    import store from '@/store'    
    const loginPath = '/login'
    const whiteList = [loginPath] // 权限白名单,任何人都能访问    
    router.beforeEach((to, from, next) => {
      const hasToken = store.getters.access_token    
      if (hasToken) { // 用于判断是否已登录
        if (to.path === loginPath) { // 已经登录过了,再登录就直接跳转首页
          next({ path: '/' })
          return
        }
      } else {
        if (whiteList.indexOf(to.path) === -1) { // 不是白名单
          next(`${loginPath}?redirect=${to.path}`)
          return
        }
      }
      next()
    })

接口请求(axios、mock)

axios
网站需要数据,就一定需要http请求,这里推荐axios
1、安装axios,在终端中执行

npm i axios

2、创建utils/request.js,使用此文件对axios进行统一处理,比如接口返回 没有权限,跳转到登录页面等。这里需要自行根据请求的后端接口风格来实现。

import axios from 'axios'
import store from '@/store'

const instance = axios.create({
  baseURL: process.env.VUE_APP_BASE_API,
  timeout: 5000, // request timeout
  withCredentials: false // 跨域请求时发送Cookie
})

// 统一拦截器,请求前调用
instance.interceptors.request.use(config => {
  if (store.getters.token) { // 已登录后,请求时附带上token
    config.headers['ane-token'] = store.getters.token
  }
  return config
}, error => {
  console.warn('axios request error:', error)
  return Promise.reject(error)
})

// 统一响应器,服务器返回时调用
instance.interceptors.response.use(response => {
  const codes = [500001]
  const res = response.data
  if (codes.indexOf(res.code) > -1) {
    // 处理异常code
  }
  return res // 仅返回 response.data,这样外面的reslove的值也是data
}, error => {
  console.warn('axios request error:', error.response)
  return Promise.reject(error)
})

export default instance

mock模拟接口

MockJs官网
MockJs Github 文档
由于前段的数据很多都是来自后端的,而这时候又不能等后端做完所有的接口,才开始做前端。这时候就只能和后端统一接口、数据等,由前段自行模拟假数据。因此就有了mock模拟接口

一种方式是直接拦截axios的方式模拟数据,但这种方式会导致请求不是真正的请求,相当于调用函数时直接返回数据了,不会出现在开发者工具的network中,如下:

 Mock.mock('/api/login', 'post', { status: true })  

// 将上方的mock数据文件直接在main.js中使用,后面再调用axios请求就会被拦截了。不推荐使用!

第二种方式
创建一个Mock-Server(模拟数据的服务器),这种方式其实就是相当于启动了一个网站,监听端口,请求等,和真正的后台接口无二。然后通过webpack的反向代理,以解决跨域问题(真正的跨域问题还是由后台服务器解决,这里只是暂时使用代理方式解决)

1. 安装mockjs! mockjs作用是生成随机数据,不需要修改既有代码,就能拦截Ajax请求,返回模拟数据等

npm install mockjs 

2. 创建mock/index.js,此文件用于动态加载modules里的所有数据文件

import Mock from 'mockjs'
import fs from 'fs'
import path from 'path'

const resolve = (dir) => { // 相对目录
  return path.join(__dirname, dir) // __dirname 就是当前目录
}

/**
 * 动态加载目录下的文件
 * @param {*} dir 要加载的目录
 * @param {*} filterRegexp  要加载的文件(正则)
 */
const requireContext = (dir, filterRegexp) => {
  let result = []
  const files = fs.readdirSync(dir) // 同步读取所有文件

  files.forEach((file, i) => {
    if (!filterRegexp || filterRegexp.test(file)) { // 根据正则表达式筛选文件
      const module = require(dir + '/' + file).default // 加载文件
      result = result.concat(module)
    }
  })
  return result
}
// 由于是需要在node环境下使用,因此无法使用webpack的require.context
const mockRoutes = requireContext(resolve('/modules'), /\.js$/) 

const responseFake = (url, type, respond) => {
  return {
    url: new RegExp(`/mock${url}`),
    type: type || 'get',
    response(req, res) {
      res.json(Mock.mock(respond instanceof Function ? respond(req, res) : respond))
    }
  }
}

// 导出一个数组
export default mockRoutes.map(route => {
  return responseFake(route.url, route.type, route.response)
})

3. 创建mock-server.js,由于这个mock-server是node的进程,因此语法还会有些许改变

const chokidar = require('chokidar') // 监听文件变化
const bodyParser = require('body-parser') // 用来解析body,也就是请求时的信息
const chalk = require('chalk') // 颜色字体(控制台)
const path = require('path')

const mockDir = path.join(process.cwd(), 'mock') // 模拟数据的目录 (根据启动时终端目录为当前目录

/**
 * 注册Mock路由
 * @param {*} app
 */
function registerRoutes(app) {
  let mockLastIndex
  const { default: mocks } = require('./index.js') // 引入mock的数据
  for (const mock of mocks) {
    app[mock.type](mock.url, mock.response)
    // console.log(mock.type + ':', mock.url)
    mockLastIndex = app._router.stack.length
  }
  const mockRoutesLength = Object.keys(mocks).length
  return {
    mockRoutesLength: mockRoutesLength,
    mockStartIndex: mockLastIndex - mockRoutesLength
  }
}

/**
 * 清除注册了的路由缓存
 */
function unregisterRoutes() {
  //  delete require.cache[require.resolve(path.join(mockDir, '../index.js'))]
  Object.keys(require.cache).forEach(i => {
    if (i.indexOf(mockDir) !== -1) {
      delete require.cache[require.resolve(i)] // 删除掉require的缓存,不删除的话,再次引入是没有任何改变的
    }
  })
}

module.exports = function(app) {
  //
  require('@babel/register') // 将index.js的语法和server的语法统一,说简单点就是 让 require = import
  app.use(bodyParser.json())
  app.use(bodyParser.urlencoded({
    extended: true
  }))
  const { mockRoutesLength, mockStartIndex } = registerRoutes(app)
  console.log(chalk.magentaBright(`\n > Mock Server启动成功!`))
  chokidar.watch(mockDir, {
    ignored: /mock-server/, // 忽略的文件或文件夹
    ignoreInitial: true // 触发增加文件或文件夹的事件
  }).on('all', (event, path) => {
    if (event === 'change' || event === 'add') { // 当目录内修改或新增了文件后,触发
      // if (require.cache[require.resolve(path)]) {
      //   delete require.cache[require.resolve(path)]
      // }
      try {
        console.log('Watch directory ' + path + ' event:' + event)
        app._router.stack.splice(mockStartIndex, mockRoutesLength)

        unregisterRoutes()

        registerRoutes(app)

        console.log(chalk.magentaBright(`\n > Mock Server Hot Reload 成功!`))
      } catch (ex) {
        console.log(chalk.redBright(ex))
      }
    }
  })
}

4. 启动Mock-Server,并在vue.config.js的devServer配置反向代理

  devServer: { // 开发时的一些服务

    // 等同 webpack-dev-server,有些值像 host、port 和 https 可能会被命令行参数覆写  https://webpack.js.org/configuration/dev-server/
    // host: '0.0.0.0', // host,默认localhost,如果希望外部访问,就是0.0.0.0
    port: port, // 监听请求的端口
    open: true, // 当open启用时,将直接打开浏览器
    // https: true // 提供https服务,这里使用自签名证书,下列为手动配置证书
    // https: {
    //     key: fs.readFileSync("/path/to/server.key"),
    //     cert: fs.readFileSync("/path/to/server.crt"),
    //     ca: fs.readFileSync("/path/to/ca.pem"),
    // }
    overlay: { // 显示警告和错误
      warnings: true,
      errors: true
    },
    proxy: {
      // 代理将 接口路径代理到mock,这样就可以模拟数据了
      [process.env.VUE_APP_BASE_API]: {
        target: `http://localhost:${port}/mock`,
        changeOrigin: true,
        pathRewrite: {
          ['^' + process.env.VUE_APP_BASE_API]: ''
        }
      }
    },
    after: require('./mock/mock-server.js') //启动mock-server 服务器
  }

Store

由于后台管理界面的状态管理很多,因此最好是通过Modules方式拆分
1、创建store/modules目录,里面存储着不同模块的状态,如app、user

const state = {

}
const mutations = {

}
const actions = {

}
export default {
  namespaced: true, // 导出默认命名空间
  state,
  mutations,
  actions
}
// app.js 基础模版,注意namespaced:true,必须有,不然不能通过文件的方式找到

2、创建store/getters.js

const getters = {
  // ...
}

export default getters

3、重构store/index.js,使其支持modules,不需要每次增删一个modules的文件,都需要改动此文件;并引入getters。

import Vue from 'vue'
import Vuex from 'vuex'
import getters from './getters'
Vue.use(Vuex)

// 通过webpack,声明一个webpackContext对象,这个对象包含该文件内所有文件
//require.context(directory, useSubdirectories = false, regExp = /^\.\//); 
const modulesFiles = require.context('./modules', false, /\.js$/)  

//遍历所有文件,生成新的对象,[文件名=>文件内导出的对象]
const modules = modulesFiles.keys().reduce((modules, modulePath) => {
  const moduleName = modulePath.replace(/^\.\/(.*)\.\w+$/, '$1')
  const value = modulesFiles(modulePath)
  modules[moduleName] = value.default
  return modules
}, {}) // 这个 {} 就是初始化的modules(第一次进入时的值)

export default new Vuex.Store({
  modules,
  getters
})