《web前端全栈成长计划》Vue第二章学习笔记(上)

举报
张辉 发表于 2020/09/07 17:22:42 2020/09/07
【摘要】 web前端全站成长计划第三阶段vue课程第二章的学习笔记(上)

本文摘自 论坛 https://bbs.huaweicloud.com/forum/forum.php?mod=viewthread&tid=71831&authorid=70062&page=1 笔者对vue学习的笔记。由于vue笔者也是刚刚接触,基本上都是以观看视频和具体动手实践的方式进行。为防止迷路,特收集整理成本博文。

文中的图片、代码很多来自于教程本身,但都来自于本人实践后截图和拷贝的结果,特此说明。

Vue 第二章 Vue项目与案例

18 使用vue-cli创建项目

18.1 创建vue项目

npm install -g vue-cli

vue的工作空间 C:\Users\zhang\WebstormProjects\

创建demo项目

webpack是模板项目之一,项目名称必须是小写

vue init webpack vue_demo

image.png

系统帮你用npm自动做npm install

image.png

cd vueDemo

18.2 启动vue项目

npm run rev

image.png

image.png

访问:http://localhost:8080

image.png

访问成功。后续需要基于此写代码。

19 基于脚手架编写项目

19.1 vue项目结构


build 配置文件夹

webpack.xxx.conf.js (xxx: base,dev,prod)

config 配置文件夹

index.js

port: 8080(如果有两个需要运行,则需要手工改一下)

autoOpenBrowser 是否自动打开浏览器

node_modules 依赖

src 源码目录

static 静态 

放全局资源

.gitkeep  GIT使用

.babelrc  作用:ES6转ES5

rc:rumtime control 运行时控制

.eslintrc.js ESLINT的配置

.eslintignore 忽略ESLINT的检查 

.gitignore GIT使用

index.html 单页应用的主页

package.json 页面描述文件

README.md 

创建vue模板文件:

image.png

入口:index.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <title>vue_demo</title>
  </head>
  <body>
    <div id="app"></div>
    <!-- built files will be auto injected -->
  </body>
</html>

App.vue

<template>
  <!-- 模板必须有个根标签 -->
  <div id="app">
<!--    <img src="./assets/logo.png">-->
    <HelloWorld/>  <!-- 3 使用组件标签 -->
  </div>
</template>

<script>
/* 引入组件 */
import HelloWorld from './components/HelloWorld'

/* 向外默认暴露一个对象 */
/* 需要在webstorm配置中disable eslint,(jslint)和jshint检查 */
/* 2 映射组件标签 */
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>

main.js

// The Vue build version to load with the `import` command
// (runtime-only or standalone) has been set in webpack.base.conf with an alias.
/* 入口js */
/* 创建vue实例 */

import Vue from 'vue' /* 注意大小写 */
import App from './App'

Vue.config.productionTip = false

/* eslint-disable no-new */
/* 对应index.html的app */
/* 引入组件 将组件的映射名变为标签 组件一般是一个局部功能界面,包含了html,css,js,img等资源 */
/* 模板插入到el匹配的页面上的div去 参见 生命周期 当时判断了 是否有template选项,如果有,则编译template,挂载到页面上去 */
new Vue({
  el: '#app',
  components: { App },
  template: '<App/>'
})

componets目录下的HelloWorld.vue

<template>
  <div class="hello">
    <img src="../assets/logo.png" alt="logo">
    <h1>{{ msg }}    <h2>Essential Links    <ul>
              <a
          href="https://vuejs.org"
          target="_blank"
        >
          Core Docs
                            <a
          href="https://forum.vuejs.org"
          target="_blank"
        >
          Forum
                            <a
          href="https://chat.vuejs.org"
          target="_blank"
        >
          Community Chat
                            <a
          href="https://twitter.com/vuejs"
          target="_blank"
        >
          Twitter
                    <br>
              <a
          href="http://vuejs-templates.github.io/webpack/"
          target="_blank"
        >
          Docs for This Template
                  </ul>
    <h2>Ecosystem    <ul>
              <a
          href="http://router.vuejs.org/"
          target="_blank"
        >
          vue-router
                            <a
          href="http://vuex.vuejs.org/"
          target="_blank"
        >
          vuex
                            <a
          href="http://vue-loader.vuejs.org/"
          target="_blank"
        >
          vue-loader
                            <a
          href="https://github.com/vuejs/awesome-vue"
          target="_blank"
        >
          awesome-vue
                  </ul>
  </div>
</template>
<!-- 配置对象与vue一致
 data可以写对象,也可以写函数,但是组件中必须写函数 -->
<script>
export default {
  name: 'HelloWorld',
  data () {
    return {
      msg: 'Hello Zhang Hui'
    }
  }
}
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
h1, h2 {
  font-weight: normal;
}
ul {
  list-style-type: none;
  padding: 0;
}
li {
  display: inline-block;
  margin: 0 10px;
}
a {
  color: #42b983;
}
</style>

显示效果:

image.png

20 打包发布项目

20.1 生成打包文件

npm run build

生成dist文件夹

image.png

20.2 准备服务器

20.2.1 使用静态服务器

image.png

serve dist

image.png

访问  http://localhost:5000

image.png


20.2.2 使用动态服务器

发布到tomcat

修改配置 webpack.prod.conf.js

output:{

    publicPath: '/vue_demo/'

}

直接将dist目录改名为vue_demo拷贝到tomcat的webapps目录下

21 vue eslint代码规范检查工具

eslint检查的基本原理:

  1. 内部定义了n个规则

  2. 对代码进行规则检查,如果发现有违反规则,则打印错误信息

  3. eslint.org介绍了所有的规则对应的处理方式

image.png

eg 期待没有空格,发现有2个空格

如何让一些操0的规则失效。

修改.eslintignore

rules: {
  // allow async-await
  'generator-star-spacing': 'off',
  // allow debugger during development
  'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
  'indent': 'off'
}

也可以在.eslintignore文件中忽略检查(不建议)


22 初始化显示

23 交互添加

24 交互删除

拆分页面:

app-->add--->List-->Item

image.png

入口:main.js

最外层: App.vue

<template>
  <!-- 模板必须有个根标签 -->
  <div id="app">
    <header class="site-header jumbotron">
      <div class="container">
        <div class="row">
          <div class="col-xs-12">
            <h1>请发表对vue的评论          </div>
        </div>
      </div>
    </header>
    <div class="container">
      <!-- 将组件中的函数传出去 -->
      <Add :addComment="addComment"/>
      <br>
      <List :comments="comments" :deleteComment="deleteComment"/>
    </div>
  </div>
</template>

<script>
/* 引入组件 */
import Add from './components/Add.vue'
import List from './components/List.vue'

/* 向外默认暴露一个对象 */
/* 需要在webstorm配置中disable eslint,(jslint)和jshint检查 */
/* 2 映射组件标签 */
/* 组件间的通讯:通过标签 */
/* 采用了HMR技术,并不是刷新整个模块 */
export default {
  name: 'App',
  /* return对象用于放实际的评论数据 */
  data() {
    return {
      comments: [
        {
          name: '张辉',
          content: 'vue还行'
        },
        {
          name: '李四',
          content: 'vue太慢'
        },
        {
          name: '王五',
          content: 'vue很难'
        }
      ]
    }
  },

  methods: {

    // 增加评论
    addComment(comment) {
      this.comments.unshift(comment)
    },
    // 删除指定下标的评论,该函数应该逐级传递给Item
    deleteComment(index) {
      this.comments.splice(index, 1)
    }

  },

  components: {
    Add,
    List
  }
}
</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>

components目录

三个组件
1.Add.vue

<template>
  <div>
<!--    Add组件-->
    <div class="col-md-4">
      <form class="form-horizontal">
        <div class="form-group">
          <label>用户名</label>
          <input type="text" class="form-control" placeholder="用户名" v-model="name">
        </div>
        <div class="form-group">
          <label>评论内容</label>
          <textarea type="text" class="form-control" rows="6" placeholder="评论内容" v-model="content"></textarea>
        </div>
        <div class="form-group">
          <div class="col-sm-offset-2 col-sm-10">
            <button type="button" class="btn bth-default pull-right" @click="add">提交</button>
          </div>
        </div>
      </form>
    </div>

  </div>
</template>

<script>
  export default {
    props: {
      addComment: { // 指定了属性名,属性值的类型以及必要性
        type: Function,
        required: true
      }
    },
    data() {
      return {
        name: '',
        content: ''
      }
    },
    methods: {
      add() {
        // 1检查输入的合法性
         const name = this.name.trim()
         const content = this.content.trim()
         if (!name || !content) {
           alert('姓名或者评论内容不能为空')
           return
         }
         // 2根据输入封装成comment对象
         const comment = {
           name,
           content
         }
         // 3添加到comments中
         // 数据在哪个组件,更新数据的行为(方法)就应该定义在哪个组件
         this.addComment(comment)

        // 4清除输入
        this.name = ''
        this.content = ''
      }

    }
  }
</script>

<style>

</style>

2.List.vue

<template>
  <div>
<!--    List组件-->
    <div class="col-md-8">
    <h3 class="reply">评论回复:</h3>
    <h2 v-show="comments.length===0">暂无评论,点击左侧添加评论!!!    <ul class="list-group">
     <Item v-for="(comment,index) in comments" :key="index" :comment="comment"
          :deleteComment="deleteComment" :index="index"/>
    </ul>
  </div>

  </div>
</template>

<script>
  import Item from './Item.vue'

  export default {
  /* 声明接收属性:这个属性会成为组件对象的属性,可以被模板直接读取,此时this是组件对象 */

  props: ['comments', 'deleteComment'],
    components: {
      Item
    }

}
</script>

<style>
.reply{
  margin-top: 0px;
}

li{
  transition: .5s;
  overflow:hidden;
}

.handle{
  width:40px;
  border: 1px solid #ccc;
  background: #fff;
  position: absolute;
  right: 10px;
  top: 1px;
  text-align:center;
}

.handle a{
  display: block;
  text-decoration: none;
}

.list-group-item .sentence {
  padding: 0px 50px;
}

.user{
  font-size: 22px;

}
</style>

3.Item.vue

<template>
  <div>
<!--    Item组件-->
    <li class="list-group-item">
      <div class="handle">
        <a href="javascript:;" @click="deleteItem">删除</a>
      </div>
      <p class="user"><span>{{comment.name }}<span>说:      <p class="sentence">{{ comment.content }}!    </li>
  </div>
</template>

<script>
export default {
  // props:['comment'] // 指定属性名
  props: {
    comment: Object,
    deleteComment: Function,
    index: Number
  },

  methods: {
    deleteItem() {
      const { comment, index, deleteComment } = this
      if (window.confirm(`确定删除${comment.name}的评论吗?`)) {
        deleteComment(index)
      }
    }
  }
}
</script>

<style>

</style>

显示效果:

原始页面:

image.png

新增:

image.png

删除确认:

image.png

删掉一个:

image.png

删完:

image.png


第二个练习 todolist

  • 添加

  • 列表显示

  • 删除

  • 统计

25 静态组件

先拆分组件

image.png

todoheader+ todolist+todofooter

list再拆分:todoitem

将静态组件html和css分别拆到各个模块中去。

image.png



26 动态初始化显示

动态组件:

实现数据的动态显示

设计todos数组 ,数组每个元素都是一个对象  标题title,勾选状态completed。

数据放在哪个组件保存?

分析后发现都需要,则todos在App.vue实现。

注意以下问题:

1.App.vue要向下传输todos,需要在 模板中 

<TodoList :todos="todos"/>

2.TodoList.vue要从App.vue获取todos,需要在props定义todos

image.png

3.TodoList.vue需要引用TodoItem.vue的循环体,需要在components引用TodoItem,并且在模板中写TodoItem

image.png

image.png

4.TodoItem.vue需要从TodoList.vue 获取todo和index,需要在props中定义todo和index

image.png

5.遇到input ,记得写v-model

6.任何地方记得引号别忘了。

显示效果:

image.png

27 header交互增加

方法类似,需要在App.vue增加addTodo函数(因为todos 在App,所以方法也需要在App)。然后传递(暴露)给TodoHeader。

在TodoHeader的input使用v-model绑定数据,且使用@keyup.enter来调用新增.(在此时调用从App传来的Function:addTodo

显示效果如下:

image.png


28 交互删除

目标:当鼠标移入时,显示删除,并将颜色修改。 点击删除后,删除当时的Todo

App需要新增deleteTodo,然后先后传递给TodoList和TodoItem;最后在btn增加deleteItem.

TodoItem需要处理onmouseenter和onmouseleave事件。在over时背景色修改为灰色,并显示删除字样,如leave时,则修改背景色为白色,并啥也不显示。

并且在li中需要进一步处理background和btn的是否显示按钮。

显示效果如下:

删除时:

image.png

删除后:

image.png


29 footer处理

分析:

  1. 显示:已完成和全部

  2. 点击全选后,用户操作清除已完成。

要点:

  1. 在App和TodoFooter中修改

  2. App传递 selectAllTodos和deleteCompleteTodos函数给footer

  3. selectAllTodos的实现:将每个todo改为check的值,check为true则都为true,check为false则都为false

  4. deleteCompleteTodos的实现:使用filter将非complte的找到。(删除已完成等于留下未完成)

  5. TodoFooter:

    1. 使用computeSize计算属性记录已选中的 this.todos.reduce((preTotal, todo) => preTotal + (todo.complete?1:0),0)

    2. 使用isAllCheck计算属性的get和set方法跟todos发生联动

显示效果:

全选:

image.png

全不选:(注意清除按钮也消失)

image.png

清除若干——清除前:

image.png

清除后:

image.png

清除全部:

image.png


30 数据保存

浏览器关闭后,如果还能保存,该怎么做?

将状态保存在文件中。利用localStorage

  1. 什么时候存:只要todos发生改变都要存(删除,添加,勾选都要存)

  2. 什么时候读:打开就应该读

  3. 存啥?todos

要点:

  1. todos初始化时从localStorage读取 todos: JSON.parse(window.localStorage.getItem('todos_key') || '[]')

  2. watch深度监视,使用handler保存最新的todos数据到localStorage中:

image.png

效果:

image.png

可以到 Application->LocalStorage查看到具体的值。


31 自定义事件(组件间通讯的第二种方式)

前面使用了:组件间通信的第一种方式:props

子组件跟父组件通信。

31.1事件作用:

1。绑定监听,

2。触发事件


31.2 用法1(绑定事件监听的第一种方式)

1.v-on: 绑定自定义事件

eg1: v-on:侦听名称:回调函数 

v-on:increment="incrementTotal"

eg2:@on(事件名eventName,回调函数function)   


2.使用 $emit(eventName,optionalPayload)触发事件


3.修改内容:

App.vue

原来:(传递函数)

<TodoHeader :addTodo="addTodo"/>

改为:

删除header props中的addTodo函数

props: {
  // 声明接收
  // addTodo: Function,
},

给TodoHeader标签绑定addTodo的事件侦听

<TodoHeader @addTodo="addTodo"/>

TodoHeader.vue

原来:

// 3.将todo对象添加到todos
this.addTodo(todo)

改为:

// 3.将todo对象添加到todos
// this.addTodo(todo)
// 触发自定义事件
this.$emit('addTodo',todo)

App父组件绑定了事件监听,当子组件发送变化时,触发了事件.


4.效果如下:

原始:

image.png

增加后:

image.png


31.3 用法2(绑定事件监听的第二种方式)

1.$on:


2.修改内容:


App.vue

原来:

<TodoHeader :addTodo="addTodo"/>

改为:(定义ref)

<TodoHeader  ref="header"/>

删除header props中的addTodo函数

props: {
  // 声明接收
  // addTodo: Function,
},

增加生命周期mounted的处理:

mounted () {
  // 在生命周期回调函数中执行绑定
  // 给TodoHeader绑定addTodo事件监听
  // this.$on('addTodo',this.addTodo) //这是给app绑定的监听,不对
  this.$refs.header.$on('addTodo',this.addTodo)
},

TodoHeader.vue

改法跟方法一一致。

3.效果如下:

image.png

第一种方式简单,适用于父子之间。但不适合经过多层(传递)的场景。


32 消息订阅和发布(组件间通讯的第三种方式)

1下载库 

npm  info pubsub-js

npm install --save pubsub-js

image.png

2.库提供了2个方法:

  • Publish

  • Subscribe

3.修改方法:

App.vue

<TodoList :todos="todos" :deleteTodo="deleteTodo"/>

改为:

<TodoList :todos="todos" />

并删除TodoList的deleteTodo函数引用

props: {
  todos: Array,
  // deleteTodo: Function
},

以及TodoList的deleteTodo操作

<TodoItem v-for="(todo, index) in todos" :key="index" :todo="todo" :index="index"
  />

进一步删除TodoItem的相关操作:

props: {
  todo: Object,
  index: Number,
  // deleteTodo: Function
},

deleteItem() {
  const {todo, index, deleteTodo} = this
  if(window.confirm(`确认删除${todo.title}`)){
    // deleteTodo(index)
  }
}

下面需要进行发布消息和订阅消息操作。

  • 订阅消息就是绑定事件监听

  • 发布消息就是触发事件

import增加

import PubSub from 'pubsub-js'

在mounted里面订阅消息:

mounted () {
  // 在生命周期回调函数中执行绑定
  // 给TodoHeader绑定addTodo事件监听
  // this.$on('addTodo',this.addTodo) //这是给app绑定的监听,不对
  this.$refs.header.$on('addTodo',this.addTodo)

  //订阅消息
  PubSub.subscribe('deleteTodo',(msg,index) => { // msg无用,使用箭头函数。
    this.deleteTodo(index)
  })
},

在TodoItem增加发布消息的处理

引入PubSub

import PubSub from 'pubsub-js'

在deleteItem发布消息:

deleteItem() {
  const {todo, index, deleteTodo} = this
  if(window.confirm(`确认删除${todo.title}`)){
    // deleteTodo(index)

    //发布消息
    PubSub.publish('deleteTodo',index)
  }
}

4.显示效果:

删除时:

image.png

删除后:

image.png

好处:两个组件通信, 没有任何位置要求。父子,兄弟都可以做。


33 Slot(组件间通讯的第四种方式)

组件间通过标签通信

原理:

以饿了么app为例

同一个组件写了2个标签

设计组件时:设计占位

image.png

使用时:向占位传递内容

组件不仅仅是数据变化,而且标签(结构)也有变化。

传什么,显示什么。

传递的内容是:标签


具体做法:

反复用到时适用slot

image.png

image.png


修改方式:

TodoFooter的修改:

<template>
  <div class="todo-footer">
    <label>
      <!-- 改为slot方式
      编写占位
      -->
<!--      <input type="checkbox" v-model="isAllCheck"/>-->
      <slot name="checkAll"></slot>
    </label>
    <span>
<!--          <span>已完成{{computeSize}} / 全部{{todos.length}}-->
      <slot name="count"></slot>
        <!--    <button class="btn btn-danger" v-show="computeSize" @click="deleteCompleteTodos">清除已完成任务</button>-->
    <slot name="delete"></slot>
  </div>
</template>

<script>
  export default {
    props: {
      // todos: Array,
      // deleteCompleteTodos: Function,
      // selectAllTodos: Function // 选择所有或者不选择所有
    },

    // computed: {
    //   computeSize() {
    //     return this.todos.reduce((preTotal, todo) => preTotal + (todo.complete?1:0),0)
    //   },
    //
    //   // 难点在于可能想不到使用计算属性
    //   isAllCheck: {
    //     get() {
    //       return this.computeSize === this.todos.length && this.computeSize>0
    //     },
    //
    //     set(value) { // value是checkbox最新值
    //       this.selectAllTodos(value)
    //     }
    //   }
    // }

  }
</script>

App的修改:

<template>
  <div class="todo-container">
    <div class="todo-wrap">
      <!-- -
        自定义事件可以代替 传递函数 给TodoHeader绑定 addTodo的事件监听
      -->
      <TodoHeader  ref="header"/>
      <TodoList :todos="todos" />
<!--      <TodoFooter :todos="todos" :deleteCompleteTodos="deleteCompleteTodos" :selectAllTodos="selectAllTodos"/>-->
      <todo-footer>
        <!--
       需要注意的是:原来在子组件编写的相关代码,都需要迁移到父组件去
        -->
        <input type="checkbox" v-model="isAllCheck" slot="checkAll"/>
        <span slot="count">已完成{{computeSize}} / 全部{{todos.length}}        <button class="btn btn-danger" v-show="computeSize" @click="deleteCompleteTodos" slot="delete">清除已完成任务</button>
      </todo-footer>
<!-- 另一种写法: todo-footer -->
    </div>
  </div>
</template>

此时需要将原来在TodoFooter实现的computed移植到App中:

computed: {
  computeSize() {
    return this.todos.reduce((preTotal, todo) => preTotal + (todo.complete?1:0),0)
  },

  // 难点在于可能想不到使用计算属性
  isAllCheck: {
    get() {
      return this.computeSize === this.todos.length && this.computeSize>0
    },

    set(value) { // value是checkbox最新值
      this.selectAllTodos(value)
    }
  }
},


演示效果:

image.png

基本跟原来一致。


另外,组件间通讯的第5种方式:vuex放在第四章讲解(尚未开放)


34 存储优化

将读写localStorage的功能单独用一个模块实现

创建util目录,创建storageUtil.js文件

/* 使用LocalStorage存储数据的工具模块
向外暴露:函数或者对象
如何选择?
一个函数是一个功能
一个对象是多个功能
选择依据是:需要暴露一个功能还是多个功能
 */
const TODOS_KEY = 'todos_key'
export default {
  saveTodos (todos) {
    window.localStorage.setItem(TODOS_KEY,JSON.stringify(value))
  },

  readTodos(){
    return JSON.parse(window.localStorage.getItem(TODOS_KEY) || '[]')
  }


}

修改app.vue

1.先import

// 引入工具
import storageUtil from './util/storageUtil'

2.修改load部分

// todos: JSON.parse(window.localStorage.getItem('todos_key') || '[]')
todos: storageUtil.readTodos()

3.修改read部分

// window.localStorage.setItem('todos_key',JSON.stringify(value))
storageUtil.saveTodos(value)

或改写handler

deep: true, // 深度监视
// handler: function( value){
//   //将todos最新值的JSON数据,保存到localStorage
//   // window.localStorage.setItem('todos_key',JSON.stringify(value))
//   storageUtil.saveTodos(value)
// }
handler: storageUtil.saveTodos

显示效果:

image.png

一切正常。

(待续)

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。