Quick Start
本項目綜合運用了 Vue3.0
的新特性,適合新手學習😁
- 基於
Composition API
即Function-based API
進行改造,配合Vue Cli
,優先體驗Vue3
特性 - 使用單例對象模式進行組件通信
- 使用
axios
庫進行網絡請求,weui
庫實現 UI 界面
# 安裝依賴
npm install
# 在瀏覽器打開localhost:8080查看頁面,並實時熱更新
npm run serve
# 發布項目
npm run build
建議配合 Visual Studio Code 和 Vue 3 Snippets 代碼插件食用Ψ( ̄∀ ̄)Ψ。
Dependencies
以下是項目運用到的依賴,@vue/composition-api
配合 vue
模塊讓我們 Vue2.0
版本可以搶先體驗 Vue3.0
的新特性,axios
是輔助我們發送網絡請求得到數據的工具庫,weui
是一套與微信原生視覺一致的基礎樣式庫,方便我們快速搭建項目頁面。
"@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
的能力。
// 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
的基礎樣式庫,方便我們可以在全局使用微信基礎樣式構建項目頁面。
// 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>
模板,等待重新引入新的組件。
<template>
<div id="app">
Hello World
</div>
</template>
<script>
export default {
name: "app",
components: {}
};
</script>
在 src/components
目錄下新建第一個組件,取名為 Header.vue
寫入以下代碼,點擊查看源代碼:
<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
版本的 beforeCreate
和 Created
這兩個生命周期,setup
返回的是一個對象,里面的所有被返回的屬性值,都會被合並到 Vue2.0
的 render
渲染函數里面,在單文件組件中,它將配合 <template>
模板的內容,完成 Model
到 View
之間的綁定,在未來版本中應該還會支持返回 JSX
代碼片段。
<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
屬性,需要注意的是加工后的對象跟原對象是不相等的,並且加工后的對象屬於深度克隆的對象。
const state = reactive({ name: 'Eno Yao' })
props
在 Vue2.0
中我們可以使用 props
屬性值完成父子通信,在這里我們需要定義 props
屬性去定義接受值的類型,然后我們可以利用 setup
的第一個參數獲取 props
使用。
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>

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

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"
/>

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
對象共享 Panel
和 Search
組件的 searchValue
值,當 Search.vue
組件從輸入框接受到 searchValue
檢索值,就放到 store.js
的 store
對象中,然后把該對象注入到 Search
組件中,那么兩個組件都可以共享 store
對象中的值,為了方便調試我們還分別封裝了 setSearchValue
和 getSearchValue
來去操作該 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
};
}
};
以下是新舊版本生命周期的對比:
-> usebeforeCreate
setup()
-> usecreated
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
,所以配合 async
和 await
可以完美的編寫異步邏輯,然后結合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();
});

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

這里我們使用可讀可寫計算屬性去處理列表數據,還記得我們上一個組件 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,你的肯定是我前進的最大動力😁