zl程序教程

您现在的位置是:首页 >  前端

当前栏目

一文帮你掌握vue3这些核心知识点

Vue3知识点 掌握 核心 一文 这些
2023-09-11 14:21:23 时间

前言

vue2已经渐行渐远,vue3早已甚嚣尘上。vue3作为尤大的苦心孤诣之作,在开源世界也是响当当的存在,三分天下有其一。尤大也携名兼千万之资远赴新加坡,全职献身给vue。

再说说为何会有vue

  • 自从有了javascript和DOM API,还有CSS后,程序员就能随意创建HTML元素,和修改它们的样式和行为了。Java Applet,ActiveX还有Flash这些网页插件技术就嗣翘翘了
  • 自从有了jQuery后,操作DOM元素就变得更方便了,而且解决了浏览器兼容性问题,统一了思想和API。前端的面貌也就焕然一新了
  • 自从有了AngularJS, Vue和React这些框架后,前端就和后端分道扬镳了,前端就工程化了,组件复用性更强了。前端职位也能p5,p6,p7往上升了,甚至出现了前端的研究员。前端成为了一门专门的饭碗,一门手艺活。

AngularJS和React已经如火如荼了,那尤大为何还整个Vue干啥?首先,2013年的时间点是群雄并起,AngularJS和React尚未一统江湖。感于Angular的繁复,不接地气,尤大说,我把一个template加上一些数据和事件处理,成为一个开箱即用,领包入住的可复用组件,岂不妙哉?于是,一个学艺术的文科生撸起袖子,干了一件理科生干的事情。这一干不得了,一个伟大的前端框架就此诞生了。

vue3的口号是”渐进式的Javascript框架“,个人理解是你可以深度使用它,也可以浅度使用它,可以由浅入深地使用它。

Vue 的口号“渐进式框架”,背后就是这个过程中形成的分层 API 设计。新手可以通过 CDN script、基于 HTML 的模板以及直观的 Options API 顺利学习入门。而专家可以通过全功能的 CLI、渲染函数以及 Composition API 来处理复杂需求。

定义

言归正传,简单回顾一下vue的几个基础概念:

  • app: 一个应用也被称为一个vue实例,能挂载到页面某个节点上。每个应用拥有自己的用于配置和全局资源的作用域。一个页面中可以多个应用共存。通过createApp()函数可以创建一个应用。
  • component: 一个以vue结尾的文件通常被称为一个vue组件,里面包含template定义,组件的数据和方法,还有样式表三大部分。组件是vue中可复用的基本单元,多个组件像搭积木一样,装配出一个vue应用。通过app.component()函数可以注册一个组件。注册一个组件后,你可以把它当HTML元素一样使用,这就是前端这些框架做的最重要的一件事情。
  • template: 一段由template包裹的html片段(通常不是完整的html页面),但具有灵魂,能和组件的数据互动。
  • plugin: 一种能为 Vue 添加全局功能的、通常是第三方的组件包,功能增强包。例如Element UI Plus就是vue的一种插件。通过app.use()函数能安装一个插件。
  • directive: 定义组件的状态和配置,表现形式上就是一个HTML元素的属性,例如v-show, v-if这些都是指令。
  • slot: 插槽,模板(template)上留出给开发者自定制的部分,把不变的部分留给自己,把可变的部分开放给别人。

vue3核心知识点

vue3总体内容既多也不多。说它多,就是细节多,与外部其它前端技术关联的东西多。说不多,就是跟我们使用相关的就那么几招,下面一一道来。

定义一个组件

一个vue组件由三部分构成:<template>, <script>和<style>,定义在一个.vue文件中。除了vue结尾的文件,通常还有.ts和.tsx文件,ts文件定义任意的typescript内容,tsx是JSX语法的typescript版。掌握jsx + typescript + composition api定义组件能大大加快大家的开发步伐。

单个vue文件定义一个组件

vue3推荐采用<script setup>语法糖定义一个组件。
cool_component.vue:

<template>
  <div>my name is {{username}}, my ID is {{id}}, {{sayHello('hi, nihao')}}</div>
  <button @click="setname('haha')">setme</button>
</template>

<script lang="ts" setup>
import {ref, defineProps} from "vue";
const props = defineProps({id: Number, email: String});
const username = ref('Hello World!');
const sayHello = (msg) => {return `Hello: ${msg}, my id is ${props.id}, my email is ${props.email}`};

const setname = (newname) => {
  username.value = newname;
}
</script>

<style scoped>
...
</style>

所谓setup语法糖就是在script标签里加了一个setup属性。使用该组件:

import CoolComponent from 'cool_component.vue'
......
<template>
   <CoolComponent id=10 email='test@user.com'>...</CoolComponent>
</template>

注意,上面组件名称CoolComponent你可以随意取,和template里保持一致即可。
如果不采用setup语法糖,则写法为:

<template>
  <div>my name is {{username}}, my ID is {{id}}, {{sayHello('Hi, nihao')}}</div>
  <button @click="setname('haha')">setme</button>
</template>

<script lang="ts">
import {ref} from "vue";

export default {
  name: "CoolComponent",
  props: {
    id: 0,
    email: ''
  },
  setup(props, ctx){
    const username = ref('Hello World!');
    const sayHello = (msg) => {return `Hello: ${msg}`};

    const setname = (newname) => {
      username.value = newname;
    }
    return{
      username,
      sayHello,
      setname,
    }
  }
};
</script>

<style scoped>

</style>

相比setup函数繁琐了不少。上面的export default {}写法也可以写成:

export default defineComponent({})

这么写更方便开发工具做类型推导,是推荐写法。当然,直接采用setup()函数定义组件更简洁。

用defineComponent函数定义一个组件

defineComponent原型:

function defineComponent(
  component: ComponentOptions | ComponentOptions['setup']
): ComponentConstructor

interface ComponentOptions {
  provide?: object | ((this: ComponentPublicInstance) => object)
}

defineComponent()用多种写法。
第一种写法,在一个.tsx文件中使用:

export const MyComponent = defineComponent(() => {
    const prop1 = ref();
    return () => <div :prop1={editor} />;
});

注意return的内容采用了JSX的语法。
还可以:

const MyLink = defineComponent({
  name: 'my-link',
  setup (_, { slots }) {
    const node = inject(nodeMetadata, {}).node
    const href = node?.value?.attrs?.href
    return () => <a target="_blank" href={href}>{slots.default?.()}</a>
  }
})

这个组件的作用是可以包裹任意一个html标签,为它添加链接。
第二种写法:还可以结合render()和h()函数定义:

export const MyComponent = defineComponent({
  name: 'MyComponent',
  setup() {    
    return {
    	//...组件的数据      
    }
  },
  render() {    
    return h(
      'span', // vnode的tag name
      { class: ns.e('label') }, // vnode的属性
      renderLabelFn ? renderLabelFn({ node, data }) : label // 第三个参数为组件vnode的内容
    )
  },
})

注意h()函数的三个参数。第一个参数可以是一个已知的html标签名称,也可以是一个vue组件。第二个参数是组件的属性。第三个参数不仅仅可以是字符串, 还可以是"VNode"或两者混合。

组件的生命周期

在这里插入图片描述

在这里插入图片描述

组件之间的通信

组件可以嵌套,形成树状结构,那就涉及到如何通信和交换数据了。

父组件调用子组件

父组件parent.vue:

<template>
	<CoolComponent ref="coolComponentRef" id=10 email='test@user.com'></CoolComponent>
</template>
<script lang="ts" setup>
import CoolComponent from "@/components/cool_component.vue";
import {onMounted, ref} from "vue";

const coolComponentRef = ref()

onMounted(() => {
  console.log(coolComponentRef.value.sayHello('OKOK'))
})
</script>

注意:CoolComponent增加了一个ref,相当于给这个组件的实例增加了一个引用,然后定义对应的变量coolComponentRef关联到它。直接运行上面父组件会报错,提示sayHello()方法找不到,这是因为我们在子组件cool_component.vue中没有暴露该方法。
需要在cool_component.vue的setup函数末尾添加一行:

defineExpose({sayHello})

子组件通知父组件

子组件的数据发生变化后,需要通知到父组件,这种通信采用vue的事件机制。父组件可以通过 v-on (缩写为 @) 来监听事件。
子组件通过defineEmits申明事件:

子组件cool_component.vue:

<template>
  <div>my name is {{username}}, my ID is {{id}}, {{sayHello('hi, nihao')}}</div>
  <button @click="setname('haha')">setme</button>
  <button @click="updateData()">updateData</button>
</template>
<script lang='ts' setup>
const emit = defineEmits(["onUpdateData"]); // 申明事件
const updateData = () => {
  emit("onUpdateData", {id: 0, content: 'just a test'});
}
// ... 其它部分定义同上
</script>

父组件定义如下:

<template>
	<CoolComponent ref="coolComponentRef" id=10 email='test@user.com' @onUpdateData="handleUpdate"/>			</template>
<script lang='ts' setup>
function handleUpdate(data:any) {
  console.log('data:', data)
}
// ... 其它部分定义同上
</script>	

Provide/inject机制

如果几个子组件都需要共用父组件的某个方法,可以在父组件里采用provide输出公用方法,然后子组件里inject注入使用。
父组件:

<script setup lang="ts">
	function doSave() {
	  const content = editorRef.value.getContent()  
	  onSave({content, contentFormat: contentFormat.value})
	}
	
	function exportPDF() {
	  exportToPDF(pageId.value, pageTitle.value)
	}
	
	provide('actions', { doSave, exportPDF })
</script>

子组件:

<script setup lang="ts">
import { inject } from "vue";

const onSave= inject("actions").doSave;
const exportPDF = inject("actions").exportPDF;
</script>
<template>
<el-menu-item index="1" @click="exportPDF">导出为PDF</el-menu-item>   
<el-menu-item index="1" @click="onSave">保存文章</el-menu-item>      
</template>

子组件通过监听props变化完成刷新

父组件里获得数据,更新子组件的属性,然后子组件采用watch()函数,变化后刷新自己:

<script setup lang="ts">
const content = ref()
const contentFormat = ref()

function doSomething() {
   content.value = 'anything...'
   contentFormat = 'html'
}

</script>
<template>
	 <CoolEditor ref="editorRef" :content="content" :contentFormat="contentFormat" @on-save="onSave"	                
	</CoolEditor> 
</template>
	

在子组件cool_editor.vue里:

<script setup lang="ts">
function refresh(content, format) {
  contentStr.value = content
  contentFormat.value = format
  if (format =='html') {
    htmlEditorRef.value.setValue(content)
  }
  if (format =='md') {
    mdEditorRef.value.setValue(content)
  }
}

watch(
    () => [props.content, props.contentFormat],
    (val) => {
        let format = val[1] || 'html'
        refresh(val[0], format)
    }
)
</script>
  

路由和Layout

到上面为止,我们可以轻松地写一个自己的组件,也能把多个组件复合在一起,形成一个更大的组件。但是,要在页面上显示我们的组件,通常离不开路由和Layout这两样东西。这个路由器是vue之外的一个单独组件完成的,名称为vue-router。

下面是一个路由定义的文件:

import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '../views/HomeView.vue'
import HomePage from "@/components/home.vue";
import SimpleLayout from "@/layout/SimpleLayout.vue";
import UserList from "@/components/users.vue";
import AdminConsole from "@/components/admin.vue";

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: [
    {
      path: '/',
      name: 'home',
      component: HomeView
    },
    {
      path: '/about',
      name: 'about',
      // route level code-splitting
      // this generates a separate chunk (About.[hash].js) for this route
      // which is lazy-loaded when the route is visited.
      component: () => import('../views/AboutView.vue')
    },
    {
      path: "/home",
      component: HomePage
    },
    {
      path: "/simplelayout",
      component: SimpleLayout,
      children: [
        {
          path: "/simplelayout/view1",
          component: UserList,
        }
      ],
    }, 
    {
      path: "/admin",
      component: AdminConsole,
      children: [
        {
          path: "/admin/users",
          component: UserList,
        }
      ],
    }
  ]
})

export default router

每个路由将path和component关联在一起,这样我们前端做点击进行导航时,相应的区域就能渲染对应的组件。这个区域可以通过router-view定义,可以是app实例的挂载点。如果我们通过router-link进行跳转,则对应的渲染区域是app实例挂载点。
大家看到,上面的定义了多条路由,每条路由可以通过children指定下级路由,下级路由对应的渲染区域是父级路由的component属性指定的vue组件。即:

import Layout from '/xxx/xxx/layout.vue'
...
 path: "/admin",
 component: Layout, //父级路由的component指定了一个vue组件,这个vue组件就是Layout组件
 children: [
   {
     path: "/admin/users",
     component: UserList,
   }
 ],

上面layout.vue的通常定义形式如下:

<template>
  <div>simple layout</div>
  <router-view>
    <template #default="{ Component, route }">
      <component
          :is="Component"
          :key="route.fullPath"
      />
    </template>
  </router-view>
</template>

<script>
export default {
  name: "Layout"
}
</script>

<style scoped>

</style>

大家看到,这里采用了router-view这个组件,这是vue-router提供的。里面定义的template部分即是被渲染的区域,这里又用到component这个组件,这是vue内置的,是用来动态显示的,:is="Component"这里的Component即是子路由传入的vue组件。看上去有一点魔法,但也无需深究,这是框架作者要完成的事。

状态的管理

之前的版本采用vuex管理状态,现在vue团队又提供了一个新的选项:pinia。关于其用法,后面笔者会补充。

掌握核心函数的用法

vue3提供了一组组合式API (Composition API) ,它是一系列API 的集合,使我们可以使用函数而不是声明选项的方式书写Vue 组件。 它是一个概括性的术语,涵盖了以下方面的API: 响应式API:例如 ref() 和 reactive() ,使我们可以直接创建响应式状态、计算属性和侦听器。所谓响应式数据,就是当数据发生变化时,界面也跟着变。谓之数据和视图绑定。

在这里插入图片描述

  • ref(), reactive(), shallowRef(),这三个函数都是定义响应式数据。ref()定义简单数据类型;reative()定义复杂类型,支持深度监听。shallowRef()不支持深度监听,也就是只响应第一层数据的变化。
  • computed(): 一个ref对象依赖另一个ref对象,则采用compute()来计算,”它变我也变“。
  • app.use(),注册插件。如果插件以对象形式传入,则该对象必须提供 install 方法。如果插件是一个函数,它会被作为 install 方法。install 方法调用时,会将 app 作为参数传入。
  • watch(): 监听响应式数据,数据发生变化时,调用相应的回调函数。
  • mixin(): 给组件添加一些额外的功能,这些功能放在一个mixin对象中。添加完后,调用mixin对象的方法就像调用自己的方法一样。适用场景就是一些公共组件以mixin的形式”“到别的对象上。
  • h(): 定义一个vnode。在一些第三方框架中使用率比较高。
  • nextTick() 是将回调推迟到下一个 DOM 更新周期之后执行。在更改了一些数据以等待 DOM 更新后立即使用它。
  • defineComponent(): 可以给组件的setup方法准确的参数类型定义;可以接受显式的自定义 props 接口或从属性验证对象中自动推断;可以正确适配无 props、数组 props 等形式;引入 defineComponent() 以正确推断 setup() 组件的参数类型。
  • provide/inject的作用: 在父组件中使用 provide 标记该变量可以被所有后代访问,无论多少层;在子组件中使用 inject 可以访问到变量。例如, 在App.vue中通过provide定义的变量,在侧边栏、顶部栏组件中使用 inject可以访问到该变量。

vue3生态一览

目前vue3+vite+ts+pinia基本成了标配,整个生态越来越完善。

构建工具

nuxt3: 一款基于Vue3的混合开发框架。
vite: 前端开发与构建工具.

工具库

vueuse: 针对vue3 composition api的工具库。

路由管理库

vue-router4

状态管理库

pinia、vuex4

UI框架

element plus、ant design vue、quasar、vuetify3beta、naive ui、prime vue,View UI Plus

移动端

ionic、vant、varlet、nutui、waveui

总结

本文简明扼要地介绍了vue3的最基本内容,帮助大家快速起步和消化vue3的关键内容。

参考