首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >使用 Vue 3 与 TypeScript 构建 Web 应用: Todo

使用 Vue 3 与 TypeScript 构建 Web 应用: Todo

作者头像
yiyun
发布于 2023-07-17 20:17:53
发布于 2023-07-17 20:17:53
1.9K0
举报
文章被收录于专栏:yiyun 的专栏yiyun 的专栏

引言 界面: Vue.js 3 JavaScript 超集: TypeScript 包管理器: pnpm 前端工程化/打包: Vite 路由: Vue Router 状态管理: Pinia CSS 预处理器: Less 代码格式化: Prettier 代码质量: ESLint 预览

技术栈

详细

  1. 界面: Vue.js 3
  2. JavaScript 超集: TypeScript
  3. 包管理器: pnpm
  4. 前端工程化/打包: Vite
  5. 路由: Vue Router
  6. 状态管理: Pinia
  7. CSS 预处理器: Less
  8. 代码格式化: Prettier
  9. 代码质量: ESLint
  10. 其它三方库

本地开发环境

代码语言:javascript
复制
🦄  node --version
v18.14.0

1. 安装 pnpm

PowerShell

代码语言:javascript
复制
iwr https://get.pnpm.io/install.ps1 -useb | iex
代码语言:javascript
复制
🦄  pnpm --version
8.6.5

2. 使用 create-vue 的模版创建项目

代码语言:javascript
复制
pnpm create vue@latest
代码语言:javascript
复制
cd vue-project
pnpm install
pnpm format
pnpm dev

pnpm install 会生成 新文件 pnpm-lock.yaml pnpm format 此模版版本不会造成文件修改

PS: VITE v4.3.9

3. 安装 less

参考:

代码语言:javascript
复制
pnpm add -D less

Vite 和 Webpack 不同,不需要 less-loader 等,只需安装 less

4. 创建组件 TodoList.vue

清空 src/components

创建 src/components/TodoList.vue

代码语言:javascript
复制
<template>
  <div>TodoList</div>
</template>

修改 src/views/HomeView.vue

代码语言:javascript
复制
<script setup lang="ts">
import TodoList from '../components/TodoList.vue'
</script>

<template>
  <main>
    <TodoList />
  </main>
</template>

修改 src/App.vue

代码语言:javascript
复制
<script setup lang="ts">
import { RouterLink, RouterView } from 'vue-router'
</script>

<template>
  <header>
    <img alt="Vue logo" class="logo" src="@/assets/logo.svg" width="125" height="125" />

    <div class="wrapper">
      <nav>
        <RouterLink to="/">Home</RouterLink>
        <RouterLink to="/about">About</RouterLink>
      </nav>
    </div>
  </header>

  <RouterView />
</template>

<style scoped>
...
</style>

预览

5. 创建组件 TodoGroup.vue

创建新文件: src/components/TodoGroup.vue

代码语言:javascript
复制
<template>
    <div>TodoGroup</div>
</template>

修改文件: src/components/TodoList.vue

代码语言:javascript
复制
<script setup lang="ts">
import TodoGroup from './TodoGroup.vue';
</script>

<template>
  <div>
    <TodoGroup />
  </div>
</template>

预览

6. 修改组件

修改 src/components/TodoGroup.vue

代码语言:javascript
复制
<script setup lang="ts">
enum TodoStatus {
  Pending = 'pending',
  InProgress = 'InProgress',
  Completed = 'completed'
}

interface Todo {
  id: number
  title: string
  description: string
  status: TodoStatus
}

const pendingTodos: Todo[] = [
  {
    id: 1,
    title: '测试标题',
    description: '测试描述',
    status: TodoStatus.Pending
  }
]
</script>

<template>
  <div>
    <h3>Pending</h3>
    <ul>
      <li v-for="todo in pendingTodos" :key="todo.id">{{ todo.title }}</li>
    </ul>
  </div>
</template>

修改 src/components/TodoList.vue

代码语言:javascript
复制
<script setup lang="ts">
import TodoGroup from './TodoGroup.vue';
</script>

<template>
  <div>
    <TodoGroup />
    <TodoGroup />
    <TodoGroup />
  </div>
</template>

预览

7. 进一步整理

将 TypeScript 公共自定义类型提取到 types.ts

src/types.ts

代码语言:javascript
复制
export enum TodoStatus {
  Pending = 'pending',
  InProgress = 'in progress',
  Completed = 'completed'
}

export interface Todo {
  id: number
  title: string
  description: string
  status: TodoStatus
}

src/components/TodoList.vue

代码语言:javascript
复制
<script setup lang="ts">
import TodoGroup from './TodoGroup.vue'
import { TodoStatus } from '@/types'
</script>

<template>
  <div>
    <TodoGroup :status="TodoStatus.Pending" />
    <TodoGroup :status="TodoStatus.InProgress" />
    <TodoGroup :status="TodoStatus.Completed" />
  </div>
</template>

src/components/TodoGroup.vue

代码语言:javascript
复制
<script setup lang="ts">
import { TodoStatus, type Todo } from '@/types'
import { computed } from 'vue'

interface Props {
  status: TodoStatus
}

const props = defineProps<Props>()
const pendingTodos: Todo[] = [
  {
    id: 1,
    title: '测试标题',
    description: '测试描述',
    status: TodoStatus.Pending
  }
]

const groupLabel = computed(() => {
  switch (props.status) {
    case TodoStatus.Pending:
      return 'Pending'
    case TodoStatus.InProgress:
      return 'In Progress'
    case TodoStatus.Completed:
      return 'Completed'
    default:
      return 'Todo Group'
  }
})
</script>

<template>
  <div>
    <h3>{{ groupLabel }}</h3>
    <ul>
      <li v-for="todo in pendingTodos" :key="todo.id">{{ todo.title }}</li>
    </ul>
  </div>
</template>

预览

8. 利用 reactive 创建本地 stores/useTodos.ts

src/stores/useTodos.ts

代码语言:javascript
复制
import { TodoStatus, type Todo } from '@/types'
import { computed, reactive } from 'vue'

interface TodoStore {
  // 行末尾的分号 ; 可省
  [TodoStatus.Pending]: Todo[]
  [TodoStatus.InProgress]: Todo[]
  [TodoStatus.Completed]: Todo[]
}

const defaultVal = {
  [TodoStatus.Pending]: [
    {
      id: 1,
      title: '测试标题',
      description: '测试描述',
      status: TodoStatus.Pending
    }
  ],
  [TodoStatus.InProgress]: [],
  [TodoStatus.Completed]: []
}

const todoStore = reactive<TodoStore>(defaultVal)

export default () => {
  const getTodosByStatus = (todoStatus: TodoStatus) => {
    return computed(() => todoStore[todoStatus])
  }

  return { getTodosByStatus }
}

src/components/TodoGroup.vue

代码语言:javascript
复制
<script setup lang="ts">
import { TodoStatus } from '@/types'
import { computed } from 'vue'
import useTodos from '@/stores/useTodos'

interface Props {
  status: TodoStatus
}

const props = defineProps<Props>()

const { getTodosByStatus } = useTodos()
const todoList = getTodosByStatus(props.status)

const groupLabel = computed(() => {
  switch (props.status) {
    case TodoStatus.Pending:
      return 'Pending'
    case TodoStatus.InProgress:
      return 'In Progress'
    case TodoStatus.Completed:
      return 'Completed'
    default:
      return 'Todo Group'
  }
})
</script>

<template>
  <div>
    <h3>{{ groupLabel }}</h3>
    <ul>
      <li v-for="todo in todoList" :key="todo.id">{{ todo.title }}</li>
    </ul>
  </div>
</template>

预览

9. 简化 TodoGroup.vue 中 computed

src/components/TodoGroup.vue

代码语言:javascript
复制
<script setup lang="ts">
...

const groupLabel = {
  [TodoStatus.Pending]: 'Pending',
  [TodoStatus.InProgress]: 'In Progress',
  [TodoStatus.Completed]: 'Completed'
}
</script>

<template>
  <div>
    <h3>{{ groupLabel[props.status] }}</h3>
    ...
  </div>
</template>

10. 添加样式

src/components/TodoList.vue

代码语言:javascript
复制
...

<template>
  <div class="groups-wrapper">
    <TodoGroup :status="TodoStatus.Pending" />
    <TodoGroup :status="TodoStatus.InProgress" />
    <TodoGroup :status="TodoStatus.Completed" />
  </div>
</template>

<style lang="less" scoped>
.groups-wrapper {
  display: flex;
  justify-content: space-around;
  gap: 20px;
}
</style>

src/components/TodoGroup.vue

代码语言:javascript
复制
...

<template>
  <div class="group-wrapper">
    <h3>{{ groupLabel[props.status] }}</h3>
    <ul>
      <li v-for="todo in todoList" :key="todo.id">
        {{ todo.title }}
        <div>
          <span class="todo-description">{{ todo.description }}</span>
        </div>
      </li>
    </ul>
  </div>
</template>

<style lang="less" scoped>
.group-wrapper {
  flex: 1;
  padding: 20px;
  background-color: rgb(56, 80, 103);
  width: 300px;
}
.group-wrapper li {
  list-style-type: none;
  background-color: aliceblue;
  color: rgb(56, 80, 103);
  padding: 2px 5px;
  cursor: grab;
  margin-bottom: 10px;
}
.todo-description {
  font-size: 12px;
}
</style>

预览

11. 可拖拽, 样式优化

参考:

代码语言:javascript
复制
pnpm add vuedraggable@next

src/components/TodoGroup.vue

代码语言:javascript
复制
<script setup lang="ts">
...
import Draggable from 'vuedraggable'

...
</script>

<template>
  <div class="group-wrapper">
    <h3>{{ groupLabel[props.status] }}</h3>
    <Draggable class="draggable" :list="todoList" group="todos" item-key="id">
      <template #item="{ element: todo }">
        <li>
          {{ todo.title }}
          <div>
            <span class="todo-description">{{ todo.description }}</span>
          </div>
        </li>
      </template>
    </Draggable>
  </div>
</template>

<style lang="less" scoped>
.group-wrapper {
  flex: 1;
  padding: 20px;
  background-color: rgb(56, 80, 103);
  width: 300px;
  h3 {
    color: rgb(207, 221, 234);
  }
  .draggable {
    min-height: 200px;
    li {
      list-style-type: none;
      background-color: aliceblue;
      color: rgb(56, 80, 103);
      padding: 2px 5px;
      cursor: grab;
      margin-bottom: 10px;
      .todo-description {
        font-size: 12px;
      }
    }
  }
}
</style>

预览, 此时即可拖拽单项 Todo

12. 添加 Todo, 删除 Todo, 拖拽改变分组

src/stores/useTodos.ts

代码语言:javascript
复制
import { TodoStatus, type Todo } from '@/types'
import { computed, reactive } from 'vue'

interface TodoStore {
  // 行末尾的分号 ; 可省
  [TodoStatus.Pending]: Todo[]
  [TodoStatus.InProgress]: Todo[]
  [TodoStatus.Completed]: Todo[]
}

const defaultVal = {
  [TodoStatus.Pending]: [
    {
      id: 1,
      title: '测试标题',
      description: '测试描述',
      status: TodoStatus.Pending
    }
  ],
  [TodoStatus.InProgress]: [],
  [TodoStatus.Completed]: []
}

const todoStore = reactive<TodoStore>(defaultVal)

export default () => {
  const getTodosByStatus = (todoStatus: TodoStatus) => {
    return computed(() => todoStore[todoStatus])
  }

  const updateTodo = (todo: Todo, newStatus: TodoStatus) => {
    todo.status = newStatus
  }

  const addNewTodo = (todo: Todo) => {
    todoStore[todo.status].push(todo)
  }

  const deleteTodo = (todoToDelete: Todo) => {
    todoStore[todoToDelete.status] = todoStore[todoToDelete.status].filter(
      (todo) => todo.id != todoToDelete.id
    )
  }

  return { getTodosByStatus, addNewTodo, deleteTodo, updateTodo }
}

src/components/CreateTodo.vue

代码语言:javascript
复制
<script setup lang="ts">
import { TodoStatus, type Todo } from '@/types'
import { reactive, ref } from 'vue'
import useTodos from '@/stores/useTodos'

interface Props {
  status: TodoStatus
}

const props = defineProps<Props>()

const shouldDisplayForm = ref(false)

const { addNewTodo } = useTodos()

// id 属性对于初始化时不能指定, 通过 Omit id 去除 id 属性
const newTodo = reactive<Omit<Todo, 'id'>>({
  title: '',
  description: '',
  status: props.status
})

const resetForm = () => {
  shouldDisplayForm.value = false
  newTodo.title = ''
  newTodo.description = ''
}

const handleSubmit = () => {
  // add new todo
  addNewTodo({
    id: Math.random() * 10000000000000000,
    ...newTodo
  })

  resetForm()
}
</script>

<template>
  <div>
    <h3 v-if="!shouldDisplayForm" @click="shouldDisplayForm = !shouldDisplayForm">Add New</h3>
    <template v-else>
      <form @submit.prevent="handleSubmit">
        <div>
          <input type="text" placeholder="Title" v-model="newTodo.title" />
        </div>
        <div>
          <input type="text" placeholder="Description" v-model="newTodo.description" />
        </div>
        <button type="submit">Submit</button>
        <button type="button" @click="resetForm">Cancel</button>
      </form>
    </template>
  </div>
</template>

<style lang="less" scoped>
h3 {
  color: rgb(207, 221, 234);
}
</style>

src/components/TodoGroup.vue

代码语言:javascript
复制
<script setup lang="ts">
import { TodoStatus } from '@/types'
import useTodos from '@/stores/useTodos'
import Draggable from 'vuedraggable'
import CreateTodo from './CreateTodo.vue'

interface Props {
  status: TodoStatus
}

const props = defineProps<Props>()

const { getTodosByStatus, deleteTodo, updateTodo } = useTodos()
const todoList = getTodosByStatus(props.status)

const groupLabel = {
  [TodoStatus.Pending]: 'Pending',
  [TodoStatus.InProgress]: 'In Progress',
  [TodoStatus.Completed]: 'Completed'
}

const onDraggableChange = (payload: any) => {
  console.log('payload', payload)
  if (payload?.added?.element) {
    // update todo status
    updateTodo(payload?.added?.element, props.status)
  }
}
</script>

<template>
  <div class="group-wrapper">
    <h3>{{ groupLabel[props.status] }}</h3>

    <Draggable
      class="draggable"
      :list="todoList"
      group="todos"
      item-key="id"
      @change="onDraggableChange"
    >
      <template #item="{ element: todo }">
        <li>
          {{ todo.title }}
          {{ todo.status }}
          <span class="icon-delete" @click="deleteTodo(todo)">x</span>
          <div>
            <span class="todo-description">{{ todo.description }}</span>
          </div>
        </li>
      </template>
    </Draggable>

    <CreateTodo :status="props.status" />
  </div>
</template>

<style lang="less" scoped>
.group-wrapper {
  flex: 1;
  padding: 20px;
  background-color: rgb(56, 80, 103);
  width: 300px;
  h3 {
    color: rgb(207, 221, 234);
  }
  .draggable {
    min-height: 200px;
    li {
      list-style-type: none;
      background-color: aliceblue;
      color: rgb(56, 80, 103);
      padding: 2px 5px;
      cursor: grab;
      margin-bottom: 10px;
      .icon-delete {
        float: right;
        cursor: pointer;
      }
      .todo-description {
        font-size: 12px;
      }
    }
  }
}
</style>

预览

13. 改为 Pinia 状态管理

src/stores/todo.ts

代码语言:javascript
复制
import { defineStore, acceptHMRUpdate } from 'pinia'
import { TodoStatus, type Todo } from '@/types'

interface TodoState {
  // 行末尾的分号 ; 可省
  [TodoStatus.Pending]: Todo[]
  [TodoStatus.InProgress]: Todo[]
  [TodoStatus.Completed]: Todo[]
}

export const useTodoStore = defineStore({
  id: 'todo',

  state: (): TodoState => ({
    [TodoStatus.Pending]: [
      {
        id: 1,
        title: '测试标题',
        description: '测试描述',
        status: TodoStatus.Pending
      }
    ],
    [TodoStatus.InProgress]: [],
    [TodoStatus.Completed]: []
  }),

  getters: {
    getTodosByStatus: (state) => {
      return (todoStatus: TodoStatus): Todo[] => state[todoStatus]
    }
  },

  actions: {
    updateTodo(todo: Todo, newStatus: TodoStatus) {
      console.log('updateTodo')
      // 注意: 经过测试, 可以这么更新, 可以在 Chrome Vue Pinia 标签页看到被正确更新到目标状态下
      // 不仅仅是 由于 getTodosByStatus 的原因 使其看起来像, 而是实际存储改变
      // 感觉挺神奇, 发现改变单个 todo 的 status 居然使其也同步转移到了 state 的 对应属性 下
      // TODO: 为什么不是仅仅改变了此 todo 的 status, 但在 state 中没有改变其所属属性, 导致属性与此 todo status 不匹配
      // 我更新 a.status 从 Pending 到 InProgress,
      // 最终居然还导致 state.Pending 中移除了 a, state.InProgress 添加了 a
      todo.status = newStatus
    },
    addNewTodo(todo: Todo) {
      console.log('addNewTodo')
      this[todo.status].push(todo)
    },
    deleteTodo(todoToDelete: Todo) {
      this[todoToDelete.status] = this[todoToDelete.status].filter(
        (todo) => todo.id != todoToDelete.id
      )
    }
  }
})

if (import.meta.hot) {
  import.meta.hot.accept(acceptHMRUpdate(useTodoStore, import.meta.hot))
}

src/components/CreateTodo.vue

代码语言:javascript
复制
 <script setup lang="ts">
 import { TodoStatus, type Todo } from '@/types'
 import { reactive, ref } from 'vue'
-import useTodos from '@/stores/useTodos'
+// import useTodos from '@/stores/useTodos'
+import { useTodoStore } from '@/stores/todo'

 interface Props {
   status: TodoStatus
@@ -11,7 +12,8 @@ const props = defineProps<Props>()

 const shouldDisplayForm = ref(false)

-const { addNewTodo } = useTodos()
+// const { addNewTodo } = useTodos()
+const todoStore = useTodoStore()

 // id 属性对于初始化时不能指定, 通过 Omit id 去除 id 属性
 const newTodo = reactive<Omit<Todo, 'id'>>({
@@ -28,7 +30,13 @@ const resetForm = () => {

 const handleSubmit = () => {
   // add new todo
-  addNewTodo({
+  // addNewTodo({
+  //   id: Math.random() * 10000000000000000,
+  //   ...newTodo
+  // })
+
+  // pinia
+  todoStore.addNewTodo({
     id: Math.random() * 10000000000000000,
     ...newTodo
   })

src/components/TodoGroup.vue

代码语言:javascript
复制
 <script setup lang="ts">
-import { TodoStatus } from '@/types'
-import useTodos from '@/stores/useTodos'
+import { TodoStatus, type Todo } from '@/types'
+// import useTodos from '@/stores/useTodos'
+import { useTodoStore } from '@/stores/todo'
 import Draggable from 'vuedraggable'
 import CreateTodo from './CreateTodo.vue'
+import { storeToRefs } from 'pinia';

 interface Props {
   status: TodoStatus
@@ -10,8 +12,13 @@ interface Props {

 const props = defineProps<Props>()

-const { getTodosByStatus, deleteTodo, updateTodo } = useTodos()
-const todoList = getTodosByStatus(props.status)
+// const { getTodosByStatus, deleteTodo, updateTodo } = useTodos()
+const todoStore = useTodoStore()
+// const todoList = getTodosByStatus(props.status)
+// 错误
+// const todoList = todoStore.getTodosByStatus(props.status)
+// 正确
+const { getTodosByStatus } = storeToRefs(todoStore)

 const groupLabel = {
   [TodoStatus.Pending]: 'Pending',
@@ -23,9 +30,17 @@ const onDraggableChange = (payload: any) => {
   console.log('payload', payload)
   if (payload?.added?.element) {
     // update todo status
-    updateTodo(payload?.added?.element, props.status)
+    // updateTodo(payload?.added?.element, props.status)
+
+    // pinia
+    todoStore.updateTodo(payload?.added?.element, props.status)
   }
 }
+
+const deleteTodo = (todo: Todo) => {
+  console.log('deleteTodo', todo)
+  todoStore.deleteTodo(todo)
+}
 </script>

 <template>
@@ -34,7 +49,7 @@ const onDraggableChange = (payload: any) => {

     <Draggable
       class="draggable"
-      :list="todoList"
+      :list="getTodosByStatus(props.status)"
       group="todos"
       item-key="id"
       @change="onDraggableChange"

将 Pending 中的 a 拖动到 In Progress 中

拖动完成

即为索引为 1 的 In Progress 添加一项, 索引为 0 的 Pending 删除一项

Chrome 扩展 Vue

添加 watch,

src/main.ts

代码语言:javascript
复制
 import './assets/main.css'

-import { createApp } from 'vue'
+import { createApp, watch } from 'vue'
 import { createPinia } from 'pinia'

 import App from './App.vue'
 import router from './router'

 const app = createApp(App)
+const pinia = createPinia()

-app.use(createPinia())
+// 你可以在 pinia 实例上使用 watch() 函数侦听整个 state。
+watch(
+  pinia.state,
+  (state) => {
+    // 每当状态发生变化时,将整个 state 持久化到本地存储。
+    console.log('watch: pinia.state', state)
+    localStorage.setItem('piniaState', JSON.stringify(state))
+  },
+  { deep: true }
+)
+
+app.use(pinia)
 app.use(router)

 app.mount('#app')

将 测试标题 从 Pending 拖动到 In Progress ,发现会神奇的触发两次, 而且两次输出都在 In Progress 内

Q&A

Q: Use volar-service-vetur instead of Vetur 参考: services/packages/vetur at master · volarjs/services · GitHub vuejs/vetur: Vue tooling for VS Code.

可尝试先禁用 Vetur 插件

补充

pnpm: node_modules

npm vs pnpm

npm command

pnpm equivalent

npm install

pnpm install

npm i <pkg>

pnpm add <pkg>

npm run <cmd>

pnpm <cmd>

Command

Meaning

pnpm add sax

Save to dependencies

pnpm add -D sax

Save to devDependencies

pnpm add -O sax

Save to optionalDependencies

pnpm add -g sax

Install package globally

pnpm add sax@next

Install from the next tag

pnpm add sax@3.0.0

Specify version 3.0.0

ESLint + Prettier

ESLint 可以同时解决代码格式和代码质量,Prettier 没有使用的必要了?但其实 ESLint 主要解决的是代码质量的问题,代码格式这部分 ESLint 并没有全部做完。Prettier 就是接管了两类问题中的代码格式,并进行自动修复

Vue 中 @click

@ 是 v-on 的语法糖, @click 为点击事件

在原生 DOM 对象中,有 onclick , 传递一个函数调用, 即 onclick="add()"

在 vue 中,即可传递函数,也可以传递函数调用(只有当事件触发时,才会执行)

代码语言:javascript
复制
<button @click="add">+1</button>

methods: {
   add() {
			// 如果在方法中要修改 data 中的数据,可以通过 this 访问到
			this.count += 1
   }
}

或者

代码语言:javascript
复制
<!-- vue 提供了内置变量,名字叫 $event(固定写法),它就是原生 DOM 的事件对象 e -->
<button @click="add(3, $event)"></button>

methods: {
   add(n, e) {
			// 如果在方法中要修改 data 中的数据,可以通过 this 访问到
			this.count += 1
   }
}

在 Vue.js 中,其中值甚至可以是一个表达式, 如 下方

代码语言:javascript
复制
<script setup>
import { ref } from 'vue'

const open = ref(false)
</script>

<template>
  <button @click="open = true">Open Modal</button>

  <div v-if="open" class="modal">
    <p>Hello from the modal!</p>
    <button @click="open = false">Close</button>
  </div>
</template>

在 React.js 中,只能传递一个函数值

代码语言:javascript
复制
<button type="button" className="bordered medium"
  onClick={onCancel}
>
  cancel
</button>

若需要在调用函数的同时传递参数的话,可以通过在其中将其包装为一个箭头函数

代码语言:javascript
复制
<button
  className=" bordered"
  onClick={() => {
    handleEditClick(project);
  }}
>
  <span className="icon-edit "></span>
  Edit
</button>

同时可以发现, Vue 中使用 双引号 , 但并不代表其值为字符串类型, 而 React 中使用 {} 表示其中为非字符串

Vue 3 中定义响应数据 (reactive/ref)

参考:

"响应数据" 就是值变化可以驱动dom变化的数据, 我们之前在 "data" 中定义的数据就是响应数据. 但是在 "setup" 中如果我们要定义数据, 这里并没有 "data" 函数, 取而代之的是 "reactive/ref"函数:

reactive

定义响应数据, 输入只能是对象类型, 返回输入对象的响应版本.

ref

同样是定义响应数据, 和"reactive"的区别是返回值响应数据的格式不同, ref 返回的数据需要用".value"访问.

代码语言:javascript
复制
const n = ref(110);
console.log(n);

reactive 和 ref 的选择

重要:

  • 如果要监视的数据是引用型数据(object)那么就是用 reactive
  • 如果是(number/boolean/string)等原始数据类型就用 ref

状态管理: Pinia

基础示例

代码语言:javascript
复制
// stores/counter.js
import { defineStore } from 'pinia'

export const useCounterStore = defineStore('counter', {
  state: () => {
    return { count: 0 }
  },
  // ☆★☆★ 也可以这样定义 ★☆★☆
  // state: () => ({ count: 0 })
  actions: {
    increment() {
      this.count++
    },
  },
})

在一个组件中使用该 store

代码语言:javascript
复制
<script setup>
import { useCounterStore } from '@/stores/counter'
const counter = useCounterStore()

// 1.
counter.count++
// 2.
// 自动补全! ✨
counter.$patch({ count: counter.count + 1 })
// 3.
// 或使用 action 代替
counter.increment()

</script>
<template>
  <!-- 直接从 store 中访问 state -->
  <div>Current Count: {{ counter.count }}</div>
</template>

为实现更多高级用法,你甚至可以使用一个函数 (与组件 setup() 类似) 来定义一个 Store:

代码语言:javascript
复制
export const useCounterStore = defineStore('counter', () => {
  const count = ref(0)
  function increment() {
    count.value++
  }
  
  // ☆★☆★ 注意: 将 状态 与 操作 都封装到一个对象中返回 ★☆★☆
  return { count, increment }
})

更真实的示例

这是一个更完整的 Pinia API 示例

代码语言:javascript
复制
import { defineStore } from 'pinia'

export const useTodos = defineStore('todos', {
  state: () => ({
    /** @type {{ text: string, id: number, isFinished: boolean }[]} */
    todos: [],
    /** @type {'all' | 'finished' | 'unfinished'} */
    filter: 'all',
    // 类型将自动推断为 number
    nextId: 0,
  }),
  getters: {
    finishedTodos(state) {
      // 自动补全! ✨
      return state.todos.filter((todo) => todo.isFinished)
    },
    unfinishedTodos(state) {
      return state.todos.filter((todo) => !todo.isFinished)
    },
    /**
     * @returns {{ text: string, id: number, isFinished: boolean }[]}
     */
    filteredTodos(state) {
      if (this.filter === 'finished') {
        // 调用其他带有自动补全的 getters ✨
        return this.finishedTodos
      } else if (this.filter === 'unfinished') {
        return this.unfinishedTodos
      }
      return this.todos
    },
  },
  actions: {
    // 接受任何数量的参数,返回一个 Promise 或不返回
    addTodo(text) {
      // 你可以直接变更该状态
      this.todos.push({ text, id: this.nextId++, isFinished: false })
    },
  },
})

购物车示例 stores/cart.ts

代码语言:javascript
复制
import { defineStore, acceptHMRUpdate } from 'pinia'
import { useUserStore } from './user'

export const useCartStore = defineStore({
  id: 'cart',
  state: () => ({
    rawItems: [] as string[],
  }),
  getters: {
    items: (state): Array<{ name: string; amount: number }> =>
      state.rawItems.reduce((items, item) => {
        const existingItem = items.find((it) => it.name === item)

        if (!existingItem) {
          items.push({ name: item, amount: 1 })
        } else {
          existingItem.amount++
        }

        return items
      }, [] as Array<{ name: string; amount: number }>),
  },
  actions: {
    addItem(name: string) {
      this.rawItems.push(name)
    },

    removeItem(name: string) {
      const i = this.rawItems.lastIndexOf(name)
      if (i > -1) this.rawItems.splice(i, 1)
    },

    async purchaseItems() {
      const user = useUserStore()
      if (!user.name) return

      console.log('Purchasing', this.items)
      const n = this.items.length
      this.rawItems = []

      return n
    },
  },
})

if (import.meta.hot) {
  import.meta.hot.accept(acceptHMRUpdate(useCartStore, import.meta.hot))
}

stores/user.ts

代码语言:javascript
复制
// @ts-check
import { defineStore, acceptHMRUpdate } from 'pinia'

/**
 * Simulate a login
 */
function apiLogin(a: string, p: string) {
  if (a === 'ed' && p === 'ed') return Promise.resolve({ isAdmin: true })
  if (p === 'ed') return Promise.resolve({ isAdmin: false })
  return Promise.reject(new Error('invalid credentials'))
}

export const useUserStore = defineStore({
  id: 'user',
  state: () => ({
    name: 'Eduardo',
    isAdmin: true,
  }),

  actions: {
    logout() {
      this.$patch({
        name: '',
        isAdmin: false,
      })

      // we could do other stuff like redirecting the user
    },

    /**
     * Attempt to login a user
     */
    async login(user: string, password: string) {
      const userData = await apiLogin(user, password)

      this.$patch({
        name: user,
        ...userData,
      })
    },
  },
})

if (import.meta.hot) {
  import.meta.hot.accept(acceptHMRUpdate(useUserStore, import.meta.hot))
}

Pinia vs Vuex

参考:

  1. Pinia 实现了 Vuex 5 大部分, 最终决定 Pinia 为 Vuex 下一代
  2. 与 Vuex 相比,Pinia 提供了一个更简单的 API,具有更少的仪式,提供了 Composition API 风格的 API
  3. Pinia 与 TypeScript 一起使用时具有可靠的类型推断支持, Vuex 之前对 TS 的支持很不友好
  4. Pinia: mutations 不再存在
    • 它经常被认为是 非常 冗长
    • 它最初带来了 devtools 集成,但这不再是问题
  5. Pinia: 不再有 modules 的嵌套结构
    • 你可以灵活使用每一个store,它们是通过扁平化的方式来相互使用的
    • 也不再有命名空间的概念,不需要记住它们的复杂关系

概念

Store 有三个核心概念

  1. state
  2. getters
  3. actions

等同于组件的 data、computed、methods

代码语言:javascript
复制
import { defineStore } from 'pinia'

// 你可以对 `defineStore()` 的返回值进行任意命名,但最好使用 store 的名字,同时以 `use` 开头且以 `Store` 结尾。(比如 `useUserStore`,`useCartStore`,`useProductStore`)
// 第一个参数是你的应用中 Store 的唯一 ID。
export const useMainStore = defineStore('main',{
  // id:'main', // 如果 defineStore 没有传入第一个参数 name, 而是直接传入一个对象,那么我们可以在这里设置 id;是等价的
  state: () => ({ counter: 0 }),
})
  • 这个 name,也称为 id,是必要的,Pinia 使用它来将 store 连接到 devtools。
  • 第一个参数是应用程序中 store 的唯一 id
  • 返回的函数统一使用useXxxStore作为命名方案,这是约定的规范;
  • xxxStore = useXxxStore()

Option Store

Option Store

与 Vue 的选项式 API 类似,我们也可以传入一个带有 state、actions 与 getters 属性的 Option 对象

代码语言:javascript
复制
export const useCounterStore = defineStore('counter', {
  state: () => ({ count: 0 }),
  getters: {
    double: (state) => state.count * 2,
  },
  actions: {
    increment() {
      this.count++
    },
  },
})

可以认为 state 是 store 的数据 (data),getters 是 store 的计算属性 (computed),而 actions 则是方法 (methods)。

Setup Store

Setup Store

也存在另一种定义 store 的可用语法。

与 Vue 组合式 API 的 setup 函数 相似,我们可以传入一个函数,该函数定义了一些响应式属性和方法,

并且返回一个带有我们想暴露出去的属性和方法的对象。

代码语言:javascript
复制
export const useCounterStore = defineStore('counter', () => {
  const count = ref(0)
  function increment() {
    count.value++
  }

  return { count, increment }
})

在 Setup Store 中:

  • ref() 就是 state 属性
  • computed() 就是 getters
  • function() 就是 actions

Setup store 比 Option Store 带来了更多的灵活性,

因为你可以在一个 store 内创建侦听器,并自由地使用任何组合式函数。

不过,请记住,使用组合式函数会让 SSR 变得更加复杂。

使用 Store

虽然我们前面定义了一个 store, 但在我们使用 <script setup> 调用 useStore()(或者使用 setup() 函数, 像所有的组件那样) 之前,store 实例是不会被创建的:

代码语言:javascript
复制
<script setup>
import { useCounterStore } from '@/stores/counter'
// 可以在组件中的任意位置访问 `store` 变量 ✨
const store = useCounterStore()
</script>

请注意,store 是一个用 reactive 包装的对象, 这意味着不需要在 getters 后面写 .value,就像 setup 中的 props 一样, 如果你写了,我们也不能解构它:

代码语言:javascript
复制
<script setup>
const store = useCounterStore()
// ❌ 这将不起作用,因为它破坏了响应性
// 这就和直接解构 `props` 一样
const { name, doubleCount } = store 
name // 将始终是 "Eduardo" 
doubleCount // 将始终是 0 
setTimeout(() => {
  store.increment()
}, 1000)
// ✅ 这样写是响应式的
// 💡 当然你也可以直接使用 `store.doubleCount`
const doubleValue = computed(() => store.doubleCount)
</script>

注意其中的下方为 错误 示范

代码语言:javascript
复制
const { name, doubleCount } = store 
name // 将始终是 "Eduardo" 
doubleCount // 将始终是 0 

为了从 store 中提取属性时保持其响应性,你需要使用 storeToRefs()。 它将为每一个响应式属性创建引用。 当你只使用 store 的状态而不调用任何 action 时,它会非常有用。 请注意,你可以直接从 store 中解构 action,因为它们也被绑定到 store 上:

代码语言:javascript
复制
<script setup>
import { storeToRefs } from 'pinia'
const store = useCounterStore()
// `name` 和 `doubleCount` 是响应式的 ref
// 同时通过插件添加的属性也会被提取为 ref
// 并且会跳过所有的 action 或非响应式 (不是 ref 或 reactive) 的属性
const { name, doubleCount } = storeToRefs(store)
// 作为 action 的 increment 可以直接解构
const { increment } = store
</script>

Pinia 中 state 的响应式

Pinia 在底层将 state 用 reactive 做了处理

Getter

Getter 完全等同于 store 的 state 的计算值。 可以通过 defineStore() 中的 getters 属性来定义它们。 推荐使用箭头函数,并且它将接收 state 作为第一个参数:

大多数时候,getter 仅依赖 state, 不过,有时它们也可能会使用其他 getter。 因此,即使在使用常规函数定义 getter 时,我们也可以通过 this 访问到整个 store 实例, 但 (在 TypeScript 中) 必须定义返回类型。 这是为了避免 TypeScript 的已知缺陷, 不过这不影响用箭头函数定义的 getter,也不会影响不使用 this 的 getter。

代码语言:javascript
复制
export const useStore = defineStore('main', {
  state: () => ({
    count: 0,
  }),
  getters: {
    // 自动推断出返回类型是一个 number
    doubleCount(state) {
      return state.count * 2
    },
    // 返回类型**必须**明确设置
    doublePlusOne(): number {
      // 整个 store 的 自动补全和类型标注 ✨
      return this.doubleCount + 1
    },
  },
})

直接访问 store 实例上的 getter

代码语言:javascript
复制
<script setup>
import { useCounterStore } from './counterStore'
const store = useCounterStore()
</script>
<template>
  <p>Double count is {{ store.doubleCount }}</p>
</template>
向 getter 传递参数

Getter 只是幕后的计算属性,所以不可以向它们传递任何参数。 不过,你可以从 getter 返回一个函数,该函数可以接受任意参数:

代码语言:javascript
复制
export const useStore = defineStore('main', {
  getters: {
    getUserById: (state) => {
      // 在内部再返回一个箭头函数
      return (userId) => state.users.find((user) => user.id === userId)
    },
  },
})

在组件中使用:

代码语言:javascript
复制
<script setup>
import { useUserListStore } from './store'
const userList = useUserListStore()
const { getUserById } = storeToRefs(userList)
// 请注意,你需要使用 `getUserById.value` 来访问
// <script setup> 中的函数
</script>

<template>
  <p>User 2: {{ getUserById(2) }}</p>
</template>

请注意,当你这样做时,getter 将不再被缓存,它们只是一个被你调用的函数。 不过,你可以在 getter 本身中缓存一些结果,虽然这种做法并不常见,但有证明表明它的性能会更好:

代码语言:javascript
复制
export const useStore = defineStore('main', {
  getters: {
    getActiveUserById(state) {
      const activeUsers = state.users.filter((user) => user.active)
      return (userId) => activeUsers.find((user) => user.id === userId)
    },
  },
})
访问其他 store 的 getter

想要使用另一个 store 的 getter 的话,那就直接在 getter 内使用就好:

代码语言:javascript
复制
import { useOtherStore } from './other-store'

export const useStore = defineStore('main', {
  state: () => ({
    // ...
  }),
  getters: {
    otherGetter(state) {
      // 就像是 在组件中访问 getter 一样
      const otherStore = useOtherStore()
      return state.localData + otherStore.data
    },
  },
})
使用 setup() 时的用法

作为 store 的一个属性,你可以直接访问任何 getter( 与 state 属性完全一样 ):

代码语言:javascript
复制
<script setup>
const store = useCounterStore()
store.count = 3
store.doubleCount // 6
</script>
使用选项式 API 的用法
代码语言:javascript
复制
// 示例文件路径:
// ./src/stores/counter.js

import { defineStore } from 'pinia'

export const useCounterStore = defineStore('counter', {
  state: () => ({
    count: 0,
  }),
  getters: {
    doubleCount(state) {
      return state.count * 2
    },
  },
})
使用 setup()
代码语言:javascript
复制
<script>
import { useCounterStore } from '../stores/counter'

export default defineComponent({
  setup() {
    const counterStore = useCounterStore()

    return { counterStore }
  },
  computed: {
    quadrupleCounter() {
      return this.counterStore.doubleCount * 2
    },
  },
})
</script>
不使用 setup()

你可以使用前一节的 state 中的 mapState() 函数来将其映射为 getters:

代码语言:javascript
复制
import { mapState } from 'pinia'
import { useCounterStore } from '../stores/counter'

export default {
  computed: {
    // 允许在组件中访问 this.doubleCount
    // 与从 store.doubleCount 中读取的相同
    ...mapState(useCounterStore, ['doubleCount']),
    // 与上述相同,但将其注册为 this.myOwnName
    ...mapState(useCounterStore, {
      myOwnName: 'doubleCount',
      // 你也可以写一个函数来获得对 store 的访问权
      double: store => store.doubleCount,
    }),
  },
}

Action

参考:

Action 相当于组件中的 method。 它们可以通过 defineStore() 中的 actions 属性来定义,并且它们也是定义业务逻辑的完美选择。

类似 getter,action 也可通过 this 访问整个 store 实例, 不同的是,action 可以是异步的, 你可以在它们里面 await 调用任何 API,以及其他 action!下面是一个使用 Mande 的例子。 请注意,你使用什么库并不重要,只要你得到的是一个Promise, 你甚至可以 (在浏览器中) 使用原生 fetch 函数:

代码语言:javascript
复制
import { mande } from 'mande'

const api = mande('/api/users')

export const useUsers = defineStore('users', {
  state: () => ({
    userData: null,
    // ...
  }),

  actions: {
    async registerUser(login, password) {
      try {
        this.userData = await api.post({ login, password })
        showTooltip(`Welcome back ${this.userData.name}!`)
      } catch (error) {
        showTooltip(error)
        // 让表单组件显示错误
        return error
      }
    },
  },
})

Action 可以像函数或者通常意义上的方法一样被调用:

代码语言:javascript
复制
<script setup>
const store = useCounterStore()
// 将 action 作为 store 的方法进行调用
store.randomizeCounter()
</script>
<template>
  <!-- 即使在模板中也可以 -->
  <button @click="store.randomizeCounter()">Randomize</button>
</template>
访问其他 store 的 action

和 getter 一样, 直接在 action 中用

代码语言:javascript
复制
import { useAuthStore } from './auth-store'

export const useSettingsStore = defineStore('settings', {
  state: () => ({
    preferences: null,
    // ...
  }),
  actions: {
    async fetchUserPreferences() {
      // 同样调用方式, 和在组件中用一样
      // 不过这里 isAuthenticated 目测不是 action 啊
      const auth = useAuthStore()
      if (auth.isAuthenticated) {
        this.preferences = await fetchPreferences()
      } else {
        throw new Error('User must be authenticated')
      }
    },
  },
})
使用选项式 API 的用法
代码语言:javascript
复制
// 示例文件路径:
// ./src/stores/counter.js

import { defineStore } from 'pinia'

const useCounterStore = defineStore('counter', {
  state: () => ({
    count: 0
  }),
  actions: {
    // 普通函数 method
    increment() {
      // 直接用 this 访问 state 内的 count
      this.count++
    }
  }
})
使用 setup()
不使用 setup()
订阅 action

参考:

Pinia: 热更新 HMR (Hot Module Replacement)

参考:

Pinia 支持热更新, 所以你可以编辑你的 store,并直接在你的应用中与它们互动,而不需要重新加载页面, 允许你保持当前的 state、并添加甚至删除 state、action 和 getter。

目前,只有 Vite 被官方支持

比方说,你有三个 store:auth.js、 cart.js 和 chat.js, 你必须在每个 store 声明后都添加(和调整)这段代码。

代码语言:javascript
复制
// auth.js
import { defineStore, acceptHMRUpdate } from 'pinia'

const useAuth = defineStore('auth', {
  // 配置...
})

// 确保传递正确的 store 声明,本例中为 `useAuth`
if (import.meta.hot) {
  import.meta.hot.accept(acceptHMRUpdate(useAuth, import.meta.hot))
}

Vue 3 风格 vs Vue 2 风格

向下兼容: Vue 3 可以部分兼容 Vue 2 写法

Vue 2 写法 + Vue 3 特性

纯 Vue 3: setup 语法糖

Vue 3 setup 语法糖,使 data , methods 直接写在了 <script setup> 内

Vue 2 : 选项式 API Vue 3 : setup: 组合式 API 注意: Pinia 定义 store 即可选项式 API,也可组合式 API 个人理解: 本质上 Vue 2 , Vue 3 都属于 申明式, 而 Vue 3 与 React 新版一样都趋向于 函数式 编程

Vue 2 响应式问题 与 Vue 3 改进

Vue 2 响应式

当点击按钮时, 对 obj.a 进行更新,可以立即在界面上看到变化, 然而对 data 中本不存在的 obj.b 进行新增, 就会发现在界面上没有变化, 当然实际上成功在 obj 上新增了 b, 只是没有在界面上产生(响应式)更新/变化 而已。 查看一下 this

查看控制台, 发现 b 没有对应的 get b, set b

在 Vue 2 中,我们可以这么解决

再次查看控制台, 发现 b 已拥有对应 get b, set b

Vue 3 响应式

死数据: 界面不会随着数据的更新而更新, 始终显示初始值

响应式: ref 使用时需要 x.value

其实 ref 里也可以放对象, 甚至在 对象 中新增属性, 也会在界面上更新(响应式), 而 Vue 2 直接用就不行

响应式 reactive 接收一个对象作为参数 无需 x.value

在 对象 上新增属性也没问题

若 给 reactive 传递一个 非对象/数组 值,例如: reactive('1')

代码语言:javascript
复制
cannot be made reactive

这时会发现点击按钮后,界面上 str 并没有更新到 2

传递数组, 修改数组某项值, 成功更新到界面

3+2 写法

Vue 2 与 Vue 3 数据拦截 不同点

Vue 2.x Object.defineProperty

Vue 3.x new Proxy

Object.defineProperty

查看控制台

但是注意

添加一个 obj.x 后

但是发现 obj.x 没有 get, set

new Proxy

查看控制台

发现全部都成功 尝试修改

查看控制台

修改 m.obj.x

会发现 Proxy 实现起来性能 高于 Object.defineProperty Object.defineProperty 实现此效果需循环以及递归

Vue 3: setup 语法糖插件: unplugin-auto-import

自动帮助引入 ref, reactive, 甚至 toRefs 等 无需再在组件内手动显式引用

代码语言:javascript
复制
npm i -D unplugin-auto-import

vite.config.js

Vue 3: toRefs

从一个 reactive 中解构数据, 会导致解构出的数据为普通数据,不具有响应式特点

查看控制台,就是字符串 "张三"

修改也就不具有响应式特点,无法在界面得到同步更新 使用 toRefs(obj) 解决这个问题

Vue 2 与 Vue 3 中 computed

Vue 2

Vue 2 加上 get(){} 和 set(val){} 使得计算属性可变

Vue 3 计算属性

添加 get(){} , set(val){} 使其可变(可更新)

Vue: watch

Vue 3

同时监听多个 发现不是很好判定是哪个数据发生了改变, str, num 的改变都会调用此函数

初始化监听 即第一次初始化时,就触发一次, 即一开始就会触发一次 watch

watch 对象时, 例如 watch 被 reactive 的 Proxy 对象时, TODO: 发现 oldVal 与 newVal 一致,这是为什么 ? 监听一个对象中的某属性

image-20230702211354232

image-20230702211336878注意,下方要用到 deep: true 否则监听不到变化

image-20230702211547353立即执行监听函数 和初始化有点像

image-20230702212029801

image-20230702212044934

image-20230702212100803

Vue: 路由

image-20230702212247418

  • Vue 3 中 useRouter 等价于 Vue 2 中 this.$router
  • Vue 3 中 UseRoute 等价于 Vue 2 中 this.$route

PS: Vue 3: 若使用了 unplugin-auto-import 插件,并配置了 vue-router ,则无需手动导入

Vue 3 监听路由

image-20230702212720908

Vue Router

image-20230702220640536导航守卫

image-20230702220827179

Vue: 生命周期

Vue 3 setup

image-20230702213812486关于 setup

image-20230702214910029

setup 执行的时机 > 在 beforeCreate 之前执行一次,this 是 undefined 。

代码语言:javascript
复制
beforeCreate(){
	console.log('beforeCreate');
},
setup(){
	console.log('setup',this);
}

Vue: 组件: 父传子

Vue 3

image-20230702221509664子组件接收 props

image-20230702222330271还有一种选项式 API 写法

image-20230702222910709

如果你没有使用 <script setup>,

props 必须以 props 选项的方式 声明 ,props 对象会作为 setup() 函数的第一个参数被传入:

代码语言:javascript
复制
export default {
  props: ['title'],
  setup(props) {
    console.log(props.title)
  }
}

Vue: 组件: 子传父

Vue 3 选项式 API

image-20230702223523586

image-20230702223613256setup 组合式 API 子

image-20230702224019162父

image-20230702224328549

Vue: v-model 传值

Vue 3

image-20230702225513895

Vue: 组件: 兄弟组件传值

用第三方包: mitt

代码语言:javascript
复制
npm install -S mitt

src/plugins/Bus.js

image-20230702230943646

A 组件

image-20230702231147965

B 组件

image-20230702231345186

Vue: 插槽

匿名插槽

image-20230704230011161

具名插槽

image-20230704230223698

PS: v-slot:xxx 可以简写为 #xxx

image-20230704230328824

作用域插槽

image-20230704231050765

动态插槽

image-20230704231249291

Vue: Teleport

传送,在指定位置展示 可用于子组件内需要在父组件范围内定位某些元素, 有些时候,封装在子组件中更为合适,或者说父组件(宿主组件)行为无法确定, 你是在写组件库等时,但需要某些元素放在此组件外部,例如某些定位行为

如果此组件在父组件中,那么可以在父组件范围内传送

image-20230704231903034

Vue: 动态组件

image-20230704233700106

PS: 其中 组件 是没有必要响应式的,于是使用 markRaw(A) 可提高性能

Vue: 异步组件

基本用法

在大型项目中,我们可能需要拆分应用为更小的块,并 仅在需要时再从服务器加载相关组件。

Vue 提供了 defineAsyncComponent 方法来实现此功能:

代码语言:javascript
复制
import { defineAsyncComponent } from 'vue'

const AsyncComp = defineAsyncComponent(() => {
  return new Promise((resolve, reject) => {
    // ...从服务器获取组件
    resolve(/* 获取到的组件 */)
  })
})
// ... 像使用其他一般组件一样使用 `AsyncComp`

如你所见,defineAsyncComponent 方法接收一个返回 Promise 的加载函数。

这个 Promise 的 resolve 回调方法应该在从服务器获得组件定义时调用。

你也可以调用 reject(reason) 表明加载失败。

ES 模块动态导入也会返回一个 Promise,所以多数情况下我们会将它和 defineAsyncComponent 搭配使用。

类似 Vite 和 Webpack 这样的构建工具也支持此语法 (并且会将它们作为打包时的代码分割点),

因此我们也可以用它来导入 Vue 单文件组件:

代码语言:javascript
复制
import { defineAsyncComponent } from 'vue'

const AsyncComp = defineAsyncComponent(() =>
  import('./components/MyComponent.vue')
)

最后 得到的 AsyncComp 是一个外层包装过的组件,仅在页面需要它渲染时才会调用加载内部实际组件的函数。

它会将接收到的 props 和插槽传给内部组件,所以你可以使用这个异步的包装组件无缝地替换原始组件,同时实现延迟加载。

与普通组件一样,异步组件可以使用 app.component() 全局注册:

代码语言:javascript
复制
app.component('MyComponent', defineAsyncComponent(() =>
  import('./components/MyComponent.vue')
))

也可以直接在父组件中直接定义它们:

代码语言:javascript
复制
<script setup>
import { defineAsyncComponent } from 'vue'

const AdminPage = defineAsyncComponent(() =>
  import('./components/AdminPageComponent.vue')
)
</script>

<template>
  <AdminPage />
</template>

例子

image-20230708163102018

image-20230708163444025

加载与错误状态

异步操作不可避免地会涉及到加载和错误状态,因此 defineAsyncComponent() 也支持在高级选项中处理这些状态:

Vue: Mixin

Mixin 混入 分发 Vue 组件中的可复用功能 mixin.js

image-20230708165029366A.vue

image-20230708164810408

另一种写法: 选项式 mixin.js

image-20230708165544846A.vue

image-20230708170134238

Vue: Provide 与 Inject

依赖注入

image-20230708172025493

Vuex

v-if vs. v-show

v-if 是“真实的”按条件渲染,因为它确保了在切换时,条件区块内的事件监听器和子组件都会被 销毁与重建。 v-if 也是惰性的:如果在初次渲染时条件值为 false,则不会做任何事。条件区块只有当条件首次变为 true 时才被渲染。 相比之下,v-show 简单许多,元素无论初始条件如何,始终会被渲染,只有 CSS display 属性会被切换。 总的来说,v-if 有更高的切换开销,而 v-show 有更高的初始渲染开销。 因此,如果需要频繁切换,则使用 v-show 较好;如果在运行时绑定条件很少改变,则 v-if 会更合适。

v-if 和 v-for

警告 同时使用 v-if 和 v-for 是不推荐的,因为这样二者的优先级不明显。 请查看风格指南获得更多信息。

当 v-if 和 v-for 同时存在于一个元素上的时候,v-if 会首先被执行。 请查看列表渲染指南获取更多细节。

TypeScript 与 组合式 API

为组件的 props 标注类型

使用 <script setup>

当使用 <script setup> 时,defineProps() 宏函数支持从它的参数中推导类型:

代码语言:javascript
复制
<script setup lang="ts">
const props = defineProps({
  foo: { type: String, required: true },
  bar: Number
})

props.foo // string
props.bar // number | undefined
</script>

这被称之为“运行时声明”,因为传递给 defineProps() 的参数会作为运行时的 props 选项使用。

然而,通过 泛型参数 来定义 props 的类型通常更直接:

代码语言:javascript
复制
<script setup lang="ts">
// 用 bar? 表示 bar 可为 undefined, 
// 注意和 C# 不同,C# 表示类型可 null 是在类型后加 '?' , eg: int?
const props = defineProps<{
  foo: string
  bar?: number
}>()
</script>

这被称之为 “基于类型的声明”。

编译器会尽可能地尝试根据类型参数推导出等价的运行时选项。

在这种场景下,我们第二个例子中编译出的运行时选项和第一个是完全一致的。

基于类型的声明 或者 运行时声明 可以择一使用,但是不能同时使用。

我们也可以将 props 的类型移入一个单独的接口中:

代码语言:javascript
复制
<script setup lang="ts">
interface Props {
  foo: string
  bar?: number
}

const props = defineProps<Props>()
</script>
Props 解构默认值

当使用基于类型的声明时,我们失去了为 props 声明默认值的能力。

这可以通过 withDefaults 编译器宏解决:

代码语言:javascript
复制
export interface Props {
  msg?: string
  labels?: string[]
}

const props = withDefaults(defineProps<Props>(), {
  msg: 'hello',
  labels: () => ['one', 'two']
})

这将被编译为等效的运行时 props default 选项。

此外,withDefaults 帮助程序为默认值提供类型检查,并确保返回的 props 类型删除了已声明默认值的属性的可选标志。

为 ref() 标注类型

ref 会根据初始化时的值推导其类型:

参考

感谢帮助!

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2023-06-29,如有侵权请联系 cloudcommunity@tencent.com 删除
目录
  • 技术栈
  • 本地开发环境
  • 1. 安装 pnpm
  • 2. 使用 create-vue 的模版创建项目
  • 3. 安装 less
  • 4. 创建组件 TodoList.vue
  • 6. 修改组件
  • 7. 进一步整理
  • 8. 利用 reactive 创建本地 stores/useTodos.ts
  • 10. 添加样式
  • 11. 可拖拽, 样式优化
  • 12. 添加 Todo, 删除 Todo, 拖拽改变分组
    • Q: Use volar-service-vetur instead of Vetur 参考: services/packages/vetur at master · volarjs/services · GitHub vuejs/vetur: Vue tooling for VS Code.
    • pnpm: node_modules
    • npm vs pnpm
    • ESLint + Prettier
    • Vue 中 @click
    • Vue 3 中定义响应数据 (reactive/ref)
      • reactive
      • ref
      • reactive 和 ref 的选择
    • 状态管理: Pinia
      • Pinia vs Vuex
      • 概念
      • Option Store
      • Setup Store
      • 使用 Store
      • Pinia 中 state 的响应式
      • Getter
      • Action
      • Pinia: 热更新 HMR (Hot Module Replacement)
    • Vue 3 风格 vs Vue 2 风格
    • Vue 2 响应式问题 与 Vue 3 改进
    • Vue 2 与 Vue 3 数据拦截 不同点
    • Vue 3: setup 语法糖插件: unplugin-auto-import
    • Vue 3: toRefs
    • Vue 2 与 Vue 3 中 computed
    • Vue: watch
    • Vue: 路由
    • Vue: 生命周期
    • Vue: 组件: 父传子
    • Vue: 组件: 子传父
    • Vue: v-model 传值
    • Vue: 组件: 兄弟组件传值
    • Vue: 插槽
    • Vue: Teleport
    • Vue: 动态组件
    • Vue: 异步组件
      • 基本用法
      • 加载与错误状态
    • Vue: Mixin
    • Vue: Provide 与 Inject
    • Vuex
    • v-if vs. v-show
    • v-if 和 v-for
    • TypeScript 与 组合式 API
      • 为组件的 props 标注类型
      • 为 ref() 标注类型
  • 参考
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档