Vue Element+Node.js开发企业通用管理后台系统笔记完

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pY01BX2c-1628951502574)(https://www.youbaobao.xyz/admin-docs/assets/img/login_process.58cab9a5.png)]

第三四章 Vue进阶

e m i t 和 emit 和 emiton

用this.$on来定义一个事件, 并且指定事件的执行对象(函数),
他主要是用来干什么的呢 --事件的定义和消费 使用this.on来定义一个事件,

20210707-222958-0495.png

好处是什么呢 可以把事件的定义和事件的消费分开,实现逻辑的解耦 可以在子组件中直接调用事件非常灵活方便

来看一下this.on 和this.emit的实现原理是什么

可以打断点试一下,通过对源码的分析可以知道on方法在定义的时候可以同时定义 多个事件,也可以为同一个事件绑定多个处理函数, 也知道try catch进行了错误处理 所以抛出异常的时候不会中断执行

directive用法(指令)

比较复杂,来看一个案例

111.gif

这是一个很常用的场景,在实际的项目开发过程中经常要用到

来看一下源码

<!------------------------------------------------------------
  文件名:   ch3-2.html
  第三章:   directive 用法
  开发平台: VSCode 1.39.1
  Vue 实战小慕读书中后台 By Sam
------------------------------------------------------------->
<html>
  <head>
    <title>directive 用法</title>
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
  </head>
  <body>
    <div id="root">
        <!-- 这里v-loading绑定了个状态叫isLoading 来判断是否要显示加载状态  把这个v-loading放到根的div也是可以的
        因为这里的loading并不依赖于某个div,他是全局的  那这个v-loading是怎么实现的呢 -->
      <div v-loading="isLoading">{
   
   {data}}</div>
      <button @click="update">更新</button>
    </div>
    <script>
        // 指令怎么定义呢--Vue.directive  调用Vue的一个静态方法directive   
        // 需要传入两个参数  一个是上面定义的v-loading   后一个是一个对象
        // 对象中要包含一些方法,这里选择的是update方法,表示界面上的元素(vnode)发生更新的时候调用的
        // 还有哪些方法呢https://cn.vuejs.org/v2/api/#Vue-directive
        // https://cn.vuejs.org/v2/guide/custom-directive.html
        // 还有bind inserted componentUpdated unbind这些方法
        // 一旦点击按钮就会修改isloading 并且在三秒之后会修改data的数据
        // 所以update方法会被调用两次
      Vue.directive('loading', {
      
      
        update(el, binding, vnode) {
      
      
      //       这里打印一下三个参数,
      //       其中binding第一次打印:
      //       name: "loading"
            // rawName: "v-loading"
            // value: true
            // expression: "isLoading"
            // modifiers: {}
            // def: {update: ƒ}
            // oldValue: false
            // oldArg: undefined
      //       第二次打印
      //       name: "loading"
      //       rawName: "v-loading"
      //       value: false
      //       expression: "isLoading"
      //       modifiers: {}
      //       def: {update: ƒ}
      //       oldValue: true
      //       oldArg: undefined
            
      //       就可以拿到binding下面的value  判断是true还是false 如果为真的话就要加载动画
      //       所以就可以动态的创建div
          if (binding.value) {
      
      
            const div = document.createElement('div')
            div.innerText = '加载中...'
            div.setAttribute('id', 'loading')
            div.style.position = 'absolute'
            div.style.left = 0
            div.style.top = 0
            div.style.width = '100%'
            div.style.height = '100%'
            div.style.display = 'flex'
            div.style.justifyContent = 'center'
            div.style.alignItems = 'center'
            div.style.color = 'white'
            div.style.background = 'rgba(0, 0, 0, .7)'
              // 插入到界面当中
            document.body.append(div)
          } else {
      
      
          //       如果isLoading为false的话就会找id为loading的dom 并且给移出
   				//       vue是通过这种方式来实现加载动画的,
          //       我们平常的加载动画是一种侵入式的加载动画,就是说在实现一个加载动画的时候必须写一个div 
          //       然后通过一个状态比如说:visible 但是这种方式对代码是侵入的,因为每写一个组件都要加上这个代码,
          //       但是我们这个是一个通用的指令,这种方法可以把它做成一个vue的插件,然后打成一个npm包,
          //       通过引入的方法就可以快速的集成到vue代码中,他可以实现一些通用的逻辑,把它封装成一个固定的静态的方法 		
              	const div= document.getElementById('loading')
            	div && document.body.removeChild(div)   //div存在的话就移除  妙
          }
        }
      })
      new Vue({
      
      
        el: '#root',
        data() {
      
      
          return {
      
      
            isLoading: false,
            data: ''
          }
        },
        methods: {
      
      
          update() {
      
      
            this.isLoading = true
            setTimeout(() => {
      
      
              this.data = '用户数据'
                // 这里的data在实际项目中会发一些请求数据,拿到数据以后返给前端 然后isloading置为false
                // 注意拿数据的时候最好加一个try catch 防止有一些未知的错误  在catch中 isloading为false
              this.isLoading = false
            }, 3000)
          }
        }
      })
    </script>
  </body>
</html>

老师后面还有一些源码分析,以后再看吧

Vue.component

用途就是定义一个组件,

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
</head>
<body>
  <div id="app">
 
    <Test :msg="message"></Test>
  </div>
  <script>
    Vue.component('Test',{
      
      
      template:'<div>{
      
      {msg}} </div>',
      props:{
      
      
        msg:{
      
      
          type:String,
          default:'hahahaha'
        }
      }
    })
    new Vue({
      
      
      el:"#app",
      data(){
      
      
        return {
      
      
          message:'test component'
        }
      },
      methods: {
      
      
        
      },
    })
  </script>
</body>
</html>

Vue.extend很重要,他就是用来构造一个组件的

20210713-193307-0098.png

Sub是个构造函数,

20210713-194011-0895.png

defination已经变成了一个function,这个function就是VueComponent
20210713-190733-0975.png

这里有很多属性,我们实例化一个组件的时候包含这些属性 初始化过程最关键的一个地方就是Vue.extend

Vue.extend

这个主要作用是什么呢 就是用来生成组件的构造函数

<!------------------------------------------------------------
  文件名:   ch3-4.html
  第三章:   Vue.extend 用法
  开发平台: VSCode 1.39.1
  Vue 实战小慕读书中后台 By Sam
------------------------------------------------------------->
<html>
  <head>
    <title>Vue.extend 用法</title>
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
  </head>
  <body>
    <div id="root">
      <Test :msg="message"></Test>
    </div>
    <script>
        //这里与上面那个不同
      const component = Vue.extend({
      
      
        template: '<div>{
      
      {msg}}</div>',
        props: {
      
      
          msg: {
      
      
            type: String,
            default: 'default message'
          }
        },
        name: 'Test'
      })
      Vue.component('Test')
      new Vue({
      
      
        el: '#root',
        data() {
      
      
          return {
      
      
            message: "Test Extend Component"
          }
        }
      })
    </script>
  </body>
</html>

Vue.extend进阶

<!------------------------------------------------------------
  文件名:   ch3-4.html
  第三章:   Vue.extend 用法2
  开发平台: VSCode 1.39.1
  Vue 实战小慕读书中后台 By Sam
------------------------------------------------------------->
<html>
  <head>
    <title>Vue.extend 用法2</title>
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <style>
      #loading-wrapper {
      
      
        position: fixed;
        top: 0;
        left: 0;
        display: flex;
        justify-content: center;
        align-items: center;
        width: 100%;
        height: 100%;
        background: rgba(0,0,0,.7);
        color: #fff;
      }
    </style>
  </head>
  <body>
    <div id="root">
      <button @click="showLoading">显示Loading</button>
    </div>
    <script>
      function Loading(msg) {
      
      
        const LoadingComponent = Vue.extend({
      
      
          template: '<div id="loading-wrapper">{
      
      {msg}}</div>',
          props: {
      
      
            msg: {
      
      
              type: String,
              default: msg
            }
          },
          name: 'LoadingComponent'
        })
        const div = document.createElement('div')
        div.setAttribute('id', 'loading-wrapper')
        document.body.append(div)
        new LoadingComponent().$mount('#loading-wrapper')   //这里组件的div覆盖了上面新创建的div
        return () => {
      
      
          document.body.removeChild(document.getElementById('loading-wrapper'))
        }
      }
      Vue.prototype.$loading = Loading
      new Vue({
      
      
        el: '#root',
        methods: {
      
      
          showLoading() {
      
      
            const hide = this.$loading('正在加载,请稍等...')
            setTimeout(() => {
      
      
              hide()
            }, 2000)
          }
        }
      })
    </script>
  </body>
</html>

如何给vue实例添加api 与前面的指令有些不一样,指令是修改某个状态加载的,这个是需要主动去触发的,等于是给vue加了个新的api 可以使项目耦合度大大下降
20210713-210214-0125.png

这里有个很妙的地方

在这里插入图片描述

Vue.use

<!------------------------------------------------------------
  文件名:   ch3-6.html
  第三章:   Vue.use 用法
  开发平台: VSCode 1.39.1
  Vue 实战小慕读书中后台 By Sam
------------------------------------------------------------->
<html>
  <head>
    <title>Vue.use 用法</title>
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <style>
      #loading-wrapper {
      
      
        position: fixed;
        top: 0;
        left: 0;
        display: flex;
        justify-content: center;
        align-items: center;
        width: 100%;
        height: 100%;
        background: rgba(0,0,0,.7);
        color: #fff;
      }
    </style>
  </head>
  <body>
    <div id="root">
      <button @click="showLoading">显示Loading</button>
    </div>
    <script>
      const loadingPlugin = {
      
      
        install: function(vm) {
      
      
          const LoadingComponent = vm.extend({
      
      
            template: '<div id="loading-wrapper">{
      
      {msg}}</div>',
            props: {
      
      
              msg: {
      
      
                type: String,
                default: 'loading...'
              }
            }
          }, 'LoadingComponent')
          function Loading(msg) {
      
      
            const div = document.createElement('div')
            div.setAttribute('id', 'loading-wrapper')
            document.body.append(div)
            new LoadingComponent({
      
      
              props: {
      
      
                msg: {
      
      
                  type: String,
                  default: msg
                }
              } 
            }).$mount('#loading-wrapper')
            return () => {
      
      
              document.body.removeChild(document.getElementById('loading-wrapper'))
            }
          }
          vm.prototype.$loading = Loading
        }
      }
      Vue.use(loadingPlugin)
      new Vue({
      
      
        el: '#root',
        methods: {
      
      
          showLoading() {
      
      
            const hide = this.$loading('正在加载,请稍等...')
            setTimeout(() => {
      
      
              hide()
            }, 2000)
          }
        }
      })
    </script>
  </body>
</html>

是用来加载vue的插件,我们把上一节vue.extend的用法做成一个插件,

这个和上面的功能一样,这样写的话可以把loadingPlugin还有style放到另外的文件当中

通过模块加载的方法来解耦

provide inject

组件通信我们知道有个props vuex 为什么还需要provide和inject呢

如果有3个组件的话我们是一层一层往下传的,比较笨重,vuex的话学习的成本比较高,这是vue就推出了provide inject 来解决这个问题,让组件的通信过程变得更加简单

<!------------------------------------------------------------
  文件名:   ch4-1.html
  第三章:   组件通信 provide 和 inject
  开发平台: VSCode 1.39.1
  Vue 实战小慕读书中后台 By Sam
------------------------------------------------------------->
<html>
  <head>
    <title>组件通信 provide 和 inject</title>
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
  </head>
  <body>
    <div id="root">
      <Test></Test>
    </div>
    <script>
      function registerPlugin() {
      
      
        Vue.component('Test', {
      
      
          template: '<div>{
      
      {message}}<Test2 /></div>',

          provide() {
      
      
            return {
      
      
               // 将自身组件打包成了一个属性elTest提供给他的子组件进行调用 
               // 在子组件中就可以通过elTest来找到父组件的实例
               // 不需要像原来那样通过props或者事件绑定的方法把事件向下传递
               // 现在就可以通过elTest来调用这个方法 
              elTest: this   
            }
          }, // function 的用途是为了获取运行时环境,否则 this 将指向 window
          data() {
      
      
            return {
      
      
              message: 'message from Test'
            }
          },
          methods: {
      
      
            change(component) {
      
      
              this.message = 'message from ' + component
            }
          }
        })
        Vue.component('Test2', {
      
      
          template: '<Test3 />'
        })
        Vue.component('Test3', {
      
      
          template: '<button @click="changeMessage">change</button>',
          inject: ['elTest'],
          methods: {
      
      
            changeMessage() {
      
      
               // message from Test3 说明是Test3发起的调用
              this.elTest.change(this.$options._componentTag)   
            }
          }
        })
      }
      Vue.use(registerPlugin)   // 加载插件
      new Vue({
      
      
        el: '#root'
      })
    </script>
  </body>
</html>

filter过滤器

可以对值进行二次处理

<html>
  <head>
    <title>过滤器 filter</title>
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
  </head>
  <body>
    <div id="root">
      {
   
   {message | lower}}
    </div>
    <script>
      new Vue({
      
      
        el: '#root',
        filters: {
      
      
          lower(value) {
      
      
            return value.toLowerCase()
          }
        },
        data() {
      
      
          return {
      
      
            message: 'Hello Vue'
          }
        }
      })
    </script>
  </body>
</html>

监听器watch

<!------------------------------------------------------------
  文件名:   ch4-3.html
  第三章:   监听器 watch
  开发平台: VSCode 1.39.1
  Vue 实战小慕读书中后台 By Sam
------------------------------------------------------------->
<html>
  <head>
    <title>监听器 watch</title>
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
  </head>
  <body>
    <div id="root">
      <h3>Watch 用法1:常见用法</h3>
      <input v-model="message">
      <span>{
   
   {copyMessage}}</span>
    </div>
    <div id="root2">
      <h3>Watch 用法2:绑定方法</h3>
      <input v-model="message">
      <span>{
   
   {copyMessage}}</span>
    </div>
    <div id="root3">
      <h3>Watch 用法3:deep + handler</h3>
      <input v-model="deepMessage.a.b">
      <span>{
   
   {copyMessage}}</span>
    </div>
    <div id="root4">
      <h3>Watch 用法4:immediate</h3>
      <input v-model="message">
      <span>{
   
   {copyMessage}}</span>
    </div>
    <div id="root5">
      <h3>Watch 用法5:绑定多个 handler</h3>
      <input v-model="message">
      <span>{
   
   {copyMessage}}</span>
    </div>
    <div id="root6">
      <h3>Watch 用法6:监听对象属性</h3>
      <input v-model="deepMessage.a.b">
      <span>{
   
   {copyMessage}}</span>
    </div>
      
    <script>
      new Vue({
      
      
        el: '#root',
        watch: {
      
      
          message(value) {
      
      
            this.copyMessage = value
          }
        },
        data() {
      
      
          return {
      
      
            message: 'Hello Vue',
            copyMessage: ''
          }
        }
      })
      new Vue({
      
      
        el: '#root2',
        watch: {
      
      
          message: 'handleMessage'
        },
        data() {
      
      
          return {
      
      
            message: 'Hello Vue',
            copyMessage: ''
          }
        },
        methods: {
      
      
          handleMessage(value) {
      
      
            this.copyMessage = value
          }
        }
      })
      new Vue({
      
      
        el: '#root3',
        watch: {
      
      
          deepMessage: {
      
      
            handler: 'handleDeepMessage',
            deep: true
          }
        },
        data() {
      
      
          return {
      
      
            deepMessage: {
      
      
              a: {
      
      
                b: 'Deep Message'
              }
            },
            copyMessage: ''
          }
        },
        methods: {
      
      
          handleDeepMessage(value) {
      
      
            this.copyMessage = value.a.b
          }
        }
      })
      new Vue({
      
      
        el: '#root4',
        watch: {
      
      
          message: {
      
      
            handler: 'handleMessage',
            immediate: true,
          }
        },
        data() {
      
      
          return {
      
      
            message: 'Hello Vue',
            copyMessage: ''
          }
        },
        methods: {
      
      
          handleMessage(value) {
      
      
            this.copyMessage = value
          }
        }
      }),
      new Vue({
      
      
        el: '#root5',
        watch: {
      
      
          message: [{
      
      
            handler: 'handleMessage',
          },
          'handleMessage2',
          function(value) {
      
      
            this.copyMessage = this.copyMessage + '...'
          }]
        },
        data() {
      
      
          return {
      
      
            message: 'Hello Vue',
            copyMessage: ''
          }
        },
        methods: {
      
      
          handleMessage(value) {
      
      
            this.copyMessage = value
          },
          handleMessage2(value) {
      
      
            this.copyMessage = this.copyMessage + '*'
          }
        }
      })
      new Vue({
      
      
        el: '#root6',
        watch: {
      
      
          'deepMessage.a.b': 'handleMessage'
        },
        data() {
      
      
          return {
      
      
            deepMessage: {
      
       a: {
      
       b: 'Hello Vue' } },
            copyMessage: ''
          }
        },
        methods: {
      
      
          handleMessage(value) {
      
      
            this.copyMessage = value
          }
        }
      })
    </script>
  </body>
</html>

class和style绑定

20210714-194145-0930.png

20210714-194646-0983.png

这里的mix方法里面用了三点运算符 学习一下

2.6新特性Vue.observable

可以方便的使用响应式属性, vue中使用响应式属性的话通常是要用vuex这个解决方案,来做集中的状态管理,

其实是对原来的observer方法做了重新的封装,让他能够在全局进行使用

应用比较简单的话直接就用observable就可以了 就不用vuex了

<html>
  <head>
    <title>Vue.observable</title>
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
  </head>
  <body>
    <div id="root">
      {
   
   {message}}
      <button @click="change">Change</button>
    </div>
    <script>
        //调用vue的全局方法observable,将需要管理的对象传入,之后提供了一个mutation,mutation中提供一个方法,
        // setMessage对state里面的message进行修改
      const state = Vue.observable({
      
       message: 'Vue 2.6' })
      const mutation = {
      
      
        setMessage(value) {
      
      
          state.message = value
        }
      }
      new Vue({
      
      
        el: '#root',
        computed: {
      
      
            //然后就可以把这个message注入到vue中 用的是计算属性,因为message发生变化需要计算属性来更新message
          message() {
      
      
            return state.message
          }
        },
        methods: {
      
      
           // change方法调用mutation里面的setMessage 
          change() {
      
      
            mutation.setMessage('Vue 3.0')
          }
        }
      })
    </script>
  </body>
</html>

新特性2slot插槽

<html>
  <head>
    <title>插槽 slot</title>
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
  </head>
  <body>
    <div id="root">
      <div>案例1:slot的基本用法</div>
      <Test>
        <template v-slot:header="{user}">
          <div>自定义header({
   
   {user.a}})</div>
        </template>
        <template v-slot="{user}">
          <div>自定义body({
   
   {user.b}})</div>
        </template>
      </Test>
    </div>
    <div id="root2">
      <div>案例2:Vue2.6新特性 - 动态slot</div>
      <Test>
        <template v-slot:[section]="{section}">
          <div>this is {
   
   {section}}</div>
        </template>
      </Test>
      <button @click="change">switch header and body</button>
    </div>
    <script>
      Vue.component('Test', {
      
      
        template: 
          '<div>' +
            '<slot name="header" :user="obj" :section="\'header\'">' +
              '<div>默认header</div>' +
            '</slot>' +
            '<slot :user="obj" :section="\'body\'">默认body</slot>' +
          '</div>',
        data() {
      
      
          return {
      
      
            obj: {
      
       a: 1, b: 2 }
          }
        }
      })
      new Vue({
      
       el: '#root' })
      new Vue({
      
       
        el: '#root2',
        data() {
      
      
          return {
      
      
            section: 'header'
          }
        },
        methods: {
      
      
          change() {
      
      
            this.section === 'header' ?
              this.section = 'default' :
              this.section = 'header'
          }
        }
      })
    </script>
  </body>
</html>

第六章vue router & vuex使用方法(小慕读书)

因为这套老师讲的是原理,我用法还没学过,所以这里看的视频是小慕读书的

1 vue router

小例子

http://www.youbaobao.xyz/mpvue-docs/guide/base_vuex.html

  • index.html:应用的入口文件
  • main.js:主js文件,初次渲染时执行
  • App.vue:根组件,在main.js中加载

vue router解决了什么问题? 解决了路由与组件的对应关系

cnpm i -S vue-router 意思是保存到dependencies vue-router是运行时仍然需要使用的库
然后通过vue的use方法来加载插件
第三步 初始化一个vue-router的对象
第四步 实例化vue对象,传入router参数
之后就可以通过router-view和router-link两个官方组件来使用vue-router

在main.js(src下)更改为以下代码

import Vue from 'vue'
import App from './App.vue'
import Router from 'vue-router'
import A from './components/A.vue'
import B from './components/B.vue'
import HelloWorld from './components/HelloWorld.vue'
Vue.config.productionTip = false
Vue.use(Router)

const routes=[
  {
    
    path:'/a',component:A},
  {
    
    path:'/b',component:B},
  {
    
    path:'/hello',component:HelloWorld}
]
const router=new Router({
    
    
  routes
})
new Vue({
    
    
  router,
  render: h => h(App),
 
}).$mount('#app')


跳转

 // eslint-disable-next-line
<template>
  <div id="app">
    <img alt="Vue logo" src="./assets/logo.png">
    <!-- <HelloWorld msg="Welcome to Your Vue.js App"/> -->
    <div>
      <div>
        <router-link to="/a">点我进入a</router-link>
      </div>
      <div>
        <router-link to="/b">点我进入B</router-link>
      </div>
      <div>
        <router-link to="/hello">点我进入 hello</router-link>
      </div>
    </div>
    <router-view></router-view>
  </div>
</template>

<script>
import HelloWorld from './components/HelloWorld.vue'

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

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

嵌套路由与重定向

嵌套路由应用场景: 左侧有个侧边栏 上面有个导航栏, 这里的/a 可能对应的就是侧边栏, /a下面可能还有个属性叫/aa aa就是对应的实际的页面内容 这就需要使用路由嵌套来实现这个需求

const routes = [{
    
    
  path: '/a',
  component: A,
  redirect: '/a/aa',
    //子路由
  children: [
    {
    
     
      path: '/a/aa',
      component: AA,
    }] 
}]

访问A的时候如何重定向到AA?

 {
    
    
    path:'/a',
    component:A,
    redirect:"/a/aa",
    children:[
      {
    
    
        path:'/a/aa',
        component:AA
      }
    ]
  },

vue router的路由参数与编程式导航

为了支持restful形式路由以及更复杂的场景时,我们可以使用动态路由,定义路由时,在路由前加上冒号即可,我们先添加AA2组件,动态路由部分通过this.$route.params进行接收:

什么是restful形式的路由呢 192.168.31.148:8080/#/a/13344444 后面的数字为商品的编号,我们需要将商品展示到页面上,这个id是不停的改变的,就是说这个路由是不停改变的(动态路由),那么动态路由该如何实现呢
首先需要一个组件来支持动态路由展示,因为虽然路由是在改变,但是对应的模板是有一定规律的,

const routes=[
 {
    
    
   path:'/a',
   component:A,
   redirect:"/a/aa",
   children:[
     {
    
    
       path:'/a/aa',
       component:AA
     },
     {
    
    
         //动态路由
       path:'/a/:id',
       component:AA2
     }
   ]
 },
 {
    
    path:'/b',component:B},
 {
    
    path:'/hello',component:HelloWorld}
]
 <div>{
   
   {$route.params.id}}</div>    //组件内接收到数据

还有一种 传递一个url参数

http://localhost:8081/#/a/aa?message=123

如何拿到123呢 不用在main.js里配置

<template>
  <div>
    我是a的子组件
    <div>{
   
   {$route.query.message}}</div>  
  </div>
</template>

编程式路由:
有很多时候我们需要手动操作路由的跳转,这时我们需要使用this.$router,以下是一些常用的操作:
不如说在b组件中增加一个按钮 点击这个按钮希望能够跳转到a页面

    jump(){
    
    
     this.$router.push('/a')
    }
  • 路由跳转
this.$router.push('/a/aa')
  • 带参数路由跳转
this.$router.push({
    
    
  path: '/a/aa',
  query: {
    
    
    message: 'hello'
  }
})
  • 不向history插入记录
this.$router.replace('/a/123')
  • 回退
this.$router.go(-1)

2 vuex

vuex解决状态管理的问题,通过集中管理状态,使得state(date),actions(method),view(template)实现松耦合,让代码更容易维护,

1111.png

const store=new Vuex.store({
    
    
  state:{
    
    
    data:"this is a data"
  },
  //同步修改
  mutations:{
    
    
    SET_DATA(state,data){
    
    
      state.data=data
    }
  },
  // actions调用mutations实现状态变更
  //可以使用异步的方式修改
  actions:{
    
    
    // 第一个是结构出来的参数
    setData({
     
     commit},data){
    
    
      commit('SET_DATA',data)
    }
  }
})
   <div>{
   
   {$store.state.data}}</div>   读取状态

修改:

 update(){
    
    
      this.$store.dispatch('setData',"update")
    }

这样就实现了视图和状态的解耦

实际项目开发中,状态众多,如果全部混在一起,则难以分辨,而且容易相互冲突,为了解决问题,vuex引入模块化的概念,解决这个问题,下面我们定义a和b两个模块:

const moduleA={
    
    
  state:{
    
    
    data:"this is a"
  },
  //同步修改
  mutations:{
    
    
    SET_DATA(state,data){
    
    
      state.data=data
    }
  },
  // actions调用mutations实现状态变更
  //可以使用异步的方式修改
  actions:{
    
    
    // 第一个是结构出来的参数
    setData({
     
     commit},data){
    
    
      commit('SET_DATA',data)
    }
  }
}


const moduleB={
    
    
  state:{
    
    
    data:"this is b"
  },
  //同步修改
  mutations:{
    
    
    SET_DATA(state,data){
    
    
      state.data=data
    }
  },
  // actions调用mutations实现状态变更
  //可以使用异步的方式修改
  actions:{
    
    
    // 第一个是解构出来的参数
    setData({
     
     commit},data){
    
    
      commit('SET_DATA',data)
    }
  }
}

const store=new Vuex.Store({
    
    
  modules:{
    
    
    a:moduleA,
    b:moduleB
  }
})

<div>{
   
   {$store.state.a.data}}</div>
<div>{
   
   {$store.state.b.data}}</div>
<button @click="update('a')">update a</button>
<button @click="update('b')">update b</button>
update(ns) {
    
    
  this.$store.dispatch(`setData`, `update ${
      
      ns}`)
}

上述代码在执行过程中,获取状态没有问题,但是修改状态会出现问题,因为两个模块出现同名actions,所以此时需要使用命名空间来解决这个问题:

const moduleA = {
    
    
  namespaced: true,
  // ...
}
update(ns) {
    
    
  this.$store.dispatch(`${
      
      ns}/setData`, `update ${
      
      ns}`)
}

第五章element-UI用法

https://element.eleme.cn/#/zh-CN/component/installation

https://www.youbaobao.xyz/admin-docs/guide/base/element.html

基本用法

import ElementUI from 'element-ui'
Vue.use(ElementUI)

这时候写一个el-button 按钮是没有样式的,需要引入样式 它包含elementUI所有的样式

import 'element-ui/lib/theme-chalk/index.css'

写一个小case

给按钮增加一个点击事件,

<el-button @click="show">这是一个按钮</el-button>
 show(){
    
    
      this.$message.success('element-ui提示')
    }

按需加载

为什么需要按需加载
我们对项目进行打包 npm run build
像我们这样全量引用的方式构建完之后的体积有多大呢
20210709-210834-0272.png

linux下的ll相当于windows下的ls

20210709-212240-0779.png

因为做了全量引用,所以占用空间比较大 未使用按需加载

现在需要把它改造成按需加载
先安装一个babel的插件

npm install babel-plugin-component -D

-D代表在develop下面创建插件

安装完之后就能做一个修改 生成了一个babel.config.js 打开它

{
    
    
  "presets": [["es2015", {
    
     "modules": false }]],
  "plugins": [
    [
      "component",
      {
    
    
        "libraryName": "element-ui",
        "styleLibraryName": "theme-chalk"
      }
    ]
  ]
}

增加一个"plugins"

 "plugins": [
    [
      "component",   这是我们安装的插件
        这些是component的配置信息
      {
    
    
        这个是指定按需加载的library的名称为element-ui
        "libraryName": "element-ui",
        样式库也做一个指定
        "styleLibraryName": "theme-chalk"
      }
    ]
  ]

这样的话那个import ‘element-ui/lib/theme-chalk/index.css’ 这个就可以去掉了

module.exports = {
    
    
  presets: ["@vue/cli-plugin-babel/preset"],

  plugins: [
    [
      "component",
      {
    
    
        libraryName: "element-ui",
        styleLibraryName: "theme-chalk"
      }
    ]
  ]
}

import Vue from 'vue'
import App from './App.vue'
// import ElementUI from 'element-ui'
// import 'element-ui/lib/theme-chalk/index.css'
import {
    
    Button,Message} from 'element-ui'
Vue.config.productionTip = false

// Vue.use(ElementUI)
// vue.component需要传入两个参数, 一个是组件的名称,第二个就是组件的构造函数
Vue.component(Button.name,Button)
// 这个message就不是作为一个component引入了 它是作为一个方法  在原型上面增加一个message方法
Vue.prototype.$message=Message
new Vue({
    
    
  render: h => h(App),
}).$mount('#app')

更方便-插件引用

我们不用手动的编写babel文件,手动使用element-ui的按需引用 直接可以使用vuecli的插件来完成引用

20210709-224337-0644.png

表单的基本用法

有两个组件用起来比较复杂,表单组件和table组件

https://element.eleme.cn/#/zh-CN/component/form

https://www.youbaobao.xyz/admin-docs/guide/base/element.html#%E8%A1%A8%E5%8D%95%E5%9F%BA%E6%9C%AC%E7%94%A8%E6%B3%95

  1. el-form 容器,通过 model 绑定数据
  2. el-form-item 容器,通过 label 绑定标签
  3. 表单组件通过 v-model 绑定 model 中的数据

20210712-085940-0327.png

el-form :model=“xxx”
el-from-item label=
el-input/select… v-model=“xxx.yy”

如果不需要表单容器,使用div也是可以的 el-form-item也可以自己写
那么表单为什么要增加el-form 和el-form item呢,最重要的功能是表单的校验 还有对表单进行控制

比如说不想让用户名为空

    onSubmit() {
    
    
      console.log(this.data)
      if(!this.data.user){
    
    
        this.$message.error("用户名不能为空");
      }
    }

但是如果这样的话在这个方法里面要做的判断非常多,需要做大量的校验逻辑 我们这个方法应该是一个提交功能,并不是想做一个校验功能, 校验应该是validate的功能,有什么方法可以简化校验过程呢,el-form的价值就体现出来了,他提供了一套校验的解决方案。
他通过rules这个属性绑定到一个对象 对象的前面需要有个属性

注意这里截图有个地方写错了 props没有:

20210712-101509-0063.png

20210712-100413-0363.png

<template>
  <div id="app">
    <el-form inline :model="data" :rules="rules">
      <!-- 注意这里的prop没加: -->
      <el-form-item label="审批人" prop="user">   
        <el-input v-model="data.user" placeholder="审批人"></el-input>
      </el-form-item>
      <el-form-item label="活动区域">
        <el-select v-model="data.region" placeholder="活动区域">
          <el-option label="区域一" value="shanghai"></el-option>
          <el-option label="区域二" value="beijing"></el-option>
        </el-select>
      </el-form-item>
      <el-form-item>
        <el-button type="primary" @click="onSubmit">查询</el-button>
      </el-form-item>
    </el-form>
  </div>
</template>

<script>
export default {
  name: "app",
  data() {
    const userValidator = (rule, value, callback) => {
      // rule :当前校验规则,value:输入的值 ,
      if (value.length > 3) {
        callback();
      } else {
        callback(new Error("用户名长度必须大于3"));
      }
    };
    return {
      //这个data是表单的数据源 通过model进行绑定
      data: {
        user: "sam",
        region: "区域二",
      },
      rules: {
        user: [
          { required: true, trigger: "change", message: "用户名必须录入" },
          { validator: userValidator, trigger: "change" },
        ],
      },
    };
  },
  methods: {
    onSubmit() {
      console.log(this.data);
      if (!this.data.user) {
        this.$message.error("aaa");
      }
    },
  },
};
</script>

那么错误如何被打印出来呢 方便对异常进行处理

两种方法一种通过绑定rules对change或者blur事件监听 另一种用el-form的api validate来实现手动校验(提交的时候做校验)

20210712-103846-0270.png

表单校验高级用法

1动态添加校验规则

动态添加校验规则
我们让rules只有一条校验规则,然后创建一个新的按钮

<template>
  <div id="app">
    <!-- validate-on-rule-change当rules发生变化会立即进行校验 -->
    <el-form 
      inline 
      :model="data" 
      :rules="rules" 
      ref="form"
      :validate-on-rule-change="false"
      >
      <!-- 注意这里的prop没加: -->
      <el-form-item label="审批人" prop="user">
        <el-input v-model="data.user" placeholder="审批人"></el-input>
      </el-form-item>
      <el-form-item label="活动区域">
        <el-select v-model="data.region" placeholder="活动区域">
          <el-option label="区域一" value="shanghai"></el-option>
          <el-option label="区域二" value="beijing"></el-option>
        </el-select>
      </el-form-item>
      <el-form-item>
        <el-button type="primary" @click="onSubmit()">查询</el-button>
        <el-button type="primary" @click="addRule">点我添加校验规则</el-button>
      </el-form-item>
    </el-form>
  </div>
</template>

<script>
export default {
  name: "app",
  data() {
    // const userValidator = (rule, value, callback) => {
    //   // rule :当前校验规则,value:输入的值 ,
    //   if (value.length > 3) {
    //     callback();
    //   } else {
    //     callback(new Error("用户名长度必须大于3"));
    //   }
    // };
    return {
      //这个data是表单的数据源 通过model进行绑定
      data: {
        user: "sam",
        region: "区域二",
      },
      rules: {
        user: [
          { required: true, trigger: "change", message: "用户名必须录入" }, // blur
          // { validator: userValidator, trigger: "change" },
        ],
      },
    };
  },
  methods: {
    onSubmit() {
      console.log(this.data);
      // if (!this.data.user) {
      //   this.$message.error("aaa");
      this.$refs.form.validate((res, err) => {
        console.log(res, err);
      });
    },
    addRule() {
      const userValidator = (rule, value, callback) => {
        if (value.length > 3) {
          callback();
        } else {
          callback(new Error("用户名长度必须大于3"));
        }
      };
      const newRule = [
        // 拼接校验规则
        ...this.rules.user,
        { validator: userValidator, trigger: "change" },
      ];
      // this.rules = Object.assign({}, this.rules, { user: newRule });  这是老师的
      // 我认为还可以这样写
      //  this.rules = Object.assign({}, { user: newRule }); 
      // 或者下面这样
       this.rules = { user: newRule }; 
      //  this.rules.user.push(newRule)   //这个不行,因为watch监听不到user的变化  只能监听rules本身的变化
    },
  },
};
</script>

2手动控制校验状态

  • validate-status:验证状态,枚举值,共四种:
    • success:验证成功
    • error:验证失败
    • validating:验证中
    • (空):未验证
  • error:自定义错误提示
<template>
  <div id="app">
    <!-- validate-on-rule-change当rules发生变化会立即进行校验 -->
    <el-form
      inline
      :model="data"
      :rules="rules"
      ref="form"
      :validate-on-rule-change="false"
      status-icon
    >
      <!-- 注意这里的prop没加: -->
      <el-form-item
        label="审批人"
        prop="user"
        :error="error"
        :validate-status="status"
      >
        <el-input v-model="data.user" placeholder="审批人"></el-input>
      </el-form-item>
      <el-form-item label="活动区域">
        <el-select v-model="data.region" placeholder="活动区域">
          <el-option label="区域一" value="shanghai"></el-option>
          <el-option label="区域二" value="beijing"></el-option>
        </el-select>
      </el-form-item>
      <el-form-item>
        <el-button type="primary" @click="onSubmit()">查询</el-button>
        <el-button type="primary" @click="addRule">点我添加校验规则</el-button>
      </el-form-item>
    </el-form>
  </div>
</template>

<script>
export default {
  name: "app",
  data() {
    // const userValidator = (rule, value, callback) => {
    //   // rule :当前校验规则,value:输入的值 ,
    //   if (value.length > 3) {
    //     callback();
    //   } else {
    //     callback(new Error("用户名长度必须大于3"));
    //   }
    // };
    return {
      //这个data是表单的数据源 通过model进行绑定
      error: "",
      status: "",
      data: {
        user: "sam",
        region: "区域二",
      },
      rules: {
        user: [
          { required: true, trigger: "change", message: "用户名必须录入" }, // blur
          // { validator: userValidator, trigger: "change" },
        ],
      },
    };
  },
  methods: {
    onSubmit() {
      console.log(this.data);
      // if (!this.data.user) {
      //   this.$message.error("aaa");
      this.$refs.form.validate((res, err) => {
        console.log(res, err);
      });
    },
    addRule() {
      const userValidator = (rule, value, callback) => {
        if (value.length > 3) {
          callback();
        } else {
          callback(new Error("用户名长度必须大于3"));
        }
      };
      const newRule = [
        // 拼接校验规则
        ...this.rules.user,
        { validator: userValidator, trigger: "change" },
      ];
      // this.rules = Object.assign({}, this.rules, { user: newRule });  这是老师的
      // 我认为还可以这样写
      //  this.rules = Object.assign({}, { user: newRule });
      // 或者下面这样
      this.rules = { user: newRule };
      //  this.rules.user.push(newRule)   //这个不行,因为watch监听不到user的变化  只能监听rules本身的变化
    },
  },
};
</script>
<template>
  <div id="app">
    <!-- validate-on-rule-change当rules发生变化会立即进行校验 -->
    <el-form
      inline
      :model="data"
      :rules="rules"
      ref="form"
      :validate-on-rule-change="false"
      status-icon
    >
      <!-- 注意这里的prop没加: -->
      <el-form-item
        label="审批人"
        prop="user"
        :error="error"
        :validate-status="status"
      >
        <el-input v-model="data.user" placeholder="审批人"></el-input>
      </el-form-item>
      <el-form-item label="活动区域">
        <el-select v-model="data.region" placeholder="活动区域">
          <el-option label="区域一" value="shanghai"></el-option>
          <el-option label="区域二" value="beijing"></el-option>
        </el-select>
      </el-form-item>
      <el-form-item>
        <el-button type="primary" @click="onSubmit()">查询</el-button>
        <el-button type="primary" @click="addRule">点我添加校验规则</el-button>
        <el-button type="success" @click="showSuccess">成功校验</el-button>
        <el-button type="danger" @click="showError">失败校验</el-button>
        <el-button type="warning" @click="showValidating">校验中</el-button>
      </el-form-item>
    </el-form>
  </div>
</template>

<script>
export default {
  name: "app",
  data() {
    // const userValidator = (rule, value, callback) => {
    //   // rule :当前校验规则,value:输入的值 ,
    //   if (value.length > 3) {
    //     callback();
    //   } else {
    //     callback(new Error("用户名长度必须大于3"));
    //   }
    // };
    return {
      //这个data是表单的数据源 通过model进行绑定
      error: "",
      status: "",
      data: {
        user: "sam",
        region: "区域二",
      },
      rules: {
        user: [
          { required: true, trigger: "change", message: "用户名必须录入" }, // blur
          // { validator: userValidator, trigger: "change" },
        ],
      },
    };
  },
  methods: {
    onSubmit() {
      console.log(this.data);
      // if (!this.data.user) {
      //   this.$message.error("aaa");
      this.$refs.form.validate((res, err) => {
        console.log(res, err);
      });
    },
    addRule() {
      const userValidator = (rule, value, callback) => {
        if (value.length > 3) {
          callback();
        } else {
          callback(new Error("用户名长度必须大于3"));
        }
      };
      const newRule = [
        // 拼接校验规则
        ...this.rules.user,
        { validator: userValidator, trigger: "change" },
      ];
      // this.rules = Object.assign({}, this.rules, { user: newRule });  这是老师的
      // 我认为还可以这样写
      //  this.rules = Object.assign({}, { user: newRule });
      // 或者下面这样
      this.rules = { user: newRule };
      //  this.rules.user.push(newRule)   //这个不行,因为watch监听不到user的变化  只能监听rules本身的变化
    },

    showError() {
      this.status = "error";
      this.error = "用户名输入有误";
    },
    showSuccess() {
      this.status = "success";
      this.error = "";
    },
    showValidating() {
      this.status = "validating";
      this.error = "";
    },
  },
};
</script>

表单常见属性(里面有provide和inject的知识点)

  • label-position:标签位置,枚举值,left 和 top
  • label-width:标签宽度
  • label-suffix:标签后缀
  • inline:行内表单
  • disabled: 设置整个 form 中的表单组件全部 disabled,优先级低于表单组件自身的 disabled 这个不是通过遍历而是用provide 设计很巧妙
/* el-input 源码 */
inputDisabled() {
    
    
  return this.disabled || (this.elForm || {
    
    }).disabled;
}
  • size:设置表单组件尺寸
/* el-input */
inputSize() {
    
    
  return this.size || this._elFormItemSize || (this.$ELEMENT || {
    
    }).size;
},
_elFormItemSize() {
    
    
  return (this.elFormItem || {
    
    }).elFormItemSize;
}
/* el-form-item */
elFormItemSize() {
    
    
  return this.size || this._formSize;
},
_formSize() {
    
    
  return this.elForm.size;
}

el-form源码解析

案例

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vZpqxp2y-1628951502651)(https://files.catbox.moe/muvrfd.png)]

<template>
  <div id="app">
    <el-form
        :model="data"
        style="width: 500px"
        label-position="left"
        label-width="100px"
        label-suffix=""
        :inline="false"
        :rules="rules"
        :disabled="false"
        status-icon
        validate-on-rule-change
        hide-required-asterisk
        :inline-message="false"
    >
      <el-form-item
          label="用户名"
          prop="user"
          :error="error"
          :validate-status="status"
      >
        <el-input v-model="data.user" placeholder="用户名" clearable></el-input>
      </el-form-item>
      <el-form-item label="活动区域" prop="region">
        <el-select v-model="data.region" placeholder="活动区域" style="width:100%">
          <el-option label="区域一" value="shanghai"></el-option>
          <el-option label="区域二" value="beijing"></el-option>
        </el-select>
      </el-form-item>
      <el-form-item>
        <el-button type="primary" @click="onSubmit">查询</el-button>
        <el-button type="primary" @click="addRule">添加校验规则</el-button>
        <el-button @click="showError">错误状态</el-button>
        <el-button @click="showSuccess">正确状态</el-button>
        <el-button @click="showValidating">验证状态</el-button>
      </el-form-item>
    </el-form>
  </div>
</template>

<script>
  export default {
      
      
    name: 'app',
    data() {
      
      
      return {
      
      
        data: {
      
      
          user: 'sam',
          region: '区域二'
        },
        error: '',
        status: '',
        rules: {
      
      
          user: [
            {
      
       required: true, trigger: 'change', message: '用户名必须录入' }
          ]
        }
      }
    },
    methods: {
      
      
      /* eslint-disable */
      onSubmit() {
      
      
        console.log(this.data)
      },
      addRule() {
      
      
        const userValidator = (rule, value, callback) => {
      
      
          if (value.length > 3) {
      
      
            callback()
          } else {
      
      
            callback(new Error('用户名长度必须大于3'))
          }
        }
        const newRule = [
          ...this.rules.user,
          {
      
       validator: userValidator, trigger: 'change' }
        ]
        this.rules = Object.assign({
      
      }, this.rules, {
      
       user: newRule })
      },
      showError() {
      
      
        this.status = 'error'
        this.error = '用户名输入有误'
      },
      showSuccess() {
      
      
        this.status = 'success'
        this.error = ''
      },
      showValidating() {
      
      
        this.status = 'validating'
        this.error = ''
      }
    }
  }
</script>

第七章 前端框架搭建

git clone https://github.com/PanJiaChen/vue-element-admin.git

cnpm i npm run dev

  • 删除 src/views 下的源码,保留:
    • dashboard:首页
    • error-page:异常页面
    • login:登录
    • redirect:重定向
  • 对 src/router/index 进行相应修改
  • 删除 src/router/modules 文件夹
  • 删除 src/vendor 文件夹

如果是线上项目,建议将 components 的内容也进行清理,以免影响访问速度,或者直接使用 vue-admin-template 构建项目,课程选择 vue-element-admin 初始化项目,是因为 vue-element-admin 实现了登录模块,包括 token 校验、网络请求等,可以简化我们的开发工作

然后对src/router/index进行修改 module也给删了

这个是做登录的认证跳转

 {
    
    
    path: '/auth-redirect',
    component: () => import('@/views/login/auth-redirect'),
    hidden: true
  },

删除 src/vendor 文件夹 这是转换成excel和zip的文件

src/setting.js:
里面有一系列的配置项

这里有个技巧,可以搜一下这个title在哪儿被引用了

20210715-102830-0627.png

还可以再看一下get-page-title在哪儿被引用了

permission.js

// 全局守卫
router.beforeEach(async(to, from, next) => {
    
    
  // start progress bar
  NProgress.start()

  // set page title
  // 优先从mate就是路由的元信息中进行获取
  document.title = getPageTitle(to.meta.title)
}

项目源码如何进行调试

修改 vue.config.js:

20210715-111817-0943.png

这一块老师本来就有 但是我的没有,所以给加上去了

将 cheap-source-map 改为 source-map,如果希望提升构建速度可以改为 eval

但是我加不加都一样,

20210715-111929-0848.png这里同样显示的源码

通常建议开发时保持 eval 配置,以增加构建速度,当出现需要源码调试排查问题时改为 source-map

是我的操作方法不对吗,改成eval之后我这儿也能看到源码啊

这个layout很重要

20210715-120134-0356.png

我们自己写的组件其实是替换到了这里

20210715-121940-0640.png

路由嵌套:最外层有个大的容器:App.vue 它会被router-view进行替换,这个对应的是layout layout对应整个页面的框架

20210715-123847-0271.png

如果把这个app-main注释掉:20210715-123548-0436.png

这里有个keep-alive 可以保证当前的组件被缓存,如果不需要缓存的话改怎么做呢

20210715-120052-0006.png

第八章 服务端框架搭建

https://github.com/expressjs/express

Node 是一个基于 V8 引擎的 Javascript 运行环境,它使得 Javascript 可以运行在服务端,直接与操作系统进行交互,与文件控制、网络交互、进程控制等

Chrome 浏览器同样是集成了 V8 引擎的 Javascript 运行环境,与 Node 不同的是他们向 Javascript 注入的内容不同,Chrome 向 Javascript 注入了 window 对象,Node 注入的是 global,这使得两者应用场景完全不同,Chrome 的 Javascript 所有指令都需要通过 Chrome 浏览器作为中介实现

npm init -y
npm i -S express

新建App.js(全局入口文件)

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基础概念

中间件,路由,异常处理

中间件

中间件是一个函数,在请求和响应周期中被顺序调用

Middleware functions are functions that have access to the request object (req), the response object (res), and the next function in the application’s request-response cycle.

const myLogger = function(req, res, next) {
    
    
  console.log('myLogger')
  next()   // 一定要调用这个next()   
}

app.use(myLogger)  // 要不然下面的语句都没发执行

路由

应用如何响应请求的一种规则

Routing refers to how an application’s endpoints (URIs) respond to client requests.

响应 / 路径的 get 请求:

app.get('/', function(req, res) {
    
    
  res.send('hello node')
})

响应 / 路径的 post 请求:

app.post('/', function(req, res) {
    
    
  res.send('hello node')
})

规则主要分两部分:

  • 请求方法: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)

20210715-215858-0348.png

TIP

使用时需要注意两点:

  • 第一,参数一个不能少,否则会视为普通的中间件
  • 第二,中间件需要在请求之后引用

项目框架搭建

安装 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”}


boom依赖可以帮助我们快速的生成异常信息

IMG_20210715_230745.jpg

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
  })
})

第九章 项目架构解析

项目需求分析

20210716-185548-0130.png

登录

  • 用户名密码校验
  • token 生成、校验和路由过滤
  • 前端 token 校验和重定向

token可以防止我们的请求被窃取 只有通过token才能完成后面的请求 生成token我们用到了jwt技术来实现。我们还要完成前后端的token校验以及路由过滤。路由过滤是指哪些请求是需要传递token的,哪些不需要传就可以直接访问。

我们在前端校验token的时候还要注意,当token校验不通过的时候该如何进行处理呢–给用户一个登陆失效的提示,并且重定向把用户引导到首页当中,如果不重定向的话因为页面没有退出,所以会看到一些异常的页面

生成token的时候还会有些细节 ,比如说多长时间更新一次

电子书上传

  • 文件上传
  • 静态资源服务器 如何将字节码转换成资源文件 用到了nodejs中的fs(file system)

电子书解析

  • epub原理

  • zip解压

  • xml解析

    epub的本质是一个压缩文件,

电子书增删改

  • mysql数据库应用
  • 前后端异常处理(查不到数据怎么办 接口返回报错以后怎么办)

主要是图书列表页面

里面有翻页控制啊,查询啊一些细节

主要联系mysql数据库应用

电子书解析

修改为.zip

html文件对应的是每个章节的信息

mimetype

vim mimetype --资源类型 application/epub/zip

如何解析?

META-INF / container.xml

这个文件规定了外层为container 里面叫rootfiles(数组) 这个数组当中包含了对象 rootfile 需要包含两个属性 一个是full-path content.opf(重要) ,media-type规定了content.opf是个什么类型

找到content.opf 也是一个opf文件
20210716-214505-0092.png

这里的spin是阅读顺序
根据idref到manifest里面找路径

然后guide是个导读,很多电子书是没有的

还有一个很重要的地方 spin后面有个toc=“ncx” 意思是在项目当中还会包含一个toc.ncx文件,这个文件是我们的目录文件指明了目录文件,后面对目录进行解析的时候就是解析这个 重点是navMap

nginx服务器配置


    server {
	  charset utf-8;
	  listen 8089;
	  server_name http_host;
	  root E:\\Compressed\\nginx-1.21.1\\epub;
	  autoindex on;
	  add_header Cache-Control "no-cache, must-revalidate";
	  location / { 
	    add_header Access-Control-Allow-Origin *;
 	 }
    }

autoindex:是否打开一个索引

add_header Cache-Control “no-cache, must-revalidate”;:不使用缓存 用户请求服务端,这个缓存是在本地,不是在服务端做缓存,是在客户端做缓存,如果说设一个小时意味着客户端一个小时不会向服务端发起请求 有好有坏

location / {
add_header Access-Control-Allow-Origin *;
}

这是一个路由 / 表示监听所有的路由,对所有的路由都生效, 对所有的路由都增加一个add_header 这个header是Access-Control-Allow-Origin 防止跨域 本地配置成*(任何人都能来访问) 实际上线了以后要改成一个允许的域名

静态资源服务器nginx启动

switchhost

https://blog.csdn.net/liangxw1/article/details/78661112

访问到线上,如何在本地也访问到呢, /etc/hosts 通过修改这个文件来实现 不方便

switchhost 快速添加删除host 访问域名的时候都会映射到本地 而且连接也是安全的

事实上,在大厂开发的时候都是通过这种方式,大厂的所有接口他的跨域限制做的是非常严格的 必须通过制定域名才能访问到资源,如果不通过这个域名是没办法访问的 这时候需要对资源进行封装,如果通过localhost是访问不到资源的 ,还有个好处是域名和线上是相同的,意味着现在的环境和线上是近视的,线上的时候是通过这个url来访问资源的

<strong>注意不要直接双击nginx.exe,这样会导致修改配置后重启、停止nginx无效,需要手动关闭任务管理器内的所有nginx进程
 在nginx.exe目录,打开命令行工具,用命令 启动/关闭/重启nginx 
 start nginx : 启动nginx
 nginx -s reload  :修改配置后重新加载生效
 nginx -s reopen  :重新打开日志文件
 nginx -t -c /path/to/nginx.conf 测试nginx配置文件是否正确
       关闭nginx:
 nginx -s stop  :快速停止nginx
 nginx -s quit  :完整有序的停止nginx
 如果遇到报错:
 bash: nginx: command not found
 有可能是你再linux命令行环境下运行了windows命令,
 如果你之前是允许 nginx -s reload报错, 试下 ./nginx -s reload
 或者 用windows系统自带命令行工具运行</strong>

mysql

记得建数据库的时候选择utf-8

第十章 登录功能开发上

登录流程分析

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bUS95wzM-1628951502683)(https://www.youbaobao.xyz/admin-docs/assets/img/login_process.58cab9a5.png)]

各种简化

  • 删除 SocialSign 组件引用
  • 删除 src/views/login/components 目录
  • 删除 afterQRScan
  • 删除 created 和 destroyed

准备工作:简化代码,如何格式化(webstorm)以及indent报错问题

预备知识

路由和权限校验

https://blog.csdn.net/xiaozhazhazhazha/article/details/118862475

详见 router

为什么要做权限校验呢,因为访问login这个路由的时候 vue-element-admin这个框架是做了些校验的 比如说登录到dashboard之后 此时将token删除的话,这时候刷新页面会看到是会退回login页面的,说明框架本身做了些事情,就是进行权限校验,权限校验是放在那里做呢 是放在路由当中做的。

这里有个具体的实例

创建组件

创建组件 src/views/book/create.vue

组件创建完毕 但是还没有办法 访问到 因此要到router中做一些配置,

配置路由

修改 src/router/index.js 的 asyncRoutes:

export const asyncRoutes = [
  {
    
    
    path: '/book',
    component: Layout,
    redirect: '/book/create',
    children: [
      {
    
    
        path: '/book/create',
        component: () => import('@/views/book/create'),
        name: 'book',
        meta: {
    
     title: '添加图书', icon: 'edit', roles: ['admin'] }
      }
    ]
  },
  // ...
]
  • 使用 editor 登录平台,无法看到"添加图书"功能
  • 使用 admin 登录平台,可以看到"添加图书"功能

篇幅有点长,放到csdn了 https://blog.csdn.net/xiaozhazhazhazha/article/details/118862475

梳理总结

关于路由处理

  • vue-element-admin 对所有访问的路由进行拦截;
  • 访问路由时会从 Cookie 中获取 Token,判断 Token 是否存在:
    • 如果 Token 存在,将根据用户角色生成动态路由,然后访问路由,生成对应的页面组件。这里有一个特例,即用户访问 /login 时会重定向至 / 路由;/路由也会重定向到dashboard
    • 如果 Token 不存在(没有登录),则会判断路由是否在白名单中,如果在白名单中将直接访问,否则说明该路由需要登录才能访问,此时会将路由生成一个 redirect 参数传入 login 组件,实际访问的路由为:/login?redirect=/xxx登录之后会做一个重定向,重定向到xxx 重定向怎么实现的在10.13

关于动态路由和权限校验

  • vue-element-admin 将路由分为:constantRoutes 和 asyncRoutes 用户登录系统的时候会动态生成路由表

  • 用户登录系统时,会动态生成路由,其中 constantRoutes 必然包含,asyncRoutes 会进行过滤;

  • asyncRoutes 过滤的逻辑是看路由下是否包含 meta 和 meta.roles 属性,如果没有该属性,所以这是一个通用路由,不需要进行权限校验,会被加到路由表中;如果包含 roles 属性则会判断用户的角色是否命中路由中的任意一个权限,如果命中,则将路由保存下来,如果未命中,则直接将该路由舍弃;

  • asyncRoutes 处理完毕后,会和 constantRoutes 合并为一个新的路由对象,并保存到 vuex 的 permission/routes 中;

  • 用户登录系统后,侧边栏会从 vuex 中获取 state.permission.routes,根据该路由动态渲染用户菜单。

一定好好掌握,这是整个框架的精髓所在

侧边栏

https://blog.csdn.net/xiaozhazhazhazha/article/details/118873782

详见 sidebar

重定向

https://blog.csdn.net/xiaozhazhazhazha/article/details/118887770

详见 redirect

面包屑导航

https://blog.csdn.net/xiaozhazhazhazha/article/details/118974389

详见 breadcrumb

第十一章 登录(中)

登录组件分析

src/views/login

在这里插入图片描述

在这里插入图片描述

可以通过validator手动添加校验

$ref.ref的名字.validate来输出信息

el-tooltip:https://element.eleme.cn/#/zh-CN/component/tooltip#tooltip-wen-zi-ti-shi

.native什么意思? 在自定义组件,如果要调用原生input上面就要用native,就是说要绑定到原生的input组件空间上去

判断大写:

 checkCapslock(e) {
    
    
      const {
    
     key } = e
      this.capsTooltip = key && key.length === 1 && (key >= 'A' && key <= 'Z')
    },
 showPwd() {
    
    
      if (this.passwordType === 'password') {
    
    
        this.passwordType = ''
      } else {
    
    
        this.passwordType = 'password'
      }
     // 注意$nextTick的用法
      this.$nextTick(() => {
    
    
        this.$refs.password.focus()
      })
    },

handleLogin:

this.$store.dispatch(‘user/login’, this.loginForm) 进入的文件

在这里插入图片描述

login 里面又传入了一个login方法

在这里插入图片描述

他的前缀是什么

可以看一下

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

然后resolve 到.then的这一部分

在这里插入图片描述

axios用法分析

官网http://www.axios-js.com/zh-cn/docs/

给我们封装好的request库

在这里插入图片描述

request 库使用了 axios 的手动实例化方法 create 来封装请求,要理解其中的用法,我们需要首先学习 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)
})

其实可以把参数放到params里

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

这里老师的接口不能用,我就用自己的了

http://47.103.29.206:3000/mv/first?limit=10

在这里插入图片描述

token是在header里面写的

在这里插入图片描述

写上token以后不知道为啥会出现跨域问题

我知道了 又看了一下官方文档

需要加上这一句xhrFields: { withCredentials: true }

不行 还是有错

不管他了 这个接口不知道怎么设跨域 后面写自己的接口的时候在设吧

如果服务端抛了个异常 比如404 (非200)可以通过catch来捕获异常

这样写是可以,但是呢会造成冗余代码,不方便维护,如果生成token的逻辑一旦变化,

  • 每个需要传入 token 的请求都需要添加 headers 对象,会造成大量重复代码
  • 每个请求都需要手动定义异常处理,而异常处理的逻辑大多是一致的,如果将其封装成通用的异常处理方法,那么每个请求都要调用一遍

我们希望发一次请求,请求的时候能够拦截header生成的过程,在header生成过程中把token给他插入进去

如果说get写一套,post写一套,其他的也写几套 其实也是会冗余的

下面来学习一下axios实例化,通过调用create方法来解决这个问题

axios.create返回的结果是个function,他不是个具体的结果,在这里,可以给他内置一些基础参数,比如baseUrl,timeout

所以说我们就可以给固定的部分生成一个构造函数

通过request函数再来发起请求 其中,他的基础参数是从create的时候传入的

created() {
    
    
    const url='/search'
    const request=axios.create({
    
    
      baseURL:'http://47.103.29.206:3000',
      timeout:5000
    })
    request({
    
    
      url,
      params:{
    
    
        keywords:'%E6%B5%B7%E9%98%94%E5%A4%A9%E7%A9%BA'
      }
    }).then((res)=>{
    
    
      console.log(res);
    })
  },

axios有更高级的功能,拦截器

上述代码完成了基本请求的功能,下面我们需要为 http 请求的 headers 中添加 token,同时进行白名单校验,如 /login 不需要添加 token,并实现异步捕获和自定义处理

created() {
    
    
    // 放入白名单中
    const whileUrl=['/login','/search']
    const url='/search'
    const request=axios.create({
    
    
      baseURL:'http://47.103.29.206:3000',
      timeout:5000
    })
    // 利用request方法里面的interceptors属性下面的request 下面的use方法
    // use方法传入两个参数  两个参数都是函数,第一个函数是拦截方法,第二个函数是异常处理方法
    request.interceptors.request.use(
      config=>{
    
    
        //config其实就是axios对象
        console.log(config);
        // 如果有符合白名单的
        if(whileUrl.some(wl=>url===wl)){
    
    
          return config
        }
        // 如果不符合 ,那么header加上token
        config.headers['token']='abcd'
        return config
      },
      err=>{
    
    
        console.log(err);
      }
    )
    request({
    
    
      url,
      params:{
    
    
        keywords:'%E6%B5%B7%E9%98%94%E5%A4%A9%E7%A9%BA'
      }
    }).then((res)=>{
    
    
      console.log(res);
    })
  },

可以尝试一下在白名单里删除search 发现会报跨域错误,就是说本来应该是加上了token的

有什么实际意义呢

我们在实际项目发起请求的时候login请求往往是不需要加token的

所以这时候就可以加一个白名单,白名单这个方法没有token也可以进入

白名单以外的进入都要加token

我们可以抛个异常,然后对他进行异常处理

也可以通过Promise.reject(err)把err注入到reject参数里面,这样就可以在reject之后通过catch方法来自己处理这个异常

然后就可以在catch里面做进一步操作

 created() {
    
    
    // 放入白名单中
    const whileUrl = ["/login", "/search"];
    const url = "/search";
    const request = axios.create({
    
    
      baseURL: "http://47.103.29.206:3000",
      timeout: 5000,
    });
    // 利用request方法里面的interceptors属性下面的request 下面的use方法
    // use方法传入两个参数  两个参数都是函数,第一个函数是拦截方法,第二个函数是异常处理方法
    request.interceptors.request.use(
      (config) => {
    
    
        //config其实就是axios对象
        console.log(config);
        // 如果有符合白名单的
        if (whileUrl.some((wl) => url === wl)) {
    
    
          return config;
        }
        // 如果不符合 ,那么header加上token
        config.headers["token"] = "abcd";
       
        return config;
      },
      (err) => {
    
    
        console.log(err);
        Promise.reject(err)
      }
    );
    request({
    
    
      url,
      params: {
    
    
        keywords: "%E6%B5%B7%E9%98%94%E5%A4%A9%E7%A9%BA",
      },
    }).then((res) => {
    
    
      console.log(res);
    }).catch((err)=>{
    
    
      console.log(err);
    })
  },

响应拦截器

request.interception.response.use

在里面可以做一系列判断,比如error_code

第一个参数 :response
第二个:err

打印一下response

在这里插入图片描述

之后就可以判断里面的error_code

data.code

 created() {
    
    
    // 放入白名单中
    const whileUrl = ["/login", "/search"];
    const url = "/search";
    const request = axios.create({
    
    
      baseURL: "http://47.103.29.206:3000",
      timeout: 5000,
    });
    // 利用request方法里面的interceptors属性下面的request 下面的use方法
    // use方法传入两个参数  两个参数都是函数,第一个函数是拦截方法,第二个函数是异常处理方法
    request.interceptors.request.use(
      (config) => {
    
    
        //config其实就是axios对象
        console.log(config);
        // 如果有符合白名单的
        if (whileUrl.some((wl) => url === wl)) {
    
    
          return config;
        }
        // 如果不符合 ,那么header加上token
        config.headers["token"] = "abcd";
       
        return config;
      },
      (err) => {
    
    
        console.log(err);
        Promise.reject(err)
      }
    );

    request.interceptors.response.use(
      response=>{
    
    
        console.log(response);
        if(response.data&&response.data.code ===200){
    
    
          // 这个做法很巧妙,在.then里面打印的话就会只打印response.data 
          // 而不是整个response了
          // 当然要确保每个接口结构都是这样的,因为这是针对所有接口的
          // 直接把data拿出来用起来方便
          return response.data
        }else{
    
    
          // 出错的话就推出去  在catch里面处理
        
          Promise.reject(response.data.code)
        }
      },
      error=>{
    
    
        Promise.reject(error)
      }
    )
    request({
    
    
      url,
      params: {
    
    
        keywords: "%E6%B5%B7%E9%98%94%E5%A4%A9%E7%A9%BA",
      },
    }).then((res) => {
    
    
      console.log(res);
    }).catch((err)=>{
    
    
      console.log(err);
    })
  },

request库源码分析

https://blog.csdn.net/xiaozhazhazhazha/article/details/119065931

登录细节分析

进入页面默认聚焦用户名密码输入框:

 if (this.loginForm.username === '') {
    
    
      this.$refs.username.focus()
    } else if (this.loginForm.password === '') {
    
    
      this.$refs.password.focus()
    }

显示密码后自动聚焦

   showPwd() {
    
    
      if (this.passwordType === 'password') {
    
    
        this.passwordType = ''
      } else {
    
    
        this.passwordType = 'password'
      }
      this.$nextTick(() => {
    
    
        this.$refs.password.focus()
      })
    },

用reduce进行过滤

    getOtherQuery(query) {
    
    
      return Object.keys(query).reduce((acc, cur) => {
    
    
        if (cur !== 'redirect') {
    
    
          acc[cur] = query[cur]
        }
        return acc
      }, {
    
    })
    }
// acc的默认值为空对象  query!=redirect时会进行叠加,最后把叠加的数组返回

关闭mock接口 修改接口地址

main.js 删掉mock相关代码

在这里插入图片描述

api里面的artical qiniu删掉不需要

删除vue.config.js相关配置

把有关mock的删掉

在这里插入图片描述

之后 .env.development

对baseapi做个修改

在这里插入图片描述

把production的api也改成这个

把当前的域名映射到本地

在这里插入图片描述

之后点开网站看看

在这里插入图片描述

这是因为request.js里面的baseurl变了

在这里插入图片描述

老师的是这样的

在这里插入图片描述

比我少了一个vue-element-admin

在这里插入图片描述

这里的问题 给他删掉

接着可以去开发接口了

第十二章 登录(下)

node+mysql数据库,前后端联动,md5加密

搭建http服务

搭建https服务的话:搭建https服务

http服务我们前面其实已经搭好了

现在把端口换一下 18082

在这里插入图片描述

因为访问47.103.29.206:18082的话会报错,因为已经注册过了 所以我后面加了个a

const express=require('express')
const router=require('./router')
const app=express()
app.use('/',router)

app.listen(18082,()=>{
    
    
  console.log("http://localhost:18082");
})

登录api开发

在user里创建一个api

随便写点测试一下

router.post('/login',(req,res)=>{
    
    
  console.log(req.body);
  res.json({
    
    
    code:0,
    msg:'登录成功'
  })
})

因为浏览器不好做post请求测试

这里可以用postman

也可以下载一个curl在dos窗口

或者git bash里面输入也可以

curl http://47.103.29.206a:18082/user/login -X POST

输出req.body为undifined 说明并没有传入参数

可以通过-d来指定body里的参数

curl http://47.103.29.206a:18082/user/login -X POST -d 'username=admin&password=1234'

但是body里面的参数仍然没有被解析 需要用到body-parser中间件来解决这个问题

安装body-parser cnpm i -S body-parser

关于body-parser的用法 :https://juejin.cn/post/6844903478830055431

不用下这个包了 已经被弃用了

在这里插入图片描述

直接用express调用bodyParser的方法就可以了

发的请求可以简化一下 ,加上-d以后他默认是post

curl http://47.103.29.206a:18082/user/login -d 'username=admin&password=1234'

记得把前端的baseAPI也改了

在这里插入图片描述

然后运行一下前端项目

到时候上线了就把a给去掉

在这里插入图片描述

跨域问题

所以在服务端写个解决跨域的方案

在这里插入图片描述

1111

可以看到参数已经传进来了

同时还收到了返回值

在这里插入图片描述

这时候前端后端已经发生了联动了 可以互通了

这里我们在 Network 中会发现发起了两次 https 请求,这是因为由于触发跨域,所以会首先进行 OPTIONS 请求,判断服务端是否允许跨域请求,如果允许才能实际进行请求

也就是说跨域请求的访问是由服务端控制的,服务端下发了这样的response header之后,

在这里插入图片描述

浏览器就知道是可以发送请求的,如果跨域条件不满足的时候,浏览器就直接抛出异常

响应结果封装

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

// new Result  
class Result {
    
    
  // data:向前端返回的数据 msg:向前端返回的信息
  constructor(data, msg = '操作成功', options) {
    
    
    this.data = null
    // 一个都没传
    if (arguments.length === 0) {
    
    
      this.msg = '操作成功'
      // 传入一个参数 认为他是个msg
    } else if (arguments.length === 1) {
    
    
      this.msg = data
    } else {
    
    
      // 传入2/3个参数
      this.data = data
      this.msg = msg
      if (options) {
    
    
        this.options = options
      }
    }
  }

  createResult() {
    
    
    //没有code默认code为success 
    if (!this.code) {
    
    
      this.code = CODE_SUCCESS
    }
    // 基础的结构是一个code 一个msg
    let base = {
    
    
      code: this.code,
      msg: this.msg
    }
    // data存在就增加一个data
    if (this.data) {
    
    
      base.data = this.data
    }
    // 如果还有额外的options 就加到base里
    if (this.options) {
    
    
      base = {
    
     ...base, ...this.options }
    }
    console.log(base)
    return base
  }

  json(res) {
    
    
    //通过res.json返回给前端
    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

还要来改造一下user.js

router.post('/login',(req,res)=>{
    
    
  console.log(req.body);
  new Result('登录成功').success(res)
})

做失效功能的时候只需要增加一个方法就行 扩展能力很好

如果success里面出现了异常,可以在index.js的异常处理语句中被处理

mysql

文档

在这里插入图片描述

密码是用的md5加盐

我们需要在node中安装mysql数据库

建一个db文件夹 专门存储db的相关操作

里面包含两个文件 config.js index.js

config.js:

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

index.js

const mysql = require('mysql')
const config = require('./config')
function connect() {
    
    
  return mysql.createConnection({
    
    
    ...config
  })
}
function querySql(sql) {
    
    
  const conn = connect()
  return new Promise((resolve, reject) => {
    
    
    try {
    
    
      conn.query(sql, (err, results) => {
    
    
        if (err) {
    
    
          reject(err)
        }
        resolve(results)
      })
    }
    catch (e) {
    
    
      reject(e)
    }
    finally {
    
    
      conn.end()
    }
  })

}
module.exports=querySql

user.js引入

const querySql=require('../db/index')
 querySql('select * from admin_user').then((res)=>{
    
    
    console.log(res);
  }).catch((err)=>{
    
    
    console.log(err);
  })

有个技巧,我们可以在关键的地方打印一些参数, 在constant里面写个debug为true

然后在需要输出的页面里引入debug

debug&&console.log(xxxx)

在上线的时候我们就可以把debug改成false 就不会再打印了

在这里插入图片描述

这是在db/index.js里面写的, 就不用再业务逻辑里面打印了

这个配置文件可以从代码中抽离出来,变成一个单独的配置文件,这样就可以在不启动代码的情况下实现debug的切换

有了这个查询语句 ,就可以对用户查询进一步封装

我们是直接将查询语句写在user.js里了

更好的做法是再建一个service层 把业务逻辑全放到service层

创建service 里面包含了个user.js

创建一个function 叫login

const querySql=require('../db/index')
function login(username,password){
    
    
  return querySql(`select * from admin_user where username='${
      
      username}' and password='${
      
      password}'`)
}
module.exports=login

在router/user.js

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

在这里插入图片描述

这时,即使我们输入正确的用户名密码也会出现无法登陆的情况

这是因为密码采用了MD5+SALT加密,所以需要对密码进行对等加密。

去constant文件

加上这句话

PWD_SALT:'admin_imooc_node'

这个就相当于一个秘钥,通过秘钥和密码混合之后生成新的密码

不知道秘钥的话外界要破解这个密码是很难破解的

接着对密码进行MD5加密

cnpm i -S crypto

新建文件utils/index.js 并写入

const crypto=require('crypto')
function md5(s){
    
    
  return crypto.createHash('md5').update(String(s)).digest('hex')
}
module.exports=md5

接着要对password进行改造

password=md5(` p a s s w o r d {password} password{PWD_SALT}`)

express-validator

https://express-validator.github.io/docs/validation-chain-api.html

https://blog.csdn.net/cuk0051/article/details/108343329

用处:可以简化对post请求的校验

可以帮助我们快速的验证表单当中的参数

安装依赖

使用它的body方法,在post请求当中,需要在第二个参数当中传入一个数组,通过数组来进行校验,

如何接受到参数呢

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

这里的err有个isEmpty方法,这个方法可以去判断数组是否为空

如果isEmpty为false表示出现了验证错误,我们就可以从errors里面拿到msg然后返回给前端抛出异常

然后通过第三个参数next来继续传递,传递给下一个中间件来执行

这里的[ ]=err.errors代表取出第一个元素

在这里插入图片描述

去前端测试一下

在这里插入图片描述

注意next 传递给下一个中间件,下一个中间件是

在这里插入图片描述

express-validator 使用技巧:

  • router.post 方法中使用 body 方法判断参数类型,并指定出错时的提示信息
  • 使用 const err = validationResult(req) 获取错误信息,err.errors 是一个数组,包含所有错误信息,如果 err.errors 为空则表示校验成功,没有参数错误
  • 如果发现错误我们可以使用 next(boom.badRequest(msg)) 抛出异常,交给我们自定义的异常处理方法进行处理

jwt

https://www.youbaobao.xyz/admin-docs/guide/extra/jwt.html

https://www.ruanyifeng.com/blog/2018/07/json_web_token-tutorial.html

https://github.com/dwyl/learn-json-web-tokens/blob/master/README.md

基本概念

Token 的用途主要有三点:

  • 拦截无效请求,降低服务器处理压力;
  • 实现第三方 API 授权,无需每次都输入用户名密码鉴权;
  • 身份校验,防止 CSRF 攻击

可以在https://jwt.io/ 调试jwt字符串

有三段,第一段是加密的算法和token的类型,第二段是具体数据,第三段是签名部分,使用加密算法

私钥是存在服务端的,别人不知道私钥的时候是无法破解里面的信息的,最后解开的时候是要用到私钥的。

生成jwt token

npm i -S jsonwebtoken

需要一个私钥和一个过期时间,过期时间不宜过短,也不宜过长,课程里设置为 1 小时,实际业务中可根据场景来判断,通常建议不超过 24 小时,保密性要求高的业务可以设置为 1-2 小时:

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)
    }
})

前端代码改造

utils/require.js

对响应拦截器做修改

在这里插入图片描述

这里的message也改成msg return Promise.reject(new Error(res.msg || 'Error'))

然后是views/login/index.vue

找到具体做登录动作的方法handleLogin

他会进入到user/login这个action

看一下这里的data 后面serToken让token保存起来了

在这里插入图片描述

token是保存起来了,但是运行起来报错,这都是因为/info接口还没有开发

来分析一下

在这里插入图片描述

进入到permission.js 全局守卫

因为去的是dashboard所以走else

之后进入到user/getInfo

在这里插入图片描述

又调用了一个getInfo方法

在这里插入图片描述

请求了后端的/user/info接口

在这里插入图片描述

我们还没有开发,所以后面的路都走不通了。 我们看看他的/info的框架是怎么实现的

在这里插入图片描述

他是直接带上token参数,我们后面要改成request里面包含token信息,拿到token信息,服务端就可以解析出用户信息,因为token信息当中有个username,我们把username解出来之后根据username查到用户信息,再返回给前端,整个实现流程是这样的状态,所以我们下面要在服务端添加一个jwt的认证,我们刚刚是生成了token,现在要来验证一下token是否在有效期范围内。

jwt认证

https://www.cnblogs.com/zkqiang/p/11810203.html

安装:npm i -S express-jwt

他的主要功能是检查所有的路由,判断当前时间是否在过期时间内,当路由中包含了没有过期的token,就可以判定为通过,

新建一个中间件router/jwt.js

这个中间件的主要用途就是做验证

这个地方卡了我好久 不是user/login而是/user/login

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

const jwtAuth = expressJwt({
    
    
  secret: PRIVATE_KEY,
  credentialsRequired: true // 设置为false就不进行校验了,游客也可以访问
}).unless({
    
    
  path: [ 
    '/',
    '/user/login'   
  ], // 设置 jwt 认证白名单
});

module.exports = jwtAuth;

在index.js中加入这个中间件

const jwtAuth = require('./jwt')

// 注册路由
const router = express.Router()

// 对所有路由进行 jwt 认证
router.use(jwtAuth)

之后去浏览器看看

有个报错 algorithms should be set

解决:https://iseeu.blog.csdn.net/article/details/108641110

algorithms:[‘HS256’]

在这里插入图片描述

抛出500错误,说明拦截已经生效了 ,因为在header里面他没有找到token,所以报错了

现在去服务端做一些处理 现在发现错误以后返回的都是-1,但是我们希望token的错误会有另外的值比如-2

在constant.js 增加:CODE_TOKEN_EXPIRED:-2

当出现token错误的时候会打印出这个

在这里插入图片描述

在这里插入图片描述

老师的是这样的,他是用name标识token错误的,我这儿没有,就用code标识吧

在这里插入图片描述

在这里插入图片描述

还有,我们以前封装了Result,可以改造一下,通过Result快速生成错误信息

在这里插入图片描述

在Result里新建一个方法

在这里插入图片描述

点击登录

在这里插入图片描述

在这里插入图片描述

对前端代码再进行改造

request-相应拦截器-error

这里有个技巧 如何看到error的详细信息

在这里插入图片描述

在这里插入图片描述

这样就能弹出自定义的msg

在这里插入图片描述

服务端else这里的代码可以用Result改造一下

在这里插入图片描述

在这里插入图片描述

目前,会出现token验证的错误,为了解决这个错误,我们需要将header里增加一个authorization(jwt规定的)

去request的请求拦截器:

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

这里的user/info其实不需要带参数

在这里插入图片描述

在这里插入图片描述

开发user/info接口

在服务端部分,在数据库index.js里增加一个queryOne的方法

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

service/user.js(把login.js重命名了 变成user.js):

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

router/user.js

router.get('/info', (req, res, next) => {
    
    
  // res.send('user/info')
  findUser('admin').then((user) => {
    
    
    console.log(user);  //不需要处理异常,因为默认情况会抛给自定义异常处理
    if (user) {
    
    
      new Result(user, '用户信息查询成功').success(res)
    } else {
    
    
      new Result('用户信息查询失败').fail(res)
    }
  })
})

在这里插入图片描述

因为不想让他出现password 所以查询语句要修改一下

function findUser(username){
    
    
  return queryOne(`select id,username,nickname,role,avatar from admin_user where username='${
      
      username}'`)
}

在这里插入图片描述

因为前端获取的是roles不是role

所以在查询成功前面需要加上user.roles=[user.role]

router.get('/info', (req, res, next) => {
    
    
  // res.send('user/info')
  findUser('admin').then((user) => {
    
    
    console.log(user);  //不需要处理异常,因为默认情况会抛给自定义异常处理
    if (user) {
    
    
      user.roles=[user.role]
      new Result(user, '用户信息查询成功').success(res)
    } else {
    
    
      new Result('用户信息查询失败').fail(res)
    }
  })

})

这样就可以进去了

继续对user/info接口进行改造 username不应该写死

username需要从token里拿,这就用到了对jwt进行解析

所以,我们要在header中拿到jwt并且对他进行解析

前端仅在 Http Header 中传入了 Token,如果通过 Token 获取 username 呢?这里就需要通过对 JWT Token 进行解析了,

需要用到jsonwebtoken里的verify方法

需要从http header里拿到authorization

/utils/index.js 中添加 decode 方法:

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

在这里插入图片描述

router.get('/info', (req, res, next) => {
    
    
  const decoded=decode(req)
  console.log({
    
    decoded});
  // res.send('user/info')
  if(decoded&&decoded.username){
    
    
    findUser(decoded.username).then((user) => {
    
    
      console.log(user);  //不需要处理异常,因为默认情况会抛给自定义异常处理
      if (user) {
    
    
        user.roles=[user.role]
        new Result(user, '用户信息查询成功').success(res)
      } else {
    
    
        new Result('用户信息查询失败').fail(res)
      }
    })
  }else{
    
    
    new Result('用户信息查询失败').fail(res)
  }
})

登出

登出的时候显示接口不存在,其实登出的时候是不需要调用任何接口的

只需要将token清空,然后重定向一下

如何解决

在这里插入图片描述

用try-catch改造 这里不走他的接口了

  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 (error) {
    
    
        reject(error)
      }
    })
  },

查询一下,发现是在NavBar里面被用到了

在这里插入图片描述

logout执行完以后他会跳到login页面

回顾

主要开发了两个api一个是登录api ,一个是获取用户信息api

前端请求api到服务端以后,服务端会检查jwt的白名单,在白名单的话直接调用controller,如果不在,进行jwt token的认证,是通过 jwt库 express-jwt来进行实现的,如果验证失败将返回401错误,验证成功将调用controller

在登录的时候会判断body参数,通过express-validator来进行判断,如果验证失败会调用boom.badRequest返回验证失败

之后会调用mysql数据库,写了两个方法,一个是login服务,一个是findUser服务,他们都是查询admin_User这张表去判定用户是否存在,如果是登录场景的话,会通过jwt生成一个token,将token返回给前端用户

第十三章 上传

将本地的电子书上传到服务端

现在的node服务位于本地 电子书就会上传到本地的文件夹,如果node服务部署在远端,比如说阿里云,那电子书就可以上传到阿里云服务器上

框架

新建book/components 把业务组件存放到components目录,把全局的通用组件放在src的components下,方便所有的页面进行复用。

新建book/components/Detail.vue

创建两个组件 book/create.vue book/edit.vue,因为电子书上传有两个场景,第一个是一本电子书都没有的时候去上传电子书,还有种场景是已经有电子书,去编辑电子书的时候。仍然是进入到电子书上传的页面(Detail.vue)

其中edit组件不希望他在我们的菜单中出现,因为编辑电子书肯定是要在列表当中选择一个,需要带入参数的。不想让他出现–hidden属性。还有希望高亮显示到图书列表,meta activeMenu指定高亮的路由

在这里插入图片描述

在edit和create组件里引入detail组件

在这里插入图片描述

在Detail.vue中接收一个参数 isEdit

在这里插入图片描述

create:在这里插入图片描述

edit: 在这里插入图片描述

Detail.vue

sticky组件 ; 按钮:编辑的时候不需要显示帮助

在这里插入图片描述

给编辑/新增电子书按钮增加v-loading 默认false

在这里插入图片描述

新增一个样式

在这里插入图片描述

这个status可以放什么值呢

可以 查一下全局样式sub-navbar

在这里插入图片描述

这里有个细节,向sticky传入的并不是通常的class绑定,而是通过className,这是因为className其实是sticly的一个属性

在这里插入图片描述

然后会绑定到对应的div上

在这里插入图片描述

如果希望把某些元素的class交给子组件或者父组件引用的时候去使用的话就可以定义一个props 然后传入className这样一个方法

到现在sticky就实现了

接着写表单

可以用el-row来编写

表单容器分为两部分,写两个el-col

上面一部分是上传的组件,写一个el-col 可以将整个页面分成24份(占满一屏)

在这里插入图片描述

我们还可以写一个Warning组件,可以做一些提醒,

aside :h5的一个新的标签

https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/aside

上传组件 &token认证

src/components/EbookUpload/index.vue

在Detail.vue里引入 在这里插入图片描述

element ui里的组件:el-upload

点击上传

在这里插入图片描述

在这里插入图片描述

意思是需要一个接口地址

这个action不需要之前的request库,因为el-upload会自动给我们上传

在这里插入图片描述

之后会定义一个/book/upload的上传接口

上传的时候就会调用这个接口

在这里插入图片描述

报401错误 鉴权问题(之前设置了白名单是/和/login 其他接口都要带上token)

所以说上传的时候也要做token验证

增加headers (可以看el-upload文档) 可以给他写成一个计算属性,因为headers是需要动态进行计算,只要计算值没有变更的时候就不需要进行变更

在headers里要加入authorization

在这里插入图片描述

这样就可以完成token认证了

在这里插入图片描述

现在报404接口不存在的错误了

接下来就可以开发接口了

上传组件开发

:multiply表示一次是否能上传多本电子书

limit

beforeUpload 上传之前被调用 这里他的作用是调用父组件的一个事件,这个逻辑交给父组件去做,接收file参数传给父组件

在这里插入图片描述

在这里插入图片描述

成功调用事件 失败调用事件 移除事件 传入超过数量事件

:file-list 在编译的时候预先就要把fileList传入到el-upload中,这样的话上传的控件里面才能看到内容

drag show-file-list accept(接收的资源类型application/epub+zip 不是这个类型的上传不了)

:disabled 当用户上传完电子书以后就不允许他上传了,可以设为true来触发这样一个效果

在这里插入图片描述

在这里插入图片描述

err打印

在这里插入图片描述

在这里插入图片描述

加个小上传图标

在这里插入图片描述

在这里插入图片描述

这是el-upload内置的样式 会对文字做一些处理

在这里插入图片描述

api开发

在这里插入图片描述

分别是电子书文件,电子书封面,解压后的电子书

在这里插入图片描述

在这里插入图片描述

这是nginx路径,为什么要把它放到nginx路径能

这样做有个好处,就是当电子书上传到这个路径之后会自动生成一个链接

在这里插入图片描述

这样就可以直接通过链接来访问到资源

之后,需要使用multer库 也是一个express的中间件来开发文件上传功能

router/book.js

在这里插入图片描述

将所有经过book的路由都委托给bookRouter来处理

这样就可以在book文件里写嵌套路由

book.js:
调用express的Router()方法生成一个router对象
然后在对象当中调用get或post来创建请求
之后再export router就ok

在这里插入图片描述

上传单个文件,同时把单个文件信息放到req.file上

在这里插入图片描述

上传试一下,可以看到已经上传成功

在这里插入图片描述

上传组件功能完善

有个细节 虽然是同一本电子书但是他们名称不一样,这是multer帮我们做的事情,不然我们还要写代码改文件名字

在这里插入图片描述

完善onSuccess 传入两个参数 response ,file

打印一下response

在这里插入图片描述

这里可以拿到服务端返回的信息

拿到msg以后就可以给用户反馈上传成功

在这里插入图片描述

触发父组件

在这里插入图片描述

移除事件

在这里插入图片描述

超出上传数量事件

在这里插入图片描述

Detail.vue

在这里插入图片描述

在这里插入图片描述

fileList用途 在编辑的时候获取到电子书以后把它放到fileList 默认的时候上传组件就会有文件展示出来,

:disabled=“isEdit” 表示处于编辑状态的时候EbookUpload是点击不了不可用的

成功和移除的事件后面再写

电子书表单开发

在点击新增电子书的时候,提交的并不是ebook-upload这个组件里的内容,而是需要解析出来电子书的内容,通过这部分的内容产生表单,然后提交表单里面的内容。

这时候添加一个表单,这个表单默认的时候是空的,通过ebook-upload组件上传,然后会从服务端返回一个data对象,这个对象就是电子书对象

这个对象可以从response里面拿(暂未开发)

在这里插入图片描述

先把表单样式写出来

在这里插入图片描述

在这里插入图片描述

用到的知识点

el-from-item

prop属性 在表单规则校验的时候使用

MDinput组件 required 必须要输入

在这里插入图片描述

el-row

el-col :sapn=“12” 一半

具体代码去这里看

https://www.youbaobao.xyz/admin-docs/guide/exercise/upload.html

解决label是上下排列 还有两个label挨得紧

label-width

在这里插入图片描述

点击封面功能

在这里插入图片描述

el-tree

第十四章 电子书解析

思路:上传电子书的时候使用了multer中间件来完成上传过程,上传之后会在req产生一个file对象,这个file对象表示一个数组来代表文件的序列,file对象下包含了文件对象,文件对象里包含了文件名,文件路径,文件资源类型等等,那到这些信息之后就可以通过这些信息生成一个book对象,这里的book对象就是所说的电子书对象,然后通过book对象来完成解析的过程

models/Book.js

这里的book就代表一本电子书,他必须要给我们提供一些能力,这些能力包含从文件当中去创建对象,还有一种是在编辑的时候,需要能够根据表单的数据把它也变成一个book对象

变成book对象以后有什么好处呢?通过解析成book对象以后,就可以写一些方法比如parse方法对book对象进行解析,就可以解析到里面的一些细节信息,比如language,title ,creator等,

可以解析出电子书的目录,同时能将book对象转换成json格式(可以直接拿给前端使用),可以转换成数据库字段名快速的生成一些sql语句,所以book对象对应我们来开发整个电子书解析部分是至关重要的,所以,电子书解析很大一部分都是在编写book对象。

电子书book对象开发

传入file表示刚上传了一个电子书的文件,如果传入一个data表示更新或者插入电子书数据,data表示向数据库中插入数据,file主要用于解析电子书数据

在这里插入图片描述

在这里插入图片描述

router/book.js调用

在这里插入图片描述

在这里插入图片描述

知道file对象的内容了以后,就可以对他进行解析了

mimetype可以给他一个默认类型

在这里插入图片描述

我们需要给文件改个名字,因为发现返回的file.path路径是没有后缀名的,去识别这个文件的时候会有些麻烦(suffix)

在这里插入图片描述

生成文件的下载路径 定义url constant.js

这里改了一下UPLOAD_PATH 不用两个反斜杠了 一个‘/’也可以

这里发现了一个上古时期的bug UPLOAD_PATH后面不应该有\book的

在这里插入图片描述

    // 生成文件下载路径 通过这个下载路径就可以快速的下载到电子书
    const url=`${
      
      UPLOAD_URL}/book/${
      
      filename}${
      
      suffix}`

解压后的文件夹同理

const {
    
    MIME_TYPE_EPUB,UPLOAD_URL,UPLOAD_PATH}=require('../utils/constant')
class Book{
    
    
  constructor(file,data){
    
    
    if(file){
    
    
      this.createBookFromFile(file)
    }else{
    
    
      this.createBookFromData(data)
    }
  }
  createBookFromFile(file){
    
    
    // console.log("createBookFromFile",file);
    const{
    
    
      destination,
      filename,
      mimetype=MIME_TYPE_EPUB,
      path
    }=file
    // 电子书的文件后缀名
    const suffix=mimetype===MIME_TYPE_EPUB?'.epub':''
    // 电子书原有路径
    const oldBookPath=path    // 原有路径
    // 电子书新路径
    const bookPath=`${
      
      destination}\\${
      
      filename}${
      
      suffix}` //新路径 
    // 生成文件下载路径 通过这个下载路径就可以快速的下载到电子书
    // 电子书的下载URL
    const url=`${
      
      UPLOAD_URL}/book/${
      
      filename}${
      
      suffix}`
    // 生成电子书解压文件夹 文件夹以文件名命名
    // 电子书解压后的文件夹路径
    const unzipPath=`${
      
      UPLOAD_PATH}\\unzip\\${
      
      filename}`
    // 这个url路径会在电子书阅读的时候使用到它
    // 电子书解压后的文件夹URL
    const unzipUrl=`${
      
      UPLOAD_URL}/unzip/${
      
      filename}`

  }
  createBookFromData(){
    
    

  }
}
module.exports=Book

接下来可以创建一下电子书的解压文件夹

  if(!fs.existsSync(unzipPath)){
    
    
      // 不存在的话迭代创建文件夹
      fs.mkdirSync(unzipPath,{
    
    recursive:true})
    }

在这里插入图片描述

接下来解压以后的文件就会丢到这个路径下面

对文件进行重命名

 // 判断当前电子书是否存在 如果存在且新的电子书不存在的情况下 
 // 调用rename对文件夹重命名的方法,把oldBookPath和bookPath传入实现重命名 
if(fs.existsSync(oldBookPath)){
    
    
      fs.renameSync(oldBookPath,bookPath)
    }

在这里插入图片描述

接下来根据前端所需要的一些字段定义book对象的一些属性

 this.filename=filename  // 无后缀的文件名
    // 写相对路径,为了兼容不同的场景 因为在服务端和客户端他的绝对路径是不一样
    this.path=`/book/${
      
      filename}${
      
      suffix}` // epub文件相对路径
    this.filePath=this.path    // 起一个别名
    this.unzipPath=`/unzip/${
      
      filename}`  // 解压后相对路径
    this.url=url     // epub文件下载链接
    this.title=''   // 标题或书名,解析后生成 
    this.author=''  
    this.publisher=''    // 出版社
    this.contents=[]    // 目录
    this.cover=''     // 封面图片url
    this.category=-1    // 分类id
    this.categoryText=''  // 分类名称
    this.language=''    // 语种
    this.unzipUrl=unzipUrl   // 解压后的文件夹链接
    this.originalname=originalname  // 原始名

看一下结果(这里两个反斜杠的都应该改成/)

在这里插入图片描述

电子书解析库epub库

epubjs库是用于浏览器场景,脱离浏览器是无法工作的,因为他主要是在浏览器场景下对浏览器进行渲染

这里的epub库是在node环境下进行使用的

https://www.youbaobao.xyz/admin-docs/guide/extra/book.html#%E7%94%B5%E5%AD%90%E4%B9%A6%E8%A7%A3%E6%9E%90-2

https://github.com/julien-c/epub/blob/master/epub.js

因为需要对他的代码进行修改,所以拷贝一下集成到项目中,不是通过npm包安装的方式

utils/epub.js

在这里插入图片描述

安装adm-zip xml2js

Epub类提供了一个parse方法

在这里插入图片描述

实际去解析的时候就用到了parse方法

看一下使用方法 是使用event来实现的

在这里插入图片描述

传入之后,就用了一个回调的方法

在这里插入图片描述

后面的function是解析成功之后的回调

什么时候开始手动解析呢 需要调用epub.parse()

通过epub实例调用getChapter方法,里面再去调用一个回调

作者,标题这些信息可以去metadata里面获取

解析成功之后可以通过epub.metadata拿到

在这里插入图片描述

flow是整个电子书渲染的次序

在这里插入图片描述

getChapter获取章节(传入章节id) 获取章节对应的文本

在这里插入图片描述

getChapterRaw表示获得的原始文本,也就是一个html格式的文件

getImage 传入图片id 拿到图片实际的内容

getFile 传入css的id,拿到css的文件

在这里插入图片描述

因为存在大量的回调情况,后面会对他进行改造

电子书解析方法上

model/Book.js引入epub库

新增一个parse方法 我们给Book增加了很多属性,但是有很多都是默认值,在parse里解析,之后再给填充上

  parse(){
    
    
    return new Promise((resolve,reject)=>{
    
    
      const bookPath=`${
      
      UPLOAD_PATH}${
      
      this.filePath}1`
      // 如果不存在文件路径 抛出错误
      if(!fs.existsSync(bookPath)){
    
    
        reject (new Error('电子书不存在'))
      } 
    })
  }

测试一下

router/book.js

在这里插入图片描述

为了验证,把路径随便改一下

在这里插入图片描述

在这里插入图片描述

前端一直卡在这里 是因为没有返回内容,可以用到boom来快速生成异常对象

.catch(err => {
    
    
        console.log("upload", err);
        // 告诉前端发生了解析异常
        next(boom.badImplementation(err))
      })

在这里插入图片描述

就是说在Book对象中使用reject包装的error 会通过路由返回给next 然后被自定义异常捕获再返回给前端,前端再进行相应的处理,这样服务端抛出的异常就可以被前端捕获到了

在这里插入图片描述

电子书解析方法下

在这里插入图片描述

看到调用parse方法之后 他会调用一个open方法

在这里插入图片描述

在这里插入图片描述

在model/Book.js:

消费

在这里插入图片描述

reject后

在这里插入图片描述

接着会到自定义异常处理

在这里插入图片描述

测试

在这里插入图片描述

在这里插入图片描述


把bookPath改回来

打印出来epub.metadata

在这里插入图片描述

需要解析出来metadata里的信息


打印book

在这里插入图片描述


epub对象:

containerFile :epub解析的第一个文件 根据这个文件找content.opf

rootFile:就是content.opf的位置,因为阅读电子书的时候其实就是要解析content.opf

只要能找到content.opf 那么后面的流程就好办了

manifest:资源文件 通过资源文件就可以找到封面图片

toc:目录

在这里插入图片描述

book对象打印

在这里插入图片描述

获取封面:通过epub库提供的getImage方法

在这里插入图片描述

这个方法需要传入两个参数,一个是id一个是callback

id就是封面图片对应的id

在这里插入图片描述

我们就是要把href里的图片拷贝到nginx目录下的img文件夹下 这样的话我们就可以拿到url链接,拿到这个链接就可以作为封面图片的链接

分析一下getImage方法的源码

    getImage(id, callback) {
    
    
      // 到manifest找链接
        if (this.manifest[id]) {
    
    
              // 判断media-type 如果存在会把前面的六个字符截取与image/对比,不相等的话抛出异常 在callback中传入一个error对象
            if ((this.manifest[id]['media-type'] || "").toLowerCase().trim().substr(0, 6)  !=  "image/") {
    
    
                return callback(new Error("Invalid mime type for image"));
            }
            // 如果是图片的话就调用getFile  把id和callback传入
            this.getFile(id, callback);
        } else {
    
    
            callback(new Error("File not found"));
        }
    };

看一下getFile如何实现的

在这里插入图片描述

来使用一下getImage方法 其实可以对库做一些翻新,用.then或者async await更好, 这里先用回调

在这里插入图片描述

在这里插入图片描述

这样data数据已经获取到了 (读取到了内存当中 还不在磁盘当中)

在这里插入图片描述

suffix根据mimetype来获取

写入文件

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

打卡Book.js代码

const {
    
     MIME_TYPE_EPUB, UPLOAD_URL, UPLOAD_PATH } = require('../utils/constant')
const fs = require('fs')
const Epub = require('../utils/epub')
class Book {
    
    
  constructor(file, data) {
    
    
    if (file) {
    
    
      this.createBookFromFile(file)
    } else {
    
    
      this.createBookFromData(data)
    }
  }
  createBookFromFile(file) {
    
    
    console.log("createBookFromFile", file);
    const {
    
    
      destination,
      filename,
      mimetype = MIME_TYPE_EPUB,
      path,
      originalname
    } = file
    // 电子书的文件后缀名
    const suffix = mimetype === MIME_TYPE_EPUB ? '.epub' : ''
    // 电子书原有路径
    const oldBookPath = path    // 原有路径
    // 电子书新路径
    const bookPath = `${
      
      destination}/${
      
      filename}${
      
      suffix}` //新路径 
    // 生成文件下载路径 通过这个下载路径就可以快速的下载到电子书
    // 电子书的下载URL
    const url = `${
      
      UPLOAD_URL}/book/${
      
      filename}${
      
      suffix}`
    // 生成电子书解压文件夹 文件夹以文件名命名
    // 电子书解压后的文件夹路径
    const unzipPath = `${
      
      UPLOAD_PATH}/unzip/${
      
      filename}`
    // 这个url路径会在电子书阅读的时候使用到它
    // 电子书解压后的文件夹URL
    const unzipUrl = `${
      
      UPLOAD_URL}/unzip/${
      
      filename}`
    // 如果不存在unzipPath,就去创建
    if (!fs.existsSync(unzipPath)) {
    
    
      // 不存在的话迭代创建文件夹
      fs.mkdirSync(unzipPath, {
    
     recursive: true })
    }
    // 判断当前电子书是否存在 如果存在且新的电子书不存在的情况下 
    // 调用rename对文件夹重命名的方法,把oldBookPath和bookPath传入实现重命名
    if (fs.existsSync(oldBookPath)) {
    
    
      fs.renameSync(oldBookPath, bookPath)
    }
    this.filename = filename  // 无后缀的文件名
    // 写相对路径,为了兼容不同的场景 因为在服务端和客户端他的绝对路径是不一样
    this.path = `/book/${
      
      filename}${
      
      suffix}` // epub文件相对路径
    this.filePath = this.path    // 起一个别名  电子书相对路径
    this.unzipPath = `/unzip/${
      
      filename}`  // 解压后相对路径
    this.url = url     // epub文件下载链接
    this.title = ''   // 标题或书名,解析后生成 
    this.author = ''
    this.publisher = ''    // 出版社
    this.contents = []    // 目录
    this.cover = ''     // 封面图片url
    this.coverPath=''
    this.category = -1    // 分类id
    this.categoryText = ''  // 分类名称
    this.language = ''    // 语种
    this.unzipUrl = unzipUrl   // 解压后的文件夹链接
    this.originalname = originalname  // 原始名

  }
  createBookFromData() {
    
    
  }
  parse() {
    
    
    return new Promise((resolve, reject) => {
    
    
      const bookPath = `${
      
      UPLOAD_PATH}${
      
      this.filePath}`
      // 如果不存在文件路径 抛出错误
      if (!fs.existsSync(bookPath)) {
    
    
        reject(new Error('电子书不存在'))
      }
      // 创建个实例
      const epub = new Epub(bookPath)
      // error回调,判断解析过程中有没有出现异常
      epub.on('error', err => {
    
    
        reject(err)
      })
      // end事件表示电子书成功解析
      epub.on('end', err => {
    
    
        if (err) {
    
    
          reject(err)
        } else {
    
    
          // console.log("epub+ ", epub.manifest);
          const {
    
    
            language,
            creator,
            creatorFileAs,
            title,
            cover,
            publisher
          } = epub.metadata

          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 =  (err, file, mimetype) =>{
    
    
              console.log(err, file, mimetype);
              if (err) {
    
    
                reject(err)
              } else {
    
    
                // 需要整个电子书解析完之后再调用resolve,而不是直接调完getImage就resolve,
                //因为getImage可能出错,调完resolve再调reject逻辑就会出现问题
                const suffix = mimetype.split('/')[1]
                const coverPath = `${
      
      UPLOAD_PATH}/img/${
      
      this.filename}.${
      
      suffix}`
                const coverUrl = `${
      
      UPLOAD_URL}/img/${
      
      this.filename}.${
      
      suffix}`
                // 把buffer写入到磁盘当中
                console.log(coverPath);
                fs.writeFileSync(coverPath,file,'binary')
                this.coverPath=`/img/${
      
      this.filename}.${
      
      suffix}`
                this.cover=coverUrl
                resolve(this)
              }
            }
            epub.getImage(cover, handleGetImage)
             // resolve(this)  不要在这里写
          }
        }
      })
      epub.parse()
    })
  }
}
module.exports = Book

封面图片解析优化

有的电子书用这种方法是获取不到封面图片的

在这里插入图片描述

看一下这个错误出现在哪里

在这里插入图片描述

打印一下cover

在这里插入图片描述

解压出来分析一下

打开package.opf

metadata里没有标签是关于cover的 说明没有办法获得封面图片的资源id了

在manifest里找找

在这里插入图片描述

能看到封面的资源文件,但是是xhtml的类型,说明他是章节的内容,并不是图片 图片应该是image开头的

这个才是封面图片

在这里插入图片描述

他是在image路径下978开头的文件

在这里插入图片描述

所以封面还有一种查询方式 就是读取item下面的properties 如果为cover-image 表示是封面图片 可以把这个图片的href获取到,然后来找到它的资源文件并且从epub当中解压出来保存到本地

需要对epub.js的getImage方法改造

如果manifest没有办法从cover下获取的时候需要改进一下这里的逻辑

大致框架

在这里插入图片描述

如何获取coverId在这里插入图片描述

   const coverId=Object.keys(this.manifest).find(key=>{
    
      //注意这里不是花括号
      
        // console.log(key,this.manifest[key]);
        this.manifest[key].properties==='cover-image'
      })
getImage(id, callback) {
    
    
    // 到manifest找链接
    if (this.manifest[id]) {
    
    
      // 判断media-type 如果存在会把前面的六个字符截取与image/对比,不相等的话抛出异常 在callback中传入一个error对象
      if ((this.manifest[id]['media-type'] || "").toLowerCase().trim().substr(0, 6) != "image/") {
    
    
        return callback(new Error("Invalid mime type for image"));
      }
      // 如果是图片的话就调用getFile  把id和callback传入
      this.getFile(id, callback);
    } else {
    
    
      // 传入的id无法用  需要获取到coverId 判断coverId是否存在
      // 这样就把符合条件的key返回
      const coverId = Object.keys(this.manifest).find(key => (
        // console.log(key,this.manifest[key]);
        this.manifest[key].properties === 'cover-image'
      ))
      console.log("coverId", coverId);
      if (coverId) {
    
    
        this.getFile(coverId, callback)
      } else {
    
    
        callback(new Error("File not found"));
      }

    }
  };


接下来开发一个比较有难度的点–解析电子书目录

在epub库并没有提供解决方案,manifest目录虽然有很多的资源文件,但是并没有形成一个顺序,我们还要确定目录的层级关系

目录解析原理和电子书解压

目录解析原理

先从spin标签下面获取toc属性 (目录的资源id)

在这里插入图片描述

之后在manifest里找

在这里插入图片描述

打开toc.ncx

在这里插入图片描述

navMap:导航

里面都是目录,目录可能会出现嵌套的情况

1.对电子书文件进行解压

解压后放unzip的文件夹下

通过之前getFile方法,能够直接获取到电子书文件,但是我们选择先对他进行解压,这样读取的效率会更高一些

来到自己编写的Book类中,

在这里插入图片描述

编写unzip方法

 unzip(){
    
    
    const AdmZip=require('adm-zip')
    const zip=new AdmZip(Book.genPath(this.path))
    // zip对象的api extractAllTo()  含义是将路径下的文件进行解压,
    // 解压以后把它放到一个新的路径下  第二个参数:是否进行覆盖
    zip.extractAllTo(Book.genPath(this.unzipPath),true)
  }
  // 生成静态方法,获得绝对路径
  static genPath(path){
    
    
    if(!path.startsWith('/')){
    
    
      path=`/${
      
      path}`
    }
    return `${
      
      UPLOAD_PATH}${
      
      path}`
  }
}

在这里插入图片描述

解压出来以后就可以对他进行解析了

unzip方法是个同步方法

unzip过后就可以定义一个parseContents了 传入epub对象 因为要去toc spin里面去找toc属性

  parseContents(epub){
    
    
    function getNcxFilePath(){
    
    
      const spine=epub&&epub.spine
      console.log("spine",spine);
    }
    getNcxFilePath()
  }

打印出spine

在这里插入图片描述

可以看到spine下面有个toc属性

可以找到toc对应的id/直接拿href也可以

如果没有href的时候,就找id->找manifest

parseContents(epub){
    
    
    function getNcxFilePath(){
    
    
      const spine=epub&&epub.spine
      const manifest=epub&&epub.manifest
      const ncx=spine.toc&&spine.toc.href
      const id=spine.toc&&spine.toc.id
      console.log("spine", spine.toc,ncx,id,manifest[id].href);
      if(ncx){
    
    
        return ncx
      }else{
    
    
        // 这个一定会存在的,因为这是电子书的目录
        return manifest[id].href
      }
    }
    getNcxFilePath()
  }

在这里插入图片描述

可以发现两种方法都可以获取到目录

之后来拿到路径

 const ncxFilePath=getNcxFilePath()

这样拿的是相对路径 需要把它拼成绝对路径

 const ncxFilePath=Book.genPath(getNcxFilePath())

这样还是不对,需要加上unzipPath

 const ncxFilePath=Book.genPath(`${
      
      this.unzipPath}/${
      
      getNcxFilePath()}`)
    console.log(ncxFilePath);

还要做一件事情,判断这个路径是否存在,如果不存在需要抛出异常

    if(fs.existsSync(ncxFilePath)){
    
    

    }else{
    
    
      throw new Error('目录对应的资源文件不存在')
    }

在这里catch到

在这里插入图片描述

最终前端会拿到错误信息

在这里插入图片描述

试验一下

   const ncxFilePath=Book.genPath(`${
      
      this.unzipPath}/${
      
      getNcxFilePath()+1}`)

在这里插入图片描述

电子书标准目录解析

打开toc.ncx

在这里插入图片描述

在ncx对象下面有个navMap

navMap下面每一个navPoint都是一个目录选项

在这里插入图片描述

navLabel:具体的目录内容

content :src 目录的路径,playOrder:目录顺序

在这里插入图片描述

目录可能存在嵌套,我们还需要对二级目录进行识别,所以需要有一个迭代的方法实现目录的识别(难点)

Book.js首先引用xml2js库

https://www.npmjs.com/package/xml2js

在这里插入图片描述

在这里插入图片描述

我们要取的是ncx下面的navMap属性

打印一下navMap

在这里插入图片描述

技巧 :看一下详细信息

在这里插入图片描述

字符串粘到json.cn里面

在这里插入图片描述

在这里插入图片描述

目录结构

在这里插入图片描述

返回的结果他给包裹在一个数组中了,如果不希望包裹在数组中,可以加个参数,

在这里插入图片描述

在这里插入图片描述

       xml2js(xml,{
    
    
          explicitArray:false,
          ignoreAttrs:false
        },function(err,result){
    
    
          if(err){
    
    
            reject(err)
          }else{
    
    
            console.log(result) 
            const navMap=result.ncx.navMap
            console.log(JSON.stringify(navMap));
          }
        })

现在的结构

在这里插入图片描述

增加findParent方法 因为这是单级的目录,所以返回一样的数组,以后会完善

    function findParent(array){
    
    
      return array.map(item=>{
    
    
        return item
      })
    }

如果是有子目录的话,是一个树状结构,树状结构是不利于前端展示的

所以需要把树状结构改成一维的结构 现在还没有这个场景,但是还是先把方法建好

 navMap.navPoint=findParent(avMap.navPoint)
 const newNavMap=flatten(navMap.navPoint)

newNavMap是对navMap做的一个浅拷贝

    function findParent(array){
    
    
      return array.map(item=>{
    
    
        return item
      })
    }
    function flatten(array){
    
    
      return [].concat(...array.map(item=>{
    
    
        return item
      }))
    }

newNavMap是复制了一份数组


epub.flow:展示顺序

在这里插入图片描述

  epub.flow.forEach((chapter,index)=>{
    
    
                if(index+1>newNavMap.length){
    
    
                  // flow里面的信息超过目录信息 就return
                  return 
                }else{
    
    
                  // 没有超过的时候
                  // 拿到目录信息
                  const nav=newNavMap[index]
                  // 增加个属性(章节url)
                  chapter.text=`${
      
      UPLOAD_URL}/unzip/${
      
      fileName}/${
      
      chapter.href}`
                  console.log(chapter.text);
                }
              })
              console.log(epub.flow);
            }else{
    
    
              reject('目录解析失败,目录树为0')
            }

在这里插入图片描述

在这里插入图片描述

有个问题,直接用epub.flow不行吗

其实用epub.flow是有一些隐含的坑的

在这里插入图片描述

有的电子书是没有order和level的,不精确

所以从navMap中获取比较正宗的目录信息

在继续向chapter里面加属性

                  if (nav && nav.navLabel) {
    
    
                    chapter.label = nav.navLabel.text || ''
                  } else {
    
    
                    chapter.label = ''
                  }
                  chapter.navId=nav['$'].id
                  chapter.fileName=fileName
                  chapter.order=index+1
                  chapters.push(chapter)
                  console.log(chapter.text);

在这里插入图片描述

嵌套目录解析

在这里插入图片描述

在findParent里做一些文章

level 默认为0 下一级为1 这样返回给前端的时候就可以根据level做缩进

传入3个参数 array,level=0,pid=0

navPoint里面是没有level字段的,可以给他加个level字段

    function findParent(array, level = 0, pid = '') {
    
    
      // 三个场景,一:没有包含navPoint:直接复值level,pid
      // 二:存在navPoint同时navPoint又是数组 说明下面包含子目录 进行迭代
      // 三:navPoint可能不是数组而是个对象的时候(只有一个目录),直接赋值
      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
      })
    }

在这里插入图片描述

flatten方法:将navPoint数组变成一个扁平状态

配合这里

在这里插入图片描述

如果不变扁平那newNavMap的长度一定小于index+1(flow)

flatten方法

    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
      }))
    }

resolve reject

在这里插入图片描述

在这里插入图片描述

可以把book返回给前端

new Result(book,‘上传成功’).success(res)

在这里插入图片描述

第十五章 电子书列表

电子书解析数据展示

EbookUpload组件里

this.emit(‘onSuccess’,response.data)

book/components/detail.vue

    handleSuccess(data){
    
    
      console.log(data);
    },

在这里插入图片描述

拿到data数据以后

在这里插入图片描述

之后写个setData方法

 setData(data) {
    
    
      const {
    
    
        title,
        author,
        publisher,
        language,
        rootFile,
        cover,
        originalName,
        url,
        contents,
        contentsTree,
        fileName,
        coverPath,
        filePath,
        unzipPath,
      } = data;
      this.postForm = {
    
    
        title,
        author,
        publisher,
        language,
        rootFile,
        cover,
        url,
        originalName,
        contents,
        fileName,
        coverPath,
        filePath,
        unzipPath,
      };
    },

重新执行

在这里插入图片描述

这里的filename大小写写错了,不过文件名称还是换成originalName更好点

在这里插入图片描述

树状目录展开

el-tree解决问题

传入data属性,data属性当中是个数组,数组中包含若干对象,每个对象对应一级的tree 里面包含label ,children

https://element.eleme.io/#/zh-CN/component/tree#tree-shu-xing-kong-jian

在这里插入图片描述

电子书目录不太一样,我们为了方便解析,只用了一维的数组,而范例是个嵌套的数组

我们去服务端来进行改造 改造成嵌套的 当然在前端做处理也可以

没有识别出pid时说明是一级目录

在这里插入图片描述

              const chapterTree=[]
              chapters.forEach(c=>{
    
    
                c.children=[]
                //没有识别出pid时说明是一级目录
                if(c.pid===''){
    
    
                  chapterTree.push(c)
                }
              })
      		  const chapterTree = []
              chapters.forEach(c => {
    
    
                // 我们前面已经定义过label属性并赋值了 
                c.children = []
                //没有识别出pid时说明是一级目录
                if (c.pid === '') {
    
    
                  chapterTree.push(c)
                } else {
    
    
                  // pid不为空,说明有parent 先要找到parent
                  const parent = chapters.find(_ =>
                    // 如果一样,就找到了parent
                    _.navId === c.pid
                  )
                  parent.children.push(c)
                }
              })
              console.log(chapterTree);

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

点击以后如何看章节内容

https://element.eleme.cn/#/zh-CN/component/tree#events

在这里插入图片描述

在这里插入图片描述

看一下data里都有啥

   onContentClick(data){
      console.log(data)
    },

在这里插入图片描述

最重要的就是text参数

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

随便点进一个章节

在这里插入图片描述

但是还是有些书有问题的 比如说这本,点进去和目录不一样 15-3有优化 这里就不做优化了

在这里插入图片描述

电子书表单验证功能开发

点击叉号清空

补充defaultForm

在这里插入图片描述

在这里插入图片描述

校验功能

看element-ui源码可以看到,会返回两个参数

在这里插入图片描述

在这里插入图片描述

    submitForm(){
    
    
      this.$refs.postForm.validate((valid,fileds)=>{
    
    
         console.log(valid,fileds);
          // 通过验证
        if(valid){
    
    
         
        }else{
    
    

        }
      })
    },

在这里插入图片描述

写个rules对象,它的key是el-form-item里面写的prop属性

在这里插入图片描述

可以直接在title里写required 这里用的自定义校验规则(传入function)

在这里插入图片描述

  const validateRequire=(rule,value,callback)=>{
    
    
      if(value===''||value.length==0){
    
    
        callback(new Error('title不能为空'))
      }else{
    
    
        callback()
      }
    }

在这里插入图片描述

可以打印一下rule

在这里插入图片描述

改造

callback(new Error(rule.field+'必须填写'))

这里有个技巧,可以做个字段映射

写个对象

在这里插入图片描述

callback(new Error(fields[rule.field]+‘必须填写’))

在这里插入图片描述

现在想拿到这句话 然后展示在页面上

在这里插入图片描述

fields[Object.keys(fields)[0]][0].message

   submitForm(){
    
    
      this.$refs.postForm.validate((valid,fields)=>{
    
    
        console.log(valid,fields);
        if(valid){
    
    
         
        }else{
    
    
          // 这是一个通用的异常处理方法
          console.log(fields[Object.keys(fields)[0]][0].message)
          const message=fields[Object.keys(fields)[0]][0].message
          this.$message({
    
    
            message,type:'error'
          })
        }
      })
    },

在这里插入图片描述

给作者 出版社 语言 也加个prop author publisher language

增加映射

在这里插入图片描述

然后在rules添加规则

    rules: {
    
    
        title: [{
    
     validator: validateRequire }],
        author: [{
    
     validator: validateRequire }],
        language: [{
    
     validator: validateRequire }],
        publisher: [{
    
     validator: validateRequire }],
      },

新增电子书前端逻辑

浅拷贝两种方法:扩展运算符/object.assign

在这里插入图片描述

请求接口

src/api/book.js

在这里插入图片描述

Detail.vue引入

在这里插入图片描述

在这里插入图片描述

接口开发

utils/index.js 用到了这里的decode

在这里插入图片描述

book.js

router.post('/create', (req, res, next) => {
    
    
  const decoded=decode(req)
  // 解出jwt
  console.log(decoded);
})

在这里插入图片描述

写一个book对象,这个book对象生成的逻辑可以从model/Book.js里写

在这里插入图片描述

在这里插入图片描述

数据库数据

在这里插入图片描述

打印一下data 一定要注意代码规范啊,该驼峰就驼峰 有几个没用驼峰后悔了

在这里插入图片描述

  createBookFromData(data) {
    
    
    // data里的字段需要与数据库字段做映射
    // console.log(69,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.filePath
    this.filePath=data.filePath
    this.unzipPath=data.unzipPath
    this.coverPath=data.coverPath
    this.createUser=data.username
    this.createDt=new Date().getTime()
    this.updateDt=new Date().getTime()
    //为0 表示是默认图书,否则代表来自于互联网(1)
    this.updateType=data.updateType===0?data.updateType:1
    this.category=data.category||99
    this.categoryText=data.categoryText||'自定义'
  }

在这里插入图片描述

book对象就生成了 这样就可以通过对象生成sql语句了

新增电子书核心逻辑思路

新建service/book.js 有关数据库的操作

const Book=require('../model/Book')
function insertBook(book){
    
    
  return new Promise((resolve,reject)=>{
    
    
    try {
    
    
      // book一定要是Book对象的实例 可以避免某些参数不存在的情况
      // 如果是实例的话有个好处 能够保证那些参数都是完备的,
      // 不然的话随便传入一个book对象做insert的话可能会产生错误
      if(book instanceof Book) {
    
    
        
      }else{
    
    
        reject(new Error('添加的图书对象不合法'))
      }
    } catch (error) {
    
    
      reject(error)
    }
  })
}
module.exports={
    
    insertBook}

router/book.js

router.post('/create', (req, res, next) => {
    
    
  const decoded = decode(req)
  // 解出jwt
  // console.log(decoded);
  // 前端传入的信息
  console.log(req.body);
  if (decoded && decoded.username) {
    
    
    // book.username=decoded.username
    req.body.username = decoded.username
  }
  // const book = new Book(null, req.body)
  const book={
    
    }
  bookService.insertBook(book).then((res)=>{
    
    
    console.log(res);
  }).catch((err)=>{
    
    
    // console.log(err);
    // 错误与前端联动 
    next(boom.badImplementation(err))
  })  
  console.log(book)
})

在这里插入图片描述

在这里插入图片描述

这就是大致框架了

判断电子书是否存在,如果已经存在,就移除掉 需要把数据库里的信息移除掉,还要把文件移除掉

在这里插入图片描述

insert以后还要把目录插入到目录表里

在这里插入图片描述

在这里插入图片描述

操作数据库会存在大量的异步操作,用到async await 变成同步方法 减少回调的次数,如果用promise的话会存在大量嵌套

逻辑就是这样,接下来写具体的方法

数据库操作

insert方法判断第一个参数传入的是否为一个对象

判断是否为对象的方法:utils/index.js

function isObject(o){
    
    
  return Object.prototype.toString.call(o)==='[object Object]'
}

用这个做判断非常精确

在这里插入图片描述

function insert(model,tableName){
    
    
  return new Promise((resolve,reject)=>{
    
    
    if(!isObject(model)){
    
    
      reject(new Error('插入数据库失败,插入数据非对象'))
    }
  })
}

来实验一下

在这里插入图片描述

在这里插入图片描述

回到insert方法

在这里插入图片描述

数据库操作有个技巧

 keys.push(`\`${
      
      key}\``)

为什么呢 比如说select from from book 其中第一个from并不是关键字而是个key,但是数据库会自动识别出他是个关键字就会报错,加了反引号之后就没有这个问题了

  // 拼sql语句
      if(keys.length>0&&values.length>0){
    
    
        let sql=`insert into \`${
      
      tableName}\`(`
        const keysString=keys.join(',')
        const valuesString=values.join(',')
        sql=`${
      
      sql}${
      
      keysString}) values (${
      
      valuesString})`
        console.log(sql)
      }

在这里插入图片描述

接着尝试执行sql语句能否成功

 // 拼sql语句
      if(keys.length>0&&values.length>0){
    
    
        let sql=`insert into \`${
      
      tableName}\`(`
        const keysString=keys.join(',')
        const valuesString=values.join(',')
        sql=`${
      
      sql}${
      
      keysString}) values (${
      
      valuesString})`
        console.log(sql)
        const conn=connect()
        try {
    
    
          conn.query(sql,(err,result)=>{
    
    
            if(err){
    
    
              reject(err)
            }else{
    
    
              resolve(result)
            }
          })
        } catch (error) {
    
    
          reject(error)
        }finally{
    
    
          conn.end()
        }
      }else{
    
    
        reject(new Error('对象中没有任何属性'))
      }

在这里插入图片描述

提示数据库没有path这个字段

这里推荐个做法

Book.js新增个方法toDb 对book对象进行过滤而不是整体都拿去使用

把path去掉

在这里插入图片描述

toDb() {
    
    
    return {
    
    
      fileName: this.fileName,
      cover: this.cover,
      title: this.title,
      author: this.author,
      publisher: this.publisher,
      bookId: this.bookId,
      language: this.language,
      rootFile: this.rootFile,
      originalName: this.originalName,
      filePath: this.filePath,
      unzipPath: this.unzipPath,
      coverPath: this.coverPath,
      createUser: this.createUser,
      createDt:this.createDt,
      updateDt: this.updateDt,
      updateType:this.updateType,
      category: this.category,
      categoryText: this.categoryText
    }
  }

这个book就要改成book.toDd() 把toDb返回的结果传给insertBook 这样会报错,因为insertBook里面会判断传入的对象是不是Book的实例

在这里插入图片描述

写在这里是合适的

在这里插入图片描述

这样的话点击添加就可以发现数据库多了条数据

在这里插入图片描述

前端交互优化

在这里插入图片描述

insertBook(book).then((res)=>{}) 这里不能写res因为success方法会自动传入这里的res 但是实际上应该传入上面红箭头所指的res

在这里插入图片描述

前端:

拿到后端的返回值,显示到前端页面

这次不用$message了

https://element.eleme.cn/#/zh-CN/component/notification#notification-tong-zhi

用一个跟他很像的 $notify

在这里插入图片描述

在这里插入图片描述

还有个细节,上传成功之后可以把列表数据都移除掉 remove方法里用过

this.postForm = Object.assign({
    
    }, defaultForm);

列表是移除了 但是还是有bug

在这里插入图片描述

这里写个setDefault方法,来解决这些问题

在这里插入图片描述

把这个置空书名就没了

    setDefault(){
    
    
      this.postForm = Object.assign({
    
    }, defaultForm);
      this.fileList=[]
    },

接下来要把校验结果消除

在这里插入图片描述

  setDefault(){
    
    
      // this.postForm = Object.assign({}, defaultForm);
      this.fileList=[]
      this.$refs.postForm.resetFields()
    },

在这里插入图片描述

这是因为没有传入prop,如果没有传入prop,他就不认为是一个表单的选项

在这里插入图片描述

目录还没有消除

    setDefault() {
    
    
      // this.postForm = Object.assign({}, defaultForm);
      this.contentsTree = []; // 消除目录
      this.fileList = []; // 标题
      this.$refs.postForm.resetFields();
    },

移除也是调用这个方法

添加目录到数据库功能

接下来写insertContents方法

需要取到book下的contents

但是前面把contents删掉了 所以传进来没有目录

前端:

在这里插入图片描述

后端接收:

在这里插入图片描述

可以编写一个getContents

在这里插入图片描述

确保能拿到contents之后就可以做插入数据库操作了

在这里插入图片描述

对比一下还是有很多冗余字段

在这里插入图片描述

可以建一个对象一次赋值,这里有个技巧—通过lodash实现

通过lodash可以使用它的方法来实现我们想要的功能

在这里插入图片描述

在这里插入图片描述

调用insert方法

await db.insert(_content,‘contents’)

在这里插入图片描述

在这里插入图片描述

电子书删除功能

下面两个逻辑都写完了 接下来写移除的逻辑

在这里插入图片描述

首先把exists函数写了 功能是判断电子书是否存在

在这里插入图片描述

为了方便测试 把前端的this.setDefault();注释掉

在这里插入图片描述

这种情况就需要把当前上传的电子书给移除

function removeBook(book) {
    
    
  if(book){
    
    
    // 删除相关文件(电子书文件,封面,解压后的文件)
    book.reset()
  }
}

进入Book.js

  // 判断路径是否存在静态方法
  static pathExists(path){
    
    
    if(path.startsWith(UPLOAD_PATH)){
    
    
      return fs.existsSync(path)
    }else{
    
    
      return fs.existsSync(Book.genPath(path))
    }
  }

来模拟一下

在这里插入图片描述

单个文件删除:unlinkSync

在这里插入图片描述

解压路径删除记得加上recursive:true fs.rmdirSync(Book.genPath(this.unzipPath),{recursive:true})

这样就只保留一个文件了

老师这里又加了个删除数据库的操作,不知道为啥要加这一步,应该没有写入数据库才对,所以我觉得不用加这个删除数据库的逻辑了

在这里插入图片描述

电子书查询api

实现编辑功能首先要拿到fileName,根据filename到数据库查询 查询目录和内容

查到之后返给前端 所以需要在路由中接收个参数

在这里插入图片描述

之后可以在Detail.vue里拿到动态路由参数

在这里插入图片描述

在这里插入图片描述

思路

在这里插入图片描述

现在要实现的就是getBook的api

src/api/book.js

注意get方法使用的是params,如果是post则使用data

在这里插入图片描述

在这里插入图片描述

服务端:

router.book.js

大致框架

在这里插入图片描述

getBook方法在service/book.js里写

在这里插入图片描述

完善getBook逻辑

需要到book表和contents表进行查询

在这里插入图片描述

在这里插入图片描述

进一步处理让数据展示出来

    resolve(book[0])

但是标题,封面,目录都没有

先解决封面的问题

在这里插入图片描述

来解决目录的显示问题

现在的contents是个数组的结构,需要将数组转换成一个contentsTree

其实前面我们已经做过这个逻辑了,

 const chapterTree = []
    chapters.forEach(c => {
    
    
      // 我们前面已经定义过label属性并赋值了 
      c.children = []
      //没有识别出pid时说明是一级目录
      if (c.pid === '') {
    
    
        chapterTree.push(c)
      } else {
    
    
        // pid不为空,说明有parent 先要找到parent
        const parent = chapters.find(_ =>
          // 如果一样,就找到了parent
          _.navId === c.pid
        )
        parent.children.push(c)

      }
    })

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

这时候目录还是出不来 前端代码改一下

在这里插入图片描述

接下来还有个问题没解决 就是文件的信息

给fileList赋值就ok

在这里插入图片描述

编辑电子书

接下来写这里的逻辑

在这里插入图片描述

前端增加个接口

在这里插入图片描述

虽然还没开发出来接口 但是还是可以去network里面看一下 可以发现接口和参数都能正确发出去了

在这里插入图片描述

现在服务端增加接口

和之前create的接口很相似(写错了 应该是update)

在这里插入图片描述

现在去service/book.js里写updateBook的逻辑

在这里插入图片描述

前端

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

这里的book.fileName应该要加引号的

在这里插入图片描述

数据库操作文件写一个update方法

在这里插入图片描述

在这里插入图片描述

第十六章 电子书编辑和删除功能开发

电子书列表

知识点 directive waves用法 v-waves

在这里插入图片描述

pagination用它封装好的组件 这些都是可以传入的参数

在这里插入图片描述

在这里插入图片描述

<template>
  <div class="app-container">
    <!-- 查询条件 -->
    <div class="filter-container">
      <!-- filter -->
      <el-input
        v-model="listQuery.title"
        placeholder="书名"
        size="normal"
        clearable
        style="width: 200px"
        class="filter-item"
        @keyup.enter.native="handleFilter"
        @clear="handleFilter"
        @blur="handleFilter"
      ></el-input>
      <el-input
        v-model="listQuery.author"
        placeholder="作者"
        size="normal"
        clearable
        style="width: 200px"
        class="filter-item"
        @keyup.enter.native="handleFilter"
        @clear="handleFilter"
        @blur="handleFilter"
      ></el-input>

      <el-select
        v-model="listQuery.category"
        placeholder="分类"
        clearable
        class="filter-item"
        @change="handleFilter"
        @clear="handleFilter"
      >
        <el-option
          v-for="item in categoryList"
          :key="item.value"
          :label="item.label"
          :value="item.value"
        >
        </el-option>
      </el-select>
      <el-button
        type="primary"
        class="filter-item"
        size="default"
        icon="el-icon-search"
        style="margin-left: 10px"
        @click="handleFilter"
      >
        点击查询
      </el-button>
      <el-button
        type="primary"
        class="filter-item"
        size="default"
        icon="el-icon-search"
        style="margin-left: 10px"
        @click="handleCreate"
      >
        点击新增
      </el-button>

      <el-checkbox
        v-model="showCover"
        label=""
        :indeterminate="false"
        class="filter-item"
        style="margin-left: 10px"
        @change="changeShowCover"
      >
        显示封面
      </el-checkbox>
    </div>
    <!-- 表格组件 -->
    <el-table></el-table>
    <!-- 翻页 -->
    <Pagination :total="0" />
  </div>
</template>

<script>
import Pagination from "../../components/Pagination/index";
import { getCategory } from "../../api/book";
// 这个老是报错,应该是依赖的问题,使用方法:v-waves
// import waves from '../../components/directive/waves'
export default {
  components: { Pagination },
  data() {
    return {
      listQuery: {},
      showCover: false,
      // 查询条件是动态的
      categoryList: [],
    };
  },
  mounted() {
    this.getCategoryList();
  },
  methods: {
    getCategoryList() {
      getCategory().then((response) => {
        this.categoryList = response.data;
      });
    },
    changeShowCover(value) {
      this.showCover = value;
      console.log(this.showCover);
    },
    handleFilter() {
      console.log("handleFilter", this.listQuery);
    },
    handleCreate() {
      // 页面切换到/book/create 切换到上传图书
      this.$router.push("/book/create");
    },
  },
};
</script>

<style lang="scss" scoped>
</style>

水波效果

在这里插入图片描述

在这里插入图片描述

api 在这里插入图片描述

在这里插入图片描述

拿到category以后就去请求,请求以后赋值,之后显示到页面

写label两种方式

在这里插入图片描述

图书分类api

在这里插入图片描述

打开category视图

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

有个疑问

在这里插入图片描述

这里return的categoryList能在then里面接收到吗,我的认知里面应该是要return一个promise对象的

我知道了,async里面return的对象会自动帮我们转换成promise

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

图书列表样式

https://element.eleme.io/#/zh-CN/component/table#ji-chu-biao-ge

https://element.eleme.io/#/zh-CN/component/table#table-column-attributes

https://element.eleme.io/#/zh-CN/component/table#table-column-attributes

el-table

sort-change事件排序

   <el-table
      :key="tableKey"
      v-loading="listLoading"
      :data="list"
      border
      fit
      highlight-current-row
      style="width: 100%"
      @sort-change="sortChange"
    >
      <el-table-column
        prop="id"
        label="ID"
        sortable="custom"
        align="center"
        width="80"
      >
      </el-table-column>
      <el-table-column label="书名" sortable="custom" align="center" width="80">
        <template slot-scope="{ row: { title } }">
          <span>{
   
   { title }}</span>
        </template>
      </el-table-column>
    </el-table>

在这里插入图片描述

接下来拿数据

在这里插入图片描述

在这里插入图片描述

虽然还没写接口逻辑,但是可以先验证一下query数据有没有传进去

在这里插入图片描述

来尝试一下

在这里插入图片描述

可以看到已经不报404了 而且后端能收到数据了 这样流程就串起来了

在这里插入图片描述

在这里插入图片描述

有个问题,author和title应该采取模糊查询

有个难点,不确定category,author,title是否传入

在这里插入图片描述

前端收到数据

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

<template>
  <div class="app-container">
    <!-- 查询条件 -->
    <div class="filter-container">
      <!-- filter -->
      <el-input
        v-model="listQuery.title"
        placeholder="书名"
        size="normal"
        clearable
        style="width: 200px"
        class="filter-item"
        @keyup.enter.native="handleFilter"
        @clear="handleFilter"
        @blur="handleFilter"
      ></el-input>
      <el-input
        v-model="listQuery.author"
        placeholder="作者"
        size="normal"
        clearable
        style="width: 200px"
        class="filter-item"
        @keyup.enter.native="handleFilter"
        @clear="handleFilter"
        @blur="handleFilter"
      ></el-input>

      <el-select
        v-model="listQuery.category"
        placeholder="分类"
        clearable
        class="filter-item"
        @change="handleFilter"
        @clear="handleFilter"
      >
        <el-option
          v-for="item in categoryList"
          :key="item.value"
          :label="item.label + '(' + item.num + ')'"
          :value="item.value"
        >
        </el-option>
      </el-select>
      <el-button
        type="primary"
        class="filter-item"
        size="default"
        icon="el-icon-search"
        style="margin-left: 10px"
        @click="handleFilter"
      >
        点击查询
      </el-button>
      <el-button
        type="primary"
        class="filter-item"
        size="default"
        icon="el-icon-search"
        style="margin-left: 10px"
        @click="handleCreate"
      >
        点击新增
      </el-button>

      <el-checkbox
        v-model="showCover"
        label=""
        :indeterminate="false"
        class="filter-item"
        style="margin-left: 10px"
        @change="changeShowCover"
      >
        显示封面
      </el-checkbox>
    </div>
    <!-- 表格组件 -->
    <el-table
      :key="tableKey"
      v-loading="listLoading"
      :data="list"
      border
      fit
      highlight-current-row
      style="width: 100%"
      @sort-change="sortChange"
    >
      <el-table-column
        prop="id"
        label="ID"
        sortable="custom"
        align="center"
        width="80"
      >
      </el-table-column>
      <el-table-column
        label="书名"
        sortable="custom"
        align="center"
        width="180"
      >
        <template slot-scope="{ row: { title } }">
          <span>{
   
   { title }}</span>
        </template>
      </el-table-column>
      <el-table-column
        label="作者"
        sortable="custom"
        align="center"
        width="100"
      >
        <template slot-scope="{ row: { author } }">
          <span>{
   
   { author }}</span>
        </template>
      </el-table-column>
      <!-- 换种方式 ,不使用插槽 -->
      <el-table-column
        label="出版社"
        prop="publisher"
        sortable="custom"
        align="center"
        width="150"
      >
      </el-table-column>
      <el-table-column
        label="分类"
        prop="categoryText"
        sortable="custom"
        align="center"
        width="150"
      >
      </el-table-column>
      <el-table-column
        label="语言"
        prop="language"
        sortable="custom"
        align="center"
        width="80"
      >
      </el-table-column>
      <el-table-column
        v-if="showCover"
        label="封面"
        prop="cover"
        align="center"
        width="150"
      >
        <template slot-scope="{ row: { cover } }">
          <a :href="cover" target="_blank">
            <img :src="cover" style="width: 120px; height: 180px" />
          </a>
        </template>
      </el-table-column>
      <!-- <el-table-column v-if="showCover" label="封面" prop="cover" align="center" width="150">
        <template slot-scope="scope">
          <a :href="scope.row.cover" target="_blank">
            <img :src="scope.row.cover"  style="width:120px;height:180px">
          </a>
        </template>
      </el-table-column> -->
      <el-table-column
        label="文件名"
        prop="fileName"
        sortable="custom"
        align="center"
        width="150"
      >
      </el-table-column>
      <el-table-column
        label="文件路径"
        prop="filePath"
        sortable="custom"
        align="center"
        width="150"
      >
      </el-table-column>
    </el-table>
    <!-- 翻页 -->
    <Pagination :total="0" />
  </div>
</template>

<script>
import Pagination from "../../components/Pagination/index";
import { getCategory, listBook } from "../../api/book";
// 这个老是报错,应该是依赖的问题,使用方法:v-waves
// import waves from '../../components/directive/waves'
export default {
  components: { Pagination },
  data() {
    return {
      // 存在多个table的时候能够对table进行区分
      tableKey: 0,
      listLoading: true,
      listQuery: {},
      showCover: false,
      // 查询条件是动态的
      categoryList: [],
      // 表格数据源
      list: [],
    };
  },
  mounted() {
    this.getCategoryList();
    this.getList();
  },
  methods: {
    getList() {
      this.listLoading = true;
      listBook(this.listQuery).then((response) => {
        console.log(response);
        const { list } = response.data;
        this.list = list;
        this.listLoading = false;
      });
    },
    // 排序事件
    sortChange(data) {
      console.log("sortChange", data);
    },
    getCategoryList() {
      getCategory().then((response) => {
        this.categoryList = response.data;
      });
    },
    changeShowCover(value) {
      this.showCover = value;
      console.log(this.showCover);
    },
    handleFilter() {
      console.log("handleFilter", this.listQuery);
      this.getList();
    },
    handleCreate() {
      // 页面切换到/book/create 切换到上传图书
      this.$router.push("/book/create");
    },
  },
};
</script>

<style lang="scss" scoped>
</style>

在这里插入图片描述

因为写了fit 所以无法左右横移

经常采用的方法:–fixed

使用前:

在这里插入图片描述

使用后

在这里插入图片描述

      <el-table-column label="操作" align="center" width="120">
        <template slot-scope="{ row }">
          <el-button
            type="text"
            size="default"
            @click="handleUpdate(row)"
            icon="el-icon-edit"
          ></el-button>
        </template>
      </el-table-column>

在这里插入图片描述

携带分页参数

在这里插入图片描述

分页和查询

在这里插入图片描述

本来pageSize应该是20的,因为我把老的数据都删掉了,所以数据比较少,pageSize写小点测试一下

在这里插入图片描述

接下来加入查询条件

在这里插入图片描述

这里的操作很妙

在这里插入图片描述

在这里插入图片描述

标题的模糊查询

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

模糊查询高亮

//new  
wrapperKeyword(k,v){
    
    
      function highlight(value){
    
    
        return `<span style='color:#18900f'>${
      
      value}</span>`
      }
      if(!this.listQuery[k]){
    
    
        return v
      }else{
    
    
        // return v.replace(new RegExp(this.listQuery[k]),v=>{
    
    
        //   return highlight(v)
        // })
        return v.replace(new RegExp(this.listQuery[k]),v=>highlight(v))
      }
    },
   getList() {
    
    
      this.listLoading = true;
      listBook(this.listQuery).then((response) => {
    
    
        console.log(response);
        const {
    
     list } = response.data;
        this.list = list;
        this.listLoading = false;
          //new 
        this.list.forEach(book=>{
    
    
          book.titleWrapper=this.wrapperKeyword('title',book.title)
          book.authorWrapper=this.wrapperKeyword('author',book.author)
        })
      });
    },

在这里插入图片描述

bug

在这里插入图片描述

bug 一个书名无法匹配多个 只能匹配第一个 还有一个不区分大小写

在这里插入图片描述

解决办法就是在正则表达式加一些参数

在这里插入图片描述

在这里插入图片描述

排序&分页优化

在这里插入图片描述

取出prop,order

在这里插入图片描述

我们设desc为-id asc为+id

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

点击以后需要再次发请求

在这里插入图片描述

服务端

解出sort之后

在这里插入图片描述

在这里插入图片描述

分页功能

先查询book表里有多少电子书

在这里插入图片描述

返回值:一个数组

在这里插入图片描述

返回给前端

在这里插入图片描述

在这里插入图片描述

看到返回的是字符串

在这里插入图片描述

修改

在这里插入图片描述

这几个参数返回给前端主要是要修改pagination这个组件

在这里插入图片描述

在这里插入图片描述

但是点击翻页按钮并没有变化,而且切换每页显示的数量也没有变化

在这里插入图片描述

这样翻页功能和改变每页的数量的功能就实现了

接下来有个显示图片的bug

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

过滤器优化表格字段显示

现在假如说有一本文件路径是空的

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

增加个上传时间

在这里插入图片描述

parseTime utils下面自带的方法

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

删除功能

在这里插入图片描述

拿到点击的fileName 通过fileName来进行后续的操作

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

服务端

在这里插入图片描述

在这里插入图片描述

优化 完结

刷新url的时候原来的查询条件全部丢失

方案:把参数保存到url中

在这里插入图片描述

实现这个功能需要进行一些改造

在created调用了parseQuery

在parseQuery里面不仅要对page,pageSize,sort进行封装,还要增加一步:获取到一个新的对象(this.route.query)

之后和当前的query做一个合并

在这里插入图片描述

   if(query){
    
    
        query.page&&(query.page = +query.page)
        query.pageSize&&(query.pageSize = +query.pageSize)
      }

在这里插入图片描述

在这里插入图片描述

getList就不适用了

在这里插入图片描述

点击查询

在这里插入图片描述

点击下面的翻页并没有更新

在这里插入图片描述

解决:

在这里插入图片描述

但是又有问题了 就是鼠标移开以后不会更新

在这里插入图片描述

虽然push了 但是路由并没有更新

这是一个难点 虽然路由更新了,但是页面并没有刷新

需要通过路由钩子来解决,监听两次路由的变化

在这里插入图片描述

在这里插入图片描述

现在的问题就是更新完了以后列表没有刷新

如何比较两个对象相不相等呢 变成json字符串来对比

在这里插入图片描述

这样就ok了

还有个问题

在这里插入图片描述

category还原的方法有很多,比较推荐的是

在这里插入图片描述

服务端:

在这里插入图片描述

还有个问题,先选到第二页,在输入查询条件(只有一页)这时候会出现这种情况

在这里插入图片描述

所以每次查询的时候都要讲页数置为1

在这里插入图片描述

现在是修排序的状态bug

在这里插入图片描述

浏览器地址栏回车以后虽然能正确排序,但是那个箭头没有变颜色

在这里插入图片描述

有个地方写错了 default-sort应该写在el-table下面

在这里插入图片描述

在这里插入图片描述

前端代码为什么要打包

因为前端的浏览器中只能识别出js html css 现在我们的代码包含的是.vue文件

这是浏览器无法识别的

还有在代码中充斥着大量的import 和export语法,这是模块化语法 import使用的是 es module规范 es6的模块化

在服务端中使用的是类似module.export 是commonjs语法

这些语法在浏览器是无法运行的

打包的目的是让代码再浏览器中运行

打包是要通过webpack进行打包

在这里插入图片描述

命令行输入npm run build:prod

打包完毕后 会生成一个dist目录

在这里插入图片描述

index.html就是入口文件,也是平常所说的单应用,因为他是单个页面的应用

他可以实现整个应用就只有一个页面index.html

里面所有的内容都是组件,通过js来进行动态的界面替换

这样打包完了以后 我们的项目就完成了,完成了以后尝试运行一下这个index.html

直接在浏览器打开

发现会报错

在这里插入图片描述

在这里插入图片描述

造成这个问题的原因是什么呢

在这里插入图片描述

因为这里写的是绝对路径

资源文件的路径设置怎么做呢

在这里插入图片描述

在重新打包一下

去index.html文件看一下

在这里插入图片描述

发现已经可以看到界面了

在这里插入图片描述

但是点击登录仍然登录不上去

没有关系 我们接着往下走


在这里插入图片描述

在这里插入图片描述

之后打开8089端口

发现是可以登进去的

目前nginx服务器现在是在本地,需要把它挪到线上


云服务器设密码

在这里插入图片描述

在这里插入图片描述

因为阿里云现在安全级别比较高

点击配置安全组规则

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

ssh采用的是22端口,所以应该是可以访问的

所以不是这个问题

应该重启一下

在这里插入图片描述

在这里插入图片描述

因为输入who没有出来东西

所以exit退出从新登了一下

在这里插入图片描述

这次可以了

这两句话可以实现免密登录(可忽略)

在这里插入图片描述

ssh-keygan生成rsa的秘钥

在这里插入图片描述

然后~/.ssh/id_rsa.pub找到文件

后面再加上远程连接的内容 [email protected]

更简便的是对host文件进行修改

在这里插入图片描述

在这里插入图片描述

这个意思是长时间不用的话连接会被服务器断开

在这里插入图片描述

解决办法:找到这个文件

在这里插入图片描述

在这里插入图片描述

这个代表每30秒客户端就会保持与服务端的连接 就可以避免自动断电的问题了

改完了他不会立即生效,需要重启一下

在这里插入图片描述

然后登出

在登录看看会不会自动断开


nodejs环境搭建

在这里插入图片描述

看看有没有生效

nvm并没有生成

在这里插入图片描述

在这里插入图片描述

vim .bash_profile

在这里插入图片描述

可以看到并没有生成nvm

在这里插入图片描述

应该是在bashrc下面

我这儿一直显示找不到nvm

后来又重新安了一遍 然后仔细看他的提示

在这里插入图片描述

这样跟着他输一遍就好了

然后输入nvm 显示出一大段东西的时候就代表安装好了

之后nvm install node

在通过node安装最新版本

在这里插入图片描述

这时候npm可以用了 npm -v

现在可以来安装cnpm了

在这里插入图片描述

cnpm的包就会安装到nvm下面

which cnpm

在这里插入图片描述


nginx安装

yum:是centos自带的 通过yum来安装centos的依赖

yum -y install pcre*

在这里插入图片描述

这些库都是要安装nginx的时候需要的依赖

再来安装openssl的依赖

yum -y install openssl*

之后来下载nginx

可以先建一个目录 叫nginx

mkdir nginx 待会就将源码放进去

在这里插入图片描述

找到nginx下载地址

我们选择源码编译形式来进行安装

在这里插入图片描述

在这里插入图片描述

因为nginx是用C语言来写的,所以要用make指令来对他进行编译 编译完了以后,使用make install来进行安装,

我们还得看看make这些指令存不存在,如果不存在还得通过yum来进行安装

用安装make 和gcc

也不知道这是安好了还是没安好

在这里插入图片描述

然后就可以来解压了

在这里插入图片描述

在这里插入图片描述

进入来看一下 这就是源码目录

在这里插入图片描述

有个绿色的configure 代表是个可执行文件

centos执行文件使用./的方式来执行的, 或者用sh 来执行

在这里插入图片描述

然后用make -j4来进行编译

在这里插入图片描述

编译完之后 make install来进行安装

安装完之后

在这里插入图片描述

可以看到nginx已经生成了

但是输入nginx 显示没找到,现在还没法直接使用

怎么办呢

进入到uer/bin目录下

cd /usr/bin

ll一下 usr/bin目录下的指令都是可以直接访问的

这里可以直接做个软连接

ln -s /usr/local/nginx/sbin/nginx nginx

可以理解为创建了一个快捷方式

在这里插入图片描述

可以看到他指向了哪

cd 回到root

这时候就可以调用nginx nginx就启动了

可以输入ps -ef|grep nginx看一下进程 进程启动了

在这里插入图片描述

可以通过nginx -s stop来停止nginx服务

来看一下nginx配置文件

在这里插入图片描述

进入conf

在这里插入图片描述

打开这个

在这里插入图片描述

可以看到默认启动的是80端口

在这里插入图片描述

路径是根路径下的html文件夹

在这里插入图片描述

打开看一下

在这里插入图片描述

现在通过nginx指令将服务启动起来

然后通过阿里云的ip来访问

但是我的并不能访问

试一下这个https://blog.csdn.net/qq_21882763/article/details/113823460 失败

https://www.cnblogs.com/songanwei/p/9239821.html

https://bbs.csdn.net/topics/396990545 这里有个评论 curl localhost失败

我知道了

需要添加一个80端口

在这里插入图片描述

这样就能访问了

在这里插入图片描述


接下来做一些个性化的定义

希望nginx能支持我们默认的配置

进入nginx目录下,创建一个nginx.conf

在这里插入图片描述

然后打开nginx的config文件

在这里插入图片描述

输入a进入编辑模式 把user改为root

在这里插入图片描述

然后到文件的最后添加一些内容

这是我们刚刚建的那个文件

在这里插入图片描述

意思是将root/nginx下面所有带有.conf的配置文件融合到当前的配置文件当中

接着打开刚刚建的nginx.conf

写入一些东西

在这里插入图片描述

因为我们监听的端口是80端口,和主配置文件有冲突,所以把主配置文件的端口改一下

给他改成9000

接着继续在nginx.conf写配置

在这里插入图片描述

接着进入upload 创建一个index.html

vim index.html

写点东西 这里写了个h1标签 里面写了hello world

检查一下配置文件

nginx -t

出错了

在这里插入图片描述

打开这个配置文件

发现并不能显示行号

那么如何显示行号呢 :set nu

是因为include那句话少了个分号,所以报错了

在这里插入图片描述

还是有错

额 又忘加分号了

重启nginx nginx -s reload

在这里插入图片描述

okkkk

现在就可以把我们的文件上传到上面了

如何实现呢

可以通过ftp工具 fileZilla / xftp 等很多软件

下载fileZilla 记得选client 而不是server

添加新站点

在这里插入图片描述

在这里插入图片描述

之后把我们最先打包好的文件拖到upload里面

在这里插入图片描述

在实际生产环境中建议大家不要开启index 重新修改nginx.conf配置文件

在这里插入图片描述

把这个关闭

不知道下面文件路径的话是没有办法访问到这边的文件的

yum install git 为了下载网易云接口

clone github项目

进入到项目

node app.js 但是退出了以后就

pm2 让它永远活着

写个脚本也可以

在这里插入图片描述


代码build好直接上传到服务器是可以的

但是如果我们新push了一些内容,还需要手动编译,编译完了以后打开ftp工具把代码上传上去手动上传上去

了解一下自动化部署

安装

在这里插入图片描述

在这里插入图片描述

因为nginx是用C语言来写的,所以要用make指令来对他进行编译 编译完了以后,使用make install来进行安装,

我们还得看看make这些指令存不存在,如果不存在还得通过yum来进行安装

用安装make 和gcc

也不知道这是安好了还是没安好

在这里插入图片描述

然后就可以来解压了

在这里插入图片描述

在这里插入图片描述

进入来看一下 这就是源码目录

在这里插入图片描述

有个绿色的configure 代表是个可执行文件

centos执行文件使用./的方式来执行的, 或者用sh 来执行

在这里插入图片描述

然后用make -j4来进行编译

在这里插入图片描述

编译完之后 make install来进行安装

安装完之后

在这里插入图片描述

可以看到nginx已经生成了

但是输入nginx 显示没找到,现在还没法直接使用

怎么办呢

进入到uer/bin目录下

cd /usr/bin

ll一下 usr/bin目录下的指令都是可以直接访问的

这里可以直接做个软连接

ln -s /usr/local/nginx/sbin/nginx nginx

可以理解为创建了一个快捷方式

在这里插入图片描述

可以看到他指向了哪

cd 回到root

这时候就可以调用nginx nginx就启动了

可以输入ps -ef|grep nginx看一下进程 进程启动了

在这里插入图片描述

可以通过nginx -s stop来停止nginx服务

来看一下nginx配置文件

在这里插入图片描述

进入conf

在这里插入图片描述

打开这个

在这里插入图片描述

可以看到默认启动的是80端口

在这里插入图片描述

路径是根路径下的html文件夹

在这里插入图片描述

打开看一下

在这里插入图片描述

现在通过nginx指令将服务启动起来

然后通过阿里云的ip来访问

但是我的并不能访问

试一下这个https://blog.csdn.net/qq_21882763/article/details/113823460 失败

https://www.cnblogs.com/songanwei/p/9239821.html

https://bbs.csdn.net/topics/396990545 这里有个评论 curl localhost失败

我知道了

需要添加一个80端口

在这里插入图片描述

这样就能访问了

在这里插入图片描述


接下来做一些个性化的定义

希望nginx能支持我们默认的配置

进入nginx目录下,创建一个nginx.conf

在这里插入图片描述

然后打开nginx的config文件

在这里插入图片描述

输入a进入编辑模式 把user改为root

在这里插入图片描述

然后到文件的最后添加一些内容

这是我们刚刚建的那个文件

在这里插入图片描述

意思是将root/nginx下面所有带有.conf的配置文件融合到当前的配置文件当中

接着打开刚刚建的nginx.conf

写入一些东西

在这里插入图片描述

因为我们监听的端口是80端口,和主配置文件有冲突,所以把主配置文件的端口改一下

给他改成9000

接着继续在nginx.conf写配置

在这里插入图片描述

接着进入upload 创建一个index.html

vim index.html

写点东西 这里写了个h1标签 里面写了hello world

检查一下配置文件

nginx -t

出错了

在这里插入图片描述

打开这个配置文件

发现并不能显示行号

那么如何显示行号呢 :set nu

是因为include那句话少了个分号,所以报错了

在这里插入图片描述

还是有错

额 又忘加分号了

重启nginx nginx -s reload

在这里插入图片描述

okkkk

现在就可以把我们的文件上传到上面了

如何实现呢

可以通过ftp工具 fileZilla / xftp 等很多软件

下载fileZilla 记得选client 而不是server

添加新站点

在这里插入图片描述

在这里插入图片描述

之后把我们最先打包好的文件拖到upload里面

在这里插入图片描述

在实际生产环境中建议大家不要开启index 重新修改nginx.conf配置文件

在这里插入图片描述

把这个关闭

不知道下面文件路径的话是没有办法访问到这边的文件的

yum install git 为了下载网易云接口

clone github项目

进入到项目

node app.js 但是退出了以后就

pm2 让它永远活着

写个脚本也可以

在这里插入图片描述


代码build好直接上传到服务器是可以的

但是如果我们新push了一些内容,还需要手动编译,编译完了以后打开ftp工具把代码上传上去手动上传上去

了解一下自动化部署

猜你喜欢

转载自blog.csdn.net/xiaozhazhazhazha/article/details/119707276