Vue開發仿京東商場app
vue3-jd-h5
專案介紹
vue3-jd-h5
是一個電商H5頁面前端專案,基於Vue 3.0.0 + Vant 3.0.0 實現,主要包括首頁、分類頁面、我的頁面、購物車等,部分效果如下圖。
本地線下程式碼vue2.6在分支demo中,使用mockjs資料進行開發,效果圖請點選這裡
master分支是線上生產環境程式碼,因為部分後臺介面已經掛了,可能無法看到實際效果。
vue3搭建步驟
- 首先,在本地選擇一個檔案,將程式碼clone到本地:
git clone https://github.com/GitHubGanKai/vue-jd-h5.git
- 檢視所有分支:
gankaideMacBook-Pro:vue-jd-h5 gankai$ git branch -a
demo
vue-next
dev
feature
gh-pages
* master
remotes/origin/HEAD -> origin/master
remotes/origin/demo
remotes/origin/vue-next
remotes/origin/dev
remotes/origin/feature
remotes/origin/gh-pages
remotes/origin/master
-
切換到分支vue-next
-
在 IDEA 命令列中執行命令:npm install,下載相關依賴;
-
開發環境 在 IDEA 命令列中執行命令:
npm run dev
,執行專案; -
在 IDEA 命令列中執行命令:
npm run dll:build
,打包專案,然後手機掃描下面二維碼體驗應用的效果。
專案的初始化
如果你在安裝包的時候速度比較慢,那是因為NPM伺服器在國外,這裡給大家推薦一個可以隨時切換NPM映象的工具NRM,有時候,我們開發的時候,為了加快安裝包的安裝速度,我們需要切換映象源為國內的,但是,如果需要釋出一些自己的元件到NPM的時候,又要來回切換回來,有了這個我們就方便多了!使用$ npm install -g nrm
nrm ls
檢視所有映象:
gankaideMacBook-Pro:~ gankai$ nrm ls
npm -------- https://registry.npmjs.org/
* yarn ------- https://registry.yarnpkg.com/
cnpm ------- http://r.cnpmjs.org/
taobao ----- https://registry.npm.taobao.org/
nj --------- https://registry.nodejitsu.com/
npmMirror -- https://skimdb.npmjs.com/registry/
edunpm ----- http://registry.enpmjs.org/
如果需要使用淘寶映象,執行: nrm use taobao
可以隨時切換源,當然了還有一個npm包版本管理工具nvm,主要是管理包版本的,如果有興趣的小夥伴,可以自己去了解一下。
安裝
進入剛才clone下來的專案根目錄,安裝相關依賴,體驗 vue3 新特性。
npm
安裝:
npm install
yarn
安裝:
yarn
CDN
<script src="https://unpkg.com/[email protected]"></script>
使用
在入口檔案main.js
中:
import Vue from 'vue';
import VueCompositionApi from '@vue/composition-api';
Vue.use(VueCompositionApi);
安裝外掛後,您就可以使用新的 Composition API 來開發元件了。
目前vue官方為vue-cli提供了一個外掛vue-cli-plugin-vue-next,你也可以直接在專案中直接新增最新的版本!
# in an existing Vue CLI project
vue add vue-next
如果有想從零開始體驗新版本的小夥伴可以採用這種方法進行安裝,由於我們這個專案有依賴第三方庫,如果全域性安裝,整個專案第三方UI庫都無法執行!所以我們還是選擇採用安裝@vue/composition-api
來進行體驗,從而慢慢過渡到vue3最新版本
Vue 3.0 Composition-API基本特性體驗
setup函式
setup()
函式是 vue3 中專門為元件提供的新屬性,相當於2.x版本中的created
函式,之前版本的元件邏輯選項,現在都統一放在這個函式中處理。它為我們使用 vue3 的 Composition API
新特性提供了統一的入口,setup 函式會在相對於2.x來說,會在 beforeCreate 之後、created 之前執行!具體可以參考如下:
vue2.x | vue3 |
---|---|
setup(替代) | |
setup(替代) | |
beforeMount | onBeforeMount |
mounted | onMounted |
beforeUpdate | onBeforeUpdate |
updated | onUpdated |
beforeDestroy | onBeforeUnmount |
destroyed | onUnmounted |
errorCaptured | onErrorCaptured |
新鉤子
除了2.x生命週期等效項之外,Composition API還提供了以下debug hooks:
onRenderTracked
onRenderTriggered
兩個鉤子都收到DebuggerEvent
類似於onTrack
和onTrigger
觀察者的選項:
export default {
onRenderTriggered(e) {
debugger
// inspect which dependency is causing the component to re-render
}
}
依賴注入
provide
和inject
啟用類似於2.x provide/inject
選項的依賴項注入。兩者都只能在setup()
當前活動例項期間呼叫。
import { provide, inject } from '@vue/composition-api'
const ThemeSymbol = Symbol()
const Ancestor = {
setup() {
provide(ThemeSymbol, 'dark')
}
}
const Descendent = {
setup() {
const theme = inject(ThemeSymbol, 'light' /* optional default value */)
return {
theme
}
}
}
inject
接受可選的預設值作為第二個引數。如果未提供預設值,並且在Provide上下文中找不到該屬性,則inject
返回undefined
。
注入響應式資料
為了保持提供的值和注入的值之間的響應式,可以使用ref
// 在父組建中
const themeRef = ref('dark')
provide(ThemeSymbol, themeRef)
// 元件中
const theme = inject(ThemeSymbol, ref('light'))
watchEffect(() => {
console.log(`theme set to: ${theme.value}`)
})
- 因為
setup
函式接收2個形參,第一個是initProps
,即父組建傳送過來的值!,第二個形參是一個上下文物件
setupContext
,這個物件的主要屬性有 :
attrs: Object // 等同 vue 2.x中的 this.$attrs
emit: ƒ () // 等同 this.$emit()
isServer: false // 是否是服務端渲染
listeners: Object // 等同 vue2.x中的this.$listeners
parent: VueComponent // 等同 vue2.x中的this.$parent
refs: Object // 等同 vue2.x中的this.$refs
root: Vue // 這個root是我們在main.js中,使用newVue()的時候,返回的全域性唯一的例項物件,注意別和單檔案組建中的this混淆了
slots: {} // 等同 vue2.x中的this.$slots
ssrContext:{} // 服務端渲染相關
注意:在 setup()
函式中無法訪問到 this
的,不管這個this
指的是全域性的的vue物件(即:在main.js 中使用new生成的那個全域性的vue例項物件),還是指單檔案組建的物件。
但是,如果我們想要訪問當前元件的例項物件,那該怎麼辦呢?我們可以引入getCurrentInstance
這個api,返回值就是當前組建的例項!
import { computed, getCurrentInstance } from "@vue/composition-api";
export default {
name: "svg-icon",
props: {
iconClass: {
type: String,
required: true
},
className: {
type: String
}
},
setup(initProps,setupContext) {
const { ctx } = getCurrentInstance();
const iconName = computed(() => {
return `#icon-${initProps.iconClass}`;
});
const svgClass = computed(() => {
if (initProps.className) {
return "svg-icon " + initProps.className;
} else {
return "svg-icon";
}
});
return {
iconName,
svgClass
};
}
};
</script>
Ref自動展開(unwrap)
ref()
函式用來根據給定的值建立一個響應式的資料物件,ref()
函式呼叫的返回值是一個被包裝後的物件(RefImpl),這個物件上只有一個 .value
屬性,如果我們在setup
函式中,想要訪問的物件的值,可以通過.value
來獲取,但是如果是在<template>
模版中,直接訪問即可,不需要.value
!
import { ref } from '@vue/composition-api'
setup() {
const active = ref("");
const timeData = ref(36000000);
console.log('輸出===>',timeData.value)
return {
active,
timeData
}
}
<template>
<p>活動狀態:{{active}}</p>
<p>活動時間:{{timeData}}</p>
</template>
⚠️注意:不要將Array
放入ref
中,陣列索引屬性無法進行自動展開,也不要使用 Array
直接存取 ref
物件:
const state = reactive({
list: [ref(0)],
});
// 不會自動展開, 須使用 `.value`
state.list[0].value === 0; // true
state.list.push(ref(1));
// 不會自動展開, 須使用 `.value`
state.list[1].value === 1; // true
當我們需要操作DOM的時候,比如我們在專案中使用swiper
需要獲取DOM,那麼我們還可以這樣!
<div class="swiper-cls">
<swiper :options="swiperOption" ref="mySwiper">
<swiper-slide v-for="(img ,index) in tabImgs.value" :key="index">
<img class="slide_img" @click="handleClick(img.linkUrl)" :src="img.imgUrl" />
</swiper-slide>
</swiper>
</div>
然後在setup
函式中定義一個const mySwiper = ref(null);
,之前在vue2.x中,我們是通過this.$refs.mySwiper
來獲取DOM物件的,現在也可以使用ref
函式代替,返回的mySwiper
要和template
中繫結的ref
相同!
import { ref, onMounted } from "@vue/composition-api";
setup(props, { attrs, slots, parent, root, emit, refs }) {
const mySwiper = ref(null);
onMounted(() => {
// 通過mySwiper.value 即可獲取到DOM物件!
// 同時也可以使用vue2.x中的refs.mySwiper ,他其實mySwiper.value 是同一個DOM物件!
mySwiper.value.swiper.slideTo(3, 1000, false);
});
return {
mySwiper
}
}
reactive
reactive()
函式接收一個普通物件,返回一個響應式的資料物件,等價於 vue 2.x
中的 Vue.observable()
函式,vue 3.x
中提供了 reactive()
函式,用來建立響應式的資料物件Observer
,ref
中我們一般存放的是基本型別資料,如果是引用型別的我們可以使用reactive
函式。
當reactive
函式中,接收的型別是一個Array
陣列的時候,我們可以在這個Array
外面在用物件包裹一層,然後給物件新增一個屬性比如:value
(這個屬性名你可以自己隨便叫什麼),他的值就是這個陣列!
<script>
// 使用相關aip之前必須先引入
import { ref, reactive } from "@vue/composition-api";
export default {
name: "home",
setup(props, { attrs, slots, parent, root, emit, refs }) {
const active = ref("");
const timeData = ref(36000000);
// 將tabImgs陣列中每個物件都變成響應式的物件
const tabImgs = reactive({
value: []
});
const ball = reactive({
show: false,
el: ""
});
return {
active,
timeData,
tabImgs,
...toRefs(ball),
};
}
};
</script>
那麼在template
模版中我們想要訪問這個陣列的時候就是需要使用.value
的形式來獲取這個陣列的值。
<template>
<div class="swiper-cls">
<swiper :options="swiperOption" ref="mySwiper">
<swiper-slide v-for="(img ,index) in tabImgs.value" :key="index">
<img class="slide_img" @click="handleClick(img.linkUrl)" :src="img.imgUrl" />
</swiper-slide>
</swiper>
</div>
</template>
isRef
isRef()
用來判斷某個值是否為 ref()
創建出來的物件;當需要展開某個可能為 ref()
創建出來的值的時候,可以使用isRef
來判斷!
import { isRef } from '@vue/composition-api'
setup(){
const headerActive = ref(false);
// 在setup函式中,如果是響應式的物件,在訪問屬性的時候,一定要加上.value來訪問!
const unwrapped = isRef(headerActive) ? headerActive.value : headerActive
return {}
}
toRefs
toRefs
函式會將響應式物件轉換為普通物件,其中返回的物件上的每個屬性都是指向原始物件中相應屬性的ref
,將一個物件上的所有屬性轉換成響應式的時候,將會非常有用!
import { reactive,toRefs } from '@vue/composition-api'
setup(){
// ball 是一個 Observer
const ball = reactive({
show: false,
el: ""
});
// ballToRefs 就是一個普通的Object,但是ballToRefs裡面的所有屬性都是響應式的(RefImpl)
const ballToRefs = toRefs(ball)
// ref和原始屬性是“連結的”
ball.show = true
console.log(ballToRefs.show) // true
ballToRefs.show.value = false
console.log(ballToRefs.show) // false
return {
...ballToRefs // 將ballToRefs物件展開,我們就可以直接在template模板中直接這樣使用這個物件上的所有屬性!
}
}
點選新增按鈕,小球飛入購物車動畫:
<template>
<div class="ballWrap">
<transition @before-enter="beforeEnter" @enter="enter" @afterEnter="afterEnter">
<!-- 可以直接使用show-->
<div class="ball" v-if="show">
<li class="inner">
<span class="cubeic-add" @click="addToCart($event,item)">
<svg-icon class="add-icon" icon-class="add"></svg-icon>
</span>
</li>
</div>
</transition>
</div>
</template>
computed
computed
函式的第一個引數,可以接收一個函式,或者是一個物件!如果是函式預設是getter
函式,併為getter
返回的值返回一個只讀的ref
物件。
import { computed } from '@vue/composition-api'
const count = ref(1)
// computed接收一個函式作為入參
const plusOne = computed(() => count.value + 1)
console.log(plusOne.value) // 2
plusOne.value++ // 錯誤,plusOne是隻讀的!
或者也可以是個物件,可以使用具有get
和set
功能的物件來建立可寫ref
物件。
const count = ref(1)
// computed接收一個物件作為入參
const plusOne = computed({
get: () => count.value + 1,
set: val => {
count.value = val - 1
}
})
plusOne.value = 1
console.log(count.value) // 0
watch
watch(source, cb, options?)
該watch
API與2.x this.$watch
(以及相應的watch
選項)完全等效。
觀察單一來源
觀察者資料來源可以是返回值的getter函式,也可以直接是ref:
// watching a getter函式
const state = reactive({ count: 0 })
watch(
() => state.count, // 返回值的getter函式
(count, prevCount,onCleanup) => {
/* ... */
}
)
// directly watching a ref
const count = ref(0)
watch(
count, // 也可以直接是ref
(count, prevCount,onCleanup) => {
/* ... */
})
watch多個來源
觀察者還可以使用陣列同時監視多個源:
const me = reactive({ age: 24, name: 'gk' })
// reactive型別的
watch(
[() => me.age, () => me.name], // 監聽reactive多個數據源,可以傳入一個數組型別,返回getter函式
([age, name], [oldAge, oldName]) => {
console.log(age) // 新的 age 值
console.log(name) // 新的 name 值
console.log(oldAge) // 舊的 age 值
console.log(oldName) // 新的 name 值
},
// options
{
lazy: true //預設 在 watch 被建立的時候執行回撥函式中的程式碼,如果lazy為true ,怎建立的時候,不執行!
}
)
setInterval(() => {
me.age++
me.name = 'oldMe'
}, 7000000)
// ref型別的
const work = ref('web')
const addres = ref('sz')
watch(
[work,address], // 監聽多個ref資料來源
([work, addres], [oldwork, oldaddres]) => {
//......
},
{
lazy: true
}
)
watch
和元件的生命週期繫結,當元件解除安裝後,watch也將自動停止。在其他情況下,它返回停止控制代碼,可以呼叫該控制代碼以顯式停止觀察程式:
// watch 返回一個函式控制代碼,我們可以決定該watch的停止和開始!
const stopWatch = watch(
[work,address], // 監聽多個ref資料來源
([work, addres], [oldwork, oldaddres]) => {
//......
},
{
lazy: true
}
)
// 呼叫停止函式,清除對work和address的監視
stopWatch()
在 watch 中清除無效的非同步任務
<div class="search-con">
<svg-icon class="search-icon" icon-class="search"></svg-icon>
<input v-focus placeholder="搜尋、關鍵詞" v-model="searchText" />
</div>
setup(props, { attrs, slots, parent, root, emit, refs }){
const CancelToken = root.$http.CancelToken
const source = CancelToken.source()
// 定義響應式資料 searchText
const searchText = ref('')
// 向後臺傳送非同步請求
const getSearchResult = searchText => {
root.$http.post("http://test.happymmall.com/search",{text:searchText}, {
cancelToken: source.token
}).then(res => {
// .....
});
return source.cancel
}
// 定義 watch 監聽
watch(
searchText,
(searchText, oldSearchText, onCleanup) => {
// 傳送axios請求,並得到取消axios請求的 cancel函式
const cancel = getSearchResult(searchText)
// 若 watch 監聽被重複執行了,則會先清除上次未完成的非同步請求
onCleanup(cancel)
},
// watch 剛被建立的時候不執行
{ lazy: true }
)
return {
searchText
}
}
最後
vue3新增 Composition API。新的 API 相容 Vue2.x,只需要在專案中單獨引入 @vue/composition-api 這個包就能夠解決我們目前 Vue2.x中存在的個別難題。比如:如何組織邏輯,以及如何在多個元件之間抽取和複用邏輯。基於 Vue 2.x 目前的 API 我們有一些常見的邏輯複用模式,但都或多或少存在一些問題:
這些模式包括:
- Mixins
- 高階元件 (Higher-order Components, aka HOCs)
- Renderless Components (基於 scoped slots / 作用域插槽封裝邏輯的元件)
總體來說,以上這些模式存在以下問題:
- 模版中的資料來源不清晰。舉例來說,當一個元件中使用了多個 mixin 的時候,光看模版會很難分清一個屬性到底是來自哪一個 mixin。HOC 也有類似的問題。
- 名稱空間衝突。由不同開發者開發的 mixin 無法保證不會正好用到一樣的屬性或是方法名。HOC 在注入的 props 中也存在類似問題。
- 效能。HOC 和 Renderless Components 都需要額外的元件例項巢狀來封裝邏輯,導致無謂的效能開銷。
vue3中,新增 Composition API
。而且新的API
相容 Vue2.x
,只需要在專案中,單獨引入 @vue/composition-api
這個包就可以,就能夠解決我們目前 以上大部分問題。同時,如果我直接升級到 Vue3.x
,我要做的事情會更多,只要當前專案中使用到的第三方ui庫,都需要重新改造,以及升級後的諸多坑要填!剛開始的時候,我就是直接在當前腳手架的基礎上 vue add vue-next
安裝升級,但是隻要是有依賴第三方生態庫的地方,就有許多的坑。。。