Vue+ts+pinia+vite的H5项目

323 阅读18分钟

搭建步骤

一、创建项目

  • 使用create-vue创建项目:npm init vue@latest
    • 好处:创建项目时可以选择vue的全家桶
  • 使用vite创建项目:npm init vite@latest vite-ts -- --template vue-ts
    • 特点:项目里只会安装vite,不会提示安装全家桶。

二、规范项目

定义规范

比如:

  • 目录、变量、组件的命名方式
  • 是否统一使用分号结尾?使用单引号/双引号?

具体我们会使用以下方式进行规范

配置eslint和prettier规范项目:
  • eslint:
    • 运行代码前就可以发现潜在错误
    • 适合用于制定团队代码规范
    • 规则分为3个等级:off、warning、error
  • prettier
    • 代码格式化工具,用于检测代码中的格式问题。
    • eslint偏向于把控代码质量,prettier偏向把控代码风格。
  • 配置步骤
    • 安装eslint和prettier(脚手架已经帮我们完成)。
    • 安装插件
      • ESLint:不符合规范的代码提示错误
      • Prettier - Code formatter:保存代码时使用这个插件进行自动修复
    • 在eslintrc.js文件中配置自定义规则:rules。
    • 配置prettier,新建.prettierrc.js文件并添加规则。
      • 注意,每次修改完.prettierrc.js文件都要reload window(ctrl+shift+p),否则规则不生效
根据规范检查代码
使用commitlint校验commit信息
  • 步骤
    • npm安装
    • 配置(新增一个js文件)
    • 执行命令,在husky中增加hooks:commit-msg
    • 参考网址,github.com/conventiona…
  • 为什么需要使用commitlint:commit记录了项目的开发进程,良好的commit信息可以回顾项目开发进展,当时的需求背景,动机。也方便做一些版本回退。
  • 如何规范:type(scope?):subject
    • type,提交的常用类型
      • feat(新功能)
      • fix(修复)
      • perf(性能)
      • refactor(重构)
    • scope,影响的范围,可选。
    • subjtct,提交的详细说明。
  • 如何起作用:commit后,commitlint借助husky使用commit-msg钩子校验填写的commit信息。
配置lint-staged来对esliint和prettier中配置的规范进行校验
  • 每次对所有文件执行lint操作没必要,通过lint-staged只对提交到暂存区的内容进行格式化
  • 步骤
    • 安装,npm i lint-staged -D
    • 在package.json文件中增加配置项
    • 修改husky的pre-commit钩子的触发命令
      npx lint-staged
      

三、安装需要的库

配置ui库
  • npm install安装组件库
  • 配置ui组件的引入方式
    • 全局注册

      • 在main.ts中import组件和样式,然后使用app.use注册组件
    • 局部注册

      • 直接在.vue文件中引入组件,但是样式文件还是需要全局引入的。
    • 按需引入组件和样式

      • 通过unplugin-vue-components插件进行按需引入
      • 步骤
        • 安装插件
        • 在vite中配置插件
        • 直接在模板中使用vant组件
        • 引入函数组件的样式:有些组件是以函数形式提供的,插件无法自动引入组件以及对应样式。所以还需要在入口文件中引入样式。
    • tree-shaking

      • 作用:消除死代码的性能优化理论,以减少最终生成包的大小。死代码包括js和css
      • js代码是通过es6的import和export语法进行tree-shaking的
      • css代码无法通过js代码那种方式进行tree-shaking,只能通过插件进行按需引入组件和样式。unplugin-vue-components
配置vue-router
  • 步骤
    • 安装(脚手架一般帮我们已经安装好了)
    • 注意一定要分模块配置路由,不要所有路由都在一个index.ts文件中。
配置HTTP请求库
  • 步骤
    • 安装,npm install axios -s。
    • 创建实例
      • 在文件/api/base.ts中创建实例
    • 配置响应拦截器和请求拦截器
  • axios有以下特性
    • 支持Promise API
    • 拦截请求和响应
    • 取消请求
    • 自动转换JSON数据

四、对代码进行必要的封装,这里主要指的是对HTTP的请求和响应进行统一的处理

  • 比如,发出请求前,统一添加cookie,或其他自定义响应头。
  • 比如,请求响应时,对错误进行统一归类处理。

五、配置好用的工具提高书写代码的效率

配置husky管理Git hooks
  • 步骤
    • 安装husky,npx husky-init。
      • 执行完命令,项目根目录下会自动创建一个.husky目录。并把husky安装到devDependencies中。
  • hooks:一些时机的回调
  • Git hooks:是在特定的Git操作(如提交代码、推送代码等)发生时自动触发的脚本,允许开发者在这些事件发生前或发生后执行自定义操作。常见的钩子:
    • pre-commit:在提交代码前执行,用于进行代码检查、格式化等操作。
    • pre-push:在推送代码前执行,用于运行测试或进行其他验证。
    • prepare-commit-msg:在生成提交消息(commit message)时执行,可用于自动化生成提交信息。
    • post-checkout:在切换分支后执行,用于更新依赖、执行其他操作等。
  • 如何使用Git hooks
    • 自己写脚本
    • 对git有一定了解
  • husky:快速管理Git钩子的工具,Husky的作用是简化Git钩子的配置和管理过程,同时提供了一套易于使用的接口。通过Husky,开发者可以在项目中的Git仓库中定义各种钩子,例如在提交代码前运行单元测试、在推送代码前执行代码风格检查等。(让大头儿子变小头爸爸)
  • 使用husky的好处:校验commit信息、运行测试代码、校验代码格式..
配置commitizen和cz git生成规范式commit信息
  • 步骤
  • commitizen是生成标准化提交信息的命令行工具
  • cz git集成了git和commitizen,使得commitizen更方便地与git交互,直接使用git cz命令就可以触发交互式提交流程,并将生成的提交信息直接应用到git的提交中。

六、做适配(如果是移动端)

使用rem方案+postcss实现移动端适配
  • 根据一个基准值,设置不同的屏幕宽度时,根字体的大小。当屏幕宽度发生变化时,自动根据基准值改变根字体大小。基准值一般通过设计稿来确认,比如设计稿为屏幕宽度为390px宽度,根字体大小为16px;这个比例就固定了,即fontSize/curDeviceWidth=16/390。再通过当前设备的实际宽度,就可以推算出根字体大小。fontSize = curDeviceWidth * (baseFontSize/baseWidth)
  • postcss
    • 是一个使用 JavaScript 工具和插件转换 CSS 代码的工具,它的主要功能其实是可以利用 JavaScript 去处理一些原生的 CSS 或者预处理器(Less、Sass)处理不了的工作。
    • 它有很多插件供我们使用,比如
      • autoprefixer:根据项目中指定的浏览器种类和版本,查询不同浏览器对css属性的兼容情况,自动为css属性添加不同的浏览器前缀。
      • postcss-pxtorem:自动将px转换成rem,即实际书写时用px即可,但是在浏览器查看样式时,会自动转换成rem单位。
  • 步骤
    • 设置根字体大小

    • npm i postcss autoprefixer postcss-pxtorem -D

    • 配置postcss.config.js文件,注意配置完之后需要重启项目,否则不生效


vite

定义

  • 一种新型的前端构建工具,能够显著提升前端开发体验

组成部分

  • 一个开发服务器:它基于原生ES模块,提供了丰富的内建功能,比如速度快到惊人的模块热更新(HMR)
  • 一套构建指令:它使用Rollup打包代码,并且它是预配置的,可输出用于生产环境的高度优化过的静态资源

为什么使用vite

  • 当应用越来越大型时会出现性能问题
    • 启动本地开发服务器的时间会很久
    • 热更新很慢
  • vite如何优化的上面两个问题
    • 启动本地开发服务器的时间会很久
      • 依赖:使用esbuild进行依赖项预打包,esbuild使用Go编写,会比javascript-based的打包工具快10-100倍
      • 源码:使用浏览器原生es module提供源码,让浏览器接管打包工具的部分工作
    • 热更新很慢
      • 使用ESM不需要重新编译:一些打包工具的开发服务器在文件更改时,需要重新构建整个项目,来获取新的模块依赖关系。而vite使用原生es模块提供源码,模块的依赖和加载是由浏览器接管的,所以文件更新时,vite不需要知道依赖关系,即不需要重新构建,只需要直接更改对应模块的引用链接即可
      • 使用浏览器缓存加速:因为模块的加载是由浏览器接管的,那么Vite利就用HTTP头来加速整个页面的重新加载
  • 开发服务器对比

image.png 基于打包的开发服务器返回给浏览器的是bundle,基于esm的开发服务器返回给浏览器的是模块本身,而bundle包括模块本身和模块间的依赖关系。因为之前的浏览器不支持模块加载,所以打包工具需要根据模块的依赖关系管理整个项目的模块加载,整个依赖关系需要重新构建并分析整个项目得到的。而vite使用浏览器原生es模块管理模块的加载,这样就同时将模块的依赖关系图托管给了浏览器,那么文件更新的时候就只需要替换对应的模块内容,以及其它模块引用该模块的链接即可。

可能遇到的问题

配置完eslint和prettier后,不符合规范的代码并没有提示错误。
  • 需要安装插件:eslint
prettier.json文件整个不生效
  • 需要在eslintrc.cjs中加配置

image.png

保存时并没有按照prettier的规范自动格式化代码
  • 需要在vscode中的settings.json文件中加一些配置

    image.png

  • 添加最后一个配置时,提示错误。

    • 未安装插件:Prettier - Code formatter
使用命令初始化项目后,发现所有引入.vue文件的地方全部飘红,提示找不到模块。
  • 需要安装插件TypeScript Vue Plugin (Volar)
    • 安装完后如果仍然飘红,检查使用的ts版本,有可能版本过低。
配置完lint-stage,执行git commit -m 命令时出错

image.png

npx

产生背景

  • 有些时候,我们只是临时想要使用一些 cli 工具,比如 create-react-app,我们可能只是需要生成一个 React 项目。但是 npm 不能够在不将包安装到本地的情况下,使用相关的依赖,所以 npx 出现了

和npm的区别

  • npm(Node Package Manager)是 JavaScript 的包管理器,用于安装、管理和发布 JavaScript 包。它是 Node.js 的默认包管理器,提供了大量的功能,例如安装依赖、管理版本、运行脚本等。通过 npm,开发者可以轻松地下载和安装其他人编写的 JavaScript 包,以便在自己的项目中使用。
  • npx是一个用于执行 Node.js 包的命令行工具。当你在命令行中使用 npx 后面跟着包的名称和命令,它会依次检查项目全局是否已经安装了该包,如果有,就执行该包对应的bin目录下的命令,如果都没有,它会自动下载并执行,执行完后就立即删除。

作用

  • 执行本地已安装的依赖包命令,不用在scripts脚本写入命令。
    • npm 本身不能够执行任何包,对于本地项目的包,如果想要执行,则需要写入到 package.json 里面,然后通过 npm 来解析 package.json 文件,解析到包的 bin 文件路径,在 bash 中执行。有了npx可以不用在scripts中配置命令直接通过npx ...就可以执行命令,比如npx lint-staged。
  • 执行未安装的依赖包的命令,先下载,用完即删。

执行命令的几种方式

  • 使用package.json的scripts脚本,在命令行中通过npm run * 运行
  • 在命令行中直接找到模块的二进制文件运行
  • 全局安装模块

开始开发项目

4-4 安装vant-ui和使用vite按需加载组件

  • 使用npm install安装vant-ui
  • 引入全局样式文件
  • 遇到的问题
    • 快速生成vue模板
      • 可以使用snippet插件,但都不是自己想要的代码片段
      • 自己在vue.json文件中配置用户代码片段
    • .vue文件提示Delete eslintprettier/prettier错误
      • 在eslint配置文件中增加一条规则,忽略结尾符的检查

4-5和4-6 配置vue-router

  • 放置内容组件
  • 配置组件和路由间的对应关系
  • 触发路由跳转的地方(如果需要)
  • 将路由实例挂载到全局(只有第一次时需要)

4-7和4-8 使用json-server搭建MockServer

  • 自定义目录结构
  • 创建服务
  • 搭建路由
    • 创建测试数据
    • 在controller中处理测试数据(可有可无)
    • 在db.js中配置路由和数据的对应关系
  • 使用nodemon自动重启服务,这样不必每次修改完文件自己手动重启。

4-9 使用vite配置请求代理

image.png

  • 将node版本从14..升级到18..

5-1 页面头部实现

课程目标
  • 安装和引入normalize.css,处理不同浏览器的默认样式
    • 我们在开发的时候会发现很多样式都自带了各自特有的默认样式,而这样样式通常会被遗忘,导致样式调整起来很繁琐。为了让样式统一,我们在开发的时候通常会对一些元素进行样式重置,已避免默认样式影响开发。Normalize.css就是一个这样的CSS样式文件,它的作用就是让HTML元素更好的实现跨浏览器一致性,是一种现代的、为HTML5准备的CSS重置替代方案。
    • 特点
      • 标准化的样式,适用于大部分HTML元素;
      • 保留有用的浏览器默认样式,而不是全部样式“重置”;
      • 修复了浏览器BUG并保证浏览器样式的一致性;
  • 安装css预处理器sass
  • 设置字体抗锯齿
  • 封装TheTop组件
额外收获
  • flex:1实现左右两侧布局,不再使用justify-content:space-between,这样减少了dom的数量。因为flex:1代表弹性盒子的子元素将平均分配剩余空间
  • 给图片加样式时,类名全部使用...-icon,使得代码更规范
  • 背景色渐变:backgroundcolor:linergradient(direction,color1,color2,...color)
  • 改变ui库的样式时,不一定要使用样式穿透。在非scoped的<style>标签中直接通过类的样式名修改即可。
  • 子组件如何接收父组件的传值,通过defineProps。它接受一个对象作为参数,该对象用于描述组件的属性和属性类型。
  • 将所有定义的类型文件全部放到types目录下,且为了引入时不必通过引入具体的文件而使用里面的类型,直接定义一个index.d.ts文件并把每个类型文件都导出*即可。

5-2~5-4 封装基础组件Search

课程目标
了解BEM规范 - 一种css命名的规范
  • 全称:Block(块) Element(元素) Modifier(修饰符)
  • -中划线:某个块或某个子元素的多单词之间的连接记号
  • __双下划线:连接块和块的子元素
  • --双中划线:描述块或者块的子元素的一种状态
如何设计一个组件
  • 布局分析
  • 功能分析(有哪些属性,有哪些事件)
    • 可以用slot定义一个自定义区域
如何给子组件传递v-model
  • 背景:vue是单向数据流,子组件可以接收父组件的props,但是不能修改,否则会导致数据的应用流向变得难以理解。但是有时候我们希望子组件数据的修改,父组件的数据会随之变化。在vue2中提供了.sync修饰符,但是在vue3中不再支持.sync,取而代之的是v-model。
  • 父组件给子组件传递单个v-model:默认情况下,子组件和父组件都使用v-model属性,子组件接收时使用modelValue来接收父组件的v-model。子组件修改modelValue值时,需要调用update:modelValue事件,父组件的v-model就会随之改变。
    • 自定义modelValue的名字
      • 如果不想使用默认的modelValue名字,想自定义名字,需要在父组件的v-model中进行声明:v-model:show,子组件就可以用show来接收父组件的v-model,事件也自然变成了update:show。
  • 父组件给子组件传递多个v-model。
<my-component v-model:hello="hello" v-model:word="word"></my-component>

image.png

  • PS:自己走的弯路:我把子组件需要给v-model绑定的值,也是通过defineProps传递的,导致报错,因为默认子组件不允许修改父组件传递的值。所以我给父组件传递了一个对象,里面定义了所要传给子组件v-model的属性。子组件接收到这些属性以后,在定义一个新的对象,也包含这些属性。然后把这些属性绑定给v-model。然后再定义一系列事件,去通知父组件这些属性的变化,父组件去手动更新每个属性。
使用defineEmits定义组件的事件
  • defineEmits用于在setup中注册自定义事件,是一个宏函数,使用时无需导入
    • defineEmit可以使用运行时声明,此时接收一个数组为参数,数组元素为自定义事件的名字;也可以使用类型声明,此时参数为一个对象,里面是一系列函数的声明。
      • 运行时声明:代码运行后,给出相关ts提示。
      • 类型声明:在代码运行之前,即编写代码时(编译阶段)就会结合IDE给出相关ts提示。
    • defineEmit返回一个触发器,用于触发事件,第一个参数是具体事件,第二个是传递的值。
如何定义css变量
  • 首先css变量要定义在伪类选择器:root内,它一般情况下是表示 <html> 元素,定义 在:root 后,所有变量都将被保存在 :root 中,并且可以在整个页面的任何位置使用。
  • sass中使用变量:var(变量名字)
额外收获
  • & 表示嵌套的上一级。这是sass和less中的语法,代表上一级选择器。
  • 插槽<slot>不能绑定事件,要在插槽外层包裹一层标签才可以绑定事件
    • 如何给插槽设置默认值:把默认值放在标签中间即可。
  • 在子组件中如何拿到父组件传过来的slot?
    • $slots['slot-name']

5-5~5-6 自定义hooks - useToggle,实现搜索页展示切换

课程目标
  • hooks介绍
    • hooks就是函数的一种写法,以函数形式将可复用性内容提取出来,和vue2中的mixin类似。
    • 和utils的区别,如果涉及到要和组件的业务进行交互,要用到页面中的状态,或者要把一个ref变量返回出去供页面使用,即要在hooks里面使用ref这些,就使用hooks,反之使用utils。hooks是有状态的,utils是无状态的纯函数。
  • 如何实现一个hooks
    • 所有的hooks都定义在use或hooks文件夹下
    • 文件命名方式为useXX.ts
  • 事件传递实现跨组件通信
    • 子组件如何和爷组件进行通信?
      • 子传父,父传父。
    //useToggle
     import { ref } from 'vue';
     import type { Ref } from 'vue';
    
     export function useToggle(initState: boolean): [Ref<boolean>, () => void] {
       const state = ref(initState);
       const toggle = function () {
         state.value = !state.value;
       };
       return [state, toggle];
     }
    
    • 注意上面定义useToggle的返回值时使用了数组而不是对象,这样做的好处:
      • 使用数组,更加简洁地表明返回值由两部分组成,使用这个函数时,可以直接通过解构赋值获取每一部分,而且可以在不同的组件中使用不同的命名。如果使用对象的话,想要在解构的时候重命名的话,较繁琐。
额外收获
  • 调用函数的时候,什么时候需要加(),什么时候不需要加()?

5-7~5-9 业务组件SearchView开发

课程目标
  • 使用<transition><transition-group>组件实现切换到搜索页时有一个渐变的动画效果
    • <transition><transition-group>组件是vue提供的用于给任意元素或组件添加进入或离开时的过渡效果的组件
    • <transition>适用于单个元素,<transition-group>适用于多个元素
    • 使用时要给过渡组件添加name属性,然后定义class时要以name属性值为前缀,这样就能够自动应用过渡样式了。
  • Search组件复用
  • computed计算属性
  • watch监听属性
  • 使用axios实例发送业务请求
  • mock请求
  • 实现utils版的防抖和hooks版的防抖
需求分析

image.png

额外收获
  • 定义接口返回数据的类型

image.png

  • 用await去接收请求的接口数据,如果发生了错误,如何接收?
    • 用try catch 或 try finally

image.png

  • 数组解构
    • 数组解构就是能快速提取数组中的指定成员(某一个值/全部值)
       const [INIT, DONE, DOING] = [-1, 0, 1] //提取全部值
       // INIT = -1; DONE = 0; DOING = 1;
       const [,,DOING] = [-1, 0, 1]//提取一个值
       // DOIING =  1;
       const [INIT,...ALL] = [-1, 0, 1]//用...提取剩余全部值
       // ALL = [0,1]
    
  • 定义泛型函数
    • 泛型函数:在函数定义中使用泛型类型参数的函数。
    function transform<T, U>(arg: T): U { }
    //参数类型是T,返回值类型是U
    
  • 如何接收一个响应式类型的参数?
    value:Ref<T>
    
  • 组件销毁时要停止监听,那么如何在组件销毁时停止watch的监听?
    const unwawtch = watch(...)
    onUnmounted(()=>{
        unwawtch()
    })
    

5-10~5-11 自定义hook - useDebounce

   //utils
   //定义
   interface IDebounceFn<T> {
     (...args: T[]): void | Promise<void>;
   }
   export function useDebounce<T>(fn: IDebounceFn<T>, delay: number) {
     let timer: number | null = null;
     return function f(this: void, ...args: T[]) {
       if (timer) {
         clearTimeout(timer);
       } else {
         timer = setTimeout(() => {
           fn.call(this, ...args);
         }, delay);
       }
     };
   }
   //使用
   watch(
     searchValue,
     useDebounce((nv) => {
       if (!nv) {
         searchResult.value = [];
         return;
       }
       onSearch(nv as string);
     }, 1000)
   );
   ```
   ```js
   //hooks
   //定义
   import type { Ref, UnwrapRef } from 'vue';
   import { ref, watch, onUnmounted } from 'vue';
   export function useDebounce<T>(value: Ref<T>, delay: number) {
     const debounceValue = ref(value.value);
     let timer: number | null = null;
     const unwatch = watch(value, (nv) => {
       if (timer) {
         clearTimeout(timer);
       }
       timer = setTimeout(() => {
         debounceValue.value = nv as UnwrapRef<T>;
       }, delay);
     });
     onUnmounted(() => {
       unwatch();
     });
     return debounceValue;
   }
   //使用
   const debounceValue = useDebounce(searchValue, 1000);
   watch(debounceValue, (nv) => {
     if (!nv) {
       searchResult.value = [];
       return;
     }
     onSearch(nv as string);
   });

5-12~5-13 自定义hook - useAsync

课程知识点
  • Promise then 和 catch的处理
  • TS声明复杂的类型结构
  • 使用jsonserver中间件延时返回数据
需求分析
  • 新建一个fetchHomePageData的api
  • 实现useAsync,将api包裹一层,处理Promise的状态
额外知识点
  • 类型UnwrapRef的作用
    • UnwrapRef是一个用于取消响应式对象封装的函数,在某些情况下,我们可能需要获取响应式对象的原始值,而不是封装后的响应式代理。这时候就可以使用UnwrapRef函数。
    • UnwrapRef函数会返回一个非响应式的对象,这样我们就可以直接获取到原始的值,而非代理对象。
  • vue3中的响应式
    • vue3.0中的响应式原理是基于proxy做的,而使用proxy的前提是,我们要代理的是对象而不是基本类型数据。所以就需要用ref将基本类型的数据包装成{value:基本类型数据},然后再对这个包装对象进行响应式处理

5-14~5-15 封装LoadingView组件

课程知识点
  • 实现加载loading和骨架屏
需求分析

image.png

  • loading=true时展示loading样式
    • loading样式有默认的,也可以自定义(通过插槽实现)
  • loading=false时展示正常样式(也要通过插槽实现,因为每个页面的样式都不一样)

5-16 业务组件Grid布局实现Transformer金刚区

课程知识点
  • Grid布局

image.png

5-17 业务组件ScrollBar 滚动提示栏组件开发

课程知识点
  • 封装hooks:useInterval & useTimeout
    • useTimeout钩子有bug待解决
  • ref获取DOM
//template
      <div ref="containerRef" style="border: 1px solid red"></div>
// script
const containerRef = ref();
const container = containerRef.value;//container即拿到了dom
//container.children可以拿到它的子元素
  • withDefaults设置props默认值
    const props = withDefaults(defineProps<IProps>(), {
        intervalTime: 3000,
        transition: 1000,
         height: 40
    });
    
  • <style>使用<script setup>里的js变量
    • 通过v-bind指令
  • v-html渲染html模板
  • transform:scale实现小于1px的边线
    • 如果直接在元素上使用 border 的话,因为会使用 transform: scale(0.2) 把边框线变细,但这样也会把整个元素也缩小,所以这里使用伪元素 :before 来画边框线,这样缩小边框线就不会影响到整个元素。
  • ScrollBar轮播组件的原理
    • 动态设置container的高度
    • 每次轮播完所有元素时,要把第一个元素接到后面,否则动画就不连续了
      • 第一个元素的动画执行完后,要把整个container重置位置。并且是要等一个transition时间后即等本次动画结束后,再重置,不能立即重置。不然会导致最后一次动画没有执行。
      • 注意:把第一个元素接到后面之后,其实整个container的位置并没有变,还是在一轮动画轮播后的那个位置。只是第一个元素的位置空了。所以要把container的位置重置。这样动画就能连续了。
      • 我的疑问是为什么不能直接把container接到后面呢,而非要把第一个元素接到后面呢?实践之后发现根本没办法把container接到后面,因为container在执行动画,你不能让它既干这又干那。
需求分析

image.png

5-21-5~24-自定义hooks-useCountDown实现倒计时逻辑

课程知识点
  • 使用requestAnimationFrame(或setTimeout)计时
    • requestAnimationFrame是浏览器提供的一个动画帧回调函数,浏览器1秒内刷新60帧。每刷新完一帧就会触发requestAnimationFrame函数。帧动画回调函数的执行次数取决于「屏幕刷新率」,以 60Hz(表示每秒钟图像刷新的次数)的屏幕来说,约 16.7ms 会刷新一次。
    • 为什么不使用setTimeout?
      • 因为setTimeout有时不准
  • useCountDown的设计
    • start方法:开始计时
    • pause方法:暂停计时
    • reset方法:重置时间
    • current变量:当前时间
    • remain变量:倒计时的剩余时间(用于暂停计时后重新计时)
  • 性能优化:毫秒级/非毫秒级更新
    • 判断是否需要毫秒级的计时
    • 当前剩余时间currentRemain是否与remain为同一秒,这样可以大大减少页面渲染的次数(不懂这里是为什么)
额外知识点
  • 获取当前时间戳:new Date().getTime() Date.now()
    • 在执行一万次时,Date.now()的性能优于new Date().getTime(),会快2ms。所以基本可以忽略不计。
  • Math.max(a,b)

5-25~5-31 基础组件swipe轮播图

课程知识点
  • useParent和useChildren使用provide/inject实现跨组件通信
  • getCurrentInstance获取组件实例
  • useExpose实现暴露组件方法
  • useTouch实现touch封装
  • useEventListener实现事件监听
  • onMountedorActivated封装生命周期
额外知识点
  • 书写vue组件的另一种方式:tsx
import { defineComponent } from 'vue';

export default defineComponent({
  name,
  props: {},
  setup() {
    return () => {
      <div></div>;
    };
  }
});
  • 作类型断言时,const可以作为类型。as const。意思是它为只读的字面量类型。指定为字面量类型后,ts不会再做额外的类型断言从而触发类型错误了。
    export function createBEM(name: string) {
       return (el?: string, mods?: Record<string, boolean>) => {
         let result = `${name}${el ? `__${el}` : ''}`;
         if (mods) {
           const modsStr = Object.keys(mods)
             .filter((mod) => mods[mod])
             .map((mod) => ` ${result}--${mod}`)
             .join('');
           result += modsStr;
         }
       };
     }
    export function createNamespace(name: string) {
       const prefixedName = `op-${name}`;
       return [prefixedName, createBEM(prefixedName)] as const;
     }
     // 如果不加as const,则使用createBEM时ts会推断出它可能为一个字符串。而我们不能对字符串进行函数调用,加上as const后,ts就不会再推断它为一个字符串了。
    
  • Record<string, boolean> 是 TypeScript 中的一种类型表示,它表示一个键为字符串、值为布尔值的对象。
  • 在 Vue 3 中,使用 computed 函数创建的计算属性返回一个带有 value 属性的响应式对象。
  • setup函数

image.png

  • 泛型语法
export function useExpose<T=Record<string,any>>(apis:T){}

image.png

  • onActivatedonDeactivated生命周期
    • 这两个生命周期只有在keep-alive模式下才可以使用。
    • onActivated:进入页面时会触发。当组件初次加载时会执行onMounted与onActivated,当从别的页面跳转到指定页面时,只有onActivated会被触发。
    • onDeactivated:离开页面时会触发。当组件销毁时会执行onUnmounted与onDeactivated,当从别的页面切回指定页面时,只有onDeactivated会被触发。(但还是不明白它俩的区别,怎么区分什么时候是组件销毁,什么时候是切页面呢?)
  • Ref
    • Ref 是用来包装响应式数据的对象。Ref 类型通常用于对一个数据进行响应式包装,使其在数据发生变化时能够触发视图的重新渲染。
  • EventTarget
    • 用来表示可以成为事件目标的对象,比如 DOM 元素

5-32 使用<component>实现标签页动态展示内容

课程知识点
  • <component>内置元素的介绍和使用
    • 内置特殊元素
      • Vue默认提供的一些特殊的模板语法
      • 具有类似组件的特性,但它们并非真正的组件,例如<component><slot><temolate>,在模板编译期间就会被编译掉。
    • <component>
      • 用于渲染组件或元素的“元组件”
      • 渲染的实际组件由is属性决定
        • 当is是字符串,它既可以是HTML标签名,也可以是组件的注册名。
        • is也可以直接绑定到组件的定义。
  • VantTab的使用
  • 粘性布局和VantSticky的使用
    • sticky组件默认隐藏,通过滚动监听sticky组件是否滚动到了原本位置,如果是,就将sticky组件设置成fixed布局。

5-35~5-37 基础组件list组件实现滚动加载

课程知识点
  • useRect.ts
  • useScrollParent.ts
额外知识点
  • onUpdated生命周期
    • 当响应式状态发生变化而更新其DOM树之后调用。

5-38~5-39 商家列表项组件开发

课程知识点
  • 阻止事件冒泡
    • 使用.stop修饰符

5-40~5-41 性能优化-使用IntersectionObserver实现图片懒加载directive

课程知识点
  • 图片懒加载的介绍以及实现原理
    • 一般跟分页加载、整屏加载有关
    • 图片在屏幕不可见区域
    • 图片被滚动到可见区域的时候,才去加载图片资源
  • IntersectionObserver API介绍
    • 什么是IntersectionObserver
      • 一种异步检测目标元素与祖先元素或viewport相交情况变化的方法
      • viewport就是视口,一般就是指可见区域
      • 除了图片懒加载可以使用,还有内容无限滚动
    • 如何使用IntersectionObserver * 创建一个intersection observer对象 * 给定一个目标元素进行观察
  • Vue3 指令Direction以及插件机制的介绍
    • Vue3指令Direction
      • 指令指的是v-model、v-if...
      • 重用涉及普通元素的底层DOM访问的逻辑
      • 由一个包含类似组件生命周期钩子的对象来定义
      • 如果我们想定义一个全局指令,可以借助插件机制来完成。这样是为了让代码看起来更简洁,清晰。也可以通过app.directive()直接注册到全局组件上,但是这样逻辑就要写在main.ts中了。以下的实例分别是通过插件和直接注册的方式来实现的懒加载指令。
      //插件机制
      import type { App, DirectiveBinding } from 'vue'
      
      const vLazy = (observer: IntersectionObserver) => {
        return {
          beforeMount: (el: HTMLImageElement, binding: DirectiveBinding) => {
            el.classList.add('op-lazyload')
            const { value } = binding
            // <img data-origin="" />
            if (value) {
              el.dataset.origin = value
              observer.observe(el)
            }
          },
          beforeUpdate: (el: HTMLImageElement) => {
            observer.unobserve(el)
          },
          updated: (el: HTMLImageElement, binding: DirectiveBinding) => {
            el.dataset.origin = binding.value
            observer.observe(el)
          },
          unmounted: (el: HTMLImageElement) => {
            observer.unobserve(el)
          },
        }
      }
      
      const lazyPlugin = {
        install(app: App) {
          const observer = new IntersectionObserver(
            (entries) => {
              entries.forEach((item) => {
                if (item.isIntersecting) {
                  // 开始加载图片,把 data-origin 的值放到 src
                  const el = item.target as HTMLImageElement
                  el.src = el.dataset.origin as string
                  el.classList.remove('op-lazyload')
                  // 停止监听
                  observer.unobserve(el)
                }
              })
            },
            {
              // 交叉视图的 100ps,才开始派发事件
              rootMargin: '0px 0px -100px 0px',
            }
          )
          app.directive('lazy', vLazy(observer))
        },
      }
      
      export default lazyPlugin
      
      
      //全局注册
      import { createApp } from 'vue'
      
      const vLazy = (observer) => {
        return {
          beforeMount(el, binding) {
            el.classList.add('op-lazyload')
            const { value } = binding
            if (value) {
              el.dataset.origin = value
              observer.observe(el)
            }
          },
          beforeUpdate(el) {
            observer.unobserve(el)
          },
          updated(el, binding) {
            el.dataset.origin = binding.value
            observer.observe(el)
          },
          unmounted(el) {
            observer.unobserve(el)
          },
        }
      }
      
      const observer = new IntersectionObserver(
        (entries) => {
          entries.forEach((item) => {
            if (item.isIntersecting) {
              const el = item.target
              el.src = el.dataset.origin
              el.classList.remove('op-lazyload')
              observer.unobserve(el)
            }
          })
        },
        {
          rootMargin: '0px 0px -100px 0px',
        }
      )
      
      const app = createApp({})
      
      app.directive('lazy', vLazy(observer))
      
      // 其他的组件注册、挂载等操作
      app.component('your-component', {})
      
      app.mount('#app')
      
      

image.png

  • Vue3插件Plugin机制
    • 一种能为vue添加全局功能的工具代码
    • 一个拥有install()方法的对象,也可以是一个安装函数
    • 插件发挥作用的常用场景
      • app.component()和app.directive()
      • app.provide()
      • app.config.globalProperties
      • 以上三种都包含 image.png
![image.png](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f707e266aafd456389b950ef5bf5d682~tplv-k3u1fbpfcp-jj-mark:0:0:0:0:q75.image#?w=982&h=359&s=113561&e=png&b=1f1f1e)

6-1 我的页面

课程知识点

无,只是搭建了页面的样式。

额外知识点
  • css布局:如果想指定一行有4个元素,可以使用grid布局。
display:grid;
grid-template-column:repeat(4,1fr);
  • css全屏样式:让div充满全屏。以前我都是指定height,但是因为手机有导航栏,且高度都不一样,所以很难准确的计算出height。如果盲目的使用height:100vh,还会导致出现不必要的纵向滚动条。现在可以使用定位来解决这个问题。
background:#ccc;
position:absolute;
top:0;
right:0;
bottom:0;
left:0;

6-2~6-5 自定义hooks-useAuth实现登录页面逻辑

课程知识点
  • 介绍整体登录的逻辑 image.png image.png

image.png

image.png

image.png

  • 登录页实现
    • 输入账号密码信息
    • 使用vant-ui的vanForm组件来实现
  • useAuth的实现
    • 调用useAuth返回的方法请求登录接口
    • server后端项目里实现token的生成和校验逻辑

6-6~6-9 自定义hooks-useUserStore实现用户状态管理

课程知识点
  • Pinia以及状态管理的介绍
    • 什么是Pinia
      • vue官方提供的一个拥有组合式API的Vue状态管理库
      • 允许跨组件或页面共享状态
        • 更强的团队协作规定
        • 与Vue DevTools集成,包括时间轴、组件内部审查和时间旅行调试
        • 模块热更新
        • 服务端渲染支持
        • 更简洁直接的API
        • 组合式风格的API
        • 更完善的类型推导
    • 什么是状态管理
      image.png
      • 每个vue组件实例都已经在管理自己的响应式状态

        • 状态:驱动整个应用的数据源
        • 视图:对状态的一种声明式映射
        • 交互:状态根据用户在视图中的输入而作出相应变更的可能方式
      • 多个组件共享一个共同的状态

        • 多个视图可能都依赖于同一份状态
        • 来自不同的视图的交互可能需要更改同一份状态
      • 如何实现共享同一个状态

        • 将共享属性提取到公共组件里面,再通过props透传下来。但是会导致透传问题,不够理想。
        • 抽取共享状态全局单例
  • 如何实现useUserStore
    • 如何实现一个store
    defineStore(id,optionData/setUpFunc)
    
       * state是store的数据(data)=> ref
       * getters是store的计算属性(computed) => computed
       * actions是方法(methods)=> function
    
    • store的缺点
      • 因为store是存在浏览器内存里的,刷新页面时,重新加载页面并重新初始化应用程序,导致浏览器的内存中存储的页面数据、js对象、样式都会被清空,所以当页面刷新时,所有的状态都会丢失。
      • 所以需要对store进行持久化存储,避免刷新页面状态丢失问题。 image.png
额外知识点
  • 箭头函数只有一条返回语句时,可以省略return关键字来声明返回值。
const getDefaultUserInfo: () => IUserInfo = () => ({  
    id: '',  
    avatar: '<https://b.yzcdn.cn/vant/icon-demo-1126.png>',  
    nickname: '请登录',  
})
  • 将函数的入参直接解构,方便使用。
  const setInfo = ({ token, userInfo }: IUserState) => {
    state.value.userInfo = userInfo
    state.value.token = token
  }

6-10 自定义hooks-useLocalStorage保存用户状态信息(store的持久化)

课程知识点
  • 为什么需要做store的持久化
    • 因为store是存储在浏览器内存中的,页面刷新时,内存清空,导致store丢失。
  • localStorag的介绍
    • 是浏览器的API,将数据保存到浏览器本地
    • 同源限制
  • useLocalStorage的实现
额外知识点
  • ??空值合并运算符

    • ?? 是 JavaScript 中的空值合并运算符,也被称为 nullish 合并运算符。它是 ECMAScript 2020 (ES11) 中引入的一种新的语法。当左侧的操作数为 null 或 undefined 时,它返回右侧的操作数,否则返回左侧的操作数。
  • 使用json.parse时,一定要对参数进行判断,因为对undefined进行json.parse时会报错。

  • 对可能出现的错误,进行try catch包裹

image.png

  • 解构时对变量进行重命名
  const {
    value: $userInfo,
    setValue: $setUserInfoValue,
    removeItem: $removeUserInfoItem,
  } = useLocalStorage('userInfo', getDefaultUserInfo())
  • 为什么不直接读取localStorage的值,而是先读取store呢?
    • 因为读取localStorage是非常耗时的操作,所以先读store。而且当store中没有数据时,如果localStorage中有,也要重新设置store的值。

7-1 业务组件-ShopView商家详情页

额外知识点
  • interface也可以像class一样使用extends关键字

7-2~7-3 业务组件 - 商家头部组件开发

7-4~7-5 商品列表组件开发

额外知识点
  • 动态组件的使用,如果每个tab签是展示不一样的内容。可以使用<component :is="">来动态渲染组件。
  • 注意定义在发起网络请求时,定义返回的数组类型以及期待返回的数据类型。
  • 获取路由参数时,通过解构的方式。
     {id} = useRoute().params
    
  • defineProps<IProps>()。这句代码中<IProps>是用来指定函数的参数类型的。
  • .shop-list[@click="handleClick"],快捷代码。

7-6~7-10 购物车控件组件开发

课程知识点
额外知识点
  • 如果发现zIndex比较小的覆盖在zIndex较大的上面,说明是层级上下文的原因。
  • 当zIndex!=0时,会有一个单独的层级上下文,会脱离原本的层级上下文。

7-11 自定义hooks-useTransition实现加入购物车效果

课程知识点
  • 实现了抛物线效果
额外知识点
  • beforeEnter
    • beforeEnter 是 Vue.js 中的过渡钩子函数,它是在元素进行过渡动画之前被调用的。具体来说,beforeEnter 钩子函数在元素被插入到 DOM 之前立即触发,在此时可以通过一些操作来准备元素的过渡。
    • 这个钩子函数通常用于设置初始状态,例如将元素的初始位置或样式进行一些调整,以便在过渡动画开始时有一个良好的起点。比如,你可以在 beforeEnter 钩子函数中设置一些初始的 CSS 属性,然后在 enter 状态执行过渡动画,最终在 afterEnter 钩子函数中实现过渡动画的结束状态。
    • 总体来说,使用 beforeEnter 钩子函数可以让你在元素过渡动画开始之前进行一些准备工作,以确保过渡动画的流畅性和准确性。

7-12 自定义hooks-useEventBus 使用事件机制实现跨组件通信

// use/useEventBus.ts
import { EventEmitter } from '@/utils/event'

let eventBus: EventEmitter

export function useEventBus() {
  // 单例模式
  if (!eventBus) {
    eventBus = new EventEmitter()
  }
  return eventBus
}

// utils/event.ts
type Fn = (...args: any[]) => void

interface Events {
  [name: string]: Fn[]
}

export class EventEmitter {
  events: Events
  constructor() {
    this.events = {}
  }
  on(type: string, fn: Fn) {
    if (!this.events[type]) {
      this.events[type] = []
    }

    this.events[type].push(fn)
  }

  off(type?: string, fn?: Fn) {
    if (!type && !fn) {
      this.events = {}
      return this
    }
    if (type) {
      if (!fn) {
        this.events[type] = []
        return this
      }
      const events = this.events[type]
      if (!events) {
        return this
      }
      let count = events.length
      while (count--) {
        if (events[count] === fn) {
          events.splice(count, 1)
        }
      }

      return this
    }
  }

  emit(type: string, ...args: any[]) {
    const events = this.events[type]
    if (!events) {
      return
    }
    let ret
    for (let i = 0; i < events.length; i++) {
      const fn = events[i]
      if (fn) {
        ret = fn.apply(this, args) as unknown
        if (ret === true) {
          return ret
        }
      }
    }
  }

  destroy() {
    this.events = {}
  }
}