2019年10月

安装 ESlint

Eslint官方链接
腾讯云的Eslint说明,较好用
eslint-plugin-vue(Vue相关的Eslint)

npm install -g eslint
eslint --init

配置规则

  • root :true, // 以当前目录为根目录,不再向上查找 .eslintrc.js
  • env : 运行的环境
  env:{  
        browser: true,
        node: true,
        commonjs: true,
        es6: true
    }
  • extends: ['eslint:recommended'], // 继承,使用值作为基础配置,可以在rules中覆盖。当前值为eslint推荐的规则

         'extends': [
       'plugin:vue/essential',
       '@vue/standard'
     ]
    
  • parserOptions:JavaScript 选项。

    "parserOptions": {

      // ECMAScript 版本
     "ecmaVersion":6,
     "sourceType":"script",//module
     // 想使用的额外的语言特性:
     "ecmaFeatures":  {
         // 允许在全局作用域下使用 return 语句
         "globalReturn":true,
         // impliedStric
         "impliedStrict":true,
         // 启用 JSX
         "jsx":true
      }
    }
    
  • globals // 全局变量

     globals: { // 允许在代码中使用全局变量
           location: true,
           setTimeout: true
       }
    
  • rules:开启规则和发生错误时报告的等级,规则的错误等级有三种:

       ●  0 或'off':关闭规则。
       ●  1 或'warn':打开规则,并且作为一个警告(并不会导致检查不通过)。
       ●  2 或'error':打开规则,并且作为一个错误(退出码为1,检查不通过)。
    

如下方indent规则说明:
值可以是 0、1、2、'off'、‘warn’、'error'、或者数组,
数组时,[par1,par2,par3],par1对应上面的值,par2是这个选项的值,par3是附带的参数

indent
强制一致的缩进

  • off : 关闭规则
  • tab :以制表符缩进
  • [2,4, {"SwitchCase":1}] : 打开规则,并且作为一个错误,检查不通过。检查规则为 4个空格,

常见规则

accessor-pairs
在对象中强制使用getter/setter 对(不能仅使用setter)

arrow-spacing
箭头符号前后都要空格

"arrow-spacing":[2, {"before": true, "after": true}]

indent
强制一致的缩进,默认为4 个空格

no-console
禁止调用console对象的方法

    'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off' //当环境是产品时报错,否则允许调用console

no-debugger
此规则不允许debugger声明

import/first
import 关键字需要放在文件头

spaced-comment
要求注释有空格

space-before-function-paren
函数括号前强制要求有间距

always:(默认)需要一个空格
never:不允许任何空格
 "space-before-function-paren": ["error", "always"], //(默认)需要一个空格
// or
"space-before-function-paren": ["error", {
    "anonymous": "always",
    "named": "always",
    "asyncArrow": "always"
}],

no-cond-assign
条件语句的条件中不允许出现赋值运算符

Vue相关规则

vue/max-attributes-per-line
多个特性的元素应该分多行撰写,每个特性一行

   "vue/max-attributes-per-line": ["error", {  // vue的属性必须每行一个
  "singleline": 1, // 一行显示几个属性
  "multiline": {
    "max": 1,
    "allowFirstLine": false //如果为true,则允许属性与该标记名称位于同一行,max=1,可以代替allowFitstLine
  }
}]

预览

创建文件

在views中创建login/login.vue 文件

QQ图片20191025112329.png

实现步骤

  1. 实现前端界面后,实现登录请求事件

    this.$store.dispatch('user/login', this.loginModel)

            .then(res => {
              // 登录成功
              this.$router.push({ path: '/' })
            })
    
  2. 在Store/modules/user.js 中实现login

    2.1登录信息,应该就算页面刷新,也保持登录,因此需要用到cookie。在utils中创建auth.js,用于实现管理缓存的Token

    import Cookie from 'js-cookie'

    const TokenKey = 'Permission-Token' // token的Cookie Key

    export function getToken() {
    return Cookie.get(TokenKey)
    }

    /**

    • 设置Token
    • @param {string} token
      */

    export function setToken(token) {
    if (!token) return clearToken()
    return Cookie.set(TokenKey, token)
    }

    export function clearToken() {
    return Cookie.remove(TokenKey)
    }

2.2 在api目录中实现user的login

//api/user.js
import request from '@/utils/request'
export const login = (loginForm) => {
  return request({
    url: '/login',
    method: 'post',
    data: loginForm
  })
}

2.3 在Store的User实现login

// store/modules/user.js
import { login } from '@/api/user'
import { getToken, setToken } from '@/utils/auth'

// state
const state = {
  access_token: getToken()
}

const mutations = {
  SET_ACCESS_TOKEN: (state, token) => {
    state.access_token = token
    setToken(token)
  }
}

const actions = {
  // 登录
  login({ commit }, userinfo) {
    return new Promise((resolve, reject) => {
      login(userinfo).then(response => {
        commit('SET_ACCESS_TOKEN', response.data.token) // 登录后缓存token
        resolve(response)
      }).catch(error => {
        reject(error)
      })
    })
  }
}

前言

此文章用于记录自己实现一个后台的历程,仅记录前端,数据暂时以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
})