linux服務器開發

小慕讀書 項目開發

下載查看vue-elemen-admin源碼css

git clone https://github.com/PanJiaChen/vue-element-admin
cd vue-element-admin
npm i 
npm run dev
項目精簡
  • 刪除src/views下的源碼,保留:html

    • dashboard:首頁
    • error-page: 異常頁面
    • login:登陸
    • redirect:重定向
  • 對 src/router/index 進行相應修改
  • 刪除 src/router/modules文件夾
  • 刪除 src/vendor文件夾
WARNING

若是是線上項目,建議將 components 的內容也進行清理,以避免影響訪問速度,或者直接使用查看 vue-admin-template 構建項目,課程選擇 vue-element-admin 初始化項目,是由於 vue-element-admin 實現了登陸模塊,包括 token 校驗、網絡請求等,能夠簡化咱們的開發工做前端

項目配置

經過src/settings.js進行局配置vue

  • title: 站點標題,進入某個頁面後,格式爲:

頁面標題-站點標題node

  • showSettings:是否顯示右側懸浮配置按鈕
  • tagsView:是否顯示頁面標籤功能條
  • fixedHeader:是否將頭部佈局固定
  • sidebarLogo:菜單欄中是否顯示LOGO
  • errorLog:默認顯示錯誤日誌的環境
源碼調試

如需進行源碼調試,修改vue.config.jsmysql

config
  // https://webpack.js.org/configuration/devtool/#development
  .when(process.env.NODE_ENV === 'development',
    config => config.devtool('cheap-source-map')
  )
項目結構
  • api:接口請求
  • assets:靜態資源
  • components:通用組件
  • directive 自定義指令
  • filters:自定義過濾器
  • icons:圖標組件
  • layout:佈局組件
  • router:路由配置
  • store:狀態管理
  • styles:自定義樣式
  • utils:通用工具方法linux

    • auth.js:token存取
    • permission.js:權限檢查
    • request.js:axios請求封裝
    • index.js:工具方法
  • views:頁面
  • permission.js:登陸認證和路由跳轉
  • setting.js:全局配置
  • main.js:全局入口文件
  • App.vue:全局入口文件
後端框架搭建
Node簡介
Node 是一個基於 V8 引擎的 Javascript 運行環境,它使得 Javascript 能夠運行在服務端,直接與操做系統進行交互,與文件控制、網絡交互、進程控制等
express簡介
express 是一個輕量級的 Node Web 服務端框架,一樣是一我的氣超高的項目,它能夠幫助咱們快速搭建基於 Node 的 Web 應用
項目初始化

建立項目webpack

mkdir admin-imooc-node
cd admin-imooc-node
npm init -y

安裝依賴
npm i -S express
建立app.jsios

const express = require('express')

// 建立 express 應用
const app = express()

// 監聽 / 路徑的 get 請求
app.get('/', function(req, res) {
  res.send('hello node')
})

// 使 express 監聽 5000 端口號發起的 http 請求
const server = app.listen(5000, function() {
  const { address, port } = server.address()
  console.log('Http Server is running on http://%s:%s', address, port)
})
Express三大基礎概念
中間件

中間件是一個函數,在請求和響應週期中被順序調用nginx

const myLogger = function(req, res, next) {
  console.log('myLogger')
  next()
}

app.use(myLogger)

規則主要分爲兩部分

  • 請求方法:get、post
  • 請求路徑:/ /user /*fly$/
異常處理

經過自定義中間件進行異常處理

app.get('/', function(req, res) {
  throw new Error('something has error...')
})

const errorHandler = function (err, req, res, next) {
  console.log('errorHandler...')
  res.status(500)
  res.send('down...')
}

app.use(errorHandler)

參數不能少 中間件要在請求以後引用

項目框架搭建

錯誤處理 安裝boom依賴
`npm i -S boom
`
建立router文件夾 建立router/index.js

const express = require('express')
const boom = require('boom')
const userRouter = require('./user')
const {
  CODE_ERROR
} = require('../utils/constant')

// 註冊路由
const router = express.Router()

router.get('/', function(req, res) {
  res.send('歡迎學習小慕讀書管理後臺')
})

// 經過 userRouter 來處理 /user 路由,對路由處理進行解耦
router.use('/user', userRouter)

/**
 * 集中處理404請求的中間件
 * 注意:該中間件必須放在正常處理流程以後
 * 不然,會攔截正常請求
 */
router.use((req, res, next) => {
  next(boom.notFound('接口不存在'))
})

/**
 * 自定義路由異常處理中間件
 * 注意兩點:
 * 第一,方法的參數不能減小
 * 第二,方法的必須放在路由最後
 */
router.use((err, req, res, next) => {
  const msg = (err && err.message) || '系統錯誤'
  const statusCode = (err.output && err.output.statusCode) || 500;
  const errorMsg = (err.output && err.output.payload && err.output.payload.error) || err.message
  res.status(statusCode).json({
    code: CODE_ERROR,
    msg,
    error: statusCode,
    errorMsg
  })
})

module.exports = router

建立router/use.js

const express = require('express')

const router = express.Router()

router.get('/info', function(req, res, next) {
  res.json('user info...')
})

module.exports = router

建立 utils/constant

module.exports = {
  CODE_ERROR: -1
}

驗證/user/info:
"user info..."
驗證/user/login

{"code":-1,"msg":"接口不存在","error":404,"errorMsg":"Not Found"}

項目需求分析
項目技術架構

  • 小慕讀書管理後臺
  • 小慕讀書小程序
  • 小慕讀書h5

項目架構

項目目標

  • 徹底在本地搭建開發環境
  • 貼近企業真實實用場景
技術難點分析
登陸
  • 用戶名密碼校驗
  • token生成、校驗和路由過濾
  • 前端token校驗和重定向
電子書上傳
  • 文件上傳
  • 靜態資源服務器
電子書解析
  • epub原理
  • zip解壓
  • xml解析
電子書增刪改
  • mysql數據庫應用
  • 先後端異常處理
epub電子書

本質是壓縮zip文件

Nginx服務器搭建

安裝nginx

修改配置文件

打開配置文件nginx.conf

  • windows位於安裝目錄
  • macOS位於:/usr/local/etc/nginx/nginx.conf

修改一:添加當前登陸用戶爲owner
user mac owner
修改二:在結尾大括號以前添加
include /Users/mac/upload/upload.conf
額外配置文件 用來添加https
修改/Users/mac/upload/upload.conf文件配置

server {
  charset utf-8;
  listen 8089;
  server_name http_host;
  root /Users/mac/upload/;
  autoindex on;
  add_header Cache-Control "no-cache, must-revalidate";
  location / {
    add_header Access-Control-Allow-Origin *;
  }
}

server {
  listen 443 default ssl;
  server_name https_host;
  root /Users/mac/upload/;
  autoindex on;
  add_header Cache-Control "no-cache, must-revalidate";
  location / {
    add_header Access-Control-Allow-Origin *;
  }
  ssl_certificate /Users/mac/Documents/https/budai.store.pem;
  ssl_certificate_key /Users/mac/Documents/https/budai.store.key;
  ssl_session_timeout 5m;
  ssl_protocols SSLv3 TLSv1;
  ssl_ciphers ALL:ADH:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv2:+EXP;
  ssl_prefer_server_ciphers  on;
}

配置證書

  • https證書:/Users/mac/Documents/https/budai.store.pem;
  • https私鑰:/Users/mac/Documents/https/budai.store.key;
啓動服務
啓動nginx服務

sudo nginx

重啓nginx

sudo nginx -s reload

中止nginx服務:

sudo nginx -s stop

檢查配置文件是否錯誤

sudo nginx -t
訪問地址

  • http:http://localhost:8089
  • https:https://localhost
MySQL數據庫搭建
安裝MySQL

地址:[https://dev.mysql.com/downloa...
](https://dev.mysql.com/downloa...

安裝Navicat

[https://www.navicat.com.cn/pr...
](https://www.navicat.com.cn/pr...

啓動mysql

windows參考:https://blog.csdn.net/ycxzuoxin/article/details/80908447
mac參考https://blog.csdn.net/ycxzuoxin/article/details/80908447

cd /usr/local/mysql-8.0.13-macos10.14-x86_64/bin
./mysql

初始化數據庫
建立數據庫book,選擇utf-8,下載book.sql https://www.youbaobao.xyz/resource/admin/book.sql
執行 source book.sql導入數據

用戶登陸

登陸流程分析

界面簡化

將login中的template改成

<template>
  <div class="login-container">
    <el-form
      ref="loginForm"
      :model="loginForm"
      :rules="loginRules"
      class="login-form"
      autocomplete="on"
      label-position="left"
    >

      <div class="title-container">
        <h3 class="title">小慕讀書</h3>
      </div>

      <el-form-item prop="username">
        <span class="svg-container">
          <svg-icon icon-class="user" />
        </span>
        <el-input
          ref="username"
          v-model="loginForm.username"
          placeholder="請輸入用戶名"
          name="username"
          type="text"
          tabindex="1"
          autocomplete="on"
        />
      </el-form-item>

      <el-tooltip v-model="capsTooltip" content="Caps lock is On" placement="right" manual>
        <el-form-item prop="password">
          <span class="svg-container">
            <svg-icon icon-class="password" />
          </span>
          <el-input
            :key="passwordType"
            ref="password"
            v-model="loginForm.password"
            :type="passwordType"
            placeholder="密碼"
            name="password"
            tabindex="2"
            autocomplete="on"
            @keyup.native="checkCapslock"
            @blur="capsTooltip = false"
            @keyup.enter.native="handleLogin"
          />
          <span class="show-pwd" @click="showPwd">
            <svg-icon :icon-class="passwordType === 'password' ? 'eye' : 'eye-open'" />
          </span>
        </el-form-item>
      </el-tooltip>
      <el-button
        :loading="loading"
        type="primary"
        style="width:100%;margin-bottom:30px;"
        @click.native.prevent="handleLogin"
      >登陸
      </el-button>

    </el-form>

  </div>
</template>
邏輯簡化
  • 刪除SocialSign組件引用
  • 刪除src/views/login/components目錄
  • 刪除 afterQRScan
  • 刪除created 和destoryed
路由處理實例
建立組件

建立組件 src/views/book/create.vue

配置路由

修改 src/router/index.js 的asyncRoutes 新加入頁面路由

{
    path: '/book',
    component: Layout,
    redirect: '/book/create',
    meta: { title: '圖書管理', icon: 'documentation', rules: ['admin'] },
    children: [
      {
        path: '/book/create',
        component: () => import('@/views/book/create'),
        meta: { title: '上傳圖書', icon: 'edit', roles: ['admin'] }
      }
    ]
  },
測試
  • 使用editor登陸,沒法看到添加圖書功能
  • 使用admin登陸平臺,能夠看到添加圖書功能

路由和權限校驗

路由處理邏輯分析

路由處理邏輯圖以下

路由場景分析
  • 已獲取Token

    • 訪問/login,重定向到 /
    • 訪問/login?redirect=/xxx 重定向到/xxx
    • 訪問/login之外的直接訪問
  • 未獲取Token

    • 訪問/login 直接訪問
    • 訪問login之外的 如/dashboard會訪問路徑/login?redirect=%2Fdashboard 登陸後直接重定向 /dashboard
路由邏輯源碼

第一步、main.js 加載了全局路由守衛
import './permission' /
第二步、permission 定義全局路由守衛

router.beforeEach(async(to, from, next) => {
  // 啓動進度條
  NProgress.start()

  // 修改頁面標題
  document.title = getPageTitle(to.meta.title)

  // 從 Cookie 獲取 Token
  const hasToken = getToken()

  // 判斷 Token 是否存在
  if (hasToken) {
    // 若是當前路徑爲 login 則直接重定向至首頁
    if (to.path === '/login') {
      next({ path: '/' })
      NProgress.done()
    } else {
      // 判斷用戶的角色是否存在
      const hasRoles = store.getters.roles && store.getters.roles.length > 0
      // 若是用戶角色存在,則直接訪問
      if (hasRoles) {
        next()
      } else {
        try {
          // 異步獲取用戶的角色
          const { roles } = await store.dispatch('user/getInfo')
          // 根據用戶角色,動態生成路由
          const accessRoutes = await store.dispatch('permission/generateRoutes', roles)
          // 調用 router.addRoutes 動態添加路由
          router.addRoutes(accessRoutes)
          // 使用 replace 訪問路由,不會在 history 中留下記錄
          next({ ...to, replace: true })
        } catch (error) {
          // 移除 Token 數據
          await store.dispatch('user/resetToken')
          // 顯示錯誤提示
          Message.error(error || 'Has Error')
          // 重定向至登陸頁面
          next(`/login?redirect=${to.path}`)
          NProgress.done()
        }
      }
    }
  } else {
    // 若是訪問的 URL 在白名單中,則直接訪問
    if (whiteList.indexOf(to.path) !== -1) {
      next()
    } else {
      // 若是訪問的 URL 不在白名單中,則直接重定向到登陸頁面,並將訪問的 URL 添加到 redirect 參數中
      next(`/login?redirect=${to.path}`)
      NProgress.done()
    }
  }
})

router.afterEach(() => {
  // 中止進度條
  NProgress.done()
})

獲取角色信息動態路由生成

動態路由分析
動態路由流程圖

動態路由源碼分析

生成動態路由的源碼位於 src/store/modules/permission.js中的 generateRoutes方法

import { asyncRoutes, constantRoutes } from '@/router'

generateRoutes({ commit }, roles) {
  // 返回 Promise 對象
  return new Promise(resolve => {
    let accessedRoutes
    if (roles.includes('admin')) {
      // 若是角色中包含 admin,則直接跳過判斷,直接將 asyncRoutes 所有返回
      accessedRoutes = asyncRoutes || []
    } else {
      // 若是角色中沒有包含 admin,則調用 filterAsyncRoutes 過濾路由
      accessedRoutes = filterAsyncRoutes(asyncRoutes, roles)
    }
    // 將路由保存到 vuex 中
    commit('SET_ROUTES', accessedRoutes)
    resolve(accessedRoutes)
  })
}

SET_ROUTES方法源碼以下

SET_ROUTES: (state, routes) => {
  // 將 routes 保存到 state 中的 addRoutes
  state.addRoutes = routes
  // 將 routes 集成到 src/router/index.js 的 constantRoutes 中
  state.routes = constantRoutes.concat(routes)
}

路由過濾的方法 filterAsyncRoutes源碼以下

/**
 * @params routes - 異步加載的路由
 * @params roles - 用戶的角色,數組形式
 */
export function filterAsyncRoutes(routes, roles) {
  const res = []

  // 遍歷所有路由
  routes.forEach(route => {
    // 對路由進行淺拷貝,注意 children 不會拷貝,由於不須要對 children 進行判斷,因此可使用淺拷貝
    const tmp = { ...route }
    // 檢查用戶角色是否具有訪問路由的權限
    if (hasPermission(roles, tmp)) {
      // 當路由具備訪問權限時,判斷路由是否具有 children 屬性
      if (tmp.children) {
        // 當路由包含 children 時,對 children 迭代調用 filterAsyncRoutes 方法
        tmp.children = filterAsyncRoutes(tmp.children, roles)
      }
      // 當路由具備訪問權限時,將 tmp 保存到 res 中
      res.push(tmp)
    }
  })

  return res
}

檢查權限方法 hasPermission 源碼以下

function hasPermission(roles, route) {
  // 檢查路由是否包含 meta 和 meta.roles 屬性
  if (route.meta && route.meta.roles) {
    // 判斷 route.meta.roles 中是否包含用戶角色 roles 中的任何一個權限,若是包含則返回 true,不然爲 false
    return roles.some(role => route.meta.roles.includes(role))
  } else {
    // 若是路由沒有 meta 或 meta.roles 屬性,則視爲該路由不須要進行權限控制,全部用戶對該路由都具備訪問權限
    return true
  }
}
相關庫分析
NProgress.start();
NProgress.done();
NProgress.configure({ showSpinner: false }) //showSpinner 能夠控制右側的環形進度條是否顯示
總結
關於路由處理
  • vue-element-admin會對訪問的全部路由進行攔截
  • 訪問路由時會從Cookies中獲取Token,判斷Token是否存在

    • 若是Token存在,將根據用戶角色生成動態路由,而後訪問路由,生成對應的頁面組件,有一個特例若是訪問/login 會重定向到 / 路由
    • 若是Token不存在,則會判斷路由是否在白名單 在白名單會直接訪問,不然說明須要登陸才能訪問 此時路由會生成一個redirect參數傳入login組件,實際訪問的路由爲/login?redirect=/xxx
關於動態路由和權限校驗
  • 路由分爲constantRoutes 和 asyncRoutes
  • 用戶登陸系統時,會動態生成路由,其中constantRoutes必然包含,而asyncRoutes會進行過濾
  • asyncRoutes過濾邏輯看路由下是否存在meta和meta.roles屬性,若是沒有此屬性,是一個通用路由,不進行權限校驗,若是roles屬性存在會判斷用戶的角色是否命中路由中的任意一個權限,命中就保留路由,反之捨棄
  • asyncRoutes 處理完畢後,會和 constantRoutes 合併爲一個新的路由對象,並保存到 vuex 的 permission/routes 中
  • 當用戶登陸時 側邊欄從vuex中獲取state.permission.routes,根據該路由動態渲染用戶菜單
側邊欄
若是讓你實現一個側邊欄,你會如何設計
源碼位置
  • sidebar引用自layout組件,layout組件位於 src/layout/index.vue
  • sidebar組件源碼位於 src/layout/components/Sidebar/index,vue
el-menu 用法解析

側邊欄的核心是將根據權限過濾後的 router和 el-menu 組件進行映射,因此熟悉el-menu是理解sidebar的起點

<el-row class="tac">
    <el-col :span="12">
      <el-menu
        default-active="1-1"
        background-color="#545c64"
        text-color="#fff"
        active-text-color="#ffd04b"
        mode="vertical"
        unique-opened
        :collapse="isCollapse"
        :collapse-transition="false"
        class="el-menu-vertical-demo"
        @open="handleOpen"
        @close="handleClose"
        @select="handleSelect"
      >
        <el-submenu index="1">
          <template slot="title">
            <i class="el-icon-location"></i>
            <span>導航一</span>
          </template>
          <el-menu-item-group>
            <template slot="title">分組一</template>
            <el-menu-item index="1-1">選項1</el-menu-item>
            <el-menu-item index="1-2">選項2</el-menu-item>
          </el-menu-item-group>
          <el-menu-item-group title="分組2">
            <el-menu-item index="1-3">選項3</el-menu-item>
          </el-menu-item-group>
          <el-submenu index="1-4">
            <template slot="title">選項4</template>
            <el-menu-item index="1-4-1">選項1</el-menu-item>
          </el-submenu>
        </el-submenu>
        <el-submenu index="2">
          <template slot="title">
            <i class="el-icon-menu"></i>
            <span slot="title">導航二</span>
          </template>
          <el-menu-item index="2-1">選項2-1</el-menu-item>
        </el-submenu>
        <el-menu-item index="3" disabled>
          <i class="el-icon-document"></i>
          <span slot="title">導航三</span>
        </el-menu-item>
        <el-menu-item index="4">
          <i class="el-icon-setting"></i>
          <span slot="title">導航四</span>
        </el-menu-item>
      </el-menu>
    </el-col>
    <el-col>
      <el-button @click="isCollapse = !isCollapse">摺疊</el-button>
    </el-col>
  </el-row>
</template>

<script>
export default {
  data() {
    return {
      isCollapse: false
    }
  },
  methods: {
    handleSelect(key, keyPath) {
      console.log('handleSelect', key, keyPath)
    },
    handleOpen(key, keyPath) {
      console.log('handleOpen', key, keyPath)
    },
    handleClose(key, keyPath) {
      console.log('handleClose', key, keyPath)
    }
  }
}
</script>
el-menu

el-menu 表示菜單容器組件

  • default-active:激活的菜單,注意若是存在子菜單,須要填入子菜單ID
  • unique-opened: 是否保存一個菜單打開
  • mode:枚舉值 分爲vertical和 horizontal兩種
  • collapse:是否水平摺疊收起菜單
  • @select:點擊菜單事件,keypath表明菜單的訪問路徑,如 1-4-1 菜單的點擊日誌爲:
handleSelect 1-4-1 (3) ["1", "1-4", "1-4-1"]

獲取keypath 咱們能夠獲取1-4-1菜單的全部父級菜單的ID

  • @open:父菜單打開時觸發事件
  • @close:父菜單關閉時觸發事件
el-submenu

子菜單容器,el-submenu與el-menu不一樣,el-menu表示整個菜單,而el-submenu表示一個具體菜單,只是該菜單保含了子菜單
el-submenu 能夠經過定製solt的title來自定義菜單樣式

<el-submenu index="1">
    <template slot="title">
      <i class="el-icon-location"></i>
      <span>導航一</span>
    </template>
</el-submenu>

el-submenu 容器內的default的solt用來存放子菜單,能夠包含三種子菜單組件

  • el-menu-item-group:菜單分組,爲一組菜單添加一個標題,el-menu-item-group容器內須要存放el-menu-item組件,支持經過title的solt來定製標題樣式
  • el-submenu:el-submenu 支持循環嵌套 el-submenu,這使得超過兩級子組件得以實現
  • el-menu-item:子菜單組件
sidebar-item源碼分析

sidebar-item 組件源碼以下:

<template>
  <div v-if="!item.hidden" class="menu-wrapper">
    <template v-if="hasOneShowingChild(item.children,item) && (!onlyOneChild.children||onlyOneChild.noShowingChildren)&&!item.alwaysShow">
      <app-link v-if="onlyOneChild.meta" :to="resolvePath(onlyOneChild.path)">
        <el-menu-item :index="resolvePath(onlyOneChild.path)" :class="{'submenu-title-noDropdown':!isNest}">
          <item :icon="onlyOneChild.meta.icon||(item.meta&&item.meta.icon)" :title="onlyOneChild.meta.title" />
        </el-menu-item>
      </app-link>
    </template>

    <el-submenu v-else ref="subMenu" :index="resolvePath(item.path)" popper-append-to-body>
      <template slot="title">
        <item v-if="item.meta" :icon="item.meta && item.meta.icon" :title="item.meta.title" />
      </template>
      <sidebar-item
        v-for="child in item.children"
        :key="child.path"
        :is-nest="true"
        :item="child"
        :base-path="resolvePath(child.path)"
        class="nest-menu"
      />
    </el-submenu>
  </div>
</template>

<script>
import path from 'path'
import { isExternal } from '@/utils/validate'
import Item from './Item'
import AppLink from './Link'
import FixiOSBug from './FixiOSBug'

export default {
  name: 'SidebarItem',
  components: { Item, AppLink },
  mixins: [FixiOSBug],
  props: {
    // route object
    item: {
      type: Object,
      required: true
    },
    isNest: {
      type: Boolean,
      default: false
    },
    basePath: {
      type: String,
      default: ''
    }
  },
  data() {
    // To fix https://github.com/PanJiaChen/vue-admin-template/issues/237
    // TODO: refactor with render function
    this.onlyOneChild = null
    return {}
  },
  methods: {
    hasOneShowingChild(children = [], parent) {
      const showingChildren = children.filter(item => {
        if (item.hidden) {
          return false
        } else {
          // Temp set(will be used if only has one showing child)
          this.onlyOneChild = item
          return true
        }
      })

      // When there is only one child router, the child router is displayed by default
      if (showingChildren.length === 1) {
        return true
      }

      // Show parent if there are no child router to display
      if (showingChildren.length === 0) {
        this.onlyOneChild = { ... parent, path: '', noShowingChildren: true }
        return true
      }

      return false
    },
    resolvePath(routePath) {
      if (isExternal(routePath)) {
        return routePath
      }
      if (isExternal(this.basePath)) {
        return this.basePath
      }
      return path.resolve(this.basePath, routePath)
    }
  }
}
</script>
sidebar-item props分析

sidebar-item的props以下:

  • item:路由對象
  • basepath:路由路徑
sidebar-item 展現邏輯分析

sidebar-item 最重要的是展現邏輯,主要分爲如下幾步:

  • 經過item.hidden 控制菜單是否展現
  • 經過 hasoneShowingChild(item.children,item)&&(!onlyOneCHild.children||onlyOneChild.noShowingChildren)&&!item.alwaysShow 邏輯判斷template菜單是否展現,template表明單一菜單

    • hasOnsShowingChild:判斷是否只有一個須要展現的子路由
    • !onlyOneChild.children || onlyOneChild.noShowingChildren:判斷須要展現的子菜單,是否包含children屬性,若是包含,則說明子菜單可能存在孫子菜單,此時須要在判斷noShowingChildren屬性
    • !item.alwaysShow:判斷路由中是否存在alwaysShow屬性,若是存在,則返回false,不展現template菜單,也就是說只要配置了alwaysShow屬性就會直接進入el-submenu組件
hasOnsShowingChild 方法源碼詳解

入參:

  • children:router對象的children屬性
  • item:router對象
hasOneShowingChild(children = [], parent) {
  const showingChildren = children.filter(item => {
    // 若是 children 中的路由包含 hidden 屬性,則返回 false
    if (item.hidden) {
      return false
    } else {
      // 將子路由賦值給 onlyOneChild,用於只包含一個路由時展現 
      this.onlyOneChild = item
      return true
    }
  })

  // 若是過濾後,只包含展現一個路由,則返回 true
  if (showingChildren.length === 1) {
    return true
  }

  // 若是沒有子路由須要展現,則將 onlyOneChild 的 path 設置空路由,並添加 noShowingChildren 屬性,表示雖然有子路由,可是不須要展現子路由
  if (showingChildren.length === 0) {
    this.onlyOneChild = { ...parent, path: '', noShowingChildren: true }
    return true
  }

  // 返回 false,表示不須要展現子路由,或者超過一個須要展現的子路由
  return false
}
  • 若是展現template組件,首先會展現app-link組件,而後是el-menu-item,最裏面嵌套的是item組件:

item組件須要路由meta中包含 title和icon屬性,不然將渲染內容爲空的vnode對象

<app-link v-if="onlyOneChild.meta" :to="resolvePath(onlyOneChild.path)">
  <el-menu-item :index="resolvePath(onlyOneChild.path)" :class="{'submenu-title-noDropdown':!isNest}">
      <item :icon="onlyOneChild.meta.icon||(item.meta&&item.meta.icon)" :title="onlyOneChild.meta.title" />
  </el-menu-item>
</app-link>
  • 若是template菜單不展現,則展現el-submenu菜單,el-submenu邏輯中採用了嵌套組件的作法,將sidebar-item 嵌套在 el-submenu 中:
<el-submenu v-else ref="subMenu" :index="resolvePath(item.path)" popper-append-to-body>
  <template slot="title">
    <item v-if="item.meta" :icon="item.meta && item.meta.icon" :title="item.meta.title" />
  </template>
  <sidebar-item
    v-for="child in item.children"
    :key="child.path"
    :is-nest="true"
    :item="child"
    :base-path="resolvePath(child.path)"
    class="nest-menu"
  />
</el-submenu>

el-submenu 中的 sidebar-item有兩點區別:

  • 第一是傳入is-nest參數
  • 第二是傳入base-path 參數
app-link 源碼分析

app-link 是一個動態組件,經過解析to參數,若是包含http前綴則變成a標籤,不然變成一個router-link組件

<template>
  <!-- eslint-disable vue/require-component-is -->
  <component v-bind="linkProps(to)">
    <slot />
  </component>
</template>

<script>
import { isExternal } from '@/utils/validate'

export default {
  props: {
    to: {
      type: String,
      required: true
    }
  },
  methods: {
    linkProps(url) {
      if (isExternal(url)) {
        return {
          is: 'a',
          href: url,
          target: '_blank',
          rel: 'noopener'
        }
      }
      return {
        is: 'router-link',
        to: url
      }
    }
  }
}
</script>

isExternal函數經過一個正則表達式匹配http連接

export function isExternal(path) {
  return /^(https?:|mailto:|tel:)/.test(path)
}
item組件源碼分析

item組件經過定義render函數完成組件渲染

<script>
export default {
  name: 'MenuItem',
  functional: true,
  props: {
    icon: {
      type: String,
      default: ''
    },
    title: {
      type: String,
      default: ''
    }
  },
  render(h, context) {
    const { icon, title } = context.props
    const vnodes = []

    if (icon) {
      vnodes.push(<svg-icon icon-class={icon}/>)
    }

    if (title) {
      vnodes.push(<span slot='title'>{(title)}</span>)
    }
    return vnodes
  }
}
</script>
總結
  • sidebar:sidebar主要包含el-menu容器組件,el-menu中遍歷vuex中的routes,生成sidebar-item 組件,sidebar主要配置項以下:

    • activeMenu:根據當前路由的meta.activeMenu 屬性控制側邊欄中的高亮菜單
    • isCollapse:根據Cookie的sidebarStatus控制側邊欄是否摺疊
    • variable:經過@/stylels/variables.scss 填充el-menu的基本樣式
  • sidebar-item:sidebar-item 分爲兩部分:

    • 第一部分是當只須要展現一個children或者沒有children時進行展現,展現的組件包括:

      • app-link:動態組件,path爲連接時,顯示爲a標籤,path爲路由時,顯示爲router-link組件
      • el-menu-item:菜單項,當sidebar-item爲非nest組件時,el-menu-item會增長submenu-title-noDropdown的class
      • item:el-menu-item裏的內容,主要是icon和title,當title爲空時,整個菜單項將不會展現
      • 第二部分是當children超過兩項時進行展現,展現的組件包括:

        • el-submenu:子菜單組件容器,用於嵌套子菜單組件
        • sidebar-item:el-submenu迭代嵌套了sidebar-item組件,在sidebar-item組件中有兩點變化

          * 設置is-nest屬性爲true
          * 根據child.path 生成了 base-path屬性傳入sidebar-item 組件
重定向
如何實現重定向功能
登陸重定向

login.vue 中對 $route 進行監聽

watch: {
  $route: {
    handler: function(route) {
      const query = route.query
      if (query) {
        this.redirect = query.redirect
        this.otherQuery = this.getOtherQuery(query)
      }
    },
    immediate: true
  }
}

this.getOtherQuery(query)的用途是獲取除redirect外的其餘查詢條件,登陸成功後:

this.$store.dispatch('user/login', this.loginForm)
.then(() => {
  this.$router.push({ path: this.redirect || '/', query: this.otherQuery })
  this.loading = false
})
.catch(() => {
  this.loading = false
})

完成重定向的代碼爲:
this.$router.push({ path: this.redirect || '/', query: this.otherQuery })

重定向組件

vue-element-admin 提供了專門的重定向組件,源碼以下:

<script>
export default {
  created() {
    const { params, query } = this.$route
    const { path } = params
    this.$router.replace({ path: '/' + path, query })
  },
  render: function(h) {
    return h() // avoid warning message
  }
}
</script>

重定向組件配置了動態路由

{
    path: '/redirect',
    component: Layout,
    hidden: true,
    children: [
      {
        path: '/redirect/:path*',
        component: () => import('@/views/redirect/index')
      }
    ]
}

還有一個細節
path: '/redirect/:path*'
表示匹配零個或多個路由,好比路由爲 /redirect 時,仍然能匹配到redirect組件,若是將路由改成
path: '/redirect/:path'
此時路由 /redirect 將只能匹配到layout組件,而沒法匹配redirect組件

麪包屑導航
如何實現麪包屑導航
el-breadcrumb-item
  • el-breadcrumb-item:麪包屑導航容器,separator控制麪包屑導航文本中的分割線
  • el-breadcrumb-item:麪包屑子項目,可使用to屬性切換路由,slot中能夠包含a標籤來跳轉到外鏈
<el-breadcrumb separator="/">
  <el-breadcrumb-item :to="{ path: '/' }">首頁</el-breadcrumb-item>
  <el-breadcrumb-item><a href="/">活動管理</a></el-breadcrumb-item>
  <el-breadcrumb-item>活動列表</el-breadcrumb-item>
  <el-breadcrumb-item>活動詳情</el-breadcrumb-item>
</el-breadcrumb>

使用to屬性和a標籤切換路由的區別是:to屬性切換路由是動態替換app.vue中的路由內容,而a標籤切換路由會刷新界面

路由與麪包屑導航映射

麪包屑導航的最大難度在於如何將路由與麪包屑導航進行映射
生成麪包屑導航

getBreadcrumb() {
  let matched = this.$route.matched.filter(item => item.meta && item.meta.title)
  const first = matched[0]

  if (!this.isDashboard(first)) {
    matched = [{ path: '/dashboard', meta: { title: 'Dashboard' }}].concat(matched)
  }

  this.levelList = matched.filter(item => item.meta && item.meta.title && item.meta.breadcrumb !== false)
}

麪包屑導航實現的邏輯以下

  • 獲取this.$route.matched 並過濾其中不包含 item.meta.title的項,生成新的麪包屑導航數組 matched
  • 判斷 matched 第一項是否爲dashboard,若是不是,則添加dashboard爲麪包屑導航第一項
  • 再次過濾matched中的item.meta.title爲空的項和item.meta.breadcrumb爲false的項
這裏的關鍵是this.$route.matched 屬性,它是一個數組,記錄了路由匹配的過程,是麪包屑導航實現的基礎

isDashboard實現以下:

isDashboard(route) {
  const name = route && route.name
  if (!name) {
    return false
  }
  return name.trim().toLocaleLowerCase() === 'Dashboard'.toLocaleLowerCase()
}
渲染麪包屑導航

麪包屑導航模板源碼

<el-breadcrumb class="app-breadcrumb" separator="/">
    <transition-group name="breadcrumb">
      <el-breadcrumb-item v-for="(item,index) in levelList" :key="item.path">
        <span v-if="item.redirect==='noRedirect'||index==levelList.length-1" class="no-redirect">{{ item.meta.title }}</span>
        <a v-else @click.prevent="handleLink(item)">{{ item.meta.title }}</a>
      </el-breadcrumb-item>
    </transition-group>
</el-breadcrumb>

el-breadcrumb-item 內作了一個判斷,若是最後一個元素或者路由的redirect屬性指定爲noRedirect 則不會生成連接,不然將使用 a標籤生成連接,可是這裏使用了 @click.prevent 阻止了默認a標籤的事件觸發,而使用自定義的 handlink 方法處理路由跳轉,handlink方法源碼以下:

handleLink(item) {
  const { redirect, path } = item
  if (redirect) {
    this.$router.push(redirect)
    return
  }
  this.$router.push(this.pathCompile(path))
}

這裏的 pathCompile 用於解決動態路由匹配問題

用戶登陸(中)

登陸組件分析

登陸組件login.vue 佈局要點以下:

  • el-form容器,包含username和password兩個 el-form-item,el-form主要屬性:

    • model爲loginForm
    • rules爲loginRules
  • password使用了el-tooltip提示,當用戶打開大小寫時,會進行提示,主要屬性:

    • manual:手動控制模式,設置爲true後,mouseenter和mouseleave事件將不會生效
    • placement:提示出現的位置
  • password對應el-input主要屬性:

    • @keyup.native='checkCapsLock' 鍵盤按鍵時綁定 checkCapslock事件
    • @keyup.enter.native='handleLogin' 監聽鍵盤enter按下後的事件
>  這裏綁定@keyup 事件須要添加 .native 修飾符,這是由於咱們的事件綁定在el-input組件上,因此若是不添加.native修飾符,事件將沒法綁定到原生的input標籤上
  • 包含一個el-button,點擊時調用handleLogin方法,並觸發loading效果
checkCapsLock方法

checkCapsLock 方法的主要用途是監聽用戶鍵盤輸入,顯示提示文字的判斷邏輯以下

  • 按住shift時輸入小寫字符
  • 未按shift時輸入大寫字符

當按下CapsLock按鍵時,若是按下後是小寫模式,則會當即消除提示文字

checkCapslock({ shiftKey, key } = {}) {
  if (key && key.length === 1) {
    if (shiftKey && (key >= 'a' && key <= 'z') || !shiftKey && (key >= 'A' && key <= 'Z')) {
      this.capsTooltip = true
    } else {
      this.capsTooltip = false
    }
  }
  if (key === 'CapsLock' && this.capsTooltip === true) {
    this.capsTooltip = false
  }
}
handleLogin 方法

handleLogin 方法處理流程以下:

  • 調用 el-form的validate方法對rules進行驗證;
  • 若是驗證經過,則會調用vuex的 user/login action 進行登陸驗證
  • 登陸驗證經過後,會重定向到redirect路由,若是redirect路由不存在,則直接重定向到 / 路由
這裏須要注意:因爲vuex中的user制定了namespaced爲true,因此dispatch時須要加上namespace,不然將沒法調用vuex中的action
handleLogin() {
  this.$refs.loginForm.validate(valid => {
    if (valid) {
      this.loading = true
      this.$store.dispatch('user/login', this.loginForm)
        .then(() => {
          this.$router.push({ path: this.redirect || '/', query: this.otherQuery })
          this.loading = false
        })
        .catch(() => {
          this.loading = false
        })
    } else {
      console.log('error submit!!')
      return false
    }
  })
}
user/login 方法

user/login 方法調用了login API,傳入username和password參數,請求成功後會從response中獲取token,而後將token保存到cookies中,以後返回,若是請求失敗,將調用reject方法,交由咱們自定義的request模塊來處理異常

login({ commit }, userInfo) {
    const { username, password } = userInfo
    return new Promise((resolve, reject) => {
      login({ username: username.trim(), password: password }).then(response => {
        const { data } = response
        commit('SET_TOKEN', data.token)
        setToken(data.token)
        resolve()
      }).catch(error => {
        reject(error)
      })
    })
}

login API的方法以下:

import request from '@/utils/request'

export function login(data) {
  return request({
    url: '/user/login',
    method: 'post',
    data
  })
}

這裏使用request方法,它是一個基於axios封裝的庫,目前咱們的/user/login 接口是經過mock實現的,用戶的數據位於 /mock/user.js

axios用法分析

request 庫使用了 axios的手動實例化方法create來封裝請求,要理解其中的用法,咱們首先須要學習axios庫的用法

axios基本案例

咱們先從一個普通的axios示例開始

import axios from 'axios'

const url = 'https://test.youbaobao.xyz:18081/book/home/v2?openId=1234'
axios.get(url).then(response => {
  console.log(response)
})

上述代碼能夠改成

import axios from 'axios'

const url = 'https://test.youbaobao.xyz:18081/book/home/v2?openId=1234'
axios.get(url).then(response => {
  console.log(response)
})

若是咱們在請求是須要在headr中添加一個token,須要將代碼修改成:

const url = 'https://test.youbaobao.xyz:18081/book/home/v2'
axios.get(url, { 
  params: { openId: '1234' },
  headers: { token: 'abcd' }
}).then(response => {
  console.log(response)
}).catch(err => {
  console.log(err)
})

這樣改動能夠實現咱們的需求,可是有兩個問題

  • 每一個須要傳入token的請求都須要添加header對象 會形成大量重複代碼
  • 每一個請求都須要手動定義異常處理,而異常處理的邏輯大可能是一致的,若是將其封裝成通用的異常處理方法,呢麼每一個請求都有調用一遍
axios.create 示例

下面咱們使用axios.create 對整個請求進行重構

const url = '/book/home/v2'
const request = axios.create({
  baseURL: 'https://test.youbaobao.xyz:18081',
  timeout: 5000
})
request({
  url, 
  method: 'get',
  params: {
    openId: '1234'
  }
})

首先咱們經過 axios.create 生成一個函數,該函數是axios示例,經過執行該方法完成請求,它與直接調用axios.get 區別以下

  • 須要傳入url參數,axios.get 方法的第一個參數是url
  • 須要傳入method參數,axios.get 方法已經表示發起get請求
axios請求攔截器

上述代碼完成了基本請求的功能,下面咱們須要爲http請求的headers中添加token,同時進行白名單校驗,如 /login 不須要添加token,並實現異步捕獲和自定義處理:

const whiteUrl = [ '/login', '/book/home/v2' ]
const url = '/book/home/v2'
const request = axios.create({
  baseURL: 'https://test.youbaobao.xyz:18081',
  timeout: 5000
})
request.interceptors.request.use(
  config => {
    // throw new Error('error...')
    const url = config.url.replace(config.baseURL, '')
      if (whiteUrl.some(wl => url === wl)) {
        return config
      }
    config.headers['token'] = 'abcd'
    return config
  },
  error => {
    return Promise.reject(error)
  }
)
request({
  url, 
  method: 'get',
  params: {
    openId: '1234'
  }
}).catch(err => {
  console.log(err)
})

這裏核心是調用了 request.interceptors.request.use 方法,即axios的請求攔截器,該方法須要傳入兩個參數,第一個參數爲攔截器方法,包含config參數,咱們能夠在這個方法中修改config而且進行回傳,第二個參數是異常處理方法,咱們可使用 Promise.reject(error)將異常返回給用戶進行處理,因此咱們在request請求後能夠經過catch捕獲異常進行自定義處理

axios響應攔截器

下面咱們進一步加強axios功能 咱們在實際開發中除了須要保障http statusCode爲 200 ,還須要保障業務代碼正確,上述案例中 我定義了 error_code 爲0時,表示業務正常,若是返回值不爲0 則說明業務處理出錯 此時咱們經過 request.interceptors.response.use 方法定義響應攔截器,它仍然須要2個參數,與請求攔截器相似,注意第二個參數主要處理 statusCode 非200的異常請求,源碼以下:

const whiteUrl = [ '/login', '/book/home/v2' ]
const url = '/book/home/v2'
const request = axios.create({
  baseURL: 'https://test.youbaobao.xyz:18081',
  timeout: 5000
})
request.interceptors.request.use(
  config => {
    const url = config.url.replace(config.baseURL, '')
    if (whiteUrl.some(wl => url === wl)) {
      return config
    }
    config.headers['token'] = 'abcd'
    return config
  },
  error => {
    return Promise.reject(error)
  }
)

request.interceptors.response.use(
  response => {
    const res = response.data
    if (res.error_code != 0) {
      alert(res.msg)
      return Promise.reject(new Error(res.msg))
    } else {
      return res
    }
  },
  error => {
    return Promise.reject(error)
  }
)

request({
  url, 
  method: 'get',
  params: {
    openId: '1234'
  }
}).then(response => {
  console.log(response)
}).catch(err => {
  console.log(err)
})
request 庫源碼分析

有了上述基礎後,咱們在看request庫源碼就很是容易了

const service = axios.create({
  baseURL: process.env.VUE_APP_BASE_API,
  timeout: 5000
})

service.interceptors.request.use(
  config => {
    // 若是存在 token 則附帶在 http header 中
    if (store.getters.token) {
      config.headers['X-Token'] = getToken()
    }
    return config
  },
  error => {
    return Promise.reject(error)
  }
)

service.interceptors.response.use(
  response => {
    const res = response.data

    if (res.code !== 20000) {
      Message({
        message: res.message || 'Error',
        type: 'error',
        duration: 5 * 1000
      })
      // 判斷 token 失效的場景
      if (res.code === 50008 || res.code === 50012 || res.code === 50014) {
        // 若是 token 失效,則彈出確認對話框,用戶點擊後,清空 token 並返回登陸頁面
        MessageBox.confirm('You have been logged out, you can cancel to stay on this page, or log in again', 'Confirm logout', {
          confirmButtonText: 'Re-Login',
          cancelButtonText: 'Cancel',
          type: 'warning'
        }).then(() => {
          store.dispatch('user/resetToken').then(() => {
            location.reload()
          })
        })
      }
      return Promise.reject(new Error(res.message || 'Error'))
    } else {
      return res
    }
  },
  error => {
    Message({
      message: error.message,
      type: 'error',
      duration: 5 * 1000
    })
    return Promise.reject(error)
  }
)

export default service
登陸細節分析
細節一:頁面啓動後自動聚焦

檢查用戶名或密碼是否爲空,若是發現爲空,則自動彙集:

mounted() {
    if (this.loginForm.username === '') {
      this.$refs.username.focus()
    } else if (this.loginForm.password === '') {
      this.$refs.password.focus()
    }
}
細節二:顯示密碼後自動彙集

切換密碼顯示狀態後,自動彙集password輸入框:

showPwd() {
  if (this.passwordType === 'password') {
    this.passwordType = ''
  } else {
    this.passwordType = 'password'
  }
  this.$nextTick(() => {
    this.$refs.password.focus()
  })
}
細節三:經過reduce過濾對象屬性
const query = {
  redirect: '/book/list',
  name: 'sam',
  id: '1234'
}
// 直接刪除 query.redirect,會直接改動 query
// delete query.redirect

// 經過淺拷貝實現屬性過濾
// const _query = Object.assign({}, query)
// delete _query.redirect

const _query = Object.keys(query).reduce((acc, cur) => {
    if (cur !== 'redirect') {
      acc[cur] = query[cur]
    }
    return acc
  }, {})
console.log(query, _query)
關閉Mock接口

去掉main.js 中mock相關代碼

import { mockXHR } from '../mock'
if (process.env.NODE_ENV === 'production') {
  mockXHR()
}

刪除 src/api目錄下2個api文件

article.js
qiniu.js

刪除 vue.config.js 中的相關配置

proxy: {
  // change xxx-api/login => mock/login
  // detail: https://cli.vuejs.org/config/#devserver-proxy
  [process.env.VUE_APP_BASE_API]: {
    target: `http://127.0.0.1:${port}/mock`,
    changeOrigin: true,
    pathRewrite: {
      ['^' + process.env.VUE_APP_BASE_API]: ''
    }
  }
},
after: require('./mock/mock-server.js')

修改後咱們的項目就不能使用mock接口,會直接請求到http接口,咱們須要打開SwitchHosts配置host映射,讓域名映射到本地node項目
127.0.0.1 budai.store

修改接口地址

咱們將發佈到開發環境和生產環境,因此須要修改 .env.development 和 .env.production兩個配置文件;
`VUE_APP_BASE_API = 'https://budai.store:18082'
`
有兩點須要注意:

  • 這裏我使用了域名 budai.store ,你們能夠將其替換爲你本身註冊的域名,若是你還沒註冊域名,使用localhost,
  • 若是沒有申請https證書,也能夠採用http協議,一樣能夠實現登陸請求,但若是要發佈到互聯網建議使用https協議安全性會更好

從新啓動項目後,發現已能夠指向指定的接口
https://budai.store:18082/user/login

用戶登陸下

後端API處理流程

搭建https服務器

首先須要將https證書拷貝到node項目中,而後添加下列代碼

const fs = require('fs')
const https = require('https')

const privateKey = fs.readFileSync('https/budai.store.key', 'utf8')
const certificate = fs.readFileSync('https/budai.store.pem', 'utf8')
const credentials = { key: privateKey, cert: certificate }
const httpsServer = https.createServer(credentials, app)
const SSLPORT = 18082
httpsServer.listen(SSLPORT, function() {
  console.log('HTTPS Server is running on: https://localhost:%s', SSLPORT)
})

啓動https服務須要證書對象credentials,包含了私鑰和證書,從新啓動node服務

node app.js

在瀏覽器輸入
https://budai.store:18082
能夠看到
`歡迎學習小慕讀書管理後臺
`
說明https服務啓動成功

建立 /user/login API

在 router/user.js 中填入如下代碼:

router.post('/login', function(req, res, next) {
  console.log('/user/login', req.body)
  res.json({
    code: 0,
    msg: '登陸成功'
  })
})
$ curl https://budai.store:18082/user/login -X POST -d "username=sam&password=123456"

{"code":0,"msg":"登陸成功"}

上面的命令能夠簡寫爲
`curl https://budai.store:18082/user/login -d "username=sam&password=123456"
`
這裏咱們能夠經過req.body 獲取POST請求中的參數,可是沒有獲取成功,咱們須要經過 body-parser 中間件來解決這個問題:
`npm i -S body-parser
`
在 app.js 中加入

const bodyParser = require('body-parser')

// 建立 express 應用
const app = express()

app.use(bodyParser.urlencoded({ extended: true }))
app.use(bodyParser.json())

返回前端按鈕請求登陸接口,發現控制檯報錯:
`Access to XMLHttpRequest at 'https://budai.store:18082/user/login' from origin 'http://localhost:9527' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.
`
這是因爲前端部署在 http://localhost:9527 然後端部署在 https://budai.store:18082,因此致使了跨域錯誤,咱們須要在 node 服務中添加跨域中間件 cors:
`npm i -S cors
`
而後修改app.js:

const cors = require('cors')

// ...
app.use(cors())

再次請求便可成功,這裏咱們在Network中會發現發起了兩次https請求,這是因爲由於觸發了跨域請求,因此會首先進行OPTIONS請求,判斷服務器是否容許跨域請求,若是容許才能實際進行請求

響應結果封裝

在 /user/login 咱們看到的返回值是

res.json({
  code: 0,
  msg: '登陸成功'
})

以後咱們還要定義錯誤返回值,但若是每一個接口都編寫以上代碼就顯得很是冗餘,並且不易維護,因此咱們建立一個Result類來解決這個問題

node響應結果封裝

建立 /models/Result.js 文件

const {
  CODE_ERROR,
  CODE_SUCCESS
} = require('../utils/constant')

class Result {
  constructor(data, msg = '操做成功', options) {
    this.data = null
    if (arguments.length === 0) {
      this.msg = '操做成功'
    } else if (arguments.length === 1) {
      this.msg = data
    } else {
      this.data = data
      this.msg = msg
      if (options) {
        this.options = options
      }
    }
  }

  createResult() {
    if (!this.code) {
      this.code = CODE_SUCCESS
    }
    let base = {
      code: this.code,
      msg: this.msg
    }
    if (this.data) {
      base.data = this.data
    }
    if (this.options) {
      base = { ...base, ...this.options }
    }
    console.log(base)
    return base
  }

  json(res) {
    res.json(this.createResult())
  }

  success(res) {
    this.code = CODE_SUCCESS
    this.json(res)
  }

  fail(res) {
    this.code = CODE_ERROR
    this.json(res)
  }
}

module.exports = Result

咱們還須要建立 /utils/constant.js

module.exports = {
  CODE_ERROR: -1,
  CODE_SUCCESS: 0
}

Result 使用了 ES6 的Class,使用方法以下

// 調用成功時
new Result().success(res)
new Result('登陸成功').success(res)
// 調用成功時,包含參數
new Result({ token }, '登陸成功').success(res)
// 調用失敗時
new Result('用戶名或密碼不存在').fail(res)

有了Result類之後咱們能夠將登陸API改成:

router.post('/login', function(req, res, next) {
  const username = req.body.username
  const password = req.body.password

  if (username === 'admin' && password === '123456') {
    new Result('登陸成功').success(res)
  } else {
    new Result('登陸失敗').fail(res)
  }
})
若是在響應前跑出error,此時的Error將被咱們自定義的異常處理捕獲,並返回500錯誤
登陸用戶數據庫查詢

安裝mysql庫
` npm i -S mysql
`
建立db目錄,新建兩個文件

index.js
config.js

config.js源碼以下

module.exports = {
  host: 'localhost',
  user: 'root',
  password: '12345678',
  database: 'book'
}
鏈接

鏈接數據庫須要提供使用mysql的

const {host,user,password,database} = require('./config')

function connect() {
  return mysql.createConnection({
    host,
    user,
    password,
    database,
    multipleStatements: true
  })
}
查詢

查詢須要調用connection對象的query方法:

function querySql(sql) {
  const conn = connect()
  debug && console.log(sql)
  return new Promise((resolve, reject) => {
    try {
      conn.query(sql, (err, results) => {
        if (err) {
          debug && console.log('查詢失敗,緣由:' + JSON.stringify(err))
          reject(err)
        } else {
          debug && console.log('查詢成功', JSON.stringify(results))
          resolve(results)
        }
      })
    } catch (e) {
      reject(e)
    } finally {
      conn.end()
    }
  })
}

咱們在 constant.js 建立一個debug參數控制打印日誌:
const debug = require('../utils/constant').debug

這裏須要注意conn對象使用完畢後須要調用end進行關閉,不然會致使內存泄漏

調用方法以下:

db.querySql('select * from book').then(result => {
  console.log(result)
})

這裏咱們須要基於mysql 查詢庫封裝一層service ,用來協調業務邏輯和數據庫邏輯,咱們不但願直接把業務邏輯放在router中,建立 /service/user.js

const { querySql } = require('../db')

function login(username, password) {
  const sql = `select * from admin_user where username='${username}' and password='${password}'`
  return querySql(sql)
}

module.exports = {
  login
}

改造 /user/login API:

router.post('/login', function(req, res, next) {
  const username = req.body.username
  const password = req.body.password

  login(username, password).then(user => {
    if (!user || user.length === 0) {
      new Result('登陸失敗').fail(res)
    } else {
      new Result('登陸成功').success(res)
    }
  })
})

此時即便咱們輸入正確的用戶名和密碼仍然沒法登陸,這是由於密碼採用了MD5+SALT加密,因此咱們須要對密碼進行對等加密,才能查詢成功。在/utils/constant.js 中加入SALT:

module.exports = {
  // ...
  PWD_SALT: 'admin_imooc_node',
}

安裝crypto庫:
`npm i -S crypto
`
而後在 /utils/index.js 中建立md5方法

const crypto = require('crypto')

function md5(s) {
  // 注意參數須要爲 String 類型,不然會出錯
  return crypto.createHash('md5')
    .update(String(s)).digest('hex');
}
const password = md5(`${req.body.password}${PWD_SALT}`)

再次輸入正確的用戶名和密碼,查詢成功:

select * from admin_user where username='admin' and password='91fe0e80d07390750d46ab6ed3a99316'
查詢成功 [{"id":1,"username":"admin","password":"91fe0e80d07390750d46ab6ed3a99316","role":"admin","nicknamedmin","avatar":"https://www.youbaobao.xyz/mpvue-res/logo.jpg"}]
{ code: 0, msg: '登陸成功' }
express-validator

express-validator 是一個功能強大的表單驗證器,它是validator.js的中間件
使用express-validator 能夠簡化POST請求的參數驗證,使用方法以下:
安裝
`npm i -S express-validator
`
驗證

const { body, validationResult } = require('express-validator')
const boom = require('boom')

router.post(
  '/login',
  [
    body('username').isString().withMessage('username類型不正確'),
    body('password').isString().withMessage('password類型不正確')
  ],
  function(req, res, next) {
    const err = validationResult(req)
    if (!err.isEmpty()) {
      const [{ msg }] = err.errors
      next(boom.badRequest(msg))
    } else {
      const username = req.body.username
      const password = md5(`${req.body.password}${PWD_SALT}`)

      login(username, password).then(user => {
        if (!user || user.length === 0) {
          new Result('登陸失敗').fail(res)
        } else {
          new Result('登陸成功').success(res)
        }
      })
    }
  })

express-validator 使用技巧:

  • 在 router.post 方法中使用body等方法判斷參數類型,並指定出錯時的提示信息
  • 使用 const err = validationResult(req) 獲取錯誤信息,err.errors 是一個數組,包含全部錯誤信息,若是錯誤信息爲空表示參數校驗成功
  • 若是發現錯誤咱們可使用 next(boom.badRequest(msg)) 跑出異常,交給咱們自定義的異常處理方法進行處理

JWT基本概念

生成JWT Token

安裝 jsonwebtoken
`npm i -S jsonwebtoken
`
使用

const jwt = require('jsonwebtoken')
const { PRIVATE_KEY, JWT_EXPIRED } = require('../utils/constant')

login(username, password).then(user => {
    if (!user || user.length === 0) {
      new Result('登陸失敗').fail(res)
    } else {
      const token = jwt.sign(
        { username },
        PRIVATE_KEY,
        { expiresIn: JWT_EXPIRED }
      )
      new Result({ token }, '登陸成功').success(res)
    }
})

這裏須要定義jwt的私鑰和過時時間,過時時間不宜太短,也不宜過長,課程裏設置爲1小時,實際業務中可根據場景來判斷,一般建議不超過24小時,保密性要求高的業務能夠設置爲1-2小時

module.exports = {
  // ...
  PRIVATE_KEY: 'admin_imooc_node_test_youbaobao_xyz',
  JWT_EXPIRED: 60 * 60, // token失效時間
}

前端再次請求,結果以下

{
  "code":0,
  "msg":"登陸成功",
  "data":{
    "token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwiaWF0IjoxNTc0NDk1NzA0LCJleHAiOjE1NzQ0OTkzMDR9.9lnxdTn1MmMbKsPvhvRHDRIufbMcUD437CWjnoJsmfo"
  }
}

咱們能夠將該token在jwt.io網站上進行驗證,能夠獲得以下結果:

{
  "username": "admin",
  "iat": 1574495704,
  "exp": 1574499304
}

能夠看到username 被正確解析,說明token生成成功

前端登陸請求改造

修改 src/utils/request.js

service.interceptors.response.use(
  response => {
    const res = response.data

    if (res.code !== 0) {
      Message({
        message: res.msg || 'Error',
        type: 'error',
        duration: 5 * 1000
      })
      // 判斷 token 失效的場景
      if (res.code === -2) {
        // 若是 token 失效,則彈出確認對話框,用戶點擊後,清空 token 並返回登陸頁面
        MessageBox.confirm('Token 失效,請從新登陸', '確認退出登陸', {
          confirmButtonText: '從新登陸',
          cancelButtonText: '取消',
          type: 'warning'
        }).then(() => {
          store.dispatch('user/resetToken').then(() => {
            location.reload()
          })
        })
      }
      return Promise.reject(new Error(res.msg || '請求失敗'))
    } else {
      return res
    }
  },
  error => {
    let message = error.message || '請求失敗'
    if (error.response && error.response.data) {
      const { data } = error.response
      message = data.msg
    }
    Message({
      message,
      type: 'error',
      duration: 5 * 1000
    })
    return Promise.reject(error)
  }
)
JWT 認證

安裝express-jwt
`npm i -S express-jwt
`
建立 /router/jwt.js

const expressJwt = require('express-jwt');
const { PRIVATE_KEY } = require('../utils/constant');

const jwtAuth = expressJwt({
  secret: PRIVATE_KEY,
  credentialsRequired: true,// 設置爲false就不進行校驗了,遊客也能夠訪問
  ,algorithms: ['HS256'] 
}).unless({
  path: [
    '/',
    '/user/login'
  ], // 設置 jwt 認證白名單
});

module.exports = jwtAuth;

在 /router/index.js中使用中間件

const jwtAuth = require('./jwt')

// 註冊路由
const router = express.Router()

// 對全部路由進行 jwt 認證
router.use(jwtAuth)

在 /utils/constans.js 中添加

module.exports = {
  // ...
  CODE_TOKEN_EXPIRED: -2
}

修改 /model/Result.js:

expired(res) {
  this.code = CODE_TOKEN_EXPIRED
  this.json(res)
}

修改自定義異常:

router.use((err, req, res, next) => {
  if (err.name === 'UnauthorizedError') {
    new Result(null, 'token失效', {
      error: err.status,
      errorMsg: err.name
    }).expired(res.status(err.status))
  } else {
    const msg = (err && err.message) || '系統錯誤'
    const statusCode = (err.output && err.output.statusCode) || 500;
    const errorMsg = (err.output && err.output.payload && err.output.payload.error) || err.message
    new Result(null, msg, {
      error: statusCode,
      errorMsg
    }).fail(res.status(statusCode))
  }
})
前端傳入 JWT Token

後端添加路由的jwt認證後,再次請求 /user/info 將拋出401 錯誤,這是因爲前端未傳遞合理的Token致使的,下面咱們就修改 /utils/request.js ,使得前端請求時能夠傳遞Token:

service.interceptors.request.use(
  config => {
    // 若是存在 token 則附帶在 http header 中
    if (store.getters.token) {
      config.headers['Authorization'] = `Bearer ${getToken()}`
    }
    return config
  },
  error => {
    return Promise.reject(error)
  }
)

前端去掉 /user/info 請求時傳入的 token ,由於咱們已經從header中傳入,修改 src/api/user.js

export function getInfo() {
  return request({
    url: '/user/info',
    method: 'get'
  })
}
用戶查詢 /user/info API

在 db/index.js 中添加:

function queryOne(sql) {
  return new Promise((resolve, reject) => {
    querySql(sql)
      .then(results => {
        if (results && results.length > 0) {
          resolve(results[0])
        } else {
          resolve(null)
        }
      })
      .catch(error => {
        reject(error)
      })
  })
}

在 /services/user.js 中添加

function findUser(username) {
  const sql = `select * from admin_user where username='${username}'`
  return queryOne(sql)
}

此時有個問題,前端僅在http Header中傳入了Token若是經過Token獲取username呢?這裏就須要經過對 JWTToken 進行解析,在 /utils/index.js 中添加 decode方法:

const jwt = require('jsonwebtoken')
const { PRIVATE_KEY } = require('./constant')

function decode(req) {
  const authorization = req.get('Authorization')
  let token = ''
  if (authorization.indexOf('Bearer') >= 0) {
    token = authorization.replace('Bearer ', '')
  } else {
    token = authorization
  }
  return jwt.verify(token, PRIVATE_KEY)
}

修改 /router/user.js

router.get('/info', function(req, res) {
  const decoded = decode(req)
  if (decoded && decoded.username) {
    findUser(decoded.username).then(user => {
      if (user) {
        user.roles = [user.role]
        new Result(user, '獲取用戶信息成功').success(res)
      } else {
        new Result('獲取用戶信息失敗').fail(res)
      }
    })
  } else {
    new Result('用戶信息解析失敗').fail(res)
  }
})

此時在前端從新登陸,終於登陸成功

修改Logout方法

修改 src/store/modules/user.js:

logout({ commit, state, dispatch }) {
    return new Promise((resolve, reject) => {
      try {
        commit('SET_TOKEN', '')
        commit('SET_ROLES', [])
        removeToken()
        resetRouter()
        // reset visited views and cached views
        // to fixed https://github.com/PanJiaChen/vue-element-admin/issues/2485
        dispatch('tagsView/delAllViews', null, { root: true })
        resolve()
      } catch (e) {
        reject(e)
      }
    })
}
關於 RefreshToken

若是你的場景須要受權給第三方app,呢麼咱們一般須要在增長一個RefreshToken的API,該API的用途是根據現有的Token獲取用戶名,而後生成一個新的Token,這樣作的目的是爲了防止Token失效後退出登陸,因此app通常會在打開時刷新一次Token,該api的實現方法比較簡單,所需的技術以前都已經介紹過,你們能夠參考以前的文檔進行實現

電子書上傳

建立上傳頁面組件

電子書上傳過程分爲新增電子書和編輯電子書,新增:

<template>
  <detail :is-edit="false" />
</template>

<script>
import Detail from './components/Detail'

export default {
  name: 'CreateBook',
  components: { Detail }
}
</script>

編輯:

<template>
  <article-detail :is-edit />
</template>

<script>
import Detail from './components/Detail'

export default {
  name: 'EditBook',
  components: { Detail }
}
</script>

Detail組件比較複雜,咱們逐步實現,首先實現Detail的大致佈局,包括一個el-form和sticky導航欄 sticky導航欄在內容較多時會產生吸頂效果:

<div class="detail">
    <el-form ref="postForm" :model="postForm" :rules="rules" class="form-container">
      <sticky :z-index="10" :class-name="'sub-navbar ' + postForm.status">
        <el-button v-if="!isEdit" @click.prevent.stop="showGuide">顯示幫助</el-button>
        <el-button v-loading="loading" style="margin-left: 10px;" type="success" @click="submitForm">
          {{ isEdit ? '編輯電子書' : '新增電子書' }}
        </el-button>
      </sticky>
      <div class="detail-container">
        <el-row>
          <Warning />
          <el-col :span="24">
            <!-- 編寫具體表單控件 -->          
          </el-col>
          <el-col :span="24">
            <!-- 編寫具體表單控件 -->          
          </el-col>
        </el-row>
      </div>
    </el-form>
</div>

<style lang="scss" scoped>
  @import "~@/styles/mixin.scss";

  .detail {
    position: relative;
    .detail-container {
      padding: 40px 45px 20px 50px;
      .preview-img {
        width: 200px;
        height: 270px;
      }
      .contents-wrapper {
        padding: 5px 0;
      }
    }
  }
</style>
上傳組件開發

這裏咱們基於 el-upload封裝了上傳組件EbookUpload,基於EbookUpload咱們再實現上傳組件就很是容易了:

<div class="upload-container">
    <el-upload
      :action="action"
      :headers="headers"
      :multuple="false"
      :limit="1"
      :before-upload="beforeUpload"
      :on-success="onSuccess"
      :on-error="onError"
      :on-remove="onRemove"
      :file-list="fileList"
      :on-exceed="onExceed"
      :disabled="disabled"
      drag
      show-file-list
      accept="application/epub+zip"
      class="image-upload"
    >
      <i class="el-icon-upload"></i>
      <div class="el-upload__text" v-if="fileList.length === 0">請將電子書拖入或 <em>點擊上傳</em></div>
     <div v-else class="el-upload__text">圖書已上傳</div>
      </el-upload>
  </div>

上傳失敗事件

onError(err) {
      const errMsg = err.message && JSON.parse(err.message)
      this.$message({
        message: (errMsg && errMsg.msg && `上傳失敗,失敗緣由:${errMsg.msg}`) || '上傳失敗',
        type: 'error'
      })
      this.$emit('onError', err)
    },
        onExceed() {
      this.$message({
        message: '每次只能上傳一本電子書',
        type: 'warning'
      })
    }
上傳API開發

指定目的nginx上傳路徑,這樣作的好處是一旦電子書拷貝到指定目錄下後,,就能夠經過nginx生成下載連接:
新建 /utils/env.js

module.exports = {
    env:'dev'
}

const { env } = require('./env')
const UPLOAD_PATH = env === 'dev' ?
  '/Users/sam/upload/admin-upload-ebook' :
  '/root/upload/admin-upload-ebook'

安裝multer:上傳
`const multer = require('multer')
`
上傳API

router.post(
  '/upload',
  multer({ dest: `${UPLOAD_PATH}/book` }).single('file'),
  function(req, res, next) {
    if (!req.file || req.file.length === 0) {
      new Result('上傳電子書失敗').fail(res)
    } else {
      const book = new Book(req.file)
      book.parse()
        .then(book => {
          new Result(book.toJson(), '上傳成功').success(res)
        })
        .catch((err) => {
          console.log('/book/upload', err)
          next(boom.badImplementation(err))
          book.reset()
        })
    }
  })
上傳組件表單

圖書表單包括如下信息:

  • 書名
  • 做者
  • 出版社
  • 語言
  • 根文件
  • 文件路徑
  • 解壓路徑
  • 封面路徑
  • 文件名稱
  • 封面
  • 目錄
<el-col :span="24">
          <el-form-item prop="title">
            <MdInput v-model="postFrom.title" :maxlength="100" name="name" required>書名</MdInput>
          </el-form-item>
          <el-row>
            <el-col :span="12">
              <el-form-item :label-width="labelWidth" label="做者:">
                <el-input
                  v-model="postFrom.author"
                  placeholder="做者"
                  style="width: 100%"
                />
              </el-form-item>
            </el-col>

            <el-col :span="12">
              <el-form-item :label-width="labelWidth" label="出版社:">
                <el-input v-model="postFrom.publisher" placeholder="出版社" style="width: 100%"/>
              </el-form-item>
            </el-col>
          </el-row>
          <el-row>
            <el-col :span="12">
              <el-form-item label="語言:" :label-width="labelWidth">
                <el-input v-model="postFrom.language" placeholder="語言" />
              </el-form-item>
            </el-col>
            <el-col :span="12">
              <el-form-item label="根文件:" :label-width="labelWidth">
                <el-input v-model="postFrom.rootFile" placeholder="根文件" disabled />
              </el-form-item>
            </el-col>
          </el-row>
          <el-row>
            <el-col :span="12">
              <el-form-item label="文件路徑:" :label-width="labelWidth">
                <el-input v-model="postFrom.filePath" placeholder="文件路徑" disabled />
              </el-form-item>
            </el-col>
            <el-col :span="12">
              <el-form-item label="解壓路徑:" :label-width="labelWidth">
                <el-input v-model="postFrom.unzipPath" placeholder="解壓路徑" disabled />
              </el-form-item>
            </el-col>
          </el-row>
          <el-row>
            <el-col :span="12">
              <el-form-item label="封面路徑:" :label-width="labelWidth">
                <el-input v-model="postFrom.language" placeholder="封面路徑" />
              </el-form-item>
            </el-col>
            <el-col :span="12">
              <el-form-item label="文件名稱:" :label-width="labelWidth">
                <el-input v-model="postFrom.rootFile" placeholder="文件名稱" />
              </el-form-item>
            </el-col>
          </el-row>
          <el-row>
            <el-col :span="24">
              <el-form-item label="封面:" :label-width="labelWidth">
                <a v-if="postFrom.cover" :href="postFrom.cover" target="_blank">
                  ![](postForm.cover)
                </a>
                <span v-else>無</span>
              </el-form-item>
            </el-col>
          </el-row>
          <el-row>
            <el-col :span="24">
              <el-form-item label="目錄:" :label-width="labelWidth">
                <div v-if="postFrom.contents && postFrom.contents.length > 0" class="contents_warpper">
                  <el-tree class="" />
                </div>
                <span v-else>無</span>
              </el-form-item>
            </el-col>
          </el-row>
        </el-col>

電子書解析方案

構造函數
Book分爲兩種場景,第一種是直接從電子書文件中解析出Book對象,第二種是從data對象生成Book對象

constructor(file, data) {
    if (file) {
      this.createBookFromFile(file)
    } else if (data) {
      this.createBookFromData(data)
    }
}
從文件建立Book對象

從文件讀取電子書後,初始化Book對象
createBookFromFile(file) {

const {
  destination: des, // 文件本地存儲目錄
  filename, // 文件名稱
  mimetype = MIME_TYPE_EPUB // 文件資源類型
} = file
const suffix = mimetype === MIME_TYPE_EPUB ? '.epub' : ''
const oldBookPath = `${des}/${filename}`
const bookPath = `${des}/${filename}${suffix}`
const url = `${UPLOAD_URL}/book/${filename}${suffix}`
const unzipPath = `${UPLOAD_PATH}/unzip/${filename}`
const unzipUrl = `${UPLOAD_URL}/unzip/${filename}`
if (!fs.existsSync(unzipPath)) {
  fs.mkdirSync(unzipPath, { recursive: true }) // 建立電子書解壓後的目錄
}
if (fs.existsSync(oldBookPath) && !fs.existsSync(bookPath)) {
  fs.renameSync(oldBookPath, bookPath) // 重命名文件
}
this.fileName = filename // 文件名
this.path = `/book/${filename}${suffix}` // epub文件路徑
this.filePath = this.path // epub文件路徑
this.url = url // epub文件url
this.title = '' // 標題
this.author = '' // 做者
this.publisher = '' // 出版社
this.contents = [] // 目錄
this.cover = '' // 封面圖片URL
this.category = -1 // 分類ID
this.categoryText = '' // 分類名稱
this.language = '' // 語種
this.unzipPath = `/unzip/${filename}` // 解壓後的電子書目錄
this.unzipUrl = unzipUrl // 解壓後的電子書連接
this.originalName = file.originalname

}

從數據建立Book對象

從表單對象中建立Book對象

createBookFromData(data) {
    this.fileName = data.fileName
    this.cover = data.coverPath
    this.title = data.title
    this.author = data.author
    this.publisher = data.publisher
    this.bookId = data.fileName
    this.language = data.language
    this.rootFile = data.rootFile
    this.originalName = data.originalName
    this.path = data.path || data.filePath
    this.filePath = data.path || data.filePath
    this.unzipPath = data.unzipPath
    this.coverPath = data.coverPath
    this.createUser = data.username
    this.createDt = new Date().getTime()
    this.updateDt = new Date().getTime()
    this.updateType = data.updateType === 0 ? data.updateType : UPDATE_TYPE_FROM_WEB
    this.contents = data.contents
}
電子書解析

初始化後,能夠調用Book實例的parse 方法解析電子書,這裏咱們使用了epub庫,咱們直接將epub源碼集成到項目中:

epub庫集成

epub庫源碼 https://github.com/julien-c/epub
咱們直接將 epub.js 拷貝到 /utils/epub.js

epub庫獲取圖片邏輯修改
getImage(id, callback) {
    if (this.manifest[id]) {
      if ((this.manifest[id]['media-type'] || '').toLowerCase().trim().substr(0, 6) != 'image/') {
        return callback(new Error('Invalid mime type for image'))
      }
      this.getFile(id, callback)
    } else {
      const coverId = Object.keys(this.manifest).find(key => (
        this.manifest[key].properties === 'cover-image'))
      if (coverId) {
        this.getFile(coverId, callback)
      } else {
        callback(new Error('File not found'))
      }
    }
};
使用epub庫解析電子書

parse() {

return new Promise((resolve, reject) => {
  const bookPath = `${UPLOAD_PATH}${this.path}`
  if (!this.path || !fs.existsSync(bookPath)) {
    reject(new Error('電子書路徑不存在'))
  }
  const epub = new Epub(bookPath)
  epub.on('error', err => {
    reject(err)
  })
  epub.on('end', err => {
    if (err) {
      reject(err)
    } else {
      let {
        title,
        language,
        creator,
        creatorFileAs,
        publisher,
        cover
      } = epub.metadata
      // title = ''
      if (!title) {
        reject(new Error('圖書標題爲空'))
      } else {
        this.title = title
        this.language = language || 'en'
        this.author = creator || creatorFileAs || 'unknown'
        this.publisher = publisher || 'unknown'
        this.rootFile = epub.rootFile
        const handleGetImage = (error, imgBuffer, mimeType) => {
          if (error) {
            reject(error)
          } else {
            const suffix = mimeType.split('/')[1]
            const coverPath = `${UPLOAD_PATH}/img/${this.fileName}.${suffix}`
            const coverUrl = `${UPLOAD_URL}/img/${this.fileName}.${suffix}`
            fs.writeFileSync(coverPath, imgBuffer, 'binary')
            this.coverPath = `/img/${this.fileName}.${suffix}`
            this.cover = coverUrl
            resolve(this)
          }
        }
        try {
          this.unzip() // 解壓電子書
          this.parseContents(epub)
            .then(({ chapters, chapterTree }) => {
              this.contents = chapters
              this.contentsTree = chapterTree
              epub.getImage(cover, handleGetImage) // 獲取封面圖片
            })
            .catch(err => reject(err)) // 解析目錄
        } catch (e) {
          reject(e)
        }
      }
    }
  })
  epub.parse()
  this.epub = epub
})

}

電子書目錄解析

電子書解析過程當中咱們須要自定義電子書目錄,第一步須要解壓電子書:

unzip() {
    const AdmZip = require('adm-zip')
    const zip = new AdmZip(Book.genPath(this.path)) // 解析文件路徑
    zip.extractAllTo(
      /*target path*/Book.genPath(this.unzipPath),
      /*overwrite*/true
    )
}

genPath 是 Book 的一個屬性方法,咱們可使用 es6 的 static 屬性來實現:

static genPath(path) {
    if (path.startsWith('/')) {
      return `${UPLOAD_PATH}${path}`
    } else {
      return `${UPLOAD_PATH}/${path}`
    }
}

電子書目錄解析算法:

parseContents(epub) {
    function getNcxFilePath() {
      const manifest = epub && epub.manifest
      const spine = epub && epub.spine
      const ncx = manifest && manifest.ncx
      const toc = spine && spine.toc
      return (ncx && ncx.href) || (toc && toc.href)
    }

    /**
     * flatten方法,將目錄轉爲一維數組
     *
     * @param array
     * @returns {*[]}
     */
    function flatten(array) {
      return [].concat(...array.map(item => {
        if (item.navPoint && item.navPoint.length) {
          return [].concat(item, ...flatten(item.navPoint))
        } else if (item.navPoint) {
          return [].concat(item, item.navPoint)
        } else {
          return item
        }
      }))
    }

    /**
     * 查詢當前目錄的父級目錄及規定層次
     *
     * @param array
     * @param level
     * @param pid
     */
    function findParent(array, level = 0, pid = '') {
      return array.map(item => {
        item.level = level
        item.pid = pid
        if (item.navPoint && item.navPoint.length) {
          item.navPoint = findParent(item.navPoint, level + 1, item['$'].id)
        } else if (item.navPoint) {
          item.navPoint.level = level + 1
          item.navPoint.pid = item['$'].id
        }
        return item
      })
    }

    if (!this.rootFile) {
      throw new Error('目錄解析失敗')
    } else {
      const fileName = this.fileName
      return new Promise((resolve, reject) => {
        const ncxFilePath = Book.genPath(`${this.unzipPath}/${getNcxFilePath()}`) // 獲取ncx文件路徑
        const xml = fs.readFileSync(ncxFilePath, 'utf-8') // 讀取ncx文件
        // 將ncx文件從xml轉爲json
        xml2js(xml, {
          explicitArray: false, // 設置爲false時,解析結果不會包裹array
          ignoreAttrs: false  // 解析屬性
        }, function(err, json) {
          if (!err) {
            const navMap = json.ncx.navMap // 獲取ncx的navMap屬性
            if (navMap.navPoint) { // 若是navMap屬性存在navPoint屬性,則說明目錄存在
              navMap.navPoint = findParent(navMap.navPoint)
              const newNavMap = flatten(navMap.navPoint) // 將目錄拆分爲扁平結構
              const chapters = []
              epub.flow.forEach((chapter, index) => { // 遍歷epub解析出來的目錄
                // 若是目錄大於從ncx解析出來的數量,則直接跳過
                if (index + 1 > newNavMap.length) {
                  return
                }
                const nav = newNavMap[index] // 根據index找到對應的navMap
                chapter.text = `${UPLOAD_URL}/unzip/${fileName}/${chapter.href}` // 生成章節的URL
                // console.log(`${JSON.stringify(navMap)}`)
                if (nav && nav.navLabel) { // 從ncx文件中解析出目錄的標題
                  chapter.label = nav.navLabel.text || ''
                } else {
                  chapter.label = ''
                }
                chapter.level = nav.level
                chapter.pid = nav.pid
                chapter.navId = nav['$'].id
                chapter.fileName = fileName
                chapter.order = index + 1
                chapters.push(chapter)
              })
              const chapterTree = []
              chapters.forEach(c => {
                c.children = []
                if (c.pid === '') {
                  chapterTree.push(c)
                } else {
                  const parent = chapters.find(_ => _.navId === c.pid)
                  parent.children.push(c)
                }
              }) // 將目錄轉化爲樹狀結構
              resolve({ chapters, chapterTree })
            } else {
              reject(new Error('目錄解析失敗,navMap.navPoint error'))
            }
          } else {
            reject(err)
          }
        })
      })
    }
}
電子書解析算法優化
function getNcxFilePath() {
            const spine = epub && epub.spine
            const ncx = spine.toc && spine.toc.href
            const id = spine.toc && spine.toc.id
            if (ncx) {
                return ncx
            } else {
                return manifest[id].href
            }

        }

        function findParent(array, level = 0, pid = '') {

            return array.map(item => {
                item.level = level
                item.pid = pid
                if (item.navPoint && item.navPoint.length > 0) {
                    item.navPoint = findParent(item.navPoint, level + 1, item['$'].id)
                } else if (item.navPoint) {
                    item.navPoint.level = level + 1
                    item.navPoint.pid = item['$'].id
                }
                return item
            })
        }


        function flatten(array) {
            return [].concat(...array.map(item => {
                if (item.navPoint && item.navPoint.length > 0) {
                    return [].concat(item, ...flatten(item.navPoint))
                } else if (item.navPoint) {
                    return [].concat(item, item.navPoint)
                }
                return item
            }))
        }

        const ncxFilePath = Book.genPath(`${this.unzipPath}/${getNcxFilePath()}`)
        console.log(ncxFilePath)
        if (fs.existsSync(ncxFilePath)) {
            return new Promise((resolve, reject) => {
                const fileName = this.fileName
                const xml = fs.readFileSync(ncxFilePath, 'utf-8')
                const dir = path.dirname(ncxFilePath).replace(UPLOAD_PATH,'')
                parseString(xml, {explicitArray: false, ignoreAttrs: false}, function (err, json) {
                    if (err) {
                        reject(err)
                    } else {
                        const navMap = json.ncx.navMap
                        if (navMap.navPoint && navMap.navPoint.length > 0) {
                            navMap.navPoint = findParent(navMap.navPoint)
                            const newNavMap = flatten(navMap.navPoint)
                            const chapters = []
                            newNavMap.forEach((chapter, index) => {
                                const  src = chapter.content['$'].src
                                console.log(src)
                                chapter.text = `${UPLOAD_URL}${dir}/${src}`
                                chapter.label = chapter.navLabel.text || ''
                                chapter.navId = chapter['$'].id
                                chapter.fileName = fileName
                                chapter.order = index + 1
                                chapters.push(chapter)
                            })
                            const chapterTree = []
                            chapters.forEach(c => {
                                c.children = []
                                if (c.pid === '') {
                                    chapterTree.push(c)
                                } else {
                                    const parent = chapters.find(_ => _.navId === c.pid)
                                    parent.children.push(c)

                                }
                            })
                            resolve({chapters, chapterTree})
                        } else {
                            reject(new Error('目錄解析失敗,目錄數爲0'))
                        }
                    }
                })
            });
        } else {

        }

    }

電子書樹狀結構解析

<el-tree :data="contentsTree" @node-click="onContentClick" />

點擊打開章節信息

onContentClick(data) {
      console.log(data)
      if (data.text) {
        window.open(data.text)
      }
    },

電子書表單驗證

onContentClick(data) {
      console.log(data)
      if (data.text) {
        window.open(data.text)
      }
    }
    onContentClick(data) {
      console.log(data)
      if (data.text) {
        window.open(data.text)
      }
    },

電子書新增邏輯

submitForm() {
      if (!this.loading) {
        this.$refs.postForm.validate((valid, fields) => {
          console.log(valid)
          if (valid) {
            this.loading = true
            const book = Object.assign({}, this.postForm)
            delete book.contentsTree
            if (!this.isEdit) {
              createBook(book).then(response => {
                const { msg } = response
                this.$notify({ title: '添加成功', message: msg, type: 'success', duration: 2000 })
                this.loading = false
                this.setDefault()
              }).catch(() => {
                this.loading = false
              })
            } else {
              // updateBook()
            }
          } else {
            const message = fields[Object.keys(fields)[0]][0].message
            this.$message({ message, type: 'error' })
          }
          // this.loading = false
        })
      }
    }

在路由中增長/book/create 接口

router.post('/create',function (req,res,next) {
    const  decode = decoded(req)
    if (decode && decode.username){
        req.body.username = decode.username
    }
    const book = new Book(null,req.body)
    bookService.insertBook(book).then(()=>{
        console.log('添加電子書成功')
        new Result('添加電子書成功').success(res)

    }).catch(err=>{
        next(boom.badImplementation(err))
    })

})

解析電子書內容 使用db數據庫插入電子書內容

function insertBook(book) {
    return new Promise(async (resolve, reject) => {
        try {
            if (book instanceof Book) {
                const result = await exists(book)
                console.log('電子書----',result)
                if (result) {
                    await removeBook(book)
                    reject(new Error('電子書已存在'))
                } else {
                    await db.insert(book.toDb(), 'book')
                    await insertContents(book)
                    resolve()
                }
            } else {
                reject(new Error('添加的圖書對象不合法'))
            }
        } catch (e) {
            reject(e)
        }
    })
}

若是電子書存在就刪除這次上傳的 同時查詢是否已插入數據庫 若是已插入則把數據刪除

async function removeBook(book) {
   if (book){
       book.reset()
    if (book.fileName){
        const  removeBookSql = `delete from book where filename='${book.fileName}'`
        const  removeContentsSql = `delete from book where filename='${book.fileName}'`
        await  db.querySql(removeBookSql)
        await  db.querySql(removeContentsSql)
    }
   }
}
電子書查詢

修改前端 /router/index中的edit路由 增長fileName路由參數

{
        name: 'bookEdit',
        path: '/book/edit/:fileName',
        component: () => import('@/views/book/edit'),
        hidden: true,
        meta: { title: '編輯圖書', icon: 'edit', roles: ['admin'], activeMenu: '/book/list'
        }
      },

經過vue生命週期 created 請求book信息

created() {
    if (this.isEdit) {
      const fileName = this.$route.params.fileName
      this.getBookData(fileName)
    }
  }
  export function getBook(fileName) {
  return request({
    url: '/book/get',
    method: 'get',
    params: { fileName: fileName }
  })
}

在後臺添加路由 /book/get

router.get('/get',function (req,res,next) {
    const  {fileName} = req.query
  if (!fileName){
      next(boom.badRequest(new Error('參數fileName不能爲空')))
  }else {
     bookService.getBook(fileName).then(book=>{
         new Result(book,'獲取圖書信息成功').success(res)
     }).catch(err=>{
         next(boom.badImplementation(err))
     })
  }

})

function getBook(fileName){
   return new Promise(async (resolve, reject) => {
       const  bookSql = `select  * from book where filename='${fileName}'`
       const  contentsSql = `select * from contents where filename='${fileName}' order by \`order\``
      const book = await  db.queryOne(bookSql)
      const contents = await  db.querySql(contentsSql)
       if (book){
           book.cover = Book.getCoverUrl(book)
       }
       console.log('book',book)
       resolve({book})
   })
}

關閉mysql MySQL GROUP BY 的問題

17-3
阿里雲購買及環境搭建

生成ssh公鑰 ssh-keygen -t rsa
拷貝到服務器 ssh-copy-id -i ~/.ssh/id_rsa.pub root@123.56.163.191
修改 ssh配置防止斷鏈 vim /etc/ssh/sshd_config 添加 ClientAliveInterval 30
重啓ssh配置項 restart sshd.service
`curl -o- https://raw.githubusercontent... | bash
`
配置

[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"  # This loads nvm
[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion"  # This loads nvm bash_completion
source ~/.bash_profile

安裝node nodejs環境搭建

nvm install node
npm install -g cnpm --registry=https://registry.npm.taobao.org
nginx服務器

安裝依賴

yum -y install pcre*
yum -y install openssl

建立nginx默認配置

cd /usr/share/nginx/
touch nginx.conf

修改nginx 將user改成root 並添加配置文件
include /usr/share/nginx/*.conf;
修改主配置文件端口號9000
nginx個性化配置

server {
   listen 80:
   server_name localhost;
   root /usr/share/nginx/upload;
   autoindex on;
   add_header Cache-Control "no-cache,must-revalidate";
   location / {
   add_header Access-Control-Allow-Origin *;
   }    
}

使用fileZilla 上傳文件到服務器

安裝git

yum install -y git

建立imooc-ebook目錄
`mkdir imooc-ebook
cd imooc-ebook
`
更新git版本
安裝依賴

yum install curl-devel expat-devel gettext-devel openssl-devel zlib-devel asciidoc
yum install  gcc perl-ExtUtils-MakeMaker
wget https://mirrors.edge.kernel.org/pub/software/scm/git/git-2.9.4.tar.xz
tar -xvf git-2.9.4.tar.xz
cd git-2.9.4
編譯並連接源碼git
make prefix=/usr/local/git all
make prefix=/usr/local/git install
cd /usr/bin
ln -s  /usr/local/git/bin/git git

git免密配置
ssh-keygen -t rsa -C "243100134_gg"
添加祕鑰到雲服務期
安裝mysql
mysql卸載
mysql安裝
新手必備

wget http://repo.mysql.com/mysql-community-release-el7-5.noarch.rpm 
# rpm -ivh mysql-community-release-el7-5.noarch.rpm

yum -y install mysql-server
service nysqld restart
cat /var/log/mysqld.log |grep password
修改默認密碼

USE mysql ; 
UPDATE user SET Password = password ( 'new-password' ) WHERE User = 'root' ;

添加3306端口
linux 查找文件夾
find / -name 'admin-vue-imooc-book' -type d
linux 移動文件夾

17-7
相關文章
相關標籤/搜索