首先這是一個出于了解 Vue3 語法及相關生態而搞的類似于 在簡書仿簡書 的項目。
具體而言這個項目是這 在簡書仿簡書 的基礎上搞的 Vue3 版本。
Vue2 版本的代碼可以到 這里 查看。
Vue3 版本的代碼可以到 這里 查看。這不給整個star。
在很久很久以前,對于 Vue3 的認識:
新項目預覽點這 road.cemcoe.com
下面是無聊流水賬:
- 創建 Vue3 項目
Step 1. 打開 Vue 的官網,看一看最新的腳手架的命令
Step2. 執行命令
npm init vue@latest
不要傻了吧唧地無腦 vue create,下面是執行操作時終端的輸出:
$ 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
好耶,這里出現了一位新朋友,名為 Pinia,這貨是來狀態管理的,有一說一,用了它之后,我是一點也不想用 VueX 了。
Step3. 跟著官網或者終端的提示把依賴裝一裝,執行一下啟動命令,瞧一瞧頁面。
cd xbook
npm install
npm run dev
毫無意外地會看到這個樣子:
Step4. 管理一下項目
當然,最好把項目用 git 給管理起來,在配置好 git config 的前提下把項目給 init 一下
git init
- 瞧一眼初始化的目錄結構
跟 Vue2 差別不是很大,比較起眼的就是 vite 了,現如今是開發時 Vite, 打包時 rollup
- 刪除(替換)一下不必要的文件
- public/favicon.ico 換成自己的
- src/assets 刪除里面的文件
- src/componets 清空里邊的文件
- 觀摩一下 APP.vue
簡化一下 APP.vue
<script setup></script>
<template>
<div class="app">
<h2>app</h2>
</div>
</template>
<style scoped></style>
觀摩一下 APP.vue,把 script 標簽放在了前面,添加了 setup 語法糖。
- 再次運行看一下有沒有錯誤
npm run dev
大概率會出現諸如文件不存在的錯誤,按照提示改一改就好。
- 初始化 css
這里用到了 normalize.css,按照官網一把梭安裝導入完事。
- 整理項目目錄結構
主要還是和 Vue2 的保持一致
- assets 靜態文件,imgs css
- components 組件目錄
- hooks 封裝的 hooks
- router 路由相關
- service/modules 分模塊管理請求
- service/request 封裝的請求函數
- store/modules 分模塊管理狀態
- utils 工具函數
- views 視圖組件
- 選用一個得力的組件庫
這是一個移動端的項目,沒得選就是 Vant 了,打開官網,自己寫著玩當然是選用最新版的啦。
npm i vant
執行之后你會發現,額,不對勁,這版本不是 4 呀。
去官網確定一下命令,你發現,額,我搞得是對的呀。
這個時候想裝上 vant@4 咋搞。
明顯的是這玩意還沒把默認版本個升到 4,但是文檔 4 對應的安裝命令沒改就很難受,這里就需要自己去找一找了。
- 安裝非正式版的 Vant4
既然官方文檔還沒更新,那就到 npm 上去看一下版本號,自己裝一下。
npm i vant@4.0.0-rc.6
等安裝完成后再打開 package.json 瞧一下 vant 的版本,欸,不錯,用上了 vant 的新 rc 版本。
"dependencies": {
"pinia": "^2.0.23",
"vant": "^4.0.0-rc.6",
"vue": "^3.2.41",
"vue-router": "^4.1.5"
},
切記不要裝 alpha 版本,除非,你真的想踩坑,你可能會遇到組件名并沒有導出的狀況,你問我怎么知道的?
當然是我試了 alpha 版本,然后組件都導不進。切記,新也要適度。
- 配置組件庫
回到 Vant 的文檔中,按需導入配一下,沒什么東西,照著文檔配就完事了。
配置好之后最好自己測試一下。
別忘了文檔中的第四步,函數組件的樣式記得手動導入一下。
// 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 目錄下創建如下文件,并填充基本結構
- home/home.vue
- following/following.vue
- profile/profile.vue
- 為主要的路由 views 配置路由
打開 router/index.js,照葫蘆畫瓢,搞就完事了。
我更喜歡把 tabbar 相關的路由放在一個單獨的文件中,比如 router/tabbar-routes.js。
這么做的目的在于,對于 tabbar 數據進行統一的管理,同時 meta 中會存儲圖片等信息。
// 其中一個路由對象
{
path: "/",
component: () => import("@/views/home/home.vue"),
meta: {
text: "首頁",
image: "tabbar/home.svg",
imageActive: "tabbar/home_active.svg",
},
},
- 配置 tabbar
到 components 下創建 tab-bar/tab-bar.vue
這是就體現了將 router/tabbar-routes.js 抽離并導出的好處了。
直接把路由信息導入
import { tabbarRoutes } from "@/router/tabbar-routes";
就很棒,不用再搞一份數據了,到 meta 中去拿就好了。
剩下的步驟就很簡單了,就是使用組件庫,具體看 vant 的文檔就行了。
中場休息
現在的大致進度應是,應用底部有一個 tabbar,點擊會切換對應的圖片以及顏色且相關的路由也會一并切換。
- 發送網絡請求
別的都不說,先把數據拿到,可供選擇的方案
- fetch
- axios
先發一個請求試一試
<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>
再將結果保存到變量中,以便渲染到頁面上去。
簡單定義一個數組,將拿到的數據給 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 渲染到頁面上,模板部分和 Vue2 沒差,這里就不展示了。
不出意外 postList 新數據是不會展示到頁面上的。
那簡單呀,上 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>
小腦袋瓜轉的真快,可還是不行,額,這里少了一個 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>
這時的代碼大抵是可用來,頁面上展示了列表了( ?? ω ?? )y
- ref 是個什么東東
啥也不說,無腦打開官方文檔瞧一瞧,直奔 API
// 從文檔上cv來的
function ref<T>(value: T): Ref<UnwrapRef<T>>;
interface Ref<T> {
value: T;
}
臨時抱一下 TypeScript 的腳。
function ref<T>(value: T): Ref<UnwrapRef<T>>;
哇,有點復雜的兄弟。簡化一下先,比如將尖括號去掉,基本的類型注解還是瞧的懂的吧。
function ref(value): Ref;
TypeScript 的一大好處就是代碼即文檔,上面代碼的意思是:
有個名為 ref 的函數,你給它一個 value,它給你一個返回值,這個返回值的類型是 Ref
那么 Ref 有是個什么鬼,不要著急,看下一段代碼咯
interface Ref<T> {
value: T;
}
interface 是定義 interface 的關鍵字(好像什么都沒說),不重要,重要是可以用來干什么?
這里還有一個 T 也是比較特殊的,這玩意和尖括號一起可以稱為泛型,名字很頂呀,簡單來說就是類型變量,可以在使用時聲明具體的類型。
下面寫幾個符合條件的變量:
interface Ref<T> {
value: T;
}
const ref1: Ref<number> = {
value: 1,
};
const ref2: Ref<string> = {
value: "hello",
};
很清楚明白,interface 約束了一個對象,而泛型 T 又約束了 value 的類型。
下面再來匯總一下代碼
// 從文檔上cv來的
function ref<T>(value: T): Ref<UnwrapRef<T>>;
interface Ref<T> {
value: T;
}
翻譯一下:
ref 是一個函數
函數的形參 value 在使用時指定類型T
函數的返回值為一個由 Ref interface 約束的對象
返回值對象有一個 key 名為value
而 value 的值則是由另一個 UnwrapRef 以及T決定。
那么這個 UnwrapRef 又是啥?文檔上我沒找到,瞧一眼源碼:
// ref.ts
export type UnwrapRef<T> = T extends ShallowRef<infer V>
? V
: T extends Ref<infer V>
? UnwrapRefSimple<V>
: UnwrapRefSimple<T>;
這里又多了一些類型,有空再接著捋下去。
- 抽一下請求函數
<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>
上面的代碼肯定是可以實現功能的,但肯定是不能這么寫的。至少也要把請求地址給搞出去的。
先簡單搞搞,搞成一個請求函數:
<script setup>
import { ref } from "vue";
let postList = ref([]);
const http = (url, options = {}) => {
const BASE_URL = "https://api.cemcoe.com/v1";
// 1. 拼湊完整的請求地址
const resource = BASE_URL + url;
// 2. 整合options
options = {
method: "GET", // 默認是GET請求
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>
上面的代碼泥,還有一個問題,那就是最終的請求結果需要到調用方,按照單一職責的原則,數據請求函數中也不應該對外部作用域的變量進行修改,ok,接著改。
<script setup>
import { ref } from "vue";
let postList = ref([]);
const http = (url, options = {}) => {
const BASE_URL = "https://api.cemcoe.com/v1";
// 1. 拼湊完整的請求地址
const resource = BASE_URL + url;
// 2. 整合options
options = {
method: "GET", // 默認是GET請求
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>
這么改吧改吧已經有了一些可用的樣子了,下面要做的就是請求攔截和響應攔截以及一些錯誤處理,這里就不展開了,畢竟,每個公司的接口規范也不盡相同。
- 各回各家
上面的代碼呢最好是分到不同的文件里。怎么起名看著來。
圖中白色的抽離下面說,先將其忽略。
看看一下現在的數據流向
用戶訪問 Home 頁面,Home 頁面執行請求函數,而請求函數定義在 service/modules/home.js,而該文件會引用封裝的請求函數 http(名字無所謂,愛叫啥叫啥),而該請求函數則是對 fetch 的封裝,當然了,不用 fetch,用 axios 也可以。
用張圖來表示一下:
這個搞的好處是什么呢?
想一下其實網絡請求是和 Vue 這個框架無關的,按照上面的方式,如果要將原先的項目升級到 Vue3 的話,或者換成 React,其實只有第一部分需要改。而后面的兩部分是不用動的。
而如果把第一部分和第二部分代碼放到 views 文件中,那改起來可就麻煩了。
- 太大了,來點組件化
隨著代碼不斷堆下去,Home.vue 文件會越來越大。
不可避免地要使用一下組件化。
組件化有哪些知識泥?
實踐是檢驗真理的唯一標準。
定義 PostList 組件,接收拿到的數據,渲染列表。
這里就涉及到了組件間的數據傳遞。
當然可以使用 props 來進行,比如:
// Home.vue
<PostList :postList="postList">
// PostList.vue
const props = defineProps({
postList: {
type: Array,
// 對象或者數組應當用工廠函數返回。
// 工廠函數會收到組件所接收的原始 props
// 作為參數
default(rawProps) {
return [];
},
},
});
組件化以后的 Home.vue 文件,其實還是有點邏輯多的。
// Home.vue
// 網絡請求拿值的邏輯
// 網絡請求拿值的邏輯
// 網絡請求拿值的邏輯
// 網絡請求拿值的邏輯
// 網絡請求拿值的邏輯
// 網絡請求拿值的邏輯
<PostList :postList="postList">
<PostList :postList="postList">
<PostList :postList="postList">
<PostList :postList="postList">
<PostList :postList="postList">
<PostList :postList="postList">
<PostList :postList="postList">
我不是閑得慌,把東西復制幾下。
這里假設每一個 PostList 組件是不同的,網絡請求邏輯也是不同的,而且 props 可能不止一個,可能還有事件的傳遞。
網絡請求還不是簡單的操作,一般都相對復雜,這么多的邏輯放在 Home.vue 文件中也不是很好。
- 結構行為和樣式分離?
前端有個東西,叫做結構行為樣式相分離。
Vue 表面上還是這種分離的寫法,三大塊分著寫。
React 干脆就把這仨貨攪和在一起。
這些框架把 DOM 操作給隱藏,將命令式編程變成了聲明式編程。
聲明式編程核心是什么?
當然就以聲明為核心咯,而聲明,它其實可以有另外一個名字,叫做狀態。
于是這種分離的思想大抵還是在的吧,自由過主角不再是 HTML CSS JS。
。。。
俺也不知。
- 上狀態
既然 Home.vue 文件中有太多的狀態,那不妨將狀態都交給一個專門管理狀態的伙計吧。
這個伙計現在是 Pinia,一個新歡。
Pinia 在使用上要比 Vuex 簡單多了,Vuex 的分模塊太難用了。
而在 Pinia 上,可以創建多個 store,但單例 store 好還是這種好泥?
我目前還沒有太多的體會。
ok,下面將 Home.vue 中的狀態給搞到 store 了。
不多說,打開 Pinia 的官網,瞧一眼。
// 從官網cv來的。
export const useCounterStore = defineStore("counter", () => {
const count = ref(0);
function increment() {
count.value++;
}
return { count, increment };
});
你說這有啥學的?有點 React 那味道了。沒有引入多余的概念,什么 state,什么 mutation,什么 getter,什么不能直接改值?
Vue 好用是好用,但引如了太多的概念,而這里最突出的就是指令,你不知道那個指令,那不好意思,你就不知道如何實現某類功能。
而 Pinia 可太爽了,沒有引入多余的概念,全都是以往的概念。
當然了,如果你(比如我)還是想用類似 Vuex 的語法,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++;
},
},
});
這里一些人可能會有種看法,那就是狀態管理,只管理全局狀態。頁面狀態就交給頁面好了。
這也是一種做法,但其實現在的 Pinia 支持多個 store,分模塊相對也比較簡單,頁面中的狀態交給它也是沒什么問題。
但頁面級別的狀態交給 Pinia,相較于交給頁面管理這里其實是需要多做一件事情的。
那就是。。。
當把狀態放到頁面里,傳數據是有點麻煩,頁面銷毀,狀態就沒了。這其實也是有好處的。
現在想象一下文章詳情頁的數據交給 Pinia,會發生什么?
用戶點擊文章 A,看到文章 A,但當用戶回到文章列表頁在點擊文章 B,此時頁面為先展示文章 A 的內容,等到文章 B 的數據拿到后才會展示文章 B 的內容。
所以,頁面級別的狀態,交給 Pinia 管理時,別忘了初始化。
這里其實就像將狀態的作用域提了一層。
究竟采用哪種方式來管理,看取舍把。存到頁面使用以及管理上不是很方便,但不用擔心初始化,當然了,全局狀態交給 Pinia 就不用選擇困難了。
- 更新下數據請求的步驟
未完,不續