Quick Start
- 项目�?�?: https://github.com/Wscats/vue-cli
本项目综�?��?用了 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
});
}
}