Vue3.0 新特性全面探索 - 基於 Composition Api 快速構建實戰項目

Quick Start

本項目綜合運用了 Vue3.0 的新特性,適合新手學習😁javascript

  • 基於 Composition APIFunction-based API 進行改造,配合 Vue Cli,優先體驗 Vue3 特性
  • 使用單例對象模式進行組件通訊
  • 使用 axios 庫進行網絡請求,weui 庫實現 UI 界面
# 安裝依賴
npm install
# 在瀏覽器打開localhost:8080查看頁面,並實時熱更新
npm run serve
# 發佈項目
npm run build

建議配合 Visual Studio Code 和 Vue 3 Snippets 代碼插件食用Ψ( ̄∀ ̄)Ψ。html

Dependencies

如下是項目運用到的依賴,@vue/composition-api 配合 vue 模塊讓咱們 Vue2.0 版本能夠搶先體驗 Vue3.0 的新特性,axios 是輔助咱們發送網絡請求獲得數據的工具庫,weui是一套與微信原生視覺一致的基礎樣式庫,方便咱們快速搭建項目頁面。vue

"@vue/composition-api": "^0.3.4",
"axios": "^0.19.0",
"core-js": "^3.4.3",
"vue": "^2.6.10",
"weui": "^2.1.3"

Directory Structure

├── src
│   ├── App.vue                          # 組件入口
│   ├── assets                           # 資源目錄
│   ├── stores/index.js                  # 狀態管理
│   ├── components                       # 組件目錄
│   │   ├── Header.vue                   # 頭部組件
│   │   ├── Search.vue                   # 搜索框組件
│   │   ├── Panel.vue                    # 列表組件
│   ├── main.js                          # 項目入口
├── public                               # 模板文件
├── vue.config.js                        # 腳手架配置文件
├── screenshot                           # 程序截圖

Composition API

npm install @vue/composition-api --save

使用 npm 命令下載了 @vue/composition-api 插件之後,引入該模塊後,須要顯式調用 Vue.use(VueCompositionApi) ,按照文檔在 main.js 引用便開啓了 Composition API 的能力。java

// main.js
import Vue from 'vue'
import App from './App.vue'
// 1.引入Composition API模塊
import VueCompositionApi from '@vue/composition-api'

Vue.config.productionTip = false
// 2.不要漏了顯式調用 VueCompositionApi
Vue.use(VueCompositionApi)

new Vue({
  render: h => h(App),
}).$mount('#app')
npm install weui --save

咱們一樣使用 npm 安裝 weui 模塊,而後在 main.js 中引入 weui的基礎樣式庫,方便咱們能夠在全局使用微信基礎樣式構建項目頁面。node

// main.js
import Vue from 'vue'
import App from './App.vue'
// 全局引入 `weui` 的基礎樣式庫
import 'weui'
import VueCompositionApi from '@vue/composition-api'

Vue.config.productionTip = false
Vue.use(VueCompositionApi)

new Vue({
  render: h => h(App),
}).$mount('#app')

回到 App.vue,保留 components 屬性值清空 <template> 模板的內容,刪除 <style> 模板,等待從新引入新的組件。react

<template>
  <div id="app">
    Hello World
  </div>
</template>
<script>
export default {
  name: "app",
  components: {}
};
</script>

src/components 目錄下新建第一個組件,取名爲 Header.vue 寫入如下代碼,點擊查看源代碼ios

<template>
  <header :style="{
    backgroundColor: color?color:defaultColor
  }">{{title}}</header>
</template>
<script>
import { reactive } from "@vue/composition-api";
export default {
  // 父組件傳遞進來更改該頭部組件的屬性值
  props: {
    // 標題
    title: String,
    // 顏色
    color: String
  },
  setup() {
    const state = reactive({
      defaultColor: "red"
    });
    return {
      ...state
    };
  }
};
</script>
<style scoped>
header {
  height: 50px;
  width: 100%;
  line-height: 50px;
  text-align: center;
  color: white;
}
</style>

setup

這裏運用了一個全新的屬性 setup ,這是一個組件的入口,讓咱們能夠運用 Vue3.0 暴露的新接口,它運行在組件被實例化時候,props 屬性被定義以後,實際上等價於 Vue2.0 版本的 beforeCreateCreated 這兩個生命週期,setup 返回的是一個對象,裏面的全部被返回的屬性值,都會被合併到 Vue2.0render 渲染函數裏面,在單文件組件中,它將配合 <template> 模板的內容,完成 ModelView 之間的綁定,在將來版本中應該還會支持返回 JSX 代碼片斷。git

<template>
  <!-- View -->
  <div>{{name}}</div>
</template>
<script>
import { reactive } from '@vue/composition-api'
export default {
  setup() {
    const state = reactive({ name: 'Eno Yao' });
    // return 暴露到 template 中
    return {
      // Model
      ...state
    }
  }
}
</script>

reactive

setup 函數裏面, 咱們適應了 Vue3.0 的第一個新接口 reactive 它主要是處理你的對象讓它通過 Proxy 的加工變爲一個響應式的對象,相似於 Vue2.0 版本的 data 屬性,須要注意的是加工後的對象跟原對象是不相等的,而且加工後的對象屬於深度克隆的對象。github

const state = reactive({ name: 'Eno Yao' })

props

Vue2.0 中咱們可使用 props 屬性值完成父子通訊,在這裏咱們須要定義 props 屬性去定義接受值的類型,而後咱們能夠利用 setup 的第一個參數獲取 props 使用。ajax

export default {
  props: {
    // 標題
    title: String,
    // 顏色
    color: String
  },
  setup(props) {
    // 這裏可使用父組件傳過來的 props 屬性值
  }
};

咱們在 App.vue 裏面就可使用該頭部組件,有了上面的 props 咱們能夠根據傳進來的值,讓這個頭部組件呈現不一樣的狀態。

<template>
  <div id="app">
    <!-- 複用組件,並傳入 props 值,讓組件呈現對應的狀態 -->
    <Header title="Eno" color="red" />
    <Header title="Yao" color="blue" />
    <Header title="Wscats" color="yellow" />
  </div>
</template>
<script>
import Header from "./components/Header.vue";
export default {
  name: "app",
  components: {
    Header,
  }
};
</script>

1.gif

context

setup 函數的第二個參數是一個上下文對象,這個上下文對象中包含了一些有用的屬性,這些屬性在 Vue2.0 中須要經過 this 才能訪問到,在 vue3.0 中,訪問他們變成如下形式:

setup(props, ctx) {
  console.log(ctx) // 在 setup() 函數中沒法訪問到 this
  console.log(this) // undefined
}

具體能訪問到如下有用的屬性:

  • root
  • parent
  • refs
  • attrs
  • listeners
  • isServer
  • ssrContext
  • emit

完成上面的 Header.vue 咱們就編寫 Search.vue 搜索框組件,繼續再 src/components 文件夾下面新建 Search.vue 文件,點擊查看源代碼

<template>
  <div :class="['weui-search-bar', {'weui-search-bar_focusing' : isFocus}]" id="searchBar">
    <form class="weui-search-bar__form">
      <div class="weui-search-bar__box">
        <i class="weui-icon-search"></i>
        <input
          v-model="searchValue"
          ref="inputElement"
          type="search"
          class="weui-search-bar__input"
          id="searchInput"
          placeholder="搜索"
          required
        />
        <a href="javascript:" class="weui-icon-clear" id="searchClear"></a>
      </div>
      <label @click="toggle" class="weui-search-bar__label" id="searchText">
        <i class="weui-icon-search"></i>
        <span>搜索</span>
      </label>
    </form>
    <a @click="toggle" href="javascript:" class="weui-search-bar__cancel-btn" id="searchCancel">取消</a>
  </div>
</template>
<script>
import { reactive, toRefs, watch } from "@vue/composition-api";
import store from "../stores";
export default {
  // setup至關於2.x版本的beforeCreate生命週期
  setup() {
    // reactive() 函數接收一個普通對象,返回一個響應式的數據對象
    const state = reactive({
      searchValue: "",
      // 搜索框兩個狀態,聚焦和非聚焦
      isFocus: false,
      inputElement: null
    });
    // 切換搜索框狀態的方法
    const toggle = () => {
      // 讓點擊搜索後出現的輸入框自動聚焦
      state.inputElement.focus();
      state.isFocus = !state.isFocus;
    };
    // 監聽搜索框的值
    watch(
      () => {
        return state.searchValue;
      },
      () => {
        // 存儲輸入框到狀態 store 中心,用於組件通訊
        store.setSearchValue(state.searchValue);
        // window.console.log(state.searchValue);
      }
    );
    return {
      // 將 state 上的每一個屬性,都轉化爲 ref 形式的響應式數據
      ...toRefs(state),
      toggle
    };
  }
};
</script>

toRefs

能夠看到咱們上面用了不少的新屬性,咱們先介紹 toRefs ,函數能夠將 reactive() 建立出來的響應式對象,轉換爲普通的對象,只不過,這個對象上的每一個屬性節點,都是 ref() 類型的響應式數據,配合 v-model 指令能完成數據的雙向綁定,在開發中很是高效。

import { reactive, toRefs } from "@vue/composition-api";
export default {
  setup() {
    const state = reactive({ name: 'Eno Yao' })
  }
  return {
    // 直接返回 state 那麼數據會是非響應式的, MV 單向綁定
    // ...state,
    // toRefs 包裝後返回 state 那麼數據會是響應式的, MVVM 雙向綁定
    ...toRefs(state),
  };
}

2.gif

template refs

這裏的輸入框擁有兩個狀態,一個是有輸入框的狀態和無輸入框的狀態,因此咱們須要一個布爾值 isFocus 來控制狀態,封裝了一個 toggle 方法,讓 isFocus 值切換真和假兩個狀態。

const toggle = () => {
  // isFocus 值取反
  state.isFocus = !state.isFocus;
};

而後配合 v-bind:class 指令,讓 weui-search-bar_focusing 類名根據 isFocus 值決定是否出現,從而更改搜索框的狀態。

<div :class="['weui-search-bar', {'weui-search-bar_focusing' : isFocus}]" id="searchBar">

這裏的搜索輸入框放入了 v-model 指令,用於接收用戶的輸入信息,方便後面配合列表組件執行檢索邏輯,還放入了 ref 屬性,用於獲取該 <input/> 標籤的元素節點,配合state.inputElement.focus() 原生方法,在切換搜索框狀態的時候光標自動聚焦到輸入框,加強用戶體驗。

<input
  v-model="searchValue"
  ref="inputElement"
/>

3.gif

watch

watch() 函數用來監視某些數據項的變化,從而觸發某些特定的操做,使用以前仍是須要按需導入,監聽 searchValue 的變化,而後觸發回調函數裏面的邏輯,也就是監聽用戶輸入的檢索值,而後觸發回調函數的邏輯把 searchValue 值存進咱們建立 store 對象裏面,方面後面和 Panel.vue 列表組件進行數據通訊:

import { reactive, watch } from "@vue/composition-api";
import store from "../stores";
export default {
  setup() {
    const state = reactive({
      searchValue: "",
    });
    // 監聽搜索框的值
    watch(
      () => {
        return state.searchValue;
      },
      () => {
        // 存儲輸入框到狀態 store 中心,用於組件通訊
        store.setSearchValue(state.searchValue);
      }
    );
    return {
      ...toRefs(state)
    };
  }
};

state management

在這裏咱們維護一份數據來實現共享狀態管理,也就是說咱們新建一個 store.js 暴露出一個 store 對象共享 PanelSearch 組件的 searchValue 值,當 Search.vue 組件從輸入框接受到 searchValue 檢索值,就放到 store.jsstore 對象中,而後把該對象注入到 Search 組件中,那麼兩個組件均可以共享 store 對象中的值,爲了方便調試咱們還分別封裝了 setSearchValuegetSearchValue 來去操做該 store 對象,這樣咱們就能夠跟蹤狀態的改變。

// store.js
export default {
    state: {
        searchValue: ""
    },
    // 設置搜索框的值
    setSearchValue(value) {
        this.state.searchValue = value
    },
    // 獲取搜索框的值
    getSearchValue() {
        return this.state.searchValue
    }
}

完成上面的 Search.vue 咱們緊接着編寫 Panel.vue 搜索框組件,繼續再 src/components 文件夾下面新建 Panel.vue 文件,點擊查看源代碼

<template>
  <div class="weui-panel weui-panel_access">
    <div v-for="(n,index) in newComputed" :key="index" class="weui-panel__bd">
      <a href="javascript:void(0);" class="weui-media-box weui-media-box_appmsg">
        <div class="weui-media-box__hd">
          <img class="weui-media-box__thumb" :src="n.author.avatar_url" alt />
        </div>
        <div class="weui-media-box__bd">
          <h4 class="weui-media-box__title" v-text="n.title"></h4>
          <p class="weui-media-box__desc" v-text="n.author.loginname"></p>
        </div>
      </a>
    </div>
    <div @click="loadMore" class="weui-panel__ft">
      <a href="javascript:void(0);" class="weui-cell weui-cell_access weui-cell_link">
        <div class="weui-cell__bd">查看更多</div>
        <span class="weui-cell__ft"></span>
      </a>
    </div>
  </div>
</template>
<script>
import { reactive, toRefs, onMounted, computed } from "@vue/composition-api";
import axios from "axios";
import store from "../stores";
export default {
  setup() {
    const state = reactive({
      // 頁數
      page: 1,
      // 列表數據
      news: [],
      // 經過搜索框的值去篩選劣列表數據
      newComputed: computed(() => {
        // 判斷是否輸入框是否輸入了篩選條件,若是沒有返回原始的 news 數組
        if (store.state.searchValue) {
          return state.news.filter(item => {
            if (item.title.indexOf(store.state.searchValue) >= 0) {
              return item;
            }
          });
        } else {
          return state.news;
        }
      }),
      searchValue: store.state
    });
    // 發送 ajax 請求獲取列表數據
    const loadMore = async () => {
      // 獲取列表數據
      let data = await axios.get("https://cnodejs.org/api/v1/topics", {
        params: {
          // 每一頁的主題數量
          limit: 10,
          // 頁數
          page: state.page
        }
      });
      // 疊加頁數
      state.page += 1;
      state.news = [...state.news, ...data.data.data];
    };
    onMounted(() => {
      // 首屏加載的時候觸發請求
      loadMore();
    });
    return {
      // 讓數據保持響應式
      ...toRefs(state),
      // 查看更多事件
      loadMore
    };
  }
};
</script>

lifecycle hooks

Vue3.0 的生命週期鉤子和以前不同,新版本都是以 onXxx() 函數註冊使用,一樣須要局部引入生命週期的對應模塊:

import { onMounted, onUpdated, onUnmounted } from "@vue/composition-api";
export default {
  setup() {
    const loadMore = () => {};
    onMounted(() => {
      loadMore();
    });
    onUpdated(() => {
      console.log('updated!')
    })
    onUnmounted(() => {
      console.log('unmounted!')
    })
    return {
      loadMore
    };
  }
};

如下是新舊版本生命週期的對比:

  • <s>beforeCreate</s> -> use setup()
  • <s>created</s> -> use setup()
  • beforeMount -> onBeforeMount
  • mounted -> onMounted
  • beforeUpdate -> onBeforeUpdate
  • updated -> onUpdated
  • beforeDestroy -> onBeforeUnmount
  • destroyed -> onUnmounted
  • errorCaptured -> onErrorCaptured

同時新版本還提供了兩個全新的生命週期幫助咱們去調試代碼:

  • onRenderTracked
  • onRenderTriggered

Panel 列表組件中,咱們註冊 onMounted 生命週期,並在裏面觸發請求方法 loadMore 以便從後端獲取數據到數據層,這裏咱們使用的是 axios 網絡請求庫,因此咱們須要安裝該模塊:

npm install axios --save

封裝了一個請求列表數據方法,接口指向的是 Cnode 官網提供的 API ,因爲 axios 返回的是 Promise ,因此配合 asyncawait 能夠完美的編寫異步邏輯,而後結合onMounted 生命週期觸發,並將方法綁定到視圖層的查看更多按鈕上,就能夠完成列表首次的加載和點擊查看更多的懶加載功能。

// 發送 ajax 請求獲取列表數據
const loadMore = async () => {
  // 獲取列表數據
  let data = await axios.get("https://cnodejs.org/api/v1/topics", {
    params: {
      // 每一頁的主題數量
      limit: 10,
      // 頁數
      page: state.page
    }
  });
  // 疊加頁數
  state.page += 1;
  // 合併列表數據
  state.news = [...state.news, ...data.data.data];
};
onMounted(() => {
  // 首屏加載的時候觸發請求
  loadMore();
});

4.gif

computed

接下來咱們就使用另一個屬性 computed 計算屬性,跟 Vue2.0 的使用方式很相近,一樣須要按需導入該模塊:

import { computed } from '@vue/composition-api';

計算屬性分兩種,只讀計算屬性和可讀可寫計算屬性:

// 只讀計算屬性
let newsComputed = computed(() => news.value + 1)
// 可讀可寫
let newsComputed = computed({
  // 取值函數
  get: () => news.value + 2,
  // 賦值函數
  set: val => {
    news.value = news.value - 3
  }
})

5.gif

這裏咱們使用可讀可寫計算屬性去處理列表數據,還記得咱們上一個組件 Search.vue 嗎,咱們能夠結合用戶在搜索框輸入的檢索值,配合 computed 計算屬性來篩選對咱們用戶有用列表數據,因此咱們首先從 store 的共享實例裏面拿到 Search.vue 搜索框共享的 searchValue ,而後利用原生字符串方法 indexOf 和 數組方法 filter 來過濾列表的數據,而後從新返回新的列表數據 newsComputed,並在視圖層上配合 v-for 指令去渲染新的列表數據,這樣作既能夠在沒搜索框檢索值的時候返回原列表數據 news ,而在有搜索框檢索值的時候返回新列表數據 newsComputed

import store from "../stores";
export default {
  setup() {
    const state = reactive({
      // 原列表數據
      news: [],
      // 經過搜索框的值去篩選後的新列表數據
      newsComputed: computed(() => {
        // 判斷是否輸入框是否輸入了篩選條件,若是沒有返回原始的 news 數組
        if (store.state.searchValue) {
          return state.news.filter(item => {
            if (item.title.indexOf(store.state.searchValue) >= 0) {
              return item;
            }
          });
        } else {
          return state.news;
        }
      }),
      searchValue: store.state
    });
  }
}

項目源碼

若是文章和筆記能帶您一絲幫助或者啓發,請不要吝嗇你的贊和 Star,你的確定是我前進的最大動力😁

相關文章
相關標籤/搜索