学完 Vue3、TypeScript 干什么,先来一个"网抑云"

haiweilian2021-07-09技术分享

1、前言

没错又是仿网易云,那么多了网易云项目了还写?纯粹是为了学习罢了。

之前学习的 Vue3Vite2TypeScript 一直没有新项目可用,控制不住自己的小手了必须写写,也为了要看源码熟悉熟悉语法。

整体代码写的比较简洁,功能也比较简洁,想练习的可以继续扩展,很多功能都没做。

项目地址

https://github.com/haiweilian/vnext-netease-musicopen in new window

项目 UI 也不知道怎么设计(部分逻辑也是),这里参考了 ssh 的开源项目

https://github.com/sl1673495/vue-netease-musicopen in new window

接口使用的 Binaryify 的开源项目。

https://github.com/Binaryify/NeteaseCloudMusicApiopen in new window

2、项目准备

之后的内容都是开发的过程中随手记录的笔记整理了一下,到这一步可以跑起来项目写写了。

扩展工具

项目依赖

"dependencies": {
  "@vueuse/core": "5.1.0",
  "axios": "^0.21.1",
  "dayjs": "^1.10.5",
  "element-plus": "^1.0.2-beta.54",
  "lrc-kit": "^1.1.1",
  "vue": "^3.1.4",
  "vue-lazyload-next": "^0.0.2",
  "vue-router": "^4.0.10",
  "vuex": "4.0.2"
}

插件统一注册

把插件都放到 modules 目录下,利用 Viteimport.meta.globEager 加载注册。

modules 下的文件都返回一个 install 函数。

// src/modules/element-plus.ts
import type { App } from "vue";
import "element-plus/lib/theme-chalk/base.css";

export const install = (app: App) => {
  app.config.globalProperties.$ELEMENT = { size: "mini" };
};

main.ts 中使用 Glob 导入统一注册。

// src/main.ts
Object.values(import.meta.globEager("./modules/*.ts")).map((i) => i.install?.(app));

Vue3

整体使用的 script setup 语法,在写的时候还没有定稿,写完之后发现定稿了看了看只有部分不兼容。详情查看 script setupopen in new window

需要全部更新下依赖包,替换成新的语法即可。更新依赖推荐使用 npm-check-updates 整个项目进行更新。

比如涉及到的变更:

  • defineEmit => defineEmits

  • useContext() -> useSlots() + useAttrs()

  • defineEmitsdefineProps 不再需要导入。

Vuex4

Vuex4 变更不大,只是对 ts 的支持基本上任何改变,比如 storecommitdispatch 都不是很好的提示。

关于 store 有一遍文章 Vue3 中让 Vuex 的 useStore 具有完整的 state 和 modules 类型推测open in new window,不过也得单独处理。

而对 commitdispatch 源码中的类型直接就是 string

export interface Dispatch {
  (type: string, payload?: any, options?: DispatchOptions): Promise<any>;
  <P extends Payload>(payloadWithType: P, options?: DispatchOptions): Promise<any>;
}

export interface Commit {
  (type: string, payload?: any, options?: CommitOptions): void;
  <P extends Payload>(payloadWithType: P, options?: CommitOptions): void;
}

最后期待一下 Vuex5,后续先用 pinia 改一版试试。

VueRouter4

路由这部分变化还是挺大的移除了多个功能,不过大部分移除的功能都能使用 customv-slot 来做。

比如使用任意的标签跳转:

<RouterLink v-slot="{ navigate, isExactActive }" :to="menu.link" custom>
  <li class="menu-song__item" :class="{'is-active': isExactActive}" @click="navigate">
    <Icon :name="menu.icon" />
    <span class="menu-song__value"> {{ menu.name }} </span>
  </li>
</RouterLink>

TypeScript

在看完官网的教程之后在写业务上基本上没什么问题,在和 Vue 结合使用主要几点。

导入类型使用 type 指定导入类型,如果不加在 xx.ts 文件里是没问题的,但在 script setup 因为会自动收集顶层变量,所以会报错 “PropType”仅表示类型,但在此处却作为值使用。。使用 type 也便于区分逻辑与类型。

import { onMounted, ref, watch } from "vue";
import type { PropType } from "vue";

在项目中避免不了使用库定义的类型,我们根据调用的函数点进去,查看里面的声明关系就可以找到没有在文档中指出的子类型之类的。

import { ElLoading } from "element-plus";
import type { ILoadingInstance } from "element-plus/packages/loading/src/loading.type";

let needLoadingRequestCount = 0;
let loading: ILoadingInstance;

VueUse

在这个项目中用到了这个库,这个库绝对能让你感受到 Vue3 好在那。

比如用到的 useStorageonClickOutsideuseMediaControls 极大的方便了开发。

3、代码规范

编码规范

各种规范集成没想折腾,是直接使用 antfuopen in new window 提炼出的常用的配置。就是把各种规则和插件给组合了形成一套插件,不想折腾的可以快速使用。可以参照这种方式封装一套公用的配置。

npm i eslint @typescript-eslint/eslint-plugin @antfu/eslint-config --save-dev

.eslintrc 文件,添加以下内容。就可以获得 eslint & typescript & vue3 & react 的格式化了。

{
  "extends": "@antfu",
  "rules": {
    "no-unused-vars": "off",
    "@typescript-eslint/no-unused-vars": "off"
  }
}

提交规范

提交规范使用 huskycommitlintcommitizenstandard-version 配置也很简单看官方文档即可。之前总结过各种配置方便使用编码规范、提交规范open in new window

4、CSS 命名

命名风格使用的 BEM 规范,里面用到了 element-plus 源码中的 mixins 函数,具体查看element-plus/theme-chalkopen in new window

vite 中使用 scss 全局导入,可以导入文件路径。注意后面的分号(😉

export default defineConfig({
  css: {
    preprocessorOptions: {
      scss: {
        additionalData: '@import "src/styles/additional.scss";',
      },
    },
  },
});

简单说下 element-plus 源码中的 @mixin b($block)@mixin e($element)@mixin m($modifier)@mixin when($state) 几个主要的 mixin

@mixin b($block)

定义生成块。参数为块的名称。

@include b(input) {
  display: inline-block;
}
.el-input {
  display: inline-block;
}

@mixin e($element)

定义生成元素。参数是元素的名称,可以传入多个。

@include b(input) {
  @include e(inner) {
    padding: 0 15px;
  }

  @include e((suffix, suffix-inner)) {
    position: absolute;
  }
}
.el-input__inner {
  padding: 0 15px;
}

.el-input__suffix,
.el-input__suffix-inner {
  position: absolute;
}

@mixin m($modifier)

定义生成修饰。参数是修饰的名称,可以传入多个,($modifier1, $modifier2, ...)

@include b(input) {
  @include m(medium) {
    height: 30px;
  }

  @include m((mini, small)) {
    height: 20px;
  }
}
.el-input--medium {
  height: 30px;
}

.el-input--mini,
.el-input--small {
  height: 20px;
}

@mixin when($state)

定义条件状态。参数是状态的名称。

@include b(input) {
  @include when(disabled) {
    cursor: not-allowed;
  }
}
.el-input.is-disabled {
  cursor: not-allowed;
}

5、SVG 图标

单独处理 SVG 是希望以组件的方式使用,做状态切换的时候也方便。使用 vite-plugin-svg-iconsopen in new window 做的处理,使用方式可查看文档。

在配置好依赖和图标目录之后,创建一个 Icon 组件。

<template>
  <svg :style="getStyle" class="icon" aria-hidden="true">
    <use :xlink:href="symbolId" />
  </svg>
</template>

<script setup lang="ts">
import { computed } from "vue";
import type { CSSProperties } from "vue";

const props = defineProps({
  prefix: {
    type: String,
    default: "icon",
  },
  name: {
    type: String,
    required: true,
  },
  size: {
    type: [Number, String],
    default: 16,
  },
});

const symbolId = computed(() => `#${props.prefix}-${props.name}`);
const getStyle = computed((): CSSProperties => {
  const { size } = props;
  let s = `${size}`;
  s = `${s.replace("px", "")}px`;
  return {
    width: s,
    height: s,
  };
});
</script>

导入或全局注册就可以用了。

<Icon :name="menu.icon" />

6、请求处理

把接口过了一遍,发现数据不好处理层级也比较深不利于渲染(因为做的简单,大部分都用不到)。所以所有的字段在使用之前使用 map() 统一做了字段的转化。

举个简单的 🌰 :

// 字段是 namea
[{ namea: "lian" }].map((user) => {
  name: user.namea;
});

// 字段是 nameb
[{ nameb: "lian" }].map((user) => {
  name: user.nameb;
});

// 经过转化后是一致的,有些层级深的也直接拉成平级。
[{ name: "lian" }];

所有的接口都写了 REST Clientopen in new window 的配置文件。

@hostname = http://localhost:3000

# 查询对应资源热门评论
GET {{hostname}}/comment/hot?id=186016&type=0 HTTP/1.1

# 查询对应资源的评论
GET {{hostname}}/comment/new?id=186016&type=0&sortType=3 HTTP/1.1

7、歌词解析

接口返回的歌词是一个字符串,用一个插件去解析的 lrc-kitopen in new window,会返回解析后的数组,解析完后直接循环可以了,我们需要做的就是定位歌词和自动滚动到居中。

在使用 lrc-kit 中可以通过 curIndex() 获取当前时间的行数,通过行数获取到歌词所在元素的偏移量并计算滚动距离v-for 中的 Ref 数组open in new window

/**
 * 获取歌词列表 ref,在检测到当前行变化的时候,定位歌词到内容中间
 */
const scroller = ref()
const lyricLineRefs = ref<HTMLElement[]>([])
const setItemRef = (el: HTMLElement): void => {
  lyricLineRefs.value.push(el)
}

watch(lineActive, (num: number) => {
  const curDom = lyricLineRefs.value[num]
  scroller.value.scrollTop = curDom.offsetTop - 130 + curDom.offsetHeight / 2
})

8、项目部署

两个项目都是部署在 vercel 上的。

9、结尾总结

感谢阅读,喜欢点个 ✨✨open in new window

经过这次的实践,在写业务应该没什么问题。对于 Ts 的一些高级类型还是用的比较少。接下来的空余时间研究 Ts 高级类型和 Vue3 源码。

最后更新时间 11/8/2023, 10:03:21 AM