首先這是一個(gè)出于了解 Vue3 語(yǔ)法及相關(guān)生態(tài)而搞的類似于 在簡(jiǎn)書(shū)仿簡(jiǎn)書(shū) 的項(xiàng)目爵卒。
具體而言這個(gè)項(xiàng)目是這 在簡(jiǎn)書(shū)仿簡(jiǎn)書(shū) 的基礎(chǔ)上搞的 Vue3 版本岂座。
Vue2 版本的代碼可以到 這里 查看大刊。
Vue3 版本的代碼可以到 這里 查看。這不給整個(gè)star魄揉。
在很久很久以前鞍盗,對(duì)于 Vue3 的認(rèn)識(shí):
新項(xiàng)目預(yù)覽點(diǎn)這 road.cemcoe.com
下面是無(wú)聊流水賬:
- 創(chuàng)建 Vue3 項(xiàng)目
Step 1. 打開(kāi) Vue 的官網(wǎng),看一看最新的腳手架的命令
Step2. 執(zhí)行命令
npm init vue@latest
不要傻了吧唧地?zé)o腦 vue create跷究,下面是執(zhí)行操作時(shí)終端的輸出:
$ npm init vue@latest
Vue.js - The Progressive JavaScript Framework
√ Project name: ... xbook
√ Add TypeScript? ... No / Yes
√ Add JSX Support? ... No / Yes
√ Add Vue Router for Single Page Application development? ... No / Yes
√ Add Pinia for state management? ... No / Yes
√ Add Vitest for Unit Testing? ... No / Yes
√ Add an End-to-End Testing Solution? ? No
√ Add ESLint for code quality? ... No / Yes
Scaffolding project in C:\Users\cemcoe\workplace\demo\xbook...
Done. Now run:
cd xbook
npm install
npm run dev
好耶姓迅,這里出現(xiàn)了一位新朋友,名為 Pinia,這貨是來(lái)狀態(tài)管理的丁存,有一說(shuō)一肩杈,用了它之后,我是一點(diǎn)也不想用 VueX 了解寝。
Step3. 跟著官網(wǎng)或者終端的提示把依賴裝一裝扩然,執(zhí)行一下啟動(dòng)命令,瞧一瞧頁(yè)面聋伦。
cd xbook
npm install
npm run dev
毫無(wú)意外地會(huì)看到這個(gè)樣子:
Step4. 管理一下項(xiàng)目
當(dāng)然夫偶,最好把項(xiàng)目用 git 給管理起來(lái),在配置好 git config 的前提下把項(xiàng)目給 init 一下
git init
- 瞧一眼初始化的目錄結(jié)構(gòu)
跟 Vue2 差別不是很大觉增,比較起眼的就是 vite 了兵拢,現(xiàn)如今是開(kāi)發(fā)時(shí) Vite, 打包時(shí) rollup
- 刪除(替換)一下不必要的文件
- public/favicon.ico 換成自己的
- src/assets 刪除里面的文件
- src/componets 清空里邊的文件
- 觀摩一下 APP.vue
簡(jiǎn)化一下 APP.vue
<script setup></script>
<template>
<div class="app">
<h2>app</h2>
</div>
</template>
<style scoped></style>
觀摩一下 APP.vue,把 script 標(biāo)簽放在了前面逾礁,添加了 setup 語(yǔ)法糖刻肄。
- 再次運(yùn)行看一下有沒(méi)有錯(cuò)誤
npm run dev
大概率會(huì)出現(xiàn)諸如文件不存在的錯(cuò)誤硝拧,按照提示改一改就好句葵。
- 初始化 css
這里用到了 normalize.css恼除,按照官網(wǎng)一把梭安裝導(dǎo)入完事。
- 整理項(xiàng)目目錄結(jié)構(gòu)
主要還是和 Vue2 的保持一致
- assets 靜態(tài)文件植捎,imgs css
- components 組件目錄
- hooks 封裝的 hooks
- router 路由相關(guān)
- service/modules 分模塊管理請(qǐng)求
- service/request 封裝的請(qǐng)求函數(shù)
- store/modules 分模塊管理狀態(tài)
- utils 工具函數(shù)
- views 視圖組件
- 選用一個(gè)得力的組件庫(kù)
這是一個(gè)移動(dòng)端的項(xiàng)目衙解,沒(méi)得選就是 Vant 了,打開(kāi)官網(wǎng)焰枢,自己寫(xiě)著玩當(dāng)然是選用最新版的啦蚓峦。
npm i vant
執(zhí)行之后你會(huì)發(fā)現(xiàn),額济锄,不對(duì)勁暑椰,這版本不是 4 呀。
去官網(wǎng)確定一下命令荐绝,你發(fā)現(xiàn)一汽,額,我搞得是對(duì)的呀低滩。
這個(gè)時(shí)候想裝上 vant@4 咋搞召夹。
明顯的是這玩意還沒(méi)把默認(rèn)版本個(gè)升到 4,但是文檔 4 對(duì)應(yīng)的安裝命令沒(méi)改就很難受恕沫,這里就需要自己去找一找了监憎。
- 安裝非正式版的 Vant4
既然官方文檔還沒(méi)更新,那就到 npm 上去看一下版本號(hào)婶溯,自己裝一下鲸阔。
npm i vant@4.0.0-rc.6
等安裝完成后再打開(kāi) package.json 瞧一下 vant 的版本偷霉,欸,不錯(cuò)褐筛,用上了 vant 的新 rc 版本类少。
"dependencies": {
"pinia": "^2.0.23",
"vant": "^4.0.0-rc.6",
"vue": "^3.2.41",
"vue-router": "^4.1.5"
},
切記不要裝 alpha 版本,除非渔扎,你真的想踩坑瞒滴,你可能會(huì)遇到組件名并沒(méi)有導(dǎo)出的狀況,你問(wèn)我怎么知道的赞警?
當(dāng)然是我試了 alpha 版本,然后組件都導(dǎo)不進(jìn)虏两。切記愧旦,新也要適度。
- 配置組件庫(kù)
回到 Vant 的文檔中定罢,按需導(dǎo)入配一下笤虫,沒(méi)什么東西,照著文檔配就完事了祖凫。
配置好之后最好自己測(cè)試一下琼蚯。
別忘了文檔中的第四步,函數(shù)組件的樣式記得手動(dòng)導(dǎo)入一下惠况。
// https://vant-ui.github.io/vant/v4/#/en-US/quickstart#4.-style-of-function-components
Some components of Vant are provided as function, including Toast, Dialog, Notify and ImagePreview. When using function components, unplugin-vue-components can not auto import the component style, so we need to import style manually.
- 生成主要的路由 views
到 src/views 目錄下創(chuàng)建如下文件遭庶,并填充基本結(jié)構(gòu)
- home/home.vue
- following/following.vue
- profile/profile.vue
- 為主要的路由 views 配置路由
打開(kāi) router/index.js,照葫蘆畫(huà)瓢稠屠,搞就完事了峦睡。
我更喜歡把 tabbar 相關(guān)的路由放在一個(gè)單獨(dú)的文件中,比如 router/tabbar-routes.js权埠。
這么做的目的在于榨了,對(duì)于 tabbar 數(shù)據(jù)進(jìn)行統(tǒng)一的管理,同時(shí) meta 中會(huì)存儲(chǔ)圖片等信息攘蔽。
// 其中一個(gè)路由對(duì)象
{
path: "/",
component: () => import("@/views/home/home.vue"),
meta: {
text: "首頁(yè)",
image: "tabbar/home.svg",
imageActive: "tabbar/home_active.svg",
},
},
- 配置 tabbar
到 components 下創(chuàng)建 tab-bar/tab-bar.vue
這是就體現(xiàn)了將 router/tabbar-routes.js 抽離并導(dǎo)出的好處了龙屉。
直接把路由信息導(dǎo)入
import { tabbarRoutes } from "@/router/tabbar-routes";
就很棒,不用再搞一份數(shù)據(jù)了满俗,到 meta 中去拿就好了转捕。
剩下的步驟就很簡(jiǎn)單了,就是使用組件庫(kù)漫雷,具體看 vant 的文檔就行了瓜富。
中場(chǎng)休息
現(xiàn)在的大致進(jìn)度應(yīng)是,應(yīng)用底部有一個(gè) tabbar降盹,點(diǎn)擊會(huì)切換對(duì)應(yīng)的圖片以及顏色且相關(guān)的路由也會(huì)一并切換与柑。
- 發(fā)送網(wǎng)絡(luò)請(qǐng)求
別的都不說(shuō)谤辜,先把數(shù)據(jù)拿到,可供選擇的方案
- fetch
- axios
先發(fā)一個(gè)請(qǐng)求試一試
<script setup>
fetch("https://api.cemcoe.com/v1/posts?page=1&per_page=10", {})
.then((res) => res.json())
.then((res) => {
const { status, data } = res;
if (status === 200) {
console.log(data.post);
}
});
</script>
再將結(jié)果保存到變量中价捧,以便渲染到頁(yè)面上去丑念。
簡(jiǎn)單定義一個(gè)數(shù)組,將拿到的數(shù)據(jù)給 push 上去结蟋。
<script setup>
let postList = [];
fetch("https://api.cemcoe.com/v1/posts?page=1&per_page=10", {})
.then((res) => res.json())
.then((res) => {
const { status, data } = res;
if (status === 200) {
console.log(data.post);
// postList = data.post;
postList.push(...data.post);
}
});
</script>
嘗試將 postList 渲染到頁(yè)面上脯倚,模板部分和 Vue2 沒(méi)差,這里就不展示了嵌屎。
不出意外 postList 新數(shù)據(jù)是不會(huì)展示到頁(yè)面上的推正。
那簡(jiǎn)單呀,上 ref宝惰,于是又有了下面的代碼:
<script setup>
import { ref } from "vue";
let postList = ref([]);
fetch("https://api.cemcoe.com/v1/posts?page=1&per_page=10", {})
.then((res) => res.json())
.then((res) => {
const { status, data } = res;
if (status === 200) {
console.log(data.post);
// postList = data.post;
postList.push(...data.post);
}
});
</script>
小腦袋瓜轉(zhuǎn)的真快植榕,可還是不行,額尼夺,這里少了一個(gè) value尊残。
于是又又有了下面的代碼:
<script setup>
import { ref } from "vue";
let postList = ref([]);
fetch("https://api.cemcoe.com/v1/posts?page=1&per_page=10", {})
.then((res) => res.json())
.then((res) => {
const { status, data } = res;
if (status === 200) {
postList.value.push(...data.post);
}
});
</script>
這時(shí)的代碼大抵是可用來(lái),頁(yè)面上展示了列表了( ?? ω ?? )y
- ref 是個(gè)什么東東
啥也不說(shuō)淤堵,無(wú)腦打開(kāi)官方文檔瞧一瞧寝衫,直奔 API
// 從文檔上cv來(lái)的
function ref<T>(value: T): Ref<UnwrapRef<T>>;
interface Ref<T> {
value: T;
}
臨時(shí)抱一下 TypeScript 的腳。
function ref<T>(value: T): Ref<UnwrapRef<T>>;
哇拐邪,有點(diǎn)復(fù)雜的兄弟慰毅。簡(jiǎn)化一下先,比如將尖括號(hào)去掉庙睡,基本的類型注解還是瞧的懂的吧事富。
function ref(value): Ref;
TypeScript 的一大好處就是代碼即文檔,上面代碼的意思是:
有個(gè)名為 ref 的函數(shù)乘陪,你給它一個(gè) value统台,它給你一個(gè)返回值,這個(gè)返回值的類型是 Ref
那么 Ref 有是個(gè)什么鬼啡邑,不要著急贱勃,看下一段代碼咯
interface Ref<T> {
value: T;
}
interface 是定義 interface 的關(guān)鍵字(好像什么都沒(méi)說(shuō)),不重要谤逼,重要是可以用來(lái)干什么贵扰?
這里還有一個(gè) T 也是比較特殊的,這玩意和尖括號(hào)一起可以稱為泛型流部,名字很頂呀戚绕,簡(jiǎn)單來(lái)說(shuō)就是類型變量,可以在使用時(shí)聲明具體的類型枝冀。
下面寫(xiě)幾個(gè)符合條件的變量:
interface Ref<T> {
value: T;
}
const ref1: Ref<number> = {
value: 1,
};
const ref2: Ref<string> = {
value: "hello",
};
很清楚明白舞丛,interface 約束了一個(gè)對(duì)象耘子,而泛型 T 又約束了 value 的類型。
下面再來(lái)匯總一下代碼
// 從文檔上cv來(lái)的
function ref<T>(value: T): Ref<UnwrapRef<T>>;
interface Ref<T> {
value: T;
}
翻譯一下:
ref 是一個(gè)函數(shù)
函數(shù)的形參 value 在使用時(shí)指定類型T
函數(shù)的返回值為一個(gè)由 Ref interface 約束的對(duì)象
返回值對(duì)象有一個(gè) key 名為value
而 value 的值則是由另一個(gè) UnwrapRef 以及T決定球切。
那么這個(gè) UnwrapRef 又是啥谷誓?文檔上我沒(méi)找到,瞧一眼源碼:
// ref.ts
export type UnwrapRef<T> = T extends ShallowRef<infer V>
? V
: T extends Ref<infer V>
? UnwrapRefSimple<V>
: UnwrapRefSimple<T>;
這里又多了一些類型吨凑,有空再接著捋下去捍歪。
- 抽一下請(qǐng)求函數(shù)
<script setup>
import { ref } from "vue";
let postList = ref([]);
fetch("https://api.cemcoe.com/v1/posts?page=1&per_page=10", {})
.then((res) => res.json())
.then((res) => {
const { status, data } = res;
if (status === 200) {
postList.value.push(...data.post);
}
});
</script>
上面的代碼肯定是可以實(shí)現(xiàn)功能的,但肯定是不能這么寫(xiě)的鸵钝。至少也要把請(qǐng)求地址給搞出去的糙臼。
先簡(jiǎn)單搞搞,搞成一個(gè)請(qǐng)求函數(shù):
<script setup>
import { ref } from "vue";
let postList = ref([]);
const http = (url, options = {}) => {
const BASE_URL = "https://api.cemcoe.com/v1";
// 1. 拼湊完整的請(qǐng)求地址
const resource = BASE_URL + url;
// 2. 整合options
options = {
method: "GET", // 默認(rèn)是GET請(qǐng)求
headers: {},
mode: "cors",
credentials: "omit",
cache: "default",
...options,
};
fetch(resource, options)
.then((res) => res.json())
.then((res) => {
const { status, data } = res;
if (status === 200) {
postList.value.push(...data.post);
}
});
};
http("/posts?page=1&per_page=10");
</script>
上面的代碼泥恩商,還有一個(gè)問(wèn)題弓摘,那就是最終的請(qǐng)求結(jié)果需要到調(diào)用方,按照單一職責(zé)的原則痕届,數(shù)據(jù)請(qǐng)求函數(shù)中也不應(yīng)該對(duì)外部作用域的變量進(jìn)行修改,ok末患,接著改研叫。
<script setup>
import { ref } from "vue";
let postList = ref([]);
const http = (url, options = {}) => {
const BASE_URL = "https://api.cemcoe.com/v1";
// 1. 拼湊完整的請(qǐng)求地址
const resource = BASE_URL + url;
// 2. 整合options
options = {
method: "GET", // 默認(rèn)是GET請(qǐng)求
headers: {},
mode: "cors",
credentials: "omit",
cache: "default",
...options,
};
return new Promise((resolve, reject) => {
fetch(resource, options)
.then((res) => {
return res.json();
})
.then((res) => {
resolve(res);
});
});
};
http("/posts?page=1&per_page=10").then((res) => {
const { status, data } = res;
if (status === 200) {
postList.value.push(...data.post);
}
});
</script>
這么改吧改吧已經(jīng)有了一些可用的樣子了,下面要做的就是請(qǐng)求攔截和響應(yīng)攔截以及一些錯(cuò)誤處理璧针,這里就不展開(kāi)了嚷炉,畢竟,每個(gè)公司的接口規(guī)范也不盡相同探橱。
- 各回各家
上面的代碼呢最好是分到不同的文件里申屹。怎么起名看著來(lái)。
圖中白色的抽離下面說(shuō)隧膏,先將其忽略哗讥。
看看一下現(xiàn)在的數(shù)據(jù)流向
用戶訪問(wèn) Home 頁(yè)面,Home 頁(yè)面執(zhí)行請(qǐng)求函數(shù)胞枕,而請(qǐng)求函數(shù)定義在 service/modules/home.js杆煞,而該文件會(huì)引用封裝的請(qǐng)求函數(shù) http(名字無(wú)所謂,愛(ài)叫啥叫啥)腐泻,而該請(qǐng)求函數(shù)則是對(duì) fetch 的封裝决乎,當(dāng)然了,不用 fetch派桩,用 axios 也可以构诚。
用張圖來(lái)表示一下:
這個(gè)搞的好處是什么呢?
想一下其實(shí)網(wǎng)絡(luò)請(qǐng)求是和 Vue 這個(gè)框架無(wú)關(guān)的铆惑,按照上面的方式范嘱,如果要將原先的項(xiàng)目升級(jí)到 Vue3 的話送膳,或者換成 React,其實(shí)只有第一部分需要改彤侍。而后面的兩部分是不用動(dòng)的肠缨。
而如果把第一部分和第二部分代碼放到 views 文件中,那改起來(lái)可就麻煩了盏阶。
- 太大了晒奕,來(lái)點(diǎn)組件化
隨著代碼不斷堆下去,Home.vue 文件會(huì)越來(lái)越大名斟。
不可避免地要使用一下組件化脑慧。
組件化有哪些知識(shí)泥?
實(shí)踐是檢驗(yàn)真理的唯一標(biāo)準(zhǔn)砰盐。
定義 PostList 組件闷袒,接收拿到的數(shù)據(jù),渲染列表岩梳。
這里就涉及到了組件間的數(shù)據(jù)傳遞囊骤。
當(dāng)然可以使用 props 來(lái)進(jìn)行,比如:
// Home.vue
<PostList :postList="postList">
// PostList.vue
const props = defineProps({
postList: {
type: Array,
// 對(duì)象或者數(shù)組應(yīng)當(dāng)用工廠函數(shù)返回冀值。
// 工廠函數(shù)會(huì)收到組件所接收的原始 props
// 作為參數(shù)
default(rawProps) {
return [];
},
},
});
組件化以后的 Home.vue 文件也物,其實(shí)還是有點(diǎn)邏輯多的。
// Home.vue
// 網(wǎng)絡(luò)請(qǐng)求拿值的邏輯
// 網(wǎng)絡(luò)請(qǐng)求拿值的邏輯
// 網(wǎng)絡(luò)請(qǐng)求拿值的邏輯
// 網(wǎng)絡(luò)請(qǐng)求拿值的邏輯
// 網(wǎng)絡(luò)請(qǐng)求拿值的邏輯
// 網(wǎng)絡(luò)請(qǐng)求拿值的邏輯
<PostList :postList="postList">
<PostList :postList="postList">
<PostList :postList="postList">
<PostList :postList="postList">
<PostList :postList="postList">
<PostList :postList="postList">
<PostList :postList="postList">
我不是閑得慌列疗,把東西復(fù)制幾下滑蚯。
這里假設(shè)每一個(gè) PostList 組件是不同的,網(wǎng)絡(luò)請(qǐng)求邏輯也是不同的抵栈,而且 props 可能不止一個(gè)告材,可能還有事件的傳遞。
網(wǎng)絡(luò)請(qǐng)求還不是簡(jiǎn)單的操作古劲,一般都相對(duì)復(fù)雜斥赋,這么多的邏輯放在 Home.vue 文件中也不是很好。
- 結(jié)構(gòu)行為和樣式分離产艾?
前端有個(gè)東西灿渴,叫做結(jié)構(gòu)行為樣式相分離。
Vue 表面上還是這種分離的寫(xiě)法胰舆,三大塊分著寫(xiě)骚露。
React 干脆就把這仨貨攪和在一起。
這些框架把 DOM 操作給隱藏缚窿,將命令式編程變成了聲明式編程棘幸。
聲明式編程核心是什么?
當(dāng)然就以聲明為核心咯倦零,而聲明误续,它其實(shí)可以有另外一個(gè)名字吨悍,叫做狀態(tài)。
于是這種分離的思想大抵還是在的吧蹋嵌,自由過(guò)主角不再是 HTML CSS JS育瓜。
。栽烂。躏仇。
俺也不知。
- 上狀態(tài)
既然 Home.vue 文件中有太多的狀態(tài)腺办,那不妨將狀態(tài)都交給一個(gè)專門(mén)管理狀態(tài)的伙計(jì)吧焰手。
這個(gè)伙計(jì)現(xiàn)在是 Pinia,一個(gè)新歡怀喉。
Pinia 在使用上要比 Vuex 簡(jiǎn)單多了书妻,Vuex 的分模塊太難用了。
而在 Pinia 上躬拢,可以創(chuàng)建多個(gè) store躲履,但單例 store 好還是這種好泥?
我目前還沒(méi)有太多的體會(huì)聊闯。
ok崇呵,下面將 Home.vue 中的狀態(tài)給搞到 store 了。
不多說(shuō)馅袁,打開(kāi) Pinia 的官網(wǎng),瞧一眼荒辕。
// 從官網(wǎng)cv來(lái)的汗销。
export const useCounterStore = defineStore("counter", () => {
const count = ref(0);
function increment() {
count.value++;
}
return { count, increment };
});
你說(shuō)這有啥學(xué)的?有點(diǎn) React 那味道了抵窒。沒(méi)有引入多余的概念弛针,什么 state,什么 mutation李皇,什么 getter削茁,什么不能直接改值?
Vue 好用是好用掉房,但引如了太多的概念茧跋,而這里最突出的就是指令,你不知道那個(gè)指令卓囚,那不好意思瘾杭,你就不知道如何實(shí)現(xiàn)某類功能。
而 Pinia 可太爽了哪亿,沒(méi)有引入多余的概念粥烁,全都是以往的概念贤笆。
當(dāng)然了,如果你(比如我)還是想用類似 Vuex 的語(yǔ)法讨阻,Pinia 也是支持的芥永。
// store home.js
import { defineStore } from "pinia";
import { getHomePostList } from "@/service/modules/home";
export const useHomeStore = defineStore("homeStore", {
state: () => {
return {
recommendPostList: [],
page: 1,
per_page: 10,
};
},
actions: {
async fetchHomePostList() {
const res = await getHomePostList(this.page, this.per_page);
this.recommendPostList.push(...res.data.post);
this.page++;
},
},
});
這里一些人可能會(huì)有種看法埋涧,那就是狀態(tài)管理,只管理全局狀態(tài)飞袋。頁(yè)面狀態(tài)就交給頁(yè)面好了。
這也是一種做法巧鸭,但其實(shí)現(xiàn)在的 Pinia 支持多個(gè) store,分模塊相對(duì)也比較簡(jiǎn)單纲仍,頁(yè)面中的狀態(tài)交給它也是沒(méi)什么問(wèn)題贸毕。
但頁(yè)面級(jí)別的狀態(tài)交給 Pinia,相較于交給頁(yè)面管理這里其實(shí)是需要多做一件事情的明棍。
那就是。摊腋。。
當(dāng)把狀態(tài)放到頁(yè)面里兴蒸,傳數(shù)據(jù)是有點(diǎn)麻煩视粮,頁(yè)面銷(xiāo)毀,狀態(tài)就沒(méi)了橙凳。這其實(shí)也是有好處的蕾殴。
現(xiàn)在想象一下文章詳情頁(yè)的數(shù)據(jù)交給 Pinia,會(huì)發(fā)生什么岛啸?
用戶點(diǎn)擊文章 A钓觉,看到文章 A,但當(dāng)用戶回到文章列表頁(yè)在點(diǎn)擊文章 B坚踩,此時(shí)頁(yè)面為先展示文章 A 的內(nèi)容议谷,等到文章 B 的數(shù)據(jù)拿到后才會(huì)展示文章 B 的內(nèi)容。
所以,頁(yè)面級(jí)別的狀態(tài)卧晓,交給 Pinia 管理時(shí)芬首,別忘了初始化。
這里其實(shí)就像將狀態(tài)的作用域提了一層逼裆。
究竟采用哪種方式來(lái)管理郁稍,看取舍把。存到頁(yè)面使用以及管理上不是很方便胜宇,但不用擔(dān)心初始化耀怜,當(dāng)然了,全局狀態(tài)交給 Pinia 就不用選擇困難了桐愉。
- 更新下數(shù)據(jù)請(qǐng)求的步驟
未完财破,不續(xù)