vue3入门- script setup详解上

<script setup> 是在单文件组件 (SFC) 中使用组合式 API 的编译时语法糖。当同时使用单文件组件与组合式 API 时该语法是默认推荐。相比于普通的 <script> 语法,它具有更多优势:

  • 更少的样板内容,更简洁的代码。
  • 能够使用纯 TypeScript 声明 props 和自定义事件。
  • 更好的运行时性能 (其模板会被编译成同一作用域内的渲染函数,避免了渲染上下文代理对象)。
  • 更好的 IDE 类型推导性能 (减少了语言服务器从代码中抽取类型的工作)。

基本语法

要启用该语法,需要在 <script> 代码块上添加 setup attribute:

js 复制代码
<script setup>
console.log('hello script setup')
</script>

里面的代码会被编译成组件 setup() 函数的内容。这意味着与普通的 <script> 只在组件被首次引入的时候执行一次不同,<script setup> 中的代码会在每次组件实例被创建的时候执行。

<script setup> 快速创建一个简单的计数器组件:

// Counter.vue

html 复制代码
<template>
  <div>
    <p>当前计数:{{ count }}</p>
    <button @click="increment">增加</button>
  </div>
</template>

<script setup>
import { ref } from 'vue'

console.log('counter loaded')

const count = ref(0)

function increment() {
  count.value++
}
</script>

如果你在父组件里多次使用 <Counter />

// Parent.vue

html 复制代码
<template>
  <Counter />
  <Counter />
  <Counter />
</template>

在 console 控制台上 counter loaded 会出现三次。

顶层的绑定会被暴露给模板

当使用 <script setup> 的时候,任何在 <script setup> 声明的顶层的绑定 (包括变量,函数声明,以及 import 导入的内容) 都能在模板中直接使用:

html 复制代码
<template>
  <button @click="log">{{ msg }}</button>
</template>
<script setup>
// 变量
const msg = 'Hello!'

// 函数
function log() {
  console.log(msg)
}
</script>

import 导入的内容也会以同样的方式暴露。这意味着我们可以在模板表达式中直接使用导入的函数,而不需要通过 methods 选项来暴露它。

html 复制代码
<template>
  <p>当前日期:{{ formatDate(currentDate) }}</p>
</template>

<script setup>
import { formatDate } from '@/utils/date'

const currentDate = new Date()
</script>

使用组件

<script setup> 范围里的值也能被直接作为自定义组件的标签名使用,无需再通过 components 进行注册

js 复制代码
<script setup>
import MyComponent from './MyComponent.vue'
</script>

<template>
  <MyComponent />
</template>

动态组件

由于组件是通过变量引用而不是基于字符串组件名注册的,在 <script setup> 中要使用动态组件的时候,应该使用动态的 :is 来绑定:

js 复制代码
<template>
  <component :is="someCondition ? Foo : Bar" />
</template>
<script setup>
import Foo from './Foo.vue'
import Bar from './Bar.vue'
</script>

递归组件

在实际项目中,可以用递归组件渲染树形结构。以下是一个具体的实现示例:

html 复制代码
<template>
  <TreeNode :node="treeData" />
</template>

<script setup>
import TreeNode from './TreeNode.vue'

const treeData = {
  name: '根节点',
  children: [
    { name: '子节点 1' },
    { 
      name: '子节点 2', 
      children: [
        { name: '子节点 2.1' },
        { name: '子节点 2.2' }
      ] 
    }
  ]
}
</script>

TreeNode.vue 的实现如下:

html 复制代码
<template>
  <div class="tree-node">
    <p>{{ node.name }}</p>
    <div v-if="node.children" class="children">
      <TreeNode 
        v-for="(child, index) in node.children" 
        :key="index" 
        :node="child" 
      />
    </div>
  </div>
</template>

<script setup>
import { defineProps } from 'vue'

const { node } = defineProps({
  node: {
    type: Object,
    required: true
  }
})
</script>

<style scoped>
.tree-node {
  margin-left: 20px;
}

.children {
  margin-top: 10px;
}
</style>

在这个实现中,TreeNode 组件通过递归调用自身来渲染树形结构的每个节点及其子节点。node 对象包含了每个节点的 name 和可选的 children 数组,用于表示子节点。

命名空间组件

可以使用带 . 的组件标签,例如 <Foo.Bar> 来引用嵌套在对象属性中的组件。这在需要从单个文件中导入多个组件的时候非常有用:

js 复制代码
<template>
  <Form.Input>
    <Form.Label>label</Form.Label>
  </Form.Input>
</template>
<script setup>
import * as Form from './form-components'
</script>

./form-components 在这个例子中是一个模块,它可以是一个包含多个组件的文件,也可以是一个目录。通过 import * as Form from './form-components' 的语法,Form 作为一个命名空间对象,包含了 ./form-components 中导出的所有内容。

以下是对这个例子的详细展开,包括 ./form-components 的可能实现方式:

./form-components 是一个文件

如果 ./form-components 是一个文件,那么它可能是一个 JavaScript 或 TypeScript 文件,导出多个组件。以下是一个可能的实现:

js 复制代码
import { defineComponent, h } from 'vue';

export const Input = defineComponent({
  name: 'Input',
  render() {
    return h('input', { type: 'text' });
  },
});

export const Label = defineComponent({
  name: 'Label',
  render() {
    return h('label', {}, this.$slots.default ? this.$slots.default() : 'label');
  },
});

在这种情况下,import * as Form from './form-components' 会将 InputLabel 作为 Form 对象的属性导入,使用时可以通过 Form.InputForm.Label 访问。

./form-components 是一个目录

如果 ./form-components 是一个目录,那么它通常包含一个 index.js 文件,用于汇总导出目录中的所有组件。目录结构可能如下:

css 复制代码
form-components/
├── Input.vue
├── Label.vue
└── index.js
  • Input.vue

    vue 复制代码
    <!-- filepath: ./form-components/Input.vue -->
    <template>
      <input type="text" />
    </template>
  • Label.vue

    vue 复制代码
    <!-- filepath: ./form-components/Label.vue -->
    <template>
      <label><slot /></label>
    </template>
  • index.js

    js 复制代码
    import Input from './Input.vue';
    import Label from './Label.vue';
    
    export { Input, Label };

在这种情况下,import * as Form from './form-components' 会从 index.js 中导入所有导出的组件。

使用说明

无论 ./form-components 是文件还是目录,最终在 Vue 组件中使用时,代码如下:

vue 复制代码
<template>
  <Form.Input>
    <Form.Label>label</Form.Label>
  </Form.Input>
</template>
<script setup>
import * as Form from './form-components';
</script>

总结

  • 如果是文件:直接导出多个组件。
  • 如果是目录:通过 index.js 汇总导出组件。
  • 这种命名空间的方式非常适合组织多个相关组件,避免命名冲突,同时提高代码的可读性和可维护性。

使用自定义指令

全局注册的自定义指令将正常工作。本地的自定义指令在 <script setup> 中不需要显式注册,但他们必须遵循 vNameOfDirective 这样的命名规范:

html 复制代码
<template>
  <h1 v-my-directive>This is a Heading</h1>
</template>

<script setup>
const vMyDirective = {
  beforeMount: (el) => {
    // 在元素上做些操作
  }
}
</script>

如果指令是从别处导入的,可以通过重命名来使其符合命名规范:

js 复制代码
<script setup>
import { myDirective as vMyDirective } from './MyDirective.js'
</script>

如下用自定义指令实现拖拽功能:

html 复制代码
<template>
  <div v-draggable>拖动我</div>
</template>

<script setup>
const vDraggable = {
  beforeMount(el) {
    el.style.position = 'absolute'
    el.onmousedown = (e) => {
      const shiftX = e.clientX - el.getBoundingClientRect().left
      const shiftY = e.clientY - el.getBoundingClientRect().top

      const moveAt = (pageX, pageY) => {
        el.style.left = pageX - shiftX + 'px'
        el.style.top = pageY - shiftY + 'px'
      }

      const onMouseMove = (event) => moveAt(event.pageX, event.pageY)
      document.addEventListener('mousemove', onMouseMove)

      el.onmouseup = () => {
        document.removeEventListener('mousemove', onMouseMove)
        el.onmouseup = null
      }
    }
  }
}
</script>

与普通的 <script> 一起使用

<script setup> 可以和普通的 <script> 一起使用。普通的 <script> 在有这些需要的情况下或许会被使用到:

  • 声明无法在 <script setup> 中声明的选项,例如 inheritAttrs 或插件的自定义选项 (在 3.3+ 中可以通过 defineOptions 替代)。
  • 声明模块的具名导出 (named exports)。
  • 运行只需要在模块作用域执行一次的副作用,或是创建单例对象。
js 复制代码
<script>
// 普通 <script>,在模块作用域下执行 (仅一次)
runSideEffectOnce()

// 声明额外的选项
export default {
  inheritAttrs: false,
  customOptions: {}
}
</script>

<script setup>
// 在 setup() 作用域中执行 (对每个实例皆如此)
</script>

在同一组件中将 <script setup><script> 结合使用的支持仅限于上述情况。具体来说:

  • 不要为已经可以用 <script setup> 定义的选项使用单独的 <script> 部分,如 propsemits
  • <script setup> 中创建的变量不会作为属性添加到组件实例中,这使得它们无法从选项式 API 中访问。我们强烈反对以这种方式混合 API。

如果你发现自己处于以上任一不被支持的场景中,那么你应该考虑切换到一个显式的 setup() 函数,而不是使用 <script setup>

defineOptions()

这个宏可以用来直接在 <script setup> 中声明组件选项,而不必使用单独的 <script> 块:

js 复制代码
<script setup>
defineOptions({
  inheritAttrs: false,
  customOptions: {
    /* ... */
  }
})
</script>

这是一个宏定义,选项将会被提升到模块作用域中,无法访问 <script setup> 中不是字面常数的局部变量。

在 Vue 3 的 <script setup> 语法里,默认组件名是由文件名决定的。不过你也能通过 defineOptions 宏来明确指定组件名。以下是一个示例:

js 复制代码
<template>
  <div>
    <p>这是 {{ name }} 组件</p>
  </div>
</template>

<script setup>
// 定义组件选项,指定组件名
defineOptions({
  name: 'MyCustomComponent'
})
</script>

顶层 await

<script setup> 中可以使用顶层 await。结果代码会被编译成 async setup()

js 复制代码
<script setup>
const post = await fetch(`/api/post/1`).then((r) => r.json())
</script>

<script setup> 中使用 await 时,Vue 会自动将代码编译为保留当前组件实例上下文的格式。这意味着在 await 表达式之后,代码仍然能够正确访问组件实例的相关数据和方法,而不会因为异步操作导致上下文丢失。例如:

js 复制代码
<script setup>
const data = await fetchData(); // 在这里使用 await
console.log(data); // 仍然可以访问组件实例的上下文
</script>

这种处理方式确保了在异步操作完成后,组件的逻辑能够继续正常运行,而无需手动绑定上下文。

async setup() 必须与 Suspense 组合使用才能保证页面正常渲染,该特性目前仍处于实验阶段。

父组件中用 Suspense 包裹 <Child> 组件以避免警告和渲染错误:

vue 复制代码
<template>
  <Suspense>
    <Child />
  </Suspense>
</template>
<script setup>
import Child from './Child.vue'
</script>

导入语句

在代码实例中,展示了 Vue 3 中 <script setup> 语法下的几种导入方式。

以下是对每种导入方式的详细讲解:

  1. 标准相对路径导入
javascript 复制代码
import { componentA } from './Components'
  • 含义 : 使用相对路径 ./Components 导入 componentA
  • 特点 :
    • ./Components 表示当前文件所在目录。
    • 这种方式是标准的 ECMAScript 模块导入方式,适用于项目中明确的文件路径。
  • 适用场景: 当文件路径明确且不需要跨目录时,推荐使用这种方式。

  1. 使用 @ 别名导入
javascript 复制代码
import { componentB } from '@/Components'
  • 含义 : 使用 @ 作为别名导入 componentB

  • 特点 :

    • @ 通常被配置为项目的 src 目录的别名。

    • 这种别名需要在构建工具(如 Vite 或 Webpack)的配置文件中定义。例如:

      javascript 复制代码
      // Vite 配置示例
      import { defineConfig } from 'vite'
      import vue from '@vitejs/plugin-vue'
      
      export default defineConfig({
        plugins: [vue()],
        resolve: {
          alias: {
            '@': '/src', // 定义 @ 为 src 目录
          },
        },
      })
  • 适用场景: 当项目结构复杂且需要频繁引用 src 目录下的文件时,使用别名可以提高代码可读性和维护性。


  1. 使用 ~ 别名导入
javascript 复制代码
import { componentC } from '~/Components'
  • 含义 : 使用 ~ 作为别名导入 componentC

  • 特点 :

    • ~ 是另一种常见的别名,具体含义取决于构建工具的配置。

    • 在某些项目中,~ 可能被配置为项目根目录或其他特定目录。

    • 例如,在 Vite 中可以这样配置:

      javascript 复制代码
      resolve: {
        alias: {
          '~': '/project-root', // 定义 ~ 为项目根目录
        },
      }
  • 适用场景 : 当需要引用项目根目录或其他特定目录的文件时,可以使用 ~ 别名。

总结:

  • 相对路径导入: 标准且无需额外配置,但路径可能较长。
  • @ 别名导入: 常用于引用 src 目录,简化路径。
  • ~ 别名导入: 灵活性更高,具体含义取决于项目配置。

在实际项目中,推荐根据团队约定和项目需求选择合适的导入方式,并确保在构建工具中正确配置别名。

相关推荐
武汉刘德华3 小时前
Flutter配置环境,运行三端- iOS、android、harmony全流程操作实践(最新)
前端
酥饼_i3 小时前
你的自动化脚本又双叒叕崩了?
前端·人工智能·ai编程
Lsx-codeShare3 小时前
前端数据可视化:基于Vue3封装 ECharts 的最佳实践
前端·javascript·echarts·vue3·数据可视化
主宰者3 小时前
WPF外部打开html文件
前端·html·wpf
jason_yang4 小时前
vue3中定义组件的4种姿势
前端·vue.js
袋鱼不重4 小时前
Gitee 与 GitHub 仓库同步:从手动操作到自动化部署
前端·github
哒哒哒就是我4 小时前
React中,函数组件里执行setState后到UI上看到最新内容的呈现,react内部会经历哪些过程?
前端·react.js·前端框架
AGG_Chan4 小时前
flutter专栏--深入剖析你的第一个flutter应用
前端·flutter
再学一点就睡4 小时前
多端单点登录(SSO)实战:从架构设计到代码实现
前端·架构