首先这是一个出于了解 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 就不用选择困难了。
- 更新下数据请求的步骤
未完,不续