1 Star 1 Fork 1

ellise/vue3-learn

加入 Gitee
与超过 1200万 开发者一起发现、参与优秀开源项目,私有仓库也完全免费 :)
免费加入
该仓库未声明开源许可证文件(LICENSE),使用请关注具体项目描述及其代码上游依赖。
克隆/下载
贡献代码
同步代码
取消
提示: 由于 Git 不支持空文件夾,创建文件夹后会生成空的 .keep 文件
Loading...
README

vue3-learn

vue3-learn是vue3的基础学习课,内容包含:

  1. vue的优势;
  2. vue的开发环境;
  3. 第一个hello word;
  4. ts vs js;
  5. vmdom vs dom;
  6. less vs css;
  7. vue特性与原理讲解;
  8. vue进阶篇: 路由;
  9. vue进阶篇: pinia状态管理;
  10. vue高级用法: 装饰器;
  11. vue的seo优化
  12. 选讲:宏任务与微任务
  13. 选讲:渲染优化之重绘与重排
  14. 选讲:TS类型推断
  15. vue发展趋势;

1. vue的优势

vue是目前最流行的前端框架之一, 提到vue为什么会流行起来,它具有什么优势?

我们就不得不提到一个话题, 前后端分离;

为什么需要前后端分离呢?

说到前后端分离, 就必须先理解web项目中分为前端渲染与后端渲染;

所谓后端渲染指的是, 界面的呈现, 是是通过后端拼接html代码, 得到的;
前端渲染指的是, 界面的是由js代码操作dom后, 得到的;

由此可知, 后端渲染是把整个html代码全部拼接好后填充给页面, 而前端渲染则是不用返回整个html页面, 仅返回数据项, 然后前端js根据数据项来动态操作dom完成界面的变化.

随机互联网技术的发展, 开发人员越来越偏向于前端渲染, 而后端渲染慢慢退出了历史舞台;

我们来看看后端渲染有哪些劣势:

  1. 后端渲染意味着大量占用服务器的运算资源去做界面拼装运算, 而客户端浏览器自身的显示能力(硬件加速)没有很好的发挥出来;
  2. 后端渲染, 因为返回大量html代码, 篇幅较大, 占用网络带宽较多, 不想数据项(json)这么精简;
  3. 后端渲染, 页面加载或页面跳转过程是白屏, 无法交互状态, 也没法维持完成的生命周期, 造成动画和事件无法连贯;
  4. 后端渲染, 因为返回html代码加载到页面上总是一次销毁原界面和创建新节点的过程, 有时只是某个dom节点很小的改动, 但却要全部销毁再创造一次, 无法精确的少量改动;
  5. 后端渲染, 因为既要写后端代码查询数据, 又要写html拼接, 容易造成代码混乱, 也很难提供专业性;

前端渲染的主要优势, 传输数据量小, 专业度提升, 渲染流畅;

同样是前端渲染框架的react, angular, 为什么vue更受欢迎?

  1. vue是国内产品, 更符合国人的使用习惯, 也有更好的中文帮助;
  2. vue的SFC单文件组件, 极大的简化了页面组件的开发, 后续章节会介绍;
  3. vue的生态覆盖了从web端到, 移动端, 甚至桌面端, 让选型没有后顾之忧;

当然其他框架也有自身的优劣势:

  • react函数式组件可自由组合使得灵活性更高但缺乏可视化, 对开发者有较高要求; -- vue则是SFC组件;
  • angular容器委托, 可优雅的改写原始dom, 与传统dom类原生js框架, 如jquery, 有非常良好的兼容性; -- vue则需要较难使用使用原生js, 好在大部分都提供相应的npm版本;

2. vue开发环境

目前vue主流版本是vue3, 其核心类库有: ts4, less4, webpack4, esnext; 后续章节会介绍

vue的开发建立在nodejs, vue@cli脚手架

2.1 安装nodejs

下载地址: https://nodejs.org/en

按照提示下载相应平台的安装包, windows/macOs/linux等 完成安装后, 打开命令行:

npm -v

看到显示版本号则安装成功

nvm是nodejs的版本控制, 有时我们需要随时切换nodejs的版本 下载地址: https://github.com/coreybutler/nvm-windows/releases

nvm -v

看到版本号则安装成功

安装指定nodejs版本

# 安装16.x.x版本nodejs
nvm install 16
# 切换16.x.x版本
nvm use 16
# 查看切换后的版本号
node -v
# 查看已安装(可切换)的版本
nvm list

2.2 安装@vue/cli脚手架

该脚手架提供了命令行级的快捷创建vue项目骨架 安装命令如下:

npm install --global @vue/cli
# 查看vue帮助, 以确保安装成功
vue --help

创建骨架项目:

# 创建名为vue3-learn骨架项目
vue create vue3-learn
# 脚手架会出现大量菜单供选择, 按照目前主流去选取: ts+less+eslint-standard

还可以给已创建出来的骨架项目添加安装项:

# 进入骨架项目
cd vue3-learn
# 为当前项目添加less支持
vue add less

2.3 启动vue项目

进入已生成的vue项目中, 执行命令:

npm run serve

此时按照启动提示的链接地址,复制到浏览器即可访问, 默认是 http://localhost:8080/

serve开发模式下, 修改任何代码, 浏览器会自动的发生改变而无需手动刷新.

如果要发布则执行打包命令:

npm run build

打包生成的文件目录是dist, 发布只需要将dist拷贝到, web服务器(如nginx)可访问的目录之下即可, 访问时, 只需要像访问静态页面一样输入web服务器对应的访问路径即可;

2.4 vue开发插件

几乎所有IDE都提供了vue的开发插件, 如Idea Intellij, HBuilder, vscode, eclipse等; 只需要到对应的插件超市搜索"vue", 进行安装即可. vue开发插件提供了, 语法高亮, 检查类型, 追踪定义与调用关系等非常实用的功能;

3. 第一个hello world

vue的定义组件采用SFC方式, 也就是单文件组件. 每个.vue文件就定义了一个组件类型

<!-- 来自骨架项目生成的.vue -->
<template>
  <div class="home">
    <img alt="Vue logo" src="../assets/logo.png">
    <HelloWorld msg="Welcome to Your Vue.js + TypeScript App"/>
  </div>
</template>

<script lang="ts">
import { Options, Vue } from 'vue-class-component'
import HelloWorld from '@/components/HelloWorld.vue' // @ is an alias to /src

@Options({
  components: {
    HelloWorld
  }
})
export default class HomeView extends Vue {}
</script>

<style lang="less">

</style>

一个vue组件组件主要包含三个部分: template, script, style

  • template定义的是类似于html5格式的vmdom, 大致理解成视图层即可;
  • script定义的是vue组件的生命周期函数, 官方翻译叫钩子函数, 是主要的model模型层;
  • style是样式层, 与html的样式是一样的作用;

这里script使用的是ts, style使用的是less, 这些在后续的章节会重点进行说明

4. ts vs js

ts全称typescript, 目前主流版本为4.5+. js全程javascript, 它是一切web代码的基础, ts最终也会翻译成js运行.

既然最终运行的都是js, 那么为什么不直接编写js, ts存在的意义是什么?

在讨论这个问题之前我们先要知道一系列的ECMAScript规范, 常见有es6, es2015, es2020, 以及esnext;

ECMAScript是一个非盈利的js语法规范制定组织, 我们都知道js诞生于1995年, 但相较于现在编程语言其语法便捷性已经远跟不上, 现在语言了, 所以才出现了ECMAScript来扩充更多的语法.

js天然的有以下不足:

  1. js是弱类型语言, 而类型检查又是现代编程的, 重要代码质量要求, 所谓写错了去检查肯定比不上直接禁止一些写法更好;
  2. js缺乏面向对象的编程方式, 虽然js后来也提供了class, 但总体上仍是面向过程的编程思想;
  3. js缺乏扩展自定义写法的语法校验, 也就是禁止写法,而不是写了再找错;
  4. js在低版本浏览器中需要自行解决兼容性写法, 如ie6, ie8等;

ts正是为了补充js不足而产生的, ts其实最大的特点是让编写代码变得可读性高, 易维护;

4.1 ts基本语法

ts常以.ts文件命名, 如果vue中需要指明<script lang="ts">, 写法上多了类型声明:

// src/router/index.ts
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'
import HomeView from '../views/HomeView.vue'

const routes: Array<RouteRecordRaw> = [
  {
    path: '/',
    name: 'home',
    component: HomeView
  },
  {
    path: '/about',
    name: 'about',
    component: () => import('../views/AboutView.vue')
  }
]

const router = createRouter({
  history: createWebHistory(process.env.BASE_URL),
  routes
})

export default router

上例中: routes被声明了Array<RouteRecordRaw>类型, 这是显式声明; router则通过createRouter()返回值的赋值自动推断类型;

4.2 import与export

es6+ 的esm规范, import与export写法如下: esm是目前主流的js/ts代码模块格式, 此外还有common, 这里不做扩展;

// default导出
const a = 123;
export default a; 
// import default, as是重新命令另一个变量来接收的意思, 以下两种写法是等效的
import a from 'a.ts';
import { default as a1 } from 'a.ts';
// export { default } from 等同于import了再export
export { default as a2 } from 'a.ts';

非default的import/export

// 输出a, b 非default
export const a = 1;
export const b = 2;
// 获得非default的所有组成a3集合, 这里是{a,b}
import * as a3 from 'a.ts';
// 单项获取a
import { a as a4 } from 'a.ts';

4.3 ts的类型声明文件

刚使用ts的开发者特别不习惯, ts的类型声明文件, 也就是以.d.ts后缀的一系列文件. 前面我们讲到, ts是一个强类型的语言, 声明时要么显式的指出变量的类型, 要么通过复制自动推断类型. 总之变量类型一旦被确定后, 就不能被其他类型赋值.

.d.ts文件就是对跨类库调用时, 获得ts的类型.

一般来说, 系统类型都会在node_modules/@types/目录下有对应的.d.ts类型声明, 如jest, 会有对应的@types/jest. 但如果是自定义的包, 则需要由tsconfig.json指定declaration: true, 并且outDir: './@types'指定生成.d.ts目录.

.d.ts(类型定义文件)的简单介绍

declare var // 声明全局变量

declare function // 声明全局方法

declare class // 声明全局类

declare enum // 声明全局枚举类型

declare namespace //声明全局对象(含有子属性)

interface // 声明全局接口

type //声明全局类型

declare module // 与namespace一样, 只不过可以路径, 而namespace是单个词

interface vs type: interface只能声明对象类型(需要new出来), 多个同名定义自动合并, 原始类型不可; type则不能二次定义.

手写d.ts:

虽说绝大多数时候,只要不作为npm库打包发布给外部安装使用,基本是不会出现手写d.ts文件,就算有d.ts声明文件的需要,typescript内置tsc工具也可以自动去生成。

但是有些特殊情况下,还是需要掌握少量的d.ts类型声明的语法,我们来看下下面的使用场景。

/// 原声明类型: 可能是tsc生成的
interface OSS {
  name: string
  pathConfig: {
    [peth: string]: {
      enable: boolean
    }
  }
}
/// PS: OSS是一个TS声明的类型,拥有name和pathConfig两个属性,其中pathConfig是一个键值对对象类型,key的部分代号为path类型为string,value的也是个对象类型
// 此数据结构,大致是表示某些路径对应的文件是否被启用

下面是调用方:

// 页面调用时可能会,只会对pathConfig进行修改,而不需要总是传递整个OSS对象
// 当需要传递一个pathConfig类型是,ts会要求你必须指定pathConfig是什么类型;
// 如下所示,思考一下,??的部分,我们怎么表示这个类型?
function changePathConfig(path: string, pathConfigValue: ?? ):void {
  ///.....
}

/// 下面我们介绍两种办法:
/// 办法一:要求OSS单独把pathConfig属性新类类型
interface OSSPathConfigValue {
  enable: boolean
}
interface OSS {
  name: string
  pathConfig: {
    [peth: string]: OSSPathConfigValue
  }
}
/// 这样??的部分就是OSSPathConfig
/// 弊端:但是这么做非常困难,首先OSS如果是公用组件,原作者和调用方,很可能不是同一个人;第二作为公用组件,会因为调用方细粒不同度拆解成复用度不多,但非常细的各种属性类型,所有属性都要分别写类型,这是不现实的;


/// 办法二:调用方自己创建符合原属性接收的类型,可以这么做,是因为ts的类型校验只是检查成员的匹配,所以没有同源(必须来自OSS下的定,成为同源,java是同源的)的要求。
interface OSSPathConfigValue {
  enable: boolean
}
function changePathConfig(path: string, pathConfigValue: OSSPathConfigValue ):void {
  ///.....
}

// 定义当前.d.ts中xxx.vue的export输出的类型
declare module 'xxx.vue' {
  // 输出类型
  import type { DefineComponent } from 'vue'
  const component: DefineComponent<{}, {}, any>
  export default component
}

interface aaa {
  a: number;
}
// 当前.d.ts输出的类型
export default aaa;

推荐学习: es6中异步编程Promise then/catch, async / await;

5. vmdom vs dom

这里说的dom指的是, 直接对原始dom进行操作.

所以本章节主要讨论vue的vmdom虚拟dom操作, 与直接对原始dom操作的区别和优略势.

对原始dom的操作通常是, 使用selector获得dom对象, 然后对其调用属性设置,样式设置,以及事件监听等完成界面的变换.

// 原始dom操作
let name = 'abc';
document.getElementById('[name="show_name"]').value = name;
document.getElementById('[name="name"]').value = name;

// vmdom操作
<template>
  <div>
    <!-- 核心代码: v-model捆绑变量name -->
    <input type="input" name="show_name" v-model="name" />
    <input type="hidden" name="name" v-model="name" />
    <button type="button" @click="setName">设置值</button>
  </div>
</template>
<script lang="ts" setup>
  import { ref } from 'vue'
  const name = ref('abc');
  function setName() {
  // 核心代码: 修改值, 内部自动去影响视图
    name.value = 'abc';
  }
</script>

代码解读:

  • 原始dom操作会每次都会真实的设置dom的值, 即使show_name和name原本值和原来一样;
    而vue的虚拟dom发现新的值与原来的值一样,则并不会产生真实的dom操作; 事实上vmdom才有了非常强大的diff算法, 既不会产生无效的dom操作, 同时也不会产生重复的dom操作, 达到性能提升;
  • 原始dom的写法, 是一种过程式编程, 还额外的产生许多为了selector选择器唯一节点的, 一些id或者class, 而且还需要关系selector出来的原始dom是什么类型, 而且多处使用时每次都要selector额外增加的查询, 既不美观也容易堆积屎山代码;
    vue则采用mvvm方式设计, 利用v-model让两个input与变量name产生双向捆绑, 修改name就会产生这两个input的值发生变化.这样一来, 关注点不在是原始dom是什么类型了, 而是如何修改变量name, 剩下的事情vue会帮你把name的变化反应给两个input.
    有关mvvm双向捆绑,将在后续章节中重点去讲

6. less vs css

less是post-css技术, 最终运行的仍然是css代码, 但post-css可以预编译和有效的组织css.

我们来看less的优势:

// less多级样式
.parent {
  font-size: 16px;
  
  .sub1 {
    color: red;
  }
  
  .sub2 {
    color: green;
  }
}
// 对应原始css
.parent {
  font-size: 16px;
}
.parent .sub1 {
  color: red;
}
.parent .sub2 {
  color: green;
}

当然less还有许多强大的功能, 如css变量, 多前缀-webkit- / -ms-样式等, 这里就不展开讨论了.

7. vue特性与原理讲解;

7.1. MVVM设计

前人在大量界面开发实践过程中, 发现适用于传统后端开发的MVC, 实际上不太适合前端使用, 原因是C层越来越臃肿, 就像前面范例中看到C层总是重复的selector出原始dom; 后来提出了简化MVC的MVVM与MVP两个开发模式, 准确的说MVVM才是最终形态, MVP是不那么完美的MVVM.

MVVM指的是Model层用于定义数据项, View层用于显示的html, ViewModel层用来实现Model层与View层的双向捆绑;
对照第5章中, ref()定义的name变量就是M层, <input/>标签就是V层, 而v-model="name"就是VM层

MVVM主要功效就是M层的变化, 通过双向绑定的表达式, 自动的同步到与之对应的V视图上.
如范例中, 它带来的便捷是改变单项数据name的值, 与之捆绑的所有视图都发生的改变, 这样就把原先界面每个dom寻找和类型的关注点处理集中到如何改变name值上, 就是model控制的页面显示

关于vue实现的双向绑定, vue3使用的是proxy+绑定记录, 这里不进行展开

  • 只有声明为ref()或者reactive()的变量才拥有双向绑定的能力;
  • 声明为v-model的视图才具备VM的双向绑定行为, 否则就需要分别绑定读与写;

7.2. 组件功能

vue的组件提供常用功能有:

  • defineProps()用于接收调用方传参;
  • defineEmits()用于上抛事件让调用方响应;
  • computed()用于定义特殊的变量, 当参与计算的变量发生改变时, 这个变量会自动重新计算;
  • watchEffect()用于监控, 参与计算的变量发生变化时, 触发回调;
  • 生命周期类函数: onBeforeMount() -> onMounted() -> onBeforeUpdate() -> onUpdated() -> onBeforeUnmount() -> onUnmounted()

参照1: https://cn.vuejs.org/api/composition-api-lifecycle.html#onbeforeunmount

参照2:https://cn.vuejs.org/api/reactivity-advanced.html#triggerref

vue支持多种定义形式, 以下几种都是等效的:

// 1. setup方式: 推荐-按需加载
<script lang="ts" setup>
/// 不用提供 export default ...
</script>

// 2. 函数方式: 不推荐-代码量太大, 全部都要写
<script lang="ts">
import { defineComponent } from 'vue';
export default defineComponent({
  ...
});
</script>
// 3. 装饰器方式: 推荐-声明与实现分开, 装饰器将在后续章节有介绍.
<script lang="ts">
import { Options, Vue } from 'vue-class-component';
import aComponent from 'aComponent.vue';

@Options({
  components: {
    aComponent,
  },
})
export default class App extends Vue{}
</script>

vue的核心功能:

<!-- 父组件:调用方 -->
<template>
  <div>
    <sub-component propA="123" @change="someFunction" />
  </div>
</template>
<script lang="ts">
function someFunction(id: number) {
  /// 响应子组件上抛事件
}
</script>

<!-- 子组件:被调用方 -->
<script lang="ts">
import { defineProps, defineEmits, computed, watchEffect, withDefaults, ref  } from 'vue';
// 下面是定义props属性, 两种等价写法
// 推荐写法
const props=withDefaults(
  defineProps<{ 
    propA: string
  }>(),
  {
    propA: 'abc',
  }
);
// or
const props = defineProps({
  propA: {
    type: String,
    default: 'abc'
  }
})
//==============

// 下面是上抛事件emit, 两种写法
const emit = defineEmits<{
  (e: 'change', id: number): void
  (e: 'update', value: string): void
}>()
// or 推荐写法
const emit = defineEmits<{
  change: [id: number] // 具名元组语法
  update: [value: string]
}>()
//=============

// 自动计算变量title, 参与计算b发生改变而改变 
const b = ref(0);
const title = computed(() => {
  return '标题为:'+ b;
})

// 监控watchEffect, 参与计算c发生改变时, watchEffect函数会触发
const c = ref(1);
watchEffect(() => {
  console.log(c);
});
</script>

7.3. 组件传参

前面说到每个.vue文件就是一个组件, 那么组件内调用另一个组件时, 怎么进行传参呢?

<script lang="ts">
// 定义aProps属性
import { defineProps } from 'vue'

defineProps({
  aProps: {
    type: String,
    default: '',
  }
});
</script>

// 调用组件时传参, "123"
<template>
  <div>
    <aComponent aProps="123" />
  </div>
</template>
<script lang="ts">
import { Component } from 'vue-class-component';
import aComponent from 'aComponent.vue';

@Component({
  components: {
    aComponent,
  },
})
</script>

多级调用时, 传参使用provide()和inject()这里不展开讨论.

推荐学习: vue实现的微任务与宏任务, nextTick()等

7.4. webpack打包工具

vue深度集成了webpack打包工具, webpack是目前主流的js编译打包工具, 上边提及的各种语法支持, 以及不同语言转译(ts->js)都是通过webpack来完成的. webpack主要提供了3类api:

  • hook钩子函数, 提供了整个编译过程的生命周期函数, 可添加监听函数, 如run, Compile, emit, done;
  • loader加载器, 一个加载器就是一次编译的过程, 常见的加载器有, ts-loader, vue-loader, less-loader;
  • plugin插件, 上面提及的hook与loader都需要以插件的形态注册到webpack中,
    plugin充当了三个作用:
  1. 什么时候插件生效,也就是注册hook;
  2. 插件使用哪个loader来编译;
  3. 插件对什么格式的路径匹配, 符合路径要求的才会启用对应的loader来编译, 如[*.vue] -> [vue-loader]

此外webpack还有许多常用的功能, 如chunk切分大小和个数, 雪碧图, 自动压缩, 代码加固混淆等都是重要的优化手段, 这里就不做展开说明了.

vue.config.js配置文件提供的静态js配置与链式chainWebpack两种配置方式, 目前主流推荐chainWebpack配置方式

```js const { defineConfig } = require('@vue/cli-service') module.exports = defineConfig({ transpileDependencies: true, chainWebpack: config => { config.watchOptions({ ignored: /node_modules/ }) } }) ```

8. vue进阶篇: 路由;

默认配置: src/router/index.ts

const routes: Array<RouteRecordRaw> = [
  {
    path: '/',
    name: 'home',
    component: HomeView
  },
  {
    path: '/about',
    name: 'about',
    component: () => import('../views/AboutView.vue')
  }
]
  • path是路由路径;
  • component是对应的界面, import是异步加载写法;

此外vue还支持路由嵌套, 这里不做展开.

9. vue进阶篇: Pinia状态管理;

Pinia = vuex5, 它是vue的状态管理框架.

所谓的状态管理, 就是指哪些全局唯一的数据项, 但又不需要被保存下来, 只是针对当前应用内存态(关闭后消失).

全局数据可能会存在, 数据争抢. 同时对这个数据修改和查询, 这就可能出现数据不一致, 也就是事务一致性的概念(那么失败回滚, 要那么全部成功, 不能出现部分成功而未来得及修改剩下的数据) 比如说, 全局数据成功时总共需要修改2项数据, 假如修改第一项与第二项中间, 差一个毫秒, 那就有可能在这一个毫秒内被另一个调用方查询到, 这就是部分成功, 另一部分还没来得及写.

所以Pinia要做的事, 就是没有中间的一毫秒;

Pinia的使用方法如下: Pinia提供了state, getter, actions

  • state定义数据结构和初始值;
  • getter是外部获得的值;
  • actions是设置状态值;
// Pinia定义
// store/others.ts
import { defineStore } from "pinia";
import Pinia from "./index";

export const otherStore = defineStore("other", {
  state: () => ({
    counter: 1,
  }),
  getters: {
    doubleCount: (state) => state.counter * 2,
  },
  actions: {
    increment() {
      this.counter++;
    },
  },
});

// 使用Pinia的方式
import { otherStore } from "@store/other"

const other = otherStore();
console.log(other.doubleCount);
other.increment();

指的注意的是, Pinia本身就是响应式的, 这意味着修改Pinia值, 与之双向绑定的视图也会发生变化;
我们经常利用这一点来使用vue的watch()来监听全局变量值发生变化的事件.

10. vue高级用法: 装饰器

如第2章节中的范例, @Options就是装饰器

<script lang="ts">
import { Component, Prop } from 'vue-class-component'
import HelloWorld from '@/components/HelloWorld.vue' // @ is an alias to /src

@Component({
  components: {
    HelloWorld,
  },
})
export default class HomeView {
  @Prop(String)
  msg!: string
}
</script>

装饰器是es7提供的, 其本身也是一个函数, 它的作用是拦截原函数调用的前后, 也就是AOP.

如上例中, export default ...的调用之前, 被@Component拦截, 并追加了components HelloWorld内容.

一般使用装饰器, 目的是把声明和实现分开, 装饰器通常充当声明的部分, 就像@Prop(String)补充了, 变量是一个props传入属性, 同时类型为String;

11. vue的seo优化

经过前面章节的介绍,我们都前端渲染必须需要依赖浏览器运行js脚本,这样就延申出另一个问题,搜索引擎收录网站时需要遍历网站上所有页面,此时搜索引擎并不能执行js,只能是静态的解析html标签。 这就会造成实际被收录的网页是空白的,进而在seo搜索时没有正确的关键字匹配,排名靠后;

针对seo优化,vue提供两种解决方法:sitemap.xml伪装SSRsitemap.xml是一种由网站管理员主动向所有引擎提供的,格式化数据,也就是告诉搜索引擎每个页面应当收录哪些关键字。 这个需要根据各家搜索引擎的规则来提供,以百度为例,它要求域名根目录下直接能访问到sitemap.xml文件;以google为例,它要求网站主自行在google账号后台上传指定域名的sitemap.xml;

本文主要从SSR方案的角度来实现seo优化。 其核心原理是,把vue预渲染生成静态的html页面,这样就能像真实的后端渲染一样支持seo;

需要使用prerender-spa-plugin插件和vue-meta-info

npm install --save-dev prerender-spa-plugin vue-meta-info

prerender-spa-plugin的作用是生成静态化html vue-meta-info的作用是动态的修改link,title,meta等检索关键字

11.1. prerender-spa-plugin使用

步骤1:安装插件

const path = require('path');
const PrerenderSPAPlugin = require('prerender-spa-plugin');
const Renderer = PrerenderSPAPlugin.PuppeteerRenderer;
module.exports = {
    //publicPath: '/', //打包路径,使用相对路径生成的dist文件夹下的index可以打开
    configureWebpack: {
      plugins: [
         new PrerenderSPAPlugin({
        // 生成文件的路径,也可以与webpakc打包的一致。
        // 下面这句话非常重要!!!
        // 这个目录只能有一级,如果目录层次大于一级,在生成的时候不会有任何错误提示,在预渲染的时候只会卡着不动。
         staticDir: path.join(__dirname,'dist'),
           // 对应自己的路由文件,比如a有参数,就需要写成 /a/param1。
          routes: [
            '/', 
            '/home',
            '/article',
            '/download',
            '/message',
          ],
          // 这个很重要,如果没有配置这段,也不会进行预编译
         renderer: new Renderer({
          injectProperty: '__PRERENDER_INJECTED',
          inject: {
            foo: 'bar'
          },
           // 在 app.vue  onMounted函数中 document.dispatchEvent(new Event('custom-render-trigger')),两者的事件名称要对应上。
          renderAfterDocumentEvent: 'custom-render-trigger',
          renderAfterTime: 6000,
          headless: false
         })
       }),
      ]
   },
}

PrerenderSPAPlugin.routes是被静态化的页面

步骤2:在页面组件中触发事件

function setup() {
    onMounted(() => {
      document.dispatchEvent(new Event('custom-render-trigger'))
    })
}

步骤3:更改路由模式hash转history

const router = createRouter({
  //history: createWebHashHistory(process.env.BASE_URL),
  history: createWebHistory(process.env.BASE_URL),
  routes
})

完成上述配置后,运行后将会在dist下生成静态html页面;

vue-meta-info使用

export default defineComponent({
  name: "Home",
  metaInfo: {
    //改变当前路由的title
    title: "前端小阳仔",
    //改变当前路由的link
    meta: [
      {
        name: "keyWords",
        content:
          "前端博客,前端技术,技术博客,前端小知识,前端作品,前端导航,前端小阳仔,前端框架",
      },
      {
        name: "description",
        content:
          "原创前端技术博客,致力于分享前端学习路上的第一手资料。专注web前端开发、移动端开发、前端工程化、前端职业发展,做最有价值的前端技术学习网站。",
      },
    ],
    link: [
      {
        rel: "前端小阳仔",
        href: "https://code-nav.top/myblog/home",
      },
    ],
  }
});

12. 选讲:宏任务与微任务

宏任务:指的是浏览器自身的提供的事件回调,

第一类宏任务: 事件监听onclick, onfocus等,

第二类宏任务: js触发的,setTimeout,setInterval,setimmediate

宏任务特性:自身就能提供代码的执行能力

微任务:指的是追加到当前宏任务末尾执行的处理函数,Promise,await/async,

我们看下面的执行顺序:

<button onclick="macroClick()">点击事件</button>

function macroClick() {
  // todo 代码块1
  new Promise(function (resolve, reject) {
    // todo 代码块2
    resolve();
  }).then(function () {
    // todo 代码块3
  })
  // todo 代码块4
}


/////////////
/// 顺序:1 -> 2 -> 4 -> 3
  • 思考1:为什么4在3前面?

我们再看下面的执行顺序:

<button onclick="macroClick2(); macroClick1();">点击事件</button>

function macroClick1() {
  // todo 代码块1
  new Promise(function (resolve, reject) {
    // todo 代码块2
    resolve();
  }).then(function () {
    // todo 代码块3
  })
  // todo 代码块4
}
async function macroClick2() {
  // todo 代码5
  await new Promise(function (resolve, reject) {
    // todo 代码块6
    resolve();
  }).then(function () {
    // todo 代码块7
  })
  // todo 代码8
}

/////////////
/// 顺序:1 -> 2 -> 4 -> 5 -> 6 -> 7 -> 8 -> 3 
  • 思考2:为什么4之后不是3,而是5?
  • 思考3:为什么6后面不是8,而是7?

重要知识点:nextTick追加到下次渲染完成之后

由于浏览器的渲染(页面看到改变)发生在最近一次宏任务结束,但有的时候我们需要页面渲染出效果之后,再追加其他代码。 如图:点击tag -> 切换input -> input获得焦点 nextTick.gif 很明显,这一连贯操作,已经跨越了多个事件,也就是多个宏任务

  • 思考题4:宏任务与微任务谁执行更快?为什么尽量使用微任务?
  • 思考题5:nextTick是怎么做到渲染之后执行的?

13. 选讲:渲染优化之重绘与重排

重绘:位置和大小不变的情况下,改变内部显示产生的绘制,渲染涉及绘制自身界面,以及子组件的界面;

重排:改变位置和框体产生的绘制,实际是触发给父组件来绘制;

绝大多数情况下,重绘与重排可能会同时发生, 重绘的性能瓶颈是渲染范围,重排的性能瓶颈是排版影响组件个数; 原则上,重绘范围越小最好,重排影响的样式范围越小越好;

vue通过对div的onChange事件监听, 可以对获得子节点的增删事件,一般被当作监听重绘重排的重要方式。

vue2中提供$forceUpdate()强制重绘,在vue3中已经取消,取而代之的时ref/reactive所捆绑的视图层级。

几个重要的参考标准:

  • 2d动画低于20帧,感觉明显卡顿,分辨率4k以上帧数/3d还要要求更高;
  • 考虑到低网速时5g/3g网络,单次请求小于200k为最佳,大于2M存在明显的不合理;图片,流媒体不算在内;
  • 非动画普通网页,一般接收白屏时间在5s内,超过者明显感觉网速太慢; 如果手机上白屏这个数字则是500ms;

思考题6:怎样减少重绘范围,怎样减少重排?

14. 选讲:TS类型推断

首先我们要明白TS类型校验是怎么实现的: 我们看下面两个例子:

interface MyAttention {
    "auid": string,//"10781982",
    "icon": string,//"http://www.myccmtv.cn/images/default/noface.gif",
    "product_name": string, //"维C使1",
    "sign_info": string,//"纽崔莱(上海)医药服务有限公司",
    "update_num": string,// "5",
    "attent_time": string,//"2023-11-08 17:22:36",
    "product_url": string,//"专区跳转链接,待定"
}

interface MyAttention1 {
  "auid": string,//"10781982",
  "icon": string,//"http://www.myccmtv.cn/images/default/noface.gif",
  "product_name": string, //"维C使1",
  "sign_info": string,//"纽崔莱(上海)医药服务有限公司",
  "update_num": string,// "5",
  "attent_time": string,//"2023-11-08 17:22:36",
  "product_url": string,//"专区跳转链接,待定"
}

/// 声明变量a为MyAttention类型
const a: MyAttention = {
  "auid": "10781982",
  "icon": "http://www.myccmtv.cn/images/default/noface.gif",
  "product_name": "维C使1",
  "sign_info": "纽崔莱(上海)医药服务有限公司",
  "update_num": "5",
  "attent_time": "2023-11-08 17:22:36",
  "product_url": "专区跳转链接,待定"
}

/// 使用MyAttention1类型的b来接收a
const b: MyAttention1 = a;

从上例中,我们发现MyAttention与MyAttention1被声明为两个不同的类型(既没有继承也没有混入),但是TS却可以让MyAttention1直接接收MyAttention变量来正常使用。

这说明TS的类型检查本质上是遍历属性签名的检查,类似与下面的伪代码:

function isMyAttention1 (a) {
  if (typeof a['auid'] === 'string' &&
    typeof a['icon'] === 'string' &&
    typeof a['product_name'] === 'string' &&
    typeof a['sign_info'] === 'string' &&
    typeof a['update_num'] === 'string' &&
    typeof a['attent_time'] === 'string' &&
    typeof a['product_url'] === 'string'
  ) {
    return true
  }

  return false
}

理解这一点后,就方便我们指导TS的类型推导是怎么做到的。

首先TS的类型检查是静态检查,所谓静态检查是这仅在编译时检查,运行或打包出去时不会含有任何TS痕迹的。

类型推导,又叫类型演算,他是对自定义的泛型,进行从key,value到取值范围在内的一系列约束。 一般写法为T infer,常与extends来联用。

TS类型推导写法上有点像三元表达式isTrue? xxx: yyy, 以never为结束,表示never只是做来填充错误的情况, 大致可以理解为任何类型不能是never,一旦满足成为never的条件时IDE(vscode/InterllJ IDEA)就报类型错误。

所以TS类型推断书写的目的就是不让类型走向never

// type类型别名
type aliasType = number;

// interface定义对象类型
interface ObjectType extends Number {
  id: number,
  avatar: string,
}

// 内置类型
// Record - 当作key,value来理解对象的属性
type StringStringMap = Record<string, string>

// 联合类型
type MyUnionType = string | number; // 任选其一,非黑即白

// 交叉类型
type MyIntersectionType = MyUnionType & StringStringMap; // 属性签名融合后,存在灰度

// Partial - 全部属性签名变成非必填
type par = Partial<{
  username: string,
  nickname?: string,
}>// 结果是 { username?, nicknaeme? }

// Required - 全部属性签名变成必填,与Partial相反,省略不讲

// Readonly - 全部属性变为只读,省略不讲

// Pick - 抽取指定名字的属性签名
type pi = Pick<{ a: string, b?: string, c?: string }, "a" | "b">;// { a , b }

// typeof - 把属性当作map解析取value的联合类型
type OT = { a: string, b: number }
type Tyof = typeof OT;// { string | number }

// keyof - 与typeof相似,获取的是key的联合类型,省略不讲

// 字面量类型枚举约束
type ChooseName = 'a' | 'b' | 'c' | 'd'; // 仅允许赋值
const n: ChooseName = 'f';// 错误f不在允许值范围

// 内置条件类型: 
// Exclude - 从一个类型中排除另一个类型,同样也是指属性签名
type Exclude<T, U> = T extends U ? never : T;
type R3 = Exclude<'a' | 'b' | 'c' | 'd', 'a' | 'b' | 'c'>; // R3: 'd'

// Extract - 从一个类型抽取出另一个类型,相同的部分
type Extract<T, U> = T extends U ? T : never;
type R4 = Extract<'a' | 'b' | 'c' | 'd', 'a' | 'b' | 'c'>; // R4: 'a' | 'b' | 'c

// NonNullable - 从类型中那种必填属性
type NonNullable<T> = T extends null | undefined ? never : T;
type R5 = NonNullable<'a' | null | undefined | 'd'>; // R5: 'a' | 'd'

// ReturnType - 从函数类型中拿走返回值类型
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : T;
function getUser() {
  return {
    name: '张三',
    age: 10
  }
}
type ReturnUser = ReturnType<typeof getUser>; // type ReturnUser = {name: string;age: number;}


// Parameters - 从函数类型中拿走参数列表类型
type Parameters<T> = T extends (...args: infer P) => any ? P : never;
function getPerson(a: string, b: number) {
  return {
    name: '李四',
    age: 18
  }
}
type ParamsType = Parameters<typeof getPerson>; // type ParamsType = [a: string, b: number]

思考题7:type与interface定义的类型有什么区别? 思考题8:联合类型与交叉类型有什么不同?什么情况下相同什么情况下不同? 思考题9:对Record类型进行keyof和typeof会得到什么类型?

15. vue发展趋势;

技术发展的趋势

reactJS = vueJS: reactJS函数式组件, vue的SFC单页面组件, 各有优缺点;

webpack -> vite: vite/rollup更灵活配置项更少,天然的按需加载,打包体积更少;

vue2 -> vue3: vue3的proxy提升双向绑定查找数据20%以上性能,响应式ref/reactive脱离VNode实例,script-setup按需加载减少代码量;

参数配置 -> 装饰器: 装饰器的优势(也就是AOP的优势),非常干净的实现多次改写,分段配置,传统做法需要大量值传递。如react reduct.

vue在社区的贡献下, 还全面支持了web向移动端ios, android, 小程序端wx, 百度小程序, 钉钉小程序的转译支持. 桌面端也有对应的实现框架.

可以说学会vue就一统大片前端技术.

空文件

简介

取消

发行版

暂无发行版

贡献者

全部

近期动态

不能加载更多了
马建仓 AI 助手
尝试更多
代码解读
代码找茬
代码优化
1
https://gitee.com/ellise/vue3-learn.git
git@gitee.com:ellise/vue3-learn.git
ellise
vue3-learn
vue3-learn
master

搜索帮助