美文网首页
一个 Vue3 项目的流水账

一个 Vue3 项目的流水账

作者: cemcoe | 来源:发表于2022-11-10 17:43 被阅读0次

    首先这是一个出于了解 Vue3 语法及相关生态而搞的类似于 在简书仿简书 的项目。

    具体而言这个项目是这 在简书仿简书 的基础上搞的 Vue3 版本。

    Vue2 版本的代码可以到 这里 查看。

    Vue3 版本的代码可以到 这里 查看。这不给整个star。

    在很久很久以前,对于 Vue3 的认识:

    新项目预览点这 road.cemcoe.com


    下面是无聊流水账:

    1. 创建 Vue3 项目

    Step 1. 打开 Vue 的官网,看一看最新的脚手架的命令


    npm_init_vuelatest.png

    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
    

    毫无意外地会看到这个样子:


    init.png

    Step4. 管理一下项目

    当然,最好把项目用 git 给管理起来,在配置好 git config 的前提下把项目给 init 一下

    git init
    
    1. 瞧一眼初始化的目录结构

    跟 Vue2 差别不是很大,比较起眼的就是 vite 了,现如今是开发时 Vite, 打包时 rollup

    vite.png
    1. 删除(替换)一下不必要的文件
    • public/favicon.ico 换成自己的
    • src/assets 删除里面的文件
    • src/componets 清空里边的文件
    1. 观摩一下 APP.vue

    简化一下 APP.vue

    <script setup></script>
    
    <template>
      <div class="app">
        <h2>app</h2>
      </div>
    </template>
    
    <style scoped></style>
    

    观摩一下 APP.vue,把 script 标签放在了前面,添加了 setup 语法糖。

    1. 再次运行看一下有没有错误
    npm run dev
    

    大概率会出现诸如文件不存在的错误,按照提示改一改就好。

    1. 初始化 css

    这里用到了 normalize.css,按照官网一把梭安装导入完事。

    1. 整理项目目录结构

    主要还是和 Vue2 的保持一致

    • assets 静态文件,imgs css
    • components 组件目录
    • hooks 封装的 hooks
    • router 路由相关
    • service/modules 分模块管理请求
    • service/request 封装的请求函数
    • store/modules 分模块管理状态
    • utils 工具函数
    • views 视图组件
    1. 选用一个得力的组件库

    这是一个移动端的项目,没得选就是 Vant 了,打开官网,自己写着玩当然是选用最新版的啦。

    vant.png
    npm i vant
    

    执行之后你会发现,额,不对劲,这版本不是 4 呀。

    vant-version.png

    去官网确定一下命令,你发现,额,我搞得是对的呀。

    vant-install-version4.png

    这个时候想装上 vant@4 咋搞。

    明显的是这玩意还没把默认版本个升到 4,但是文档 4 对应的安装命令没改就很难受,这里就需要自己去找一找了。

    1. 安装非正式版的 Vant4

    既然官方文档还没更新,那就到 npm 上去看一下版本号,自己装一下。

    vant-version4.png
    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 版本,然后组件都导不进。切记,新也要适度。

    1. 配置组件库

    回到 Vant 的文档中,按需导入配一下,没什么东西,照着文档配就完事了。

    import-on-demand.png

    配置好之后最好自己测试一下。

    别忘了文档中的第四步,函数组件的样式记得手动导入一下。

    // 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.
    
    1. 生成主要的路由 views

    到 src/views 目录下创建如下文件,并填充基本结构

    • home/home.vue
    • following/following.vue
    • profile/profile.vue
    1. 为主要的路由 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",
      },
    },
    
    1. 配置 tabbar

    到 components 下创建 tab-bar/tab-bar.vue
    这是就体现了将 router/tabbar-routes.js 抽离并导出的好处了。

    直接把路由信息导入

    import { tabbarRoutes } from "@/router/tabbar-routes";
    

    就很棒,不用再搞一份数据了,到 meta 中去拿就好了。

    剩下的步骤就很简单了,就是使用组件库,具体看 vant 的文档就行了。

    中场休息

    现在的大致进度应是,应用底部有一个 tabbar,点击会切换对应的图片以及颜色且相关的路由也会一并切换。


    1. 发送网络请求

    别的都不说,先把数据拿到,可供选择的方案

    • 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

    1. ref 是个什么东东

    啥也不说,无脑打开官方文档瞧一瞧,直奔 API

    ref.png
    // 从文档上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>;
    

    这里又多了一些类型,有空再接着捋下去。

    1. 抽一下请求函数
    <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>
    

    这么改吧改吧已经有了一些可用的样子了,下面要做的就是请求拦截和响应拦截以及一些错误处理,这里就不展开了,毕竟,每个公司的接口规范也不尽相同。

    1. 各回各家

    上面的代码呢最好是分到不同的文件里。怎么起名看着来。

    div.png

    图中白色的抽离下面说,先将其忽略。

    看看一下现在的数据流向

    用户访问 Home 页面,Home 页面执行请求函数,而请求函数定义在 service/modules/home.js,而该文件会引用封装的请求函数 http(名字无所谓,爱叫啥叫啥),而该请求函数则是对 fetch 的封装,当然了,不用 fetch,用 axios 也可以。

    用张图来表示一下:

    data.png

    这个搞的好处是什么呢?

    想一下其实网络请求是和 Vue 这个框架无关的,按照上面的方式,如果要将原先的项目升级到 Vue3 的话,或者换成 React,其实只有第一部分需要改。而后面的两部分是不用动的。

    而如果把第一部分和第二部分代码放到 views 文件中,那改起来可就麻烦了。

    1. 太大了,来点组件化

    随着代码不断堆下去,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 文件中也不是很好。

    1. 结构行为和样式分离?

    前端有个东西,叫做结构行为样式相分离。

    Vue 表面上还是这种分离的写法,三大块分着写。

    React 干脆就把这仨货搅和在一起。

    这些框架把 DOM 操作给隐藏,将命令式编程变成了声明式编程。

    声明式编程核心是什么?

    当然就以声明为核心咯,而声明,它其实可以有另外一个名字,叫做状态。

    于是这种分离的思想大抵还是在的吧,自由过主角不再是 HTML CSS JS。

    。。。

    俺也不知。

    1. 上状态

    既然 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 就不用选择困难了。

    1. 更新下数据请求的步骤

    未完,不续

    相关文章

      网友评论

          本文标题:一个 Vue3 项目的流水账

          本文链接:https://www.haomeiwen.com/subject/vpjitdtx.html