You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

369 lines
15 KiB
Markdown

This file contains invisible Unicode characters!

This file contains invisible Unicode characters that may be processed differently from what appears below. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to reveal hidden characters.

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 06 | 新的代码组织方式Composition API + <script setup> 到底好在哪里?
你好,我是大圣,欢迎进入课程的第六讲。
在上一讲中,我带你搭建了项目的雏形,这是后面项目开发的起点。从今天开始,我就带你在这个骨架结构的基础之上,开始项目的实战开发。首先我们要掌握的,就是 Vue 3 的Composition API + <script setup>这种最新的代码组织方式。
![](https://static001.geekbang.org/resource/image/6f/0a/6fd86f3d33a0200d64c7423bc88e890a.png?wh=1222x432)
我们在前面的第三讲中,有详细地讲到过 Composition API 相信你对这个API 的语法细节已经有所掌握了。那你肯定会很好奇,这个<script setup>又是什么?为什么尤雨溪要在微博上强推<script setup>呢?
别急今天我就带你使用Composition API 和 <script setup> 去重构第二讲的清单应用。在重构的过程中,你能逐渐明白,**Composition API 可以让我们更好地组织代码结构**,而让你感到好奇的 <script setup>本质上是以一种更精简的方式来书写Composition API 。
## Composition API 和 <script setup> 上手
首先我想提醒你,我们在这一讲中写代码的方式,就和前面的第二讲有很大的区别。
在第二讲中,我们开发清单应用时,是直接在浏览器里使用 Options API 的方式写代码;但在接下来的开发中,我们会直接用单文件组件——也就是 `.vue` 文件,的开发方式。这种文件格式允许我们把 Vue 组件的HTML、CSS和JavaScript写在单个文件内容中。下面我带你用单文件组件的方式去重构第二讲做的清单应用。
我们现在已经搭建好了项目的骨架,以后在这个骨架之内会有很多页面和组件。从这里开始,我们就要逐步适应组件化的开发思路,新的功能会以组件的方式来组织。
按照上一讲制定的规范首先我们打开项目文件夹下面的src下的components目录新建一个Todolist.vue ,并在这个文件里写出下面的代码:
```xml
<template>
<div>
<h1 @click="add">{{count}}</h1>
</div>
</template>
<script setup>
import { ref } from "vue";
let count = ref(1)
function add(){
count.value++
}
</script>
<style>
h1 {
color: red;
}
</style>
```
在上述代码中我们使用template标签放置模板、script 标签放置逻辑代码并且用setup标记我们使用<script setup>的语法style标签放置CSS样式。
从具体效果上看,这段代码实现了一个累加器。在 <script setup> 语法中我们使用引入的ref函数包裹数字返回的count变量就是响应式的数据使用add函数实现数字的修改。需要注意的是对于ref返回的响应式数据我们需要修改 `.value` 才能生效,而在 <script setup> 标签内定义的变量和函数,都可以在模板中直接使用。
实现累加器以后我们再回到src/pages/Home.vue 组件中使用如下代码显示清单应用。在这段代码里我们直接import TodoList.vue组件然后<script setup>会自动把组件注册到当前组件这样我们就可以直接在template中使用 来显示清单的功能。
```xml
<template>
<h1>这是首页</h1>
<TodoList />
</template>
<script setup>
import TodoList from '../components/TodoList.vue'
</script>
```
这个时候我们就把清单功能独立出来了,可以在任意你需要的地方复用。在课程的后续内容中,我会详细给你介绍基于组件去搭建应用的方式。**通过这种方式,你可以实现对业务逻辑的复用。这样做的好处就是,如果有其他页面也需要用到这个功能,可以直接复用过去。**
然后我们就可以基于新的语法实现之前的清单应用。下面的代码就是把之前的代码移植过来后使用ref包裹的响应式数据。在你修改title和todos的时候注意要修改响应式数据的value属性。
```xml
<template>
<div>
<input type="text" v-model="title" @keydown.enter="addTodo" />
<ul v-if="todos.length">
<li v-for="todo in todos">
<input type="checkbox" v-model="todo.done" />
<span :class="{ done: todo.done }"> {{ todo.title }}</span>
</li>
</ul>
</div>
</template>
<script setup>
import { ref } from "vue";
let title = ref("");
let todos = ref([{title:'学习Vue',done:false}])
function addTodo() {
todos.value.push({
title: title.value,
done: false,
});
title.value = "";
}
</script>
```
## 计算属性
在第二讲开发的清单应用中我们也用到了计算属性在Composition API的语法中计算属性和生命周期等功能都可以脱离Vue的组件机制单独使用 。我们向TodoList.vue代码块中加入下面的代码
```xml
<template>
<div>
<input type="text" v-model="title" @keydown.enter="addTodo" />
<button v-if="active < all" @click="clear">清理</button>
<ul v-if="todos.length">
<li v-for="todo in todos">
<input type="checkbox" v-model="todo.done" />
<span :class="{ done: todo.done }"> {{ todo.title }}</span>
</li>
</ul>
<div v-else>暂无数据</div>
<div>
全选<input type="checkbox" v-model="allDone" />
<span> {{ active }} / {{ all }} </span>
</div>
</div>
</template>
<script setup>
import { ref,computed } from "vue";
let title = ref("");
let todos = ref([{title:'学习Vue',done:false}])
function addTodo() {
...
}
function clear() {
todos.value = todos.value.filter((v) => !v.done);
}
let active = computed(() => {
return todos.value.filter((v) => !v.done).length;
});
let all = computed(() => todos.value.length);
let allDone = computed({
get: function () {
return active.value === 0;
},
set: function (value) {
todos.value.forEach((todo) => {
todo.done = value;
});
},
});
</script>
```
在这这段代码中具体的计算属性的逻辑和第二讲一样区别仅在于computed的用法上。你能看到第二讲的computed是组件的一个配置项而这里的computed的用法是单独引入使用。
## Composition API 拆分代码
讲到这里可能你就会意识到之前的累加器和清单虽然功能都很简单但也属于两个功能模块。如果在一个页面里有这两个功能那就需要在data和methods里分别进行配置。但这样的话数据和方法相关的代码会写在一起在组件代码行数多了以后就不好维护。**所以我们需要使用Composition API 的逻辑来拆分代码,把一个功能相关的数据和方法都维护在一起。**
但是所有功能代码都写在一起的话也会带来一些问题随着功能越来越复杂script 内部的代码也会越来越多。因此,我们可以进一步对代码进行拆分,把功能独立的模块封装成一个独立的函数,真正做到按需拆分。
在下面,我们新建了一个函数 useTodos
```xml
function useTodos() {
let title = ref("");
let todos = ref([{ title: "学习Vue", done: false }]);
function addTodo() {
todos.value.push({
title: title.value,
done: false,
});
title.value = "";
}
function clear() {
todos.value = todos.value.filter((v) => !v.done);
}
let active = computed(() => {
return todos.value.filter((v) => !v.done).length;
});
let all = computed(() => todos.value.length);
let allDone = computed({
get: function () {
return active.value === 0;
},
set: function (value) {
todos.value.forEach((todo) => {
todo.done = value;
});
},
});
return { title, todos, addTodo, clear, active, all, allDone };
}
```
这个函数就是把那些和清单相关的所有数据和方法,都放在函数内部定义并且返回,这样这个函数就可以放在任意的地方来维护。
而我们的组件入口,也就是<script setup>中的代码就可以变得非常简单和清爽了。在下面的代码中我们只需要调用useTodos并且获取所需要的变量即可具体的实现逻辑可以去useTodos内部维护代码可维护性大大增强。
```xml
<script setup>
import { ref, computed } from "vue";
let count = ref(1)
function add(){
count.value++
}
let { title, todos, addTodo, clear, active, all, allDone } = useTodos();
</script>
```
我们在使用Composition API 拆分功能时也就是执行useTodos的时候ref、computed等功能都是从 Vue 中单独引入而不是依赖this上下文。其实你可以把组件内部的任何一段代码从组件文件里抽离出一个独立的文件进行维护。
现在我们引入追踪鼠标位置的需求进行讲解比如我们项目中可能有很多地方需要显示鼠标的坐标位置那我们就可以在项目的src/utils文件夹下面新建一个mouse.js。我们先从 Vue 中引入所需要的ref函数然后暴露一个函数函数内部和上面封装的useTodos类似不过这次独立成了文件放在utils文件下独立维护提供给项目的所有组件使用。
```xml
import {ref} from 'vue'
export function useMouse(){
const x = ref(0)
const y = ref(0)
return {x, y}
}
```
想获取鼠标的位置我们就需要监听mousemove事件。这需要在组件加载完毕后执行在Composition API中我们可以直接引入onMounted和onUnmounted来实现生命周期的功能。
看下面的代码组件加载的时候会触发onMounted生命周期我们执行监听mousemove事件从而去更新鼠标位置的x和y的值组件卸载的时候会触发onUnmounted生命周期解除mousemove事件。
```xml
import {ref, onMounted,onUnmounted} from 'vue'
export function useMouse(){
const x = ref(0)
const y = ref(0)
function update(e) {
x.value = e.pageX
y.value = e.pageY
}
onMounted(() => {
window.addEventListener('mousemove', update)
})
onUnmounted(() => {
window.removeEventListener('mousemove', update)
})
return { x, y }
}
```
完成了上面的鼠标事件封装这一步之后我们在组件的入口就可以和普通函数一样使用useMouse函数。在下面的代码中上面的代码返回的x和y的值可以在模板任意地方使用也会随着鼠标的移动而改变数值。
```xml
import {useMouse} from '../utils/mouse'
let {x,y} = useMouse()
```
相信到这里,你一定能体会到 Composition API 对代码组织方式的好处。简单来看,**因为ref和computed等功能都可以从 Vue 中全局引入,所以我们就可以把组件进行任意颗粒度的拆分和组合**,这样就大大提高了代码的可维护性和复用性。
## <script setup> 好用的功能
Composition API 带来的好处你已经掌握了,而<script setup>是为了提高我们使用Composition API 的效率而存在的。我们还用累加器来举例,如果没有<script setup>,那么我们需要写出下面这样的代码来实现累加器。
```xml
<script >
import { ref } from "vue";
export default {
setup() {
let count = ref(1)
function add() {
count.value++
}
return {
count,
add
}
}
}
</script>
```
在上面的代码中,我们要在<script>中导出一个对象。我们在setup配置函数中写代码时和Options的写法比也多了两层嵌套。并且我们还要在setup函数中返回所有需要在模板中使用的变量和方法。上面的代码中setup函数就返回了count和add。
**使用 <script setup> 可以让代码变得更加精简,这也是现在开发 Vue 3 项目必备的写法**。除了我们上面介绍的功能,<script setup>还有其它一些很好用的功能比如能够使用顶层的await去请求后端的数据等等我们会在后面的项目中看到这种使用方法。
## style样式的特性
除了script相关的配置我也有必要给你介绍一下style样式的配置。比如在style标签上当我们加上scoped这个属性的时候我们定义的CSS就只会应用到当前组件的元素上这样就很好地避免了一些样式冲突的问题。
我们项目中的样式也可以加上如下标签:
```xml
<style scoped>
h1 {
color: red;
}
</style>>
```
这样组件就会解析成下面代码的样子。标签和样式的属性上新增了data-的前缀,确保只在当前组件生效。
```xml
<h1 data-v-3de47834="">1</h1>
<style scoped>
h1[data-v-3de47834] {
color: red;
}
</style>
```
如果在scoped内部你还想写全局的样式那么你可以用:global来标记这样能确保你可以很灵活地组合你的样式代码后面项目中用到的话我还会结合实战进行讲解。而且我们甚至可以通过v-bind函数直接在CSS中使用JavaScript中的变量。
在下面这段代码中, 我在script里定义了一个响应式的color变量并且在累加的时候将变量随机修改为红或者蓝。在style内部我们使用v-bind函数绑定color的值就可以动态地通过JavaScript的变量实现CSS的样式修改点击累加器的时候文本颜色会随机切换为红或者蓝。
```xml
<template>
<div>
<h1 @click="add">{{ count }}</h1>
</div>
</template>
<script setup>
import { ref } from "vue";
let count = ref(1)
let color = ref('red')
function add() {
count.value++
color.value = Math.random()>0.5? "blue":"red"
}
</script>
<style scoped>
h1 {
color:v-bind(color);
}
</style>>
```
点击累加器时文本颜色的切换效果,如下图所示:
![图片](https://static001.geekbang.org/resource/image/59/18/5974c0d7dbce32306bd2a207a6a37f18.gif?wh=343x105)
## 总结
我们来总结一下今天都学到了什么吧。今天的主要任务就是使用Composition API +<script setup>的语法复现第二讲的清单应用我们首先通过累加器的例子介绍了ref这个函数的使用之后我们讲到在Composition API的语法中所有的功能都是通过全局引入的方式使用的并且通过<script setup>的功能,我们定义的变量、函数和引入的组件,都不需要额外的生命周期,就可以直接在模板中使用。
然后我们通过把功能拆分成函数和文件的方式掌握到Composition API组织代码的方式我们可以任意拆分组件的功能抽离出独立的工具函数大大提高了代码的可维护性。
最后我们还学习了style标签的特殊属性通过标记scoped可以让样式只在当前的组件内部生效还可以通过v-bind函数来使用JavaScript中的变量去渲染样式如果这个变量是响应式数据就可以很方便地实现样式的切换。
相信学完今天这一讲你一定会对我们为什么需要Composition API有更进一步的认识而对于<script setup>来说则可以帮助我们更好且更简洁的写Compostion的语法。在后面我们的项目会全部使用Composition API + <script setup>来进行书写。
## 思考题
最后给你留一个思考题Composition API 和 <script setup> 虽然能提高开发效率但是带来的一些新的语法比如ref返回的数据就需要修改 value属性响应式和生命周期也需要import后才能使用等等很多人也在社区批评这是 Vue 造的“方言” ,那你怎么看呢?
欢迎你在留言区分享你的想法,当然也推荐你把这一讲推荐给你自己的朋友、同事。我们下一讲见!