vue 快速入門 系列 —— vue 的基礎應用(下)

彭加李發表於2021-04-18

其他章節請看:

vue 快速入門 系列

vue 的基礎應用(下)

上篇聚焦於基礎知識的介紹;本篇聚焦於基礎知識的應用

遞迴元件

元件是可以在它們自己的模板中呼叫自身的。不過它們只能通過 name 選項來做這件事。我們實現一個自定義樹的元件。請看示例:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <script src='vue.js'></script>
</head>
<body>
  <div id='app'>
    <custom-tree :list='treeData'></custom-tree>
  </div>

<script>
  // 遞迴元件 - 自定義樹
  Vue.component('custom-tree', {
   // 給元件命名
   name: 'custom-tree',               // {1}
   props: ['list'],
   template: `
    <ul>
      <li v-for='item in list'>
        {{item.name}}
        <!-- v-if 指定退出的條件,防止無線遞迴 -->
        <custom-tree 
          :list='item.children' 
          v-if='item.children'        
        ></custom-tree>  
      </li>
    </ul>
    `
  })
  const app = new Vue({
     el: '#app',
     // 資料也需要符合遞迴的條件
     data: {
       treeData: [                   // {2}
         {
           name: 'a',
           children: [
             {
               name: 'b'
             },
             {
               name: 'c'
             }
           ]
         },
         {
           name: 'd',
           children: [
             {
               name: 'e',
               children: [
                 {
                   name: 'f'
                 },
                 {
                   name: 'g'
                 }
               ]
             }
           ]
         }
       ]
     }
   })
</script>
</body>
</html>
// 頁面輸出:
a
  b
  c
d
  e
    f
    g

有3點需要注意:

  • 給元件設定 name (行{1})
  • 使用一個條件來結束無限遞迴。這裡使用了 v-if
  • 資料得滿足遞迴(行{2})

Tip: 後續不在提供完整的程式碼,省略 head、body 等。

動態元件

vue 提供了 <component> 來動態的掛載元件。請看示例:

<div id='app'>
  <!-- vue 提供了 <component> 來動態的掛載元件 -->
  <component v-bind:is="currentComponent"></component>
  <button @click='switchHandle'>切換元件</button>
</div>

<script>
  var comC = {
    template: `<p>我是元件 C</p>`
  };

  var app = new Vue({
    el: '#app',
    data: {
      currentComponent: 'comB'
    },
    // 區域性註冊。components 選項中定義你想要使用的元件
    components: {
      comA: {
        template: `<p>我是元件 A</p>`
      },
      comB: {
        template: `<p>我是元件 B</p>`
      },
      comC: comC
    },
    methods: {
      switchHandle: function(){
        let map = {
          'comA': 'comB',
          'comB': 'comC',
          'comC': 'comA'
        };
        // 動態切換元件
        this.currentComponent = map[this.currentComponent]
      }
    }
  })
</script>
// 頁面輸出:
我是元件 A

切換元件

// 點選按鈕(‘切換元件’),依次顯示'我是元件 B'、'我是元件 C'...

內建的元件 component 根據屬性 is 的值來決定哪個元件被渲染。

nextTick

Vue.nextTick( [callback, context] )

用法
在下次 DOM 更新迴圈結束之後執行延遲迴調。在修改資料之後立即使用這個方法,獲取更新後的 DOM。—— 不明白的化,請看示例:

  <div id="example">{{message}}</div>

  <script>
  var vm = new Vue({
    el: '#example',
    data: {
      message: '123'
    }
  })
  vm.message = 'new message' // 更改資料               //        {20}
  console.log(vm.$el.textContent === 'new message')   // false  {21}
  Vue.nextTick(function () {
    console.log(vm.$el.textContent === 'new message') // true   {22}
  })
  </script>

更改資料後(行{20},dom 元素中的內容其實沒有得到更新,輸出 false(行{21});在 Vue.nextTick() 方法中才被更新,輸出 true(行{22})。

這裡涉及到 vue 中一個概念:非同步更新

假如在更改狀態(行{20})後,dom 元素立馬得到更新(行{21}),也就是輸入出 true,那麼使用者使用 for 迴圈改變某個狀態 100 次,dom 元素就得更新 100 次,是否覺得浪費!所以 vue 的策略是:使用非同步更新,也就是不會馬上更新 dom。

手動掛載

vm.$mount( [elementOrSelector] )

用法
如果 Vue 例項在例項化時沒有收到 el 選項,則它處於“未掛載”狀態。我們可以使用 vm.$mount() 方法手動。

我們建立一個元件,三秒後再掛載它。請看示例:

<div id="app"></div>

<script>
  // Vue.extend() 使用基礎 Vue 構造器,建立一個“子類”
  var MyComponent = Vue.extend({
    template: '<div>Hello!</div>'
  })

  // 建立並掛載到 #app (會替換 #app)
  setTimeout(function(){
    // 3 秒後頁面上才會看見 Hello!
    new MyComponent().$mount('#app')
  }, 3000)
</script>

3 秒後,頁面上才會看見 Hello!

數字輸入框元件

需求:數字輸入框只能輸入數字,有兩個按鈕,分別是減1和加1。此外還可以設定初始值、最大值、最小值,數字改變時通知父元件。

請看完整程式碼:

<div id='app'>
    父元件 value = {{value}}
    <!--  v-model 實現雙向繫結 -->
    <custom-number 
      v-model='value'
      :max-value='maxValue' 
      :min-value='minValue'
      :step='10'
    ></custom-number>
</div>

<script>
  Vue.component('custom-number', {
    props:{
      // 必須是一個數字
      value: {
        type: Number,
        default: 0
      },
      maxValue: {
        type: Number,
        // 正無窮大
        default: Number.POSITIVE_INFINITY
      },
      minValue: {
        type: Number,
        // 負無窮大
        default: Number.NEGATIVE_INFINITY
      },
      // 預設加減 1
      step: {
        type: Number,
        default: 1
      }
    },
    data: function(){
      return {
        inputValue: this.value
      }
    },
    created: function(){
      // 處理:初始值不在最大值和最小值的範圍內
      this.update(this.value)
    },
    computed: {
      // 減(-)不可點
      minIsDisabled: function(){
        return this.inputValue <= this.minValue
      },
      maxIsDisabled: function(){
        return this.inputValue >= this.maxValue
      }
    },
    watch: {
      // 監聽 inputValue,通知父元件
      inputValue: function(val, oldVal){
        this.$emit('input', val)
      },
      // 父元件改變值,子元件的值也跟著改變
      value: function(val){
        this.update(val);
      }
    },
    template: `
      <div>
        <button :disabled="minIsDisabled" @click='minus'> - </button> 
        <input :value='inputValue' @change='changeHandle' > 
        <button :disabled="maxIsDisabled" @click='add'> + </button>
        <p>
          子元件 inputValue = {{inputValue}}
        </p>
      </div>
      `,
    methods: {
      // 如果輸入值不是數字,則不會更改值
      changeHandle: function(e){
        var val = e.target.value;
        this.update(val, e.target)
      },
      // obj 是否是數字。摘自 jquery
      isNumeric: function(obj) {
        return !isNaN( parseFloat(obj) ) && isFinite( obj );
      },
      minus: function(){
        this.update(this.inputValue - this.step);
      },
      add: function(){
        this.update(this.inputValue + this.step);
      },
      // 是數字才更新
      update: function(val, target={}){
        if(!this.isNumeric(val)){
          // 將 input 值置為上次的值
          target.value = this.inputValue;
          return;
        }
        val = Number(val);
        // 大於最大值,則重置為最大值
        if (val > this.maxValue){
          val = this.maxValue
        }else if(val < this.minValue){
          val = this.minValue
        }
        this.inputValue = val;
      }
    }
  });

  var app = new Vue({
    el: '#app',
    data: {
      value: 10,
      maxValue: 100,
      minValue: 1
    }
  })
</script>
// 頁面輸出:
父元件 value = 10
- 10 +
子元件 inputValue = 10

// 點選 +(每次加減10)
父元件 value = 20
- 20 +
子元件 inputValue = 20

// 繼續點選2次 
// 減號(-)變為不可點
父元件 value = 1
- 1 +
子元件 inputValue = 1

Tabs 標籤頁

需求:實現一個常用的元件 —— tabs 標籤頁。

:不明白需求的可以看element-ui-tabs

思路:

  • 定義元件 el-tabs
  • 定義 el-tabs 的子元件 tab-pane
  • 父子元件通訊使用 vm.$parent 和 vm.$children

請看完整程式碼:

<style>
  ul{margin:0;padding: 0;border-bottom: 1px solid;margin-bottom: 10px;}
  li{display:inline-block;margin-right:10px;cursor:pointer;}
  .active{color:#409eff;}
</style>

<div id='app'>
  <el-tabs v-model="activeKey">
    <el-tab-pane label="使用者管理">
      使用者管理內容
      <p>我是 A</p>
    </el-tab-pane>
    <el-tab-pane label="配置管理" name="second">配置管理內容</el-tab-pane>
    <el-tab-pane label="角色管理">
      角色管理內容
      <p>我是 C</p>
    </el-tab-pane>
    <el-tab-pane label="定時任務補償" name="fourth">定時任務補償內容</el-tab-pane>
  </el-tabs>
</div>

<script>
  // 父元件
  Vue.component('el-tabs', {
    props:{
      value:{
        type: [String, Number]
      }
    },
    data: function(){
      return {
        currentTab: this.value,
        // 存放 tab
        tabLists: []
      }
    },
    watch: {
      currentTab: function(){
        this.updateStatus();
      },
      // 處理:父元件更改 value
      value: function(val, oldVal){
        this.currentTab = val
      }
    },
    template: `
      <div>
        <ul>
          <li 
            v-for='(item, index) in tabLists' 
            :class='{active: item.name === currentTab}'
            @click='handleClick(index)'
          >{{item.label}}</li>
        </ul>
        <slot></slot>
      </div>
    `,
    methods: {
      // 取得 tab-pane
      getTabPanes: function(){
        return this.$children.filter(item => {
          return item.$options.name === 'tab-pane'
        })
      },
      // 更新 tabLists
      updateTabLists: function(){
        let tabLists = [];
        this.getTabPanes().forEach((item, index) => {
          if(!item.id){
            item.id = index
          }
          tabLists.push({
            label: item.label,
            name: item.id
          })

          // 預設展示第一個
          if(index === 0){
            if(!this.currentTab){
              this.currentTab = item.id;
            }
          }
        })
        this.tabLists = tabLists;
        this.updateStatus()
      },
      handleClick: function(index){
        this.currentTab = this.tabLists[index].name;
        console.log(`name = ${this.currentTab}`)
        this.updateStatus()
      },
      // 讓子元件顯示或隱藏
      updateStatus: function(){
        this.getTabPanes().forEach(item => {
          item.updateShow(this.currentTab === item.id)
        })
      }
    }
  });

  // 子元件
  Vue.component('el-tab-pane', {
    name: 'tab-pane',
    props: {
      // 標籤標題
      label:{
        type: String,
        default: ''
      },
      // pane 的名字,不必填
      name: [String, Number]
    },
    data: function(){
      return {
        // 顯示與否,由父元件決定
        show: false,
        // 不允許通過父元件更改 props 中的屬性 name
        // 用 id 來替代 name 屬性
        id: this.name
      }
    },
    created: function(){
      this.$parent.updateTabLists();
    },
    template: `
      <div v-if='show'>
        <slot></slot>
      </div>
    `,
    methods: {
      updateShow: function(v){
        this.show = v;
      }
    }
  });

  const app = new Vue({
    el: '#app',
    data: {
      // 當前選中的 tab
      activeKey: 2
    }
  })
</script>
// 頁面輸出:
// 第一行是 4 個 tab,現在是`角色管理`被選中
使用者管理 配置管理 角色管理 定時任務補償

角色管理內容

我是 C

其他章節請看:

vue 快速入門 系列

相關文章