Composition API最佳实践

626 阅读4分钟

Options API

在Vue2中,我们组织一个组件代码的方式是(且只能是)Options API。Options API的特点是将组件需要的状态(data)和方法(methods)等全部写在一个对象,比如以下计数器的例子:

Options API的弊端

因为只需要将代码写在指定的地方,Options API对新手很友好,这也是Vue容易上手的原因。但随着组件的复杂度增长,处理相同逻辑的代码分散在文件的各处,这些复杂的组件往往有几百上千行代码,阅读和导航经常需要上下反复横跳。 比如以下的文档编辑组件,主要包含3个功能

image.png

  1. 修改文档名称
  2. 编辑器
  3. 分享

使用Options API的代码是这样的:

<template>
  <div v-if="hasInit" class="doc-container">
    <div class="header aic-jcsb">
      <div class="left">
        <div class="left-content">
          <doc-name-input class="doc-name" :value="docDetail?.name" @blur="saveDocName" />
          <!--          <clickable-input class="doc-name" :value="docDetail?.name" @blur="saveDocName" />-->

          <el-tooltip content="查看历史版本" size="small" placement="bottom">
            <div class="doc-status" @click="viewHistory">
              <!--  保存中  -->
              <div v-if="isEditorSaving">正在保存中…</div>

              <!--  内容已被修改   -->
              <div v-else-if="isEditorModify">待保存</div>

              <!-- 最近修改  -->
              <div v-else-if="formatUpdateDate">最近修改:{{ formatUpdateDate }}</div>
            </div>
          </el-tooltip>
        </div>
      </div>

      <div class="right">
        <el-button icon="share" @click="showCopyLink = true"> 分享 </el-button>

        <el-button v-if="isEdit" :loading="isEditorSaving" type="primary" icon="finished" @click="() => {
          saveEditorContent()
        }
          ">
          更新
        </el-button>

        <el-button v-if="!isEditMode" :type="isEdit ? 'default' : 'primary'" :icon="isEdit ? 'switch-button' : 'edit'"
          @click="toggleEditorReadOnly">{{ isEdit ? '退出编辑' : '编辑' }}</el-button>
      </div>
    </div>

    <div class="editor-wrap">
      <Editor class="editor" ref="refEditor" holder="doc-editor" :config="editorConfig" @ready="onEditorReady"
        @change="onEditorChange" />

      <doc-outline class="doc-outline" :editor="editor" :blocks="docDetail?.content?.blocks || []" />
    </div>

    <!-- 复制分享链接弹窗   -->
    <dialog-copy-share-link v-model="showCopyLink" :book-hash-id="book.node.hash_id" :share-link="shareLink"
      @success="broadcastRefreshEvent" />
  </div>
</template>

<script>
// 省略模块引入


// 处理自动保存,内容变化后,2分钟保存一次
let saveTimer = null

export default {
  name: 'OptionsApiDoc',
  components: {
    Editor,
    DocOutline,
    DocNameInput,
    DialogCopyShareLink
  },
  inject: ['book', 'space'],
  setup() {
    // 知识库详情eventbus
    const bookDetailEventBus = useEventBus(EventBookDetail.EVENT_BUS_NAME)

    return {
      bookDetailEventBus
    }
  },
  data() {
    return {
      hasInit: false,
      docDetail: null,
      editor: null,
      isEditorModify: false,
      isEditorSaving: false,
      isEdit: false,
      isEditMode: false,
      showCopyLink: false
    }
  },
  computed: {
    // 编辑器配置
    editorConfig() {
      return {
        data: this.docDetail?.content,
        readOnly: !this.isEdit,
        placeholder: ''
      }
    },
    // 分享链接
    shareLink() {
      const domain = getSpaceDomain(this.space)
      return `${domain}/book/${this.book?.node.share_id}/${this.docDetail?.node.hash_id}`
    },
    formatUpdateDate() {
      if (!this.docDetail?.updated_at) return ''
      return dayjs(this.docDetail?.updated_at).format('MM月DD号 HH:mm')
    }
  },
  watch: {
    // 监听编辑态模式,赋值编辑态
    book: {
      handler(val) {
        this.isEditMode = val.node.is_edit
        this.isEdit = this.isEditMode
      },
      deep: true,
      immediate: true
    },

    // 监听编辑态取消时,自动保存内容
    isEdit: {
      async handler(val) {
        if (!val && this.isEditorModify) {
          await this.saveEditorContent()
        }
        // 切换编辑器状态
        this.$nextTick(() => {
          this.$refs.refEditor.initEditor()
        })
      }
    },
    isEditorModify(val) {
      if (val) {
        saveTimer = setTimeout(() => {
          this.saveEditorContent()
        }, 2 * 60 * 1000)
      } else {
        clearTimeout(saveTimer)
        saveTimer = null
      }
    },
    $route: {
      handler: debounce(async (val) => {
        if (val) {
          // 清除自动保存timer
          if (saveTimer) {
            clearTimeout(saveTimer)
            saveTimer = null
          }

          // 保存编辑器数据
          await this.saveEditorContent(true)

          // 页面初始化
          await this.initData()
        }
      }, 500)
    }
  },
  beforeMount() {
    this.initData()
    // 文档详情eventbus
    const docDetailEventBus = useEventBus(EventDocDetail.EVENT_BUS_NAME)
    docDetailEventBus.on(this.onReceiveEvent)
  },
  beforeUnmount() {
    // 销毁组件前保存编辑器数据
    this.saveEditorContent(true)

    // 页面销毁时,清除监听事件
    document.onkeydown = null
  },
  methods: {
    // 广播知识库刷新事件
    broadcastRefreshEvent() {
      this.bookDetailEventBus.emit(EventBookDetail.EVENT_MAP.REFRESH)
    },
    // 打开编辑器编辑态
    async toggleEditorReadOnly() {
      this.isEdit = !this.isEdit
    },
    async getDocDetail() {
      const { data } = await apiDoc.getDocDetail(this.$route.params.node_hash_id)
      this.docDetail = data

      // 设置标题
      document.title = getPageTitle(this.docDetail.name)
    },
    async saveDocName(val) {
      if (!val.trim()) return

      if (val.trim() !== this.docDetail.name) {
        // 更新文档名称
        await apiDoc.updateDoc(this.docDetail.id, {
          name: val
        })

        // 通知外层刷新数据
        this.broadcastRefreshEvent()

        // 获取文档详情
        await this.getDocDetail()
      }
    },
    // 监听编辑器初始化完成
    onEditorReady(editorInstance) {
      this.editor = editorInstance
    },
    async onEditorChange(api, event) {
      // 如果是标题变更,需要更新文档内容,以便大纲可以得到更新
      const formatEvent = Array.isArray(event) ? event : [event]
      if (formatEvent.some((item) => item?.detail?.target?.name === 'header')) {
        const content = await api.saver.save()
        this.docDetail.content = content
      }

      this.isEditorModify = true
    },
    // 保存编辑器内容
    async saveEditorContent(quickAndSave = false) {
      if (this.isEditorModify) {
        this.isEditorSaving = true

        // 获取编辑器内容
        const content = await this.editor.save()

        // 保存内容
        await apiDoc.updateDoc(this.docDetail.id, {
          content
        })

        this.isEditorSaving = false
        this.isEditorModify = false

        if (quickAndSave) {
          ElMessage.success(`「${this.docDetail?.name}」自动保存成功`)
        } else {
          // 刷新节点内容
          await this.getDocDetail()
        }
      }

      if (!quickAndSave) {
        ElMessage.success('保存成功')
      }
    },
    // 初始化编辑器的快捷键
    initEditorShortCuts() {
      this.$nextTick(() => {
        // 处理快捷键事件
        const handleShortCuts = (e) => {
          // 保存操作
          if ((e.ctrlKey || e.metaKey) && e.key === 's') {
            // 阻止默认事件
            e.preventDefault()
            if (this.isEdit) {
              // 保存编辑器内容
              this.saveEditorContent()
            }
          }
        }

        document.onkeydown = handleShortCuts
      })
    },

    // 初始化数据
    async initData() {
      this.hasInit = false
      this.isEditorModify = false
      this.isEditorSaving = false
      this.isEdit = this.isEditMode

      // 获取文档详情
      await this.getDocDetail()

      this.hasInit = true

      // 设置编辑器中的快捷键
      this.initEditorShortCuts()

      // 滚动到内容顶部
      document.querySelector('#layout-book-content').scrollTop = 0
    },
    // 查看文档历史记录
    viewHistory() {
      const { book_node_hash_id, node_hash_id } = this.$route.params
      this.$router.push(`/edit/book/${book_node_hash_id}/${node_hash_id}/history`)
    },
    // 监听文档刷新事件
    onReceiveEvent(event) {
      if (event === EventDocDetail.EVENT_MAP.REFRESH) {
        this.getDocDetail()
      }
    }
  }
}
</script>

其中和分享文档相关的代码,分散在文件的85行、97-101行、172-175行,Vue3中新增了Composition API来解决这个问题。

Composition API

Composition API是一系列API的集合,它允许我们使用引入的函数来组织代码,包括:

  • 响应式API: ref() reactive() ...
  • 生命周期钩子函数:onBeforeMount()...
  • 依赖注入:provide() inject()

对上面用Options API写的计数器例子进行重构

image.png 对比Options API,Composition API不需要再通过export default导出包含选项的对象,组件的状态和方法都写在了一个<script setup></script>标签中。data中的数据需要使用ref()包裹一层,同时在中访问的时候需要加上.value,methods中的方法在Composition API中对应的是函数。

Composition API的好处

由于Composition API是通过导入的函数去定义组件,因此它可以

  1. 更好地组织代码:相同逻辑的代码全部写在一块,大大提升的阅读体验和可维护性,不再需要上下反复横跳,相比Options API更灵活。但灵活是把双刃剑,容易写出不好维护的代码,所以我们需要探索Composition API的最佳实践。具体来说就是如何组织setup中的代码,让代码的阅读性和可维护性更好。
  2. 能更好地复用:有了Composition API,我们就能将项目中能复用的逻辑抽成一个js文件,过去我们是用Options API的mixin实现复用的,Composition API提供了比mixin更灵活,更直观的方式来复用。

现在来用Composition API来重构文档编辑页组件的代码

<script setup>
// 省略模块引入

// 知识库详情eventbus
const bookDetailEventBus = useEventBus(EventBookDetail.EVENT_BUS_NAME)

// 广播知识库刷新事件
const broadcastRefreshEvent = () => {
  bookDetailEventBus.emit(EventBookDetail.EVENT_MAP.REFRESH)
}

const book = inject('book')

const space = inject('space')

// 路由
const route = useRoute()

const router = useRouter()

// 页面是否完成初始化
const hasInit = ref(false)

// 文档内容
const docDetail = ref()

// 编辑器实例
let editor = ref()

// 编辑器组件示例
const refEditor = ref()

// 编辑器内容是否修改
const isEditorModify = ref(false)

// 编辑器内容保存中
const isEditorSaving = ref(false)

// 是否编辑态
const isEdit = ref(false)

// 是否编辑态模式
const isEditMode = ref(false)

// 编辑器配置
const editorConfig = computed(() => {
  return {
    data: docDetail.value?.content,
    readOnly: !isEdit.value,
    placeholder: ''
  }
})

// 监听编辑态模式,赋值编辑态
watch(
  () => book.value?.node.is_edit,
  (val) => {
    isEditMode.value = val
    isEdit.value = isEditMode.value
  },
  {
    immediate: true
  }
)

// 监听编辑态取消时,自动保存内容
watch(
  () => isEdit.value,
  async (val) => {
    if (!val && isEditorModify.value) {
      await saveEditorContent()
    }
    // 切换编辑器状态
    nextTick(() => {
      refEditor.value.initEditor()
    })
  }
)

// 打开编辑器编辑态
const toggleEditorReadOnly = async () => {
  isEdit.value = !isEdit.value
}

// 分享链接
const shareLink = computed(() => {
  const domain = getSpaceDomain(space.value)
  return `${domain}/book/${book.value?.node.share_id}/${docDetail.value?.node.hash_id}`
})

// 是否展示分享弹窗
const showCopyLink = ref(false)

// 文档更新时间
const formatUpdateDate = computed(() => {
  if (!docDetail.value?.updated_at) return ''
  return dayjs(docDetail.value?.updated_at).format('MM月DD号 HH:mm')
})

// 获取文档详情
const getDocDetail = async () => {
  const { data } = await apiDoc.getDocDetail(route.params.node_hash_id)
  docDetail.value = data

  // 设置标题
  document.title = getPageTitle(docDetail.value.name)
}

// 保存文档名称
const saveDocName = async (val) => {
  console.log('保存文档名称', val);
  if (!val.trim()) return

  if (val.trim() !== docDetail.value.name) {
    // 更新文档名称
    await apiDoc.updateDoc(docDetail.value.id, {
      name: val
    })

    // 通知外层刷新数据
    broadcastRefreshEvent()

    // 获取文档详情
    await getDocDetail()
  }
}

// 监听编辑器初始化完成
const onEditorReady = (editorInstance) => {
  editor.value = editorInstance
}

const onEditorChange = async (api, event) => {
  // 如果是标题变更,需要更新文档内容,以便大纲可以得到更新
  const formatEvent = Array.isArray(event) ? event : [event]
  if (formatEvent.some((item) => item?.detail?.target?.name === 'header')) {
    const content = await api.saver.save()
    docDetail.value.content = content
  }

  isEditorModify.value = true
}

// 保存编辑器内容
const saveEditorContent = async (quickAndSave = false) => {
  if (isEditorModify.value) {
    isEditorSaving.value = true

    // 获取编辑器内容
    const content = await editor.value.save()

    // 保存内容
    await apiDoc.updateDoc(docDetail.value.id, {
      content
    })

    isEditorSaving.value = false
    isEditorModify.value = false

    if (quickAndSave) {
      ElMessage.success(`「${docDetail.value?.name}」自动保存成功`)
    } else {
      // 刷新节点内容
      await getDocDetail()
    }
  }

  if (!quickAndSave) {
    ElMessage.success('保存成功')
  }
}

// 初始化编辑器的快捷键
const initEditorShortCuts = () => {
  nextTick(() => {
    // 处理快捷键事件
    const handleShortCuts = (e) => {
      // 保存操作
      if ((e.ctrlKey || e.metaKey) && e.key === 's') {
        // 阻止默认事件
        e.preventDefault()
        if (isEdit.value) {
          // 保存编辑器内容
          saveEditorContent()
        }
      }
    }

    document.onkeydown = handleShortCuts
  })
}

// 初始化数据
const initData = async () => {
  hasInit.value = false
  isEditorModify.value = false
  isEditorSaving.value = false
  isEdit.value = isEditMode.value

  // 获取文档详情
  await getDocDetail()

  hasInit.value = true

  // 设置编辑器中的快捷键
  initEditorShortCuts()

  // 滚动到内容顶部
  document.querySelector('#layout-book-content').scrollTop = 0
}

// 查看文档历史记录
const viewHistory = () => {
  const { book_node_hash_id, node_hash_id } = route.params
  router.push(`/edit/book/${book_node_hash_id}/${node_hash_id}/history`)
}

// 监听文档刷新事件
const onReceiveEvent = (event) => {
  if (event === EventDocDetail.EVENT_MAP.REFRESH) {
    getDocDetail()
  }
}

// 文档详情eventbus
const docDetailEventBus = useEventBus(EventDocDetail.EVENT_BUS_NAME)
docDetailEventBus.on(onReceiveEvent)

// 处理自动保存,内容变化后,2分钟保存一次
let saveTimer = null
watch(
  () => isEditorModify.value,
  (val) => {
    if (val) {
      saveTimer = setTimeout(() => {
        saveEditorContent()
      }, 2 * 60 * 1000)
    } else {
      clearTimeout(saveTimer)
      saveTimer = null
    }
  }
)

// 监听路由中的文档hashId
// 获取文档内容
watch(
  () => route.params.node_hash_id,
  debounce(async (val) => {
    if (val) {
      // 清除自动保存timer
      if (saveTimer) {
        clearTimeout(saveTimer)
        saveTimer = null
      }

      // 保存编辑器数据
      await saveEditorContent(true)

      // 页面初始化
      await initData()
    }
  }, 500)
)

// 页面进来时初始化数据
onBeforeMount(() => {
  initData()
})

onBeforeUnmount(() => {
  // 销毁组件前保存编辑器数据
  saveEditorContent(true)

  // 页面销毁时,清除监听事件
  document.onkeydown = null
})
</script>

<template>
  <div v-if="hasInit" class="doc-container">
    <div class="header aic-jcsb">
      <div class="left">
        <div class="left-content">
          <doc-name-input class="doc-name" :value="docDetail?.name" @blur="saveDocName" />
          <!--          <clickable-input class="doc-name" :value="docDetail?.name" @blur="saveDocName" />-->

          <el-tooltip content="查看历史版本" size="small" placement="bottom">
            <div class="doc-status" @click="viewHistory">
              <!--  保存中  -->
              <div v-if="isEditorSaving">正在保存中…</div>

              <!--  内容已被修改   -->
              <div v-else-if="isEditorModify">待保存</div>

              <!-- 最近修改  -->
              <div v-else-if="formatUpdateDate">最近修改:{{ formatUpdateDate }}</div>
            </div>
          </el-tooltip>
        </div>
      </div>

      <div class="right">
        <el-button icon="share" @click="showCopyLink = true"> 分享 </el-button>

        <el-button v-if="isEdit" :loading="isEditorSaving" type="primary" icon="finished" @click="() => {
          saveEditorContent()
        }
          ">
          更新
        </el-button>

        <el-button v-if="!isEditMode" :type="isEdit ? 'default' : 'primary'" :icon="isEdit ? 'switch-button' : 'edit'"
          @click="toggleEditorReadOnly">{{ isEdit ? '退出编辑' : '编辑' }}</el-button>
      </div>
    </div>

    <div class="editor-wrap">
      <Editor class="editor" ref="refEditor" holder="doc-editor" :config="editorConfig" @ready="onEditorReady"
        @change="onEditorChange" />

      <doc-outline class="doc-outline" :editor="editor" :blocks="docDetail?.content?.blocks || []" />
    </div>

    <!-- 复制分享链接弹窗   -->
    <dialog-copy-share-link v-model="showCopyLink" :book-hash-id="book.node.hash_id" :share-link="shareLink"
      @success="broadcastRefreshEvent" />
  </div>
</template>

85~92行是和分享文档相关的代码,和Options API不同的是,功能相关的数据和computed写在了一起。正如前面所说,灵活是把双刃剑,Composition API允许我们将处理相同逻辑的代码写在一起,但大多数组件都不会只有一个功能,随着功能越来越复杂,写在script中的代码也会越来越多,参考上面的代码,虽然相同逻辑的代码都在一块,但还是不好阅读和维护。因此我们可以进一步对代码进行拆分,将独立的功能逻辑写在一个函数中,这样的函数在Vue中被称为组合式函数

组合式函数

在 Vue 应用的概念中,“组合式函数”(Composables) 是一个利用 Vue 的组合式 API 来封装和复用有状态逻辑的函数。

组合式函数的概念就不多介绍了,可参考文档组合式函数。简单来说就是一个函数,定义了数据(响应式的)和方法。 我们尝试用组合式函数再次重构文档编辑组件的代码:

<script setup>
// 省略模块引入

const route = useRoute()
const router = useRouter()

const book = inject('book')
const space = inject('space')
// 编辑器组件示例
const refEditor = ref()

const { docDetail, broadcastRefreshEvent, saveDocName, formatUpdateDate, getDocDetail } = useEditDocName()
const { showCopyLink, shareLink } = useCopyShareLink()
const {
    editor,
    editorConfig,
    isEditorModify,
    isEditorSaving,
    isEdit,
    isEditMode,
    onEditorReady,
    onEditorChange,
    toggleEditorReadOnly,
    saveEditorContent
} = useEditor({
    book,
    docDetail,
    getDocDetail,
    refEditor
})

function useEditDocName() {
    const docDetail = ref(null)
    // 知识库详情eventbus
    const bookDetailEventBus = useEventBus(EventBookDetail.EVENT_BUS_NAME)

    // 广播知识库刷新事件
    const broadcastRefreshEvent = () => {
        console.log('onReceiveEvent');
        bookDetailEventBus.emit(EventBookDetail.EVENT_MAP.REFRESH)
    }

    // 文档更新时间
    const formatUpdateDate = computed(() => {
        if (!docDetail.value?.updated_at) return ''
        return dayjs(docDetail.value?.updated_at).format('MM月DD号 HH:mm')
    })

    // 获取文档详情
    async function getDocDetail() {
        const { data } = await apiDoc.getDocDetail(route.params.node_hash_id)
        docDetail.value = data

        // 设置标题
        document.title = getPageTitle(docDetail.value.name)
    }

    // 保存文档名称
    async function saveDocName(val) {
        console.log('保存文档名称', val);
        if (!val.trim()) return

        if (val.trim() !== docDetail.value.name) {
            // 更新文档名称
            await apiDoc.updateDoc(docDetail.value.id, {
                name: val
            })

            // 通知外层刷新数据
            broadcastRefreshEvent()

            // 获取文档详情
            await getDocDetail()
        }
    }

    onBeforeMount(() => {
        getDocDetail()
    })

    return {
        docDetail,
        broadcastRefreshEvent,
        saveDocName,
        formatUpdateDate,
        getDocDetail
    }
}

function useCopyShareLink() {
    // 是否展示分享弹窗
    const showCopyLink = ref(false)
    // 分享链接
    const shareLink = computed(() => {
        const domain = getSpaceDomain(space.value)
        return `${domain}/book/${book.value?.node.share_id}/${docDetail.value?.node.hash_id}`
    })

    return {
        showCopyLink,
        shareLink
    }
}

function useEditor({ book, docDetail, getDocDetail, refEditor }) {
     // 省略具体实现

    return {
        editor,
        editorConfig,
        onEditorReady,
        onEditorChange,
        isEditorModify,
        isEditorSaving,
        toggleEditorReadOnly,
        isEdit,
        isEditMode,
        saveEditorContent
    }
}

// 查看文档历史记录
function viewHistory() {
    const { book_node_hash_id, node_hash_id } = route.params
    router.push(`/edit/book/${book_node_hash_id}/${node_hash_id}/history`)
}

// 监听文档刷新事件
function onReceiveEvent(event) {
    if (event === EventDocDetail.EVENT_MAP.REFRESH) {
        console.log('doc 监听文档刷新事件')
        getDocDetail()
    }
}

// 文档详情eventbus
const docDetailEventBus = useEventBus(EventDocDetail.EVENT_BUS_NAME)
docDetailEventBus.on(onReceiveEvent)


onBeforeUnmount(() => {
    // 销毁组件前保存编辑器数据
    saveEditorContent(true)

    // 页面销毁时,清除监听事件
    document.onkeydown = null
})

</script>

<template>
    <div v-if="docDetail" class="doc-container">
        <div class="header aic-jcsb">
            <div class="left">
                <div class="left-content">
                    <doc-name-input class="doc-name" :value="docDetail?.name" @blur="saveDocName" />
                    <!--          <clickable-input class="doc-name" :value="docDetail?.name" @blur="saveDocName" />-->

                    <el-tooltip content="查看历史版本" size="small" placement="bottom">
                        <div class="doc-status" @click="viewHistory">
                            <div v-if="isEditorSaving">正在保存中…</div>

                            <div v-else-if="isEditorModify">待保存</div>

                            <div v-else-if="formatUpdateDate">最近修改:{{ formatUpdateDate }}</div>
                        </div>
                    </el-tooltip>
                </div>
            </div>

            <div class="right">
                <el-button icon="share" @click="showCopyLink = true"> 分享 </el-button>

                <el-button v-if="isEdit" :loading="isEditorSaving" type="primary" icon="finished" @click="() => {
                    saveEditorContent()
                }
                    ">
                    更新
                </el-button>

                <el-button v-if="!isEditMode" :type="isEdit ? 'default' : 'primary'"
                    :icon="isEdit ? 'switch-button' : 'edit'" @click="toggleEditorReadOnly">{{ isEdit ? '退出编辑' : '编辑'
                    }}</el-button>
            </div>
        </div>

        <div class="editor-wrap">
            <Editor class="editor" ref="refEditor" holder="doc-editor" :config="editorConfig" @ready="onEditorReady"
                @change="onEditorChange" />

            <doc-outline class="doc-outline" :editor="editor" :blocks="docDetail?.content?.blocks || []" />
        </div>

        <!-- 复制分享链接弹窗   -->
        <dialog-copy-share-link v-model="showCopyLink" :book-hash-id="book.node.hash_id" :share-link="shareLink"
            @success="broadcastRefreshEvent" />
    </div>
</template>

在上面的代码,我们定义了3个组合式函数useEditDocName useCopyShareLink useEditor,分别处理“修改文档名称”、“分享文档”和“编辑器”。一个组合式函数的结果可以作为另一个组合式函数的参数,使用组合式函数后页面的逻辑就是由各个组合式函数的调用组成,后续新增功能就加一个组合式函数,移除功能就移除一个组合式函数,不需要上下横跳地找找到对应的data、computed、method等进行删除。当组合式函数的逻辑很复杂时,如useEditor,我们还可以在组件文件夹下新建一个hooks文件夹,将组合式函数抽离到单独的js文件中。

最佳实践

  1. 怎么组织代码不能一概而论,需要根据组件的复杂度:
  • 简单组件:单个功能,或者多个很简单的功能。直接写逻辑,按照一定顺序。
  • 复杂组件:多个功能,或者单个很复杂的功能,使用组合式函数,对于逻辑很复杂组合式函数应抽离至组件目录下的hooks目录。

对于组件复杂度的判断,应该是根据开发经验判断,不能只依赖功能的数量,比如一个组件只有增删改3个功能,也不能算是复杂组件。再如上面编辑页的例子,同样是组合式函数,处理编辑器逻辑的useEditor应该抽离到hooks中。

  1. 能复用的逻辑抽离组合式函数到hooks中。

参考项目

  1. v3-admin-vite

通过注释的方式标记功能的起始 region表示区域的起始,方便阅读和跳转

//#region 改
const currentUpdateId = ref<undefined | string>(undefined)
const handleUpdate = (row: GetTableData) => {
  currentUpdateId.value = row.id
  formData.username = row.username
  dialogVisible.value = true
}
//#endregion

优点:

  • 组织结构清晰:通过注释标记代码区域,可以清晰地看到代码的组织结构,知道哪部分代码属于同一功能模块。
  • 提高可读性:有助于其他开发者理解代码的功能和结构,提高代码的可读性。
  • 方便导航:在某些IDE(如Visual Studio Code或IntelliJ IDEA)中,可以利用这种注释方便地在不同的代码区域之间跳转,提高开发效率。

缺点:

  • 增加代码量:这种注释会增加代码量,可能会给代码带来一些冗余。
  • 维护成本:如果代码的结构发生改变,需要同时更新注释,增加了维护成本。
  • 可能引起混淆:如果注释和实际代码的功能不匹配,可能会引起混淆,误导其他开发者。
  • 不是标准做法:这种做法并不是广泛接受的标准做法,不同的开发者可能有不同的理解和使用方式。

总的来说,这种方法在组织大型、复杂的代码文件时可能会有所帮助,但是也需要注意其可能带来的问题。

  1. Vue官方示例

将组件的功能划分为一个个组合式函数,组合式函数返回状态或方法,提供下个组合式函数或者模板调用,组合式 api可以让我们像组织JavaScript那样组织组件。

  1. element-plus
  1. github.com/ShetlandJ/V…

  2. github.com/vuesomedev/…

  3. www.yuque.com/vueconf/mkw…

写在最后

目前Vue3实践经验还不足,后面通过大量实践后还需要完善开发的规范,包括Composition API的调用顺序等。