侧边栏壁纸
博主头像
M酷博主等级

一帆风顺 ⛵️⛵️⛵️

  • 累计撰写 45 篇文章
  • 累计创建 40 个标签
  • 累计收到 456 条评论

目 录CONTENT

文章目录

Vue3 + Vite2 + ElementPlus + TS 项目常见问题

M酷
2022-06-14 / 0 评论 / 29 点赞 / 15,277 阅读 / 36,765 字 / 正在检测是否收录...
广告 广告

1、ts 报错 Could not find a declaration file for module

ts 提示这个错误通常由以下几种情况:

  1. 引入的 npm 包不是用 typescript 编写的;
  2. 没有找到正确的类型定义文件;

针对第一种,我们可以尝试安装 @types/包名 来安装这个包对应的类型文件,如:
npm i @types/lodash -D

如果没有找到,我们也可以手动为它声明一下,此时可以直接到 Vite 项目自动生成的 env.d.ts 中添加声明,如果没有这个文件,可以在根目录下新建 shims-vue.d.ts,(只要以 .d.ts 结尾就行,但是这个文件中不能包含 import 语句,不然 declare module 失效,如果你要 import,建议再建一个文件),然后输入以下内容,注意最后要重新启动服务才生效。

Typescript 书写声明文件(可能是最全的)
TypeScript 声明文件全解析
declaration 声明文件

// declare 声明一个ambient module(即:没有内部实现的 module声明) 
declare module '*.vue' {
  import Vue from 'vue'
  export default Vue
}

declare module 'XX' // 当前文件中不能包含 import 语句,不然 declare module 失效
// xx即你包不能找到声明的包名,如 declare module 'lodash'

2、vue3+vite中 process.env 的配置&使用方法

vite 中的环境变量通过 import.meta.env 来暴露出来,但是不是所有文件中都可以获取到,如果我们想通过 process.env 的方式来读取,则需要像下面这样配置一下。

a. 在 vite.config.js 中,添加以下部分代码

import { defineConfig, loadEnv } from 'vite'

export default ({ mode }) =>
  defineConfig({
    define: {
      'process.env': loadEnv(mode, process.cwd())
    },
    ...
}

b. 在文件中使用变量值

// 创建axios实例
const service = axios.create({
  baseURL: process.env.BASE_API, // api 的 base_url
  timeout: 5000 // 请求超时时间
})

3、vue + ts 配置路径别名后 ts 报错

有 2 个位置需要配置一下,一个 vite.config.js,一个是 tsconfig.json(baseUrl 和 paths)

export default ({ mode }) =>
  defineConfig({
    base: './',
    resolve: {
      alias: {
        '@': path.resolve(__dirname, 'src')
      }
    }
}
{
  "baseUrl": "./",
  "compilerOptions": {
    //...
    "paths": {
      "@/*": [
        "./src/*"
      ]
    },
  },
  "include": [
    "src/**/*.ts",
    "src/**/*.tsx",
    "**/*.d.ts",
    "src/**/*.vue"
  ]
}

4、commitlint 无效,commit-msg 不执行

可能是配置不正确或者 commitlint 本身的缓存问题导致的,建议先删除 .husky 目录下的 commit-msg 文件,然后使用下面的命令重新生成 commit-msg 文件即可,注意内容格式不要错 。

npx husky add .husky/commit-msg "npx --no-install commitlint --edit "$1""

5、commitlint 校验不通过,本地修改全部丢失,如何找回?

由于 commit 有规范校验,当我们提交的格式不正确时候,它会把内容弹出去,不会 commit 上去,同时默认会把本地的修改全部重做。此时,大家不要慌,commitlint 其实会自动帮我们在 stash 里做一个备份,我们可以通过 git stash list 查看一下,发现有这样的一条或多条记录。

此时,我们就可以使用 git stash popgit stash apply 弹出草稿箱里的数据来恢复。类似的丢失,我们也可以先用 git reflog 先查看日志,然后配合 git reset --hard 版本号 进行恢复。

6、ElementPlus 按需引入后,单独调用 ElMessage、ElLoading 或其它组件时,出现样式丢失问题。

就是由于按需引入导致的,当前单独使用某个组件时(比如在 axios 中),Vite 并不知道你要用到这个组件,所以并没有去加载它对应的样式,此时我们只需要补上相关组件的样式即可,当然你全量引入也许。

方法一:在 main.ts 中单独引入那些组件的样式

// 引入Elmessage和Elloading的css样式文件
import 'element-plus/theme-chalk/el-loading.css'
import 'element-plus/theme-chalk/el-message.css'

方法二(推荐):通过 vite-plugin-style-importconsola 来实现自动引入

a. 首先安装一下,

yarn add vite-plugin-style-import consola -D

b. 在 vite.config.ts 中添加如下配置

// vite.config.ts
import {
  createStyleImportPlugin,
  ElementPlusResolve,
} from 'vite-plugin-style-import'

export default {
  plugins: [
    // ...
    createStyleImportPlugin({
      resolves: [ElementPlusResolve()],
      libs: [
        {
          libraryName: 'element-plus',
          esModule: true,
          resolveStyle: (name: string) => {
            return `element-plus/theme-chalk/${name}.css`
          },
        },
      ]
    }),
  ],
}

7、Non-relative paths are not allowed when ‘baseUrl’ is not set. Did you forget a leading ‘./’

出现此问题,需要你在 tsconfig.json 中配置 baseUrl 为 “.” 或 “./”

8、vite.config.ts 中注入的 scss/less 变量无效

我们通常会使用 preprocessorOptions 字段来注入 scss/less 变量和样式,以便在组件中直接使用,但有时,我们配置后发现并不生效,在 vue 组件中读取不到注入的变量(组件库按需引入场景),此时可以按如下步骤排查问题:

  1. 检查 vite.config.ts 中是否正确配置 additionalData 字段;
  2. vue 文件中的 style 标签是否配置了 lang="scss"lang="less"

有时,控制台还会提示 @forward rules must be written before any other 这个错误,意思是某些样式必须要在其它样式之前引入,这里通常是指我们自定义过的 ElementUI 样式,我们稍微修改下配置,像下面这样既可以完美解决,这样 main.ts 中就不需要再去引入 elementui 的样式了。参考:element-plus 更换主题色报错

css: {
      preprocessorOptions: {
        scss: {
          prependData: '@use "@/assets/style/element/index.scss" as *;',  // elementui,如果你想导入所有样式 @use "element-plus/theme-chalk/src/index.scss" as *;
          additionalData: `@import "@/assets/style/vars.scss";`  // 注入scss变量
        }
      }
    }

你也可以把 vars.scss 放到 index.scss 中,然后只引入一个文件

// element/index.scss
/* 覆盖elementui的变量值 */
@forward 'element-plus/theme-chalk/src/common/var.scss' with (
  $colors: (
    'primary': (
      'base': #3ca483
    )
  )
);

@import '../vars.scss';
@import '../mixin.scss';

// vite.config.ts
css: {
      preprocessorOptions: {
        scss: {
          additionalData: '@use "@/assets/style/element/index.scss" as *;', // 引入elementui变量和自定义scss变量
        }
      }
}

最后,如果打包的时候报错,提示样式重复引入了(见下图 this module is already being loaded)。你可能需要如下的配置(根据自己实际情况修改):

css: {
      preprocessorOptions: {
        scss: {
          additionalData: (content, loaderContext) => {
            if (
              loaderContext.endsWith('assets/style/element/index.scss') ||
              loaderContext.endsWith('assets/style/mixin.scss') ||
              loaderContext.endsWith('assets/style/vars.scss')
            ) {
              return content
            }

            return `@use "@/assets/style/element/index.scss" as *; ${content}`
          } // 引入elementui变量和自定义scss变量
        }
      }
  	}

扩展知识:

@use@import 的区别在于:

  1. 不管使用了多少次样式表,@use都只会引入和执行一次。
  2. 与全局使用相反,@use是有命名空间的,而且只在当前样式表中生效。
  3. 以 —或者 _开头的命名空间被认为是私有的,不会被引入其他样式表。
    element-plus 自动引入修改主题色
    unipapp 解决无法编译sass

9、推荐使用 VSCode 和 Vue 官方拓展 Volar,它为 Vue 3 提供了全面的 IDE 支持

你需要在 VSCode 中禁用(工作区) Vetur,避免两者冲突。

10、Uncaught TypeError: Cannot read properties of undefined (reading ‘config’)

使用 app.config.globalProperties 取代 Vue.prototype

11、ElementPlus 组件默认展示的文字是英文

因为 Element-Plus 框架默认显示的是英文版,需要修改语言配置,有两种方法来设置。

a. 如果你是全量引入 ElementPlus,可以在 main.ts 中添加如下代码:

import { createApp } from "vue";
import App from "./App.vue";
import ElementPlus from "element-plus";
import "element-plus/dist/index.css";
import 'dayjs/locale/zh-cn';
import locale from "element-plus/lib/locale/lang/zh-cn";

createApp(App).use(ElementPlus, { locale }).mount("#app");

b. 如果你是按需引入 ElementPlus,main.ts 中可能无法直接配置,此时可以利用 ElementPlus 提供的内置组件 ConfigProvider 来实现。我们可以在 App.vue 文件中像如下这样配置:

<template>
  <el-config-provider :locale="locale">
    <Layout />
  </el-config-provider>
</template>

<script setup lang="ts">
  import Layout from '@/layout/index.vue'
  import { ElConfigProvider } from 'element-plus'
  import zhCN from 'element-plus/lib/locale/lang/zh-cn'
  const locale = zhCN
</script>

<style>
  #app {
    font-family: Avenir, Helvetica, Arial, sans-serif;
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
  }
</style>

12、vite 项目修改 node_modules 中的模块后,引入的文件依然是旧的

为了提高运行速度,vite 在首次运行时,对 node_modules 中的包进行了 esmodule 化,存储在 node_modules/.vite 目录下,这样下次就可以直接使用浏览器读取,提高加载速度。当我们修改了某一个 node_modules 后,vite 并不知道,也不会去更新 .vite 目录,所有我们只需要先删除 .vite 目录,然后重新运行项目即可。

13、setTimeout 和 setInterval 的类型定义

当我们再 ts 中使用 setTimeoutsetInterval 时,发现并没有 Timer 这个基本类型,于是我们直接使用 let timer:null | number = null 这种方式来定义,但是当我们给 timer 再次赋值的时候,发现还是会报错,提示 setTimeout 返回的不是 number 类型,此时只需要在 setTimeout 或 setInterval 前面加上 window 来调用即可。

let timer:null | number = null;
timer = window.setTimeout(() => {}, 1000)

14、TS 中如何定义 defineProps 和 defineEmits 的类型

interface Props {
  mode?: string
  type?: string
  data?: any
}
interface Emit {
  (e: 'approve' | 'cancel' | 'remove'): void
  (e: 'revise-record' | 'update-progress', id: number): void
  (e: 'save', form: Forms): void
}
  
// withDefaults 用来指定props的默认值,如果不需要可以直接写成
// const props = defineProps<Props>()
const props = withDefaults(defineProps<Props>(), {
  mode: 'add',
  type: 'form',
  data: {}
})
const emit = defineEmits<Emit>()

// 按上面的写法,当 Emit 中只有一项时,Eslint 可能会提示 prefer-function-type,意思是建议你使用函数类型声明
// 所以我们把 interface 修改为 type 即可解决
type Emit = (e: 'select' | 'remove', data: Person, index: number) => void
const emit = defineEmits<Emit>()
  
// 当然你也可以在 eslintrc.js 中禁用该规则   
// .eslintrc.json
{
  "rules": {
    "@typescript-eslint/prefer-function-type": "off"
  }
}

15、Vue3 setup 语法为组件添加 name 属性

Vue3 默认会根据文件名来推断组件的名称,如果组件对应的文件名为 index.vue,那么在 vue-dev-tools 中看到的组件就都是 Index,不太好区分,而且在使用 keep-aliveincludeexclude 功能时,必须显示的声明 name 才能正常执行逻辑。
在以往的 Vue 版本中,我们可以直接在 script 中通过 name 属性为组件指定名字,但是如果你使用 vue3setup 语法,是不支持直接定义 name 的,你需要通过 vite-plugin-vue-setup-extend-plus 插件来实现。

注意:vite-plugin-vue-setup-extend 这个老版本插件会影响 debug 功能,导致断点位置不准确,所以一定要安装 vite-plugin-vue-setup-extend-plus

a、安装

yarn add vite-plugin-vue-setup-extend-plus -D

b、配置 vite.config.ts

import { defineConfig } from 'vite'
import VueSetupExtend from 'vite-plugin-vue-setup-extend-plus'
export default defineConfig({
  plugins: [ VueSetupExtend() ]
})

c、使用

<script lang="ts" setup name="Demo"></script>

16、watch 监听多个数据

const country = ref()
const province = ref()

// 监听单个数据
watch(
  () => country.value,
  newCountry => {
    console.log('--country---', newCountry)
  }
)
// 监听多个数据
// 第一个参数 () => [country.value, province.value] 传入要监听的数据
// 第二个参数是回调函数,参数分别代表更改后与更改前的值,([newCountry, newProvince], [oldCountry, oldProvince])
watch(
  () => [country.value, province.value],
  ([newCountry, newProvince], [oldCountry, oldProvince]) => {
    console.log(oldCountry, '--country---', newCountry)
    console.log(oldProvince, '--province---', newProvince)
  }
)

17、Vue3 + TS 中使用 svg 图标

直接参考使用 vite-plugin-svg-icons 来支持,具体请查看它的 文档,这里简单附上我项目中的配置:

import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'

// 获取svg路径
const getSvgPath = (folder: string) => {
  return path.resolve(process.cwd(), 'src/assets/svg/' + folder)
}

export default ({ mode }) => {
  return defineConfig({
	plugins: [
      vue(),
      // ...
      createSvgIconsPlugin({
        iconDirs: [getSvgPath('portal'), getSvgPath('menu')], // 指定需要缓存的图标文件夹(支持多个)
        symbolId: 'icon-[name]' // 指定symbolId格式
      })
    ]
  })

18、获取当前组件实例

const { appContext } = getCurrentInstance()!

19、使用最新版 vue-router 配置 404 页面时提示如下错误

Catch all routes (“*“) must now be defined using a param with a custom regexp
原因:Vue Router 不再使用 path-to-regexp,而是实现了自己的解析系统,该系统允许路由排名并启用动态路由。由于我们通常会在每个项目中添加一条单独的包罗万象的路线,因此支持的特殊语法没有太大的好处。参数的编码是跨路线编码,无一例外使事情更容易预测。

 [{
    path: '/404',
    name: '404',
    component: () => import('@/views/404.vue')
  },
  {
    path: '/:pathMatch(.*)',
    redirect: '/404'
  }]

20、Vue3 v-for 循环中如何赋值和获取 ref

由于数组中每一个元素 key 都是唯一的,可以通过 key 值给对应内容赋值

<div v-for=(item, index) in arrList>
	<div :ref="el => myRefs[index]=el"></div>
</div>

<script setup lang="ts">
	const myRefs = ref<Ref<string[]>>([])
	return {
		myRefs
	}
</script>

21、Element UI 中 ElMessageBox.prompt 中 inputValidator 的用法

当我们在使用 ElMessageBox.prompt 时,如果要对其中的 input 内容进行校验,需要用到 inputValidator 这个字段,配置自定义的验证规则,它是一个函数,返回 true 和 false,如果返回的是字符串,将作为展示的错误文案。代码如下:

ElMessageBox.prompt('量化单位:', '', {
      customClass: 'prompt-custom-unit',
      showClose: false,
      closeOnClickModal: false,
      closeOnPressEscape: false,
      dangerouslyUseHTMLString: true,
      // inputErrorMessage: '量化单位不能为空',
      inputValidator: (value: string) => {
        if (!value) return '量化单位不能为空'
        if (value.length > 10) return '量化单位最多10个字符'
        return true
      },
      beforeClose: (action, instance, done) => {
        if (action === 'confirm') {
          const value = instance.inputValue
          row.unit = value
        } else {
          row.unit = ''
        }
        done()
      }
    })

22、Vue3 Extraneous non-props attributes (id) were passed to component but could not be automatically

extraneous_non_props

原因:

  1. 一个组件可能有多个根节点,请确保组件在单一根节点下
  2. 外部组件不要直接放在 template 下,最外层加div包裹,如下图:

vue3_props_no_warn

23、对 el-form 中的表单进行验证时,提示的错误文案是英文的

当我们给 el-form-item 添加了 required 属性之后,它内容的表单就需要进行验证,但是验证时,发现提示的错误文案是英文的,并不是我们手动配置的 message,这是由于 ElementUI 的国际化没有处理完善导致的,我们可以通过一下方法解决:

去掉 el-form-item 上的 required 属性,在 rules 中配置 required: true,如果有其他判断条件,可以通过 validator 配置单独的验证方法(注意:当表单元素不存在时,el-form 的校验是不生效的)。

<el-form-item class="form-item-extra-name" prop="name">
              <el-checkbox v-model="saveAsGroup">保存为分组</el-checkbox>
              <el-input
                v-model="ruleForm.name"
                clearable
                :maxlength="20"
                placeholder="请输入名称"
              />
      </el-form-item>

      const validateName = (rule, value, callback) => {
        if (!saveAsGroup.value) return callback()
        if (!value) return callback(rule.message)
      }
      const rules = {
        name: [
          {
            required: true,
            message: '请输入分组名称',
            validator: validateName,
            trigger: 'change'
          }
        ]
      }

24、vue3 + vite2 引入 assets 目录下的图片时路径不对

<!-- vite 下无效 -->
<img :src="require('@/assets/img/happy.png')" />

vue + webpack 的架构下,当我们想引入 src/assets/img 下的图片时,通过会使用 require 方式来引入,但 vite 架构下已经行不通了,因为 requirewebpack 架构下的方法,我们可以通过以下方法解决:

1. 在资源 url 前添加 @ 或者 src

<!-- 路径前直接添加 @ 前缀,需要在 vite.config.ts 中配置资源别名 -->
<img src="@/assets/img/happy.png" />

<!-- 路径前直接添加 src 前缀 -->
<img :src="`src/assets/img/${item.icon}.png`" />

但是经过尝试,发现打包后,仍然无法访问图片资源,看打包后的代码,发现图片已经被转换为 base64,且做了路径的映射,那应该还是路径不对导致的。继续查询 vite 文档,于是有了第 2 种方法。

v_map_base64

2. 通过 vite 文档中提到的 new URL + import.meta.url 方式来解决,我们可以封装一个方法,在其它地方直接用,如下所示:

import.meta.url 是一个 ESM 的原生功能,会暴露当前模块的 URL。将它与原生的 URL 构造器 组合使用,在一个 JavaScript 模块中,通过相对路径我们就能得到一个被完整解析的静态资源 URL。

/*
 * @name: getAssets 获取assets目录的文件路径
 * @name: 文件名,需要包括扩展名
 * @folder:  需要读取的目录,默认为img目录
 */
export const getAssets = (name: string, folder = 'img') => {
  // 注意路径一定要以../assets开头,开发环境下,vite 会自动拼上 src
  return new URL(`../assets/${folder}/${name}`, import.meta.url).href
}

在 vue 文件中使用

<template>
   <img :src="imgUrl" />
</template>

<script setup lang="ts">
import { getAssets } from "./utils/index";
const imgUrl = getAssets('happy.png')
</script>

scss 中引入背景图

.el-slider {
    &::before {
      content: ' ';
      width: 20px;
      height: 20px;
      display: inline-block;
      background: url('@/assets/img/happy.png');
      background-size: 100%;
      position: absolute;
      margin-left: -35px;
      top: -9px;
    }
}

相关文章

vue3+vite assets动态引入图片的几种方式,解决打包后图片路径错误不显示的问题
vite+vue3打包后图片404问题:已解决

25、el-autocomplete 组件下拉框重复弹出的问题

在使用 ElementPlus 中的 el-autocomplete 组件时,发现了一个问题,就是当我搜索一个字符串,匹配到数据后,当我点击了下拉框中的数据后,下拉框消失后会再次弹出,影响我的其它操作。
经过查阅资料,发现这是个由来已久的 issue,官方 demo 也有这个问题。
我们可以用如下方法自行解决。

a、首先给 el-autocomplete 组件绑定一个 ref

const elAutoComplete = ref<any>()

b、然后在选择数据的方法里调用组件内容的方法,手动取消组件的激活状态

elAutoComplete.value.close()
elAutoComplete.value.inputRef.blur()

完整代码如下:

<template>
	<el-autocomplete
        ref="elAutoComplete"
        v-model="searchStr"
        popper-class="autocomplete-search-person"
        style="width: 100%"
        clearable
        value-key="nick_name"
        :maxlength="32"
        placeholder="请输入姓名"
        :debounce="500"
        :fetch-suggestions="querySearch"
        @select="choosePerson"
      >
        <template #prefix>
          <el-icon><Search /></el-icon>
        </template>
        <template #default="{ item }">
          <div
            :class="[
              'search-item',
              { disabled: selected.includes(item.passport) }
            ]"
          >
            <Avatar
              :size="30"
              :name="item.nick_name"
              :src="item.avatar"
              randomColor
            />
            <p :title="item.nick_name">{{ item.nick_name }}</p>
          </div>
        </template>
      </el-autocomplete>
</template>

<script setup lang="ts" name="ObjectiveList">
const elAutoComplete = ref<any>()
const choosePerson = (data: userInfoRes | {}) => {
  if (!data) return
  const result = data as userInfoRes

  if (personList.value.length === limit) {
    return ElMessage.warning(`一次最多查看${limit}人!`)
  }
  if (
    personList.value
      .map((item: userInfoRes) => item.passport)
      .includes(result.passport)
  ) {
    return ElMessage.warning('请不要重复添加!')
  }

  curPerson.value = result
  personList.value.push(result)

  // 手动关闭下拉框,避免出现两次(就是这两句)
  elAutoComplete.value.close()
  elAutoComplete.value.inputRef.blur()

  savePersonList()
  getObjectiveList(result.passport)
}
</script>

26、el-popover 根据内容和滚动容器动态调整展示位置

在使用 el-popover 的过程中,发现了一个问题,当 el-popover 中包含动态内容的时候,el-popover 组件展示的位置会出现异常,表现出来的现象就是,内容更新后,popover 没有重新更新位置,导致展示异常或者内容被遮挡,具体请看下图:

a、初始情况,popover 内部的内容没有展开,是正常的。
popover_fold

b、当我把内部的内容展开后,就出现了内容被切掉,被浏览器顶部遮挡的情况,并且滚动页面时并不会重新计算 popover 的位置。
popover_boob

而正常应该是如下情况才对,并且需要在页面滚动时,动态计算 popover 的位置:
popover_normal

通过查阅文档和百度,发现了一个属性 popper-options 可以用来控制 popover 的行为,el-popover 内部其实是通过 popper.js 实现的,所以依然支持传入 popper.js 的配置。但是很遗憾,ElementPlus 官方文档并没有给出相关示例,且网上大部分都是针对 Vue2 + ElementUI 的配置(如:element-ui 组件 Popover 弹出框,el-popover 样式、定位以及二次确认弹出框自动关闭问题),在 Vue3 + ElementPlus 的场景下没效果,还报错,提示配置项不存在,应该是相关版本都更新了用法和配置的写法 。

后面经过查阅 ElementPlus 的相关 issue 以及 popper.js 的文档,才最终得到了正确的配置方式,更多配置可以参考 popper.js 文档中的 modifiers 配置。
● 相关 issue:(Feature Request) popover auto placement
popper.js modifiers 配置

自测有效的 popper-options 的配置如下:

:popper-options="{
      placement: 'top', // 默认的 placement
      modifiers: [
        {
          name: 'flip', // 只是一个名字
          options: {
            boundariesElement: 'viewport', // 边界元素,默认是 body
            fallbackPlacements: ['bottom'], // 供组件动态选择的 placement 
            removeOnDestroy: true // 是否在组件销毁时移除 DOM
          }
        }
      ]
    }"

请参考如下完整代码:

<template>
  <el-popover
    ref="popoverAlignTopRef"
    popper-class="popover-hover-aligned"
    :width="360"
    :show-after="600"
    :hide-after="600"
    trigger="hover"
    :popper-options="{
      placement: 'top',
      modifiers: [
        {
          name: 'flip',
          options: {
            boundariesElement: 'viewport',
            fallbackPlacements: ['bottom'],
            removeOnDestroy: true
          }
        }
      ]
    }"
  >
    <template #reference>
      <Avatar :size="32" :src="item.avatar" />
    </template>
    <div class="popover-wrapper">
      <span :class="['expander',{ expanded: item.expanded }]" @click="togglePopoverExpand(item, 'top')">
         <el-icon v-if="item.kt_list.length > 0"><CaretRight/></el-icon>
         任务({{ item.kt_list.length }})
      </span>
    </div>
  </el-popover>
</template>

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

// 展开/收起的交互
const togglePopoverExpand = async (row, type: 'top' | 'bottom') => {
  row.expanded = !row.expanded
  
  // 执行相关组件实例中的 update 方法,重新计算 popover 的位置,记得加上 nextTick
  await nextTick()
  const currentPopover =
    type === 'top' ? popoverAlignTopRef : popoverAlignBottomRef
  currentPopover.value[0].popperRef.popperInstanceRef.update()
}
</script>

27、Vite 打包后报错 globalThis is not defined

使用 vite2 打包后,在低版本浏览器中访问,发现控制台报错:Uncaught (in promise) ReferenceError: globalThis is not defined

通过查阅资料,发现 globalThis 在 chrome 71 以上才支持,老版本浏览器没有这个属性,因此才出现这个错误提示。而 vite 默认并不会处理这些兼容问题,需要我们手动引入 @vitejs/plugin-legacy 这个插件才解决。

// vite.config.ts
import legacy from '@vitejs/plugin-legacy'

export default defineConfig({
	plugins: [
		legacy({
			targets: ['Chrome 63'],
			additionalLegacyPolyfills: ['regenerator-runtime/runtime'],
			modernPolyfills: true
		})
	],
	build:{
   		target:'es2015'
	}
})

28、WARN [@vue/compiler-sfc] ::v-deep usage as a combinator has been deprecated.

在我们运行项目的时候,控制台可能会有很多这种黄色警告信息,提示 ::v-deep 的用法已经过时了,主要是我们的样式文件中或者组件库的样式中有使用 ::v-deep 这种已过时的样式穿透符导致的,在 vue3 开发环境下,我们只需要把它替换成 :deep() 即可,示例如下:

// 错误代码:
::v-deep .ant-select-selector {
	height: 30px;
}
// 更改后
:deep(.ant-select-selector) {
	height: 30px;
}

29、This expression is not callable. Not all constituents of type ‘string | ((searchTerm: string) => Promise) | []’ are callable

当我们在使用 hooks 来抽离公用代码和逻辑的时候,可能会遇到这个错误提示(该表达式不能被调用),比如像下面这个 hooks,看起来都是正常定义没什么问题,但是控制台就会给我们抛出这类 TS 异常信息。

// 定义 hooks
import store from '@/store';
  
export const useViewType = () => {
  const setViewType = (value: any): void => {
    store.dispatch('configs/setViewType', value)
  }
  const viewType = computed({
    get: () => (store as any).state.configs.viewType,
    set: setViewType
  })
  return [viewType, setViewType];
}

// 使用 hooks
import { useViewType } from '@/hooks/config'
const [viewType, setViewType] = useViewType()

搜索良久,在 stackoverflow 找到了解释(见源问答),意思就是说 return 语句后面的写法无法保证 hooks 解构后原有元素的顺序,可能导致异常,并提供了两种解决方法,见下图:

hooks_callable

此时,我们简单调整一下写法即可规避这个报错:

export const useViewType = () => {
 const setViewType = (value: any): void => {
   store.dispatch('configs/setViewType', value)
 }
 const viewType = computed({
   get: () => (store as any).state.configs.viewType,
   set: setViewType
 })
 return [viewType, setViewType] as const;  //  注意这里多了 as const
}

30、TS 中无法直接在 window 上挂载变量

在 TypeScript 中,当我们需要给window对象添加全局变量(如 testName),或者需要使用 window 下自定义创建的变量(以 testName 为例)。会出现以下 ts 报错:类型 Window & typeof globalThis 上不存在属性 testName。产生类型报错的原因是因为 window 数据类型定义如下:

declare var window: Window & typeof globalThis

我们可以通过如下几种方式来处理:

a、增加自定义属性声明,在类型声明文件 typings.d.ts 中,增加如下声明

interface Window {
  testName: string
}

b、将 window 类型强制转换为 any

(window as any).testName = 'Eric'
// 或
const win:any = window
win.testName = 'Eric'

c、使用 方括号

window['testName'] = 'Eric'

31、WangEditor 默认值不为空,导致表单验证不正常

在项目中不小心用到了 WangEditor 这个富文本编辑器,正常用没什么问题,可是当我在表单验证的场景下使用的时候,发现它的值默认不是空字符串,而是一段 html 片段 <p><br/></p>,看起来像是为了某种目的做的占位符。这个会导致绑定的表单值在初始状态下值不为空,进而造成校验不正常。

为了解决这个问题,看了一下官方文档和这个工具的 issue,发现确实存在这个问题,见 issue4619

最后,经过尝试,直接使用实例上的 isEmpty() 方法就可以判断富文本是否为空。如果为空,手动返回空字符串即可,实际业务逻辑里大概是向下面这样:

  const handleBlur = editor => {
  showEditor.value = false
  emit(
    'change',
    editor.isEmpty() ? '' : editor.getHtml(),
    editor.getText(),
    true
  )
}

32、vue3 setup 语法中使用 jsx 以及 render 函数的使用

最近在项目中对 ElementPlusel-table 进行二次封装的时候,发现 vue 文件中不能直接写 jsx 代码,render 函数也和 vue2.x 里的用法也不一样了。

为了解决这个问题,我查阅了相关资料,发现需要做如下操作才可以支持:

  • a、安装 @vitejs/plugin-vue-jsx 这个插件,并在 vite.config.ts 中引入。
// vite.config.ts
import vueJsx from "@vitejs/plugin-vue-jsx";

export default {
  plugins: [vueJsx()]
}
  • b、在 ts.config.json 中添加 "jsx": "preserve" 这个属性。
// tsconfig.ts
{
   "jsx": "preserve",
   "jsxFactory": "h",
   "jsxFragmentFactory": "fragment"
}
  • c、在 vue 文件中的 script 标签上添加 lang="tsx" 或者 lang="jsx",项目用 ts 编写就用 tsx,这样就可以支持 jsx 了。
<script lang="tsx">
import { defineComponent, ref } from "vue";
 
export default defineComponent({
   setup() {
     const msg = ref("tsx component");
     return () => {
         return <div>{msg.value}</div>;
     };
   }
});
</script>

注意:setup 语法糖中是不可以使用 render 函数的,必须要用传统的 setup 选项,在末尾 return 才可以。
可以在 setup 选项里直接 return ()=> h() 或者多个 return ()=> [h(), h()],不用再写 render了,如果 return 了渲染函数,那么就不能再返回其它属性了,不过可以通过 expose 暴露出去。

可以参考如下写法:

<script lang="tsx">
import { h,ref,renderSlot,reactive} from 'vue'
import Hello from './hello.vue'

interface Data {
  [key: string]: unknown
}
interface SetupContext {
  expose: (exposed?: Record<string, any>) => void
}
setup(props:Data, { expose }: SetupContext) {
	function sayhello() {
        console.log("Hello world!")
    }
  
    expose({
    	sayhello
    })
  
    return () => h(Hello)
}
</script>

参考文章:

33、手动触发 el-popover ,点击组件外区域自动关闭

业务中经常会用到 el-popover 这个组件来封装一些筛选组件,大部分情况下组件提供的属性都够我们用了,但是有些交互情况下,默认的属性并不太好实现。

比如如下图的一个人员筛选,需要在点击表头筛选图标后展示这个筛选组件(基于 el-popover),并且当我点击组件之外的区域时自动关闭组件,这个需求默认的属性是可以实现的,只需要配置 triggerclick 即可。
但是这个组件内部还有一个搜索框,我用的 el-autocomplete 这个组件,我发现在搜索框搜索的时候,会触发 el-popover 的失焦导致整个组件被关闭,这就很苦恼。

user-select-picker

经过再次查看文档,发现可以把 trigger 改为 "manual" 来自己控制 el-popover 的打开和关闭。这个时候,我再次搜索人员,发现已经不会导致组件自动关闭了,但是新的问题出现了,当我想点击组件外部区域关闭组件时,没效果(文档里也说了,manual 模式下这个行为不会触发关闭)。

看来得自己实现一个 clickoutside 了,不过我觉得很麻烦,这时我想到了 vue-use 这个 hooks 库里面有现成的,直接用吧,下面是官方的使用方法,只需要绑定一个 ref 就完事儿了,这样就解决了无法关闭的问题。

<template>
  <div ref="target">
    Hello world
  </div>
  <div>
    Outside element
  </div>
</template>

<script>
import { ref } from 'vue'
import { onClickOutside } from '@vueuse/core'

export default {
  setup() {
    const target = ref(null)
    onClickOutside(target, (event) => console.log(event))
    return { target }
  }
}
</script>

那么在我的组件中,把 ref 加到组件的包裹元素上即可(我这里还加了一个变量 isPicking 来标记是否正在搜索人员,避免选人过程中触发了关闭),附上这个组件的源码。

<template>
  <el-popover
    v-model:visible="visible"
    popper-class="user-filter-popper"
    :placement="placement"
    :title="title"
    :width="width"
    :show-arrow="false"
    :trigger="trigger"
  >
    <template #reference>
      <el-icon class="user-filter-trigger" @click="open">
        <Filter />
      </el-icon>
    </template>
    <div ref="refPopover" class="filter-wrapper">
      <div class="filter-body">
        <el-autocomplete
          ref="refAutoComplete"
          v-model="searchStr"
          popper-class="autocomplete-search-person"
          style="width: 100%"
          clearable
          value-key="nick_name"
          :maxlength="20"
          placeholder="请输入员工姓名"
          :debounce="500"
          :fetch-suggestions="querySearch"
          @select="choosePerson"
        >
          <template #prefix>
            <el-icon><Search /></el-icon>
          </template>
          <template #default="{ item }">
            <div
              :class="[
                'search-item',
                { disabled: selected?.includes(item.passport) }
              ]"
            >
              <Avatar
                :size="30"
                :name="item.nick_name"
                :src="item.avatar"
                randomColor
              />
              <p :title="item.nick_name">{{ item.nick_name }}</p>
            </div>
          </template>
        </el-autocomplete>
        <el-checkbox
          v-if="resultList.length"
          v-model="checkAll"
          class="checkbox-all"
          :indeterminate="isIndeterminate"
          @change="handleCheckAllChange"
          >全选</el-checkbox
        >
        <el-empty v-else :image-size="60" description="无数据" />
        <el-checkbox-group v-model="checkedList" @change="handleCheckedChange">
          <el-checkbox
            v-for="item in resultList"
            :key="item.passport"
            :label="item.passport"
            >{{ item.nick_name }}</el-checkbox
          >
        </el-checkbox-group>
      </div>
      <div class="filter-footer">
        <el-button size="small" @click="reset(false)">重置</el-button>
        <el-button type="primary" size="small" @click="confirm">确定</el-button>
      </div>
    </div>
  </el-popover>
</template>

<script setup lang="ts" name="UserFilter">
import { onClickOutside } from '@vueuse/core'
import { searchEmployee } from '@/api/statistic'
import { Filter, Search } from '@element-plus/icons-vue'

const props = withDefaults(
  defineProps<{
    value?: any
    placement?: string
    title?: string
    width?: string | number
    trigger?: string
  }>(),
  {
    value: '',
    placement: 'bottom',
    title: '',
    width: 200,
    trigger: 'manual'
  }
)
const emits = defineEmits<{
  (e: 'change', data: any)
}>()
const visible = ref(false)
const refPopover = ref<any>(null)
const refAutoComplete = ref<any>(null)
const searchStr = ref<string>('')
const curPerson = ref<any>()
const resultList = ref<Array<any>>([])
const checkedList = ref<Array<any>>([])
const isPicking = ref(false) // 是否正在选人
const checkAll = ref(false)
const isIndeterminate = ref(true)

const handleCheckAllChange = (val: boolean) => {
  checkedList.value = val ? resultList.value.map(item => item.passport) : []
  isIndeterminate.value = false
}
const handleCheckedChange = (value: string[]) => {
  const checkedCount = value.length
  checkAll.value = checkedCount === resultList.value.length
  isIndeterminate.value =
    checkedCount > 0 && checkedCount < checkedList.value.length
}
const querySearch = (str: string, cb: Function) => {
  if (!searchStr.value) return cb([])
  searchEmployee({
    cycle_id: 0,
    department_list: [],
    role_list: [],
    keyword: searchStr.value
  }).then(res => {
    let result = res.data || ([] as any)
    cb(result)
    isPicking.value = true
  })
}
const choosePerson =  (data: any) => {
  if (!data) return
  const result = data
  if (resultList.value.find(item => item.passport === result.passport))
    return ElMessage.warning('请不要重复添加!')

  // 手动关闭下拉框,避免出现两次
  refAutoComplete.value.close()
  refAutoComplete.value.inputRef.blur()

  curPerson.value = result
  resultList.value.push(result)
  searchStr.value = ''

  isPicking.value = false
}
const handleChange = (field, value) => {
  emits('change', checkedList.value)
}
const open = () => {
  visible.value = true
}
const close = () => {
  visible.value = false
}
const confirm = () => {
  emits('change', checkedList.value)
  close()
}
const reset = (emmitChange = false) => {
  resultList.value = []
  checkedList.value = []
  emmitChange && emits('change', checkedList.value)
}

// trigger为manual时需要单独处理点击组件外部自动关闭
onClickOutside(refPopover, event => {
  !isPicking.value && close()
})

defineExpose({
  reset
})
</script>

<style lang="scss">
.user-filter-trigger {
  position: relative;
  top: 2px;
  margin-left: 6px;
  cursor: pointer;
  width: 1em;
  color: $color_grey;
  &[aria-describedby],
  &:hover {
    color: $color_theme;
  }
}
</style>
<style lang="scss">
.user-filter-popper {
  padding: 16px 16px 0 !important;
  .filter-wrapper {
  }
  .filter-body {
    margin-bottom: 12px;
    .el-autocomplete {
      margin-bottom: 10px;
    }
    .checkbox-all {
      display: flex;
      padding: 10px 0;
    }
    .el-checkbox-group {
      .el-checkbox {
        display: flex;
        width: 100%;
        height: 28px;
        margin-right: 0;
        .el-checkbox__label {
          flex: 1;
          overflow: hidden;
          text-overflow: ellipsis;
          white-space: nowrap;
        }
      }
    }
    .el-empty {
      padding: 20px 0;
    }
  }
  .filter-footer {
    padding: 8px 0;
    display: flex;
    justify-content: flex-end;
    border-top: 1px solid $border_color1;
  }
}
</style>

34、动态设置 el-datepicker 的 disabled-date 属性无效

在使用 el-datepicker 的过程中,需求往往会需要限制它的可选择范围,比如只能选择今天及之后的时间,或者根据开始时间和结束时间来限定,ElementPlus 的 el-datepicker 为我们提供了一个 disabled-date 属性,我们可以使用它来支持这种特性(通过传入一个返回布尔值的函数来实现)。

el-datepicker-disabled

问题是,在 vue3 中,当我想动态的设置 disabled-date 的值时(函数内部的相关值都是响应式的 ref 数据),发现传入的配置并不会生效(其实 disabled-date 已经是最新的了),达不到限制选择范围的作用,但是我尝试直接写死 disabled-date 函数是可以生效的。

经过测试,发现是由于 disabled-date 更新后,el-datepicker 组件没有重新渲染导致的。于是,我修改了一下写法,最开始给 disabled-date 赋值为 undefined。在 template 中给 el-datepicker 组件添加一个 v-if = "disabled-date" ,这样,只有当 disabled-date 有值之后才会去渲染,果然解决了这个问题(当然,你也可以通过添加动态 key 的方式来触发组件重新渲染)。相关代码参考如下:

  <template>
  <div :class="['select-date', { disabled }]">
    <label>时间:</label>
    <el-date-picker
      v-if="disabledDate"
      v-model="params.date"
      :default-value="params.date"
      :disabled="disabled"
      class="select-item-date"
      type="daterange"
      :disabled-date="disabledDate"
      :editable="false"
      start-placeholder="开始时间"
      end-placeholder="结束时间"
      value-format="x"
    />
  </div>
</template>

<script setup lang="ts" name="SelectDate">
import { UnwrapNestedRefs } from 'vue'
import { formatStampTime } from '@/utils/common'
import storeStorage from '@/utils/storage'

interface SelectRolePropsInterface {
  disabled?: boolean
}

const props = withDefaults(defineProps<SelectRolePropsInterface>(), {
  disabled: false
})

const emit = defineEmits<{ (e: 'change', value: any) }>()

const cycleInfo = ref<any>()
const params: UnwrapNestedRefs<{
  date: undefined | [Date, Date]
}> = reactive({
  date: undefined
})
const disabledDate = ref<undefined | Function>(undefined) // 日期选择器可选范围

onMounted(() => init())

// 初始化
const init = () => {
  getCycleFromStorage()

  // 设置日期可选范围
  const { start, end } = cycleInfo.value
  const startVal = start * 1000
  const endVal = end * 1000
  params.date = [new Date(startVal), new Date(endVal)]

  disabledDate.value = (time: Date) =>
    time.getTime() < startVal || time.getTime() > endVal
}
// 从缓存中获取周期数据
const getCycleFromStorage = () => {
  const cacheData = storeStorage.get('cycleId')

  if (cacheData.value) {
    cycleInfo.value = JSON.parse(cacheData.value)
  }
}
</script>

35、自定义 el-datepicker 的快捷选项

在使用 el-datepicker 的过程中,往往会用到快捷选项这个功能,就是下图所示的这种 本周、本月 等快捷菜单,实现一键快速选择指定日期范围,ElementPlus 的文档里也简单说明了配置方式,主要是通过 shortcuts 这个属性进行配置的,但是并没有详细的文档。

el_datepicker_shortcuts

此时,我的需求是点击菜单,并高亮该菜单,然后自动选择该范围的日期,并关闭选择框。但是官方文档里并没有看到相关的配置属性,于是自己实现一下。

a、配置快捷方式
shortcuts 中单个菜单的配置由 textvalueonClick 三个属性组成,value 可以为 Date对象 或者 FunctiononClick 代表点击后的回调函数,用来实现一些自定义的功能(注意:当 value 有值的时候,onClick 将被忽略),源码见 useShortcut

  const shortcuts = [
    {
      text: '全周期',
      value: [startVal, endVal]
    },
    {
      text: '上周',
      value: () => {
        const start = getLastWeekFirstDay() // 上周第一天
        const end = getLastWeekLastDay() // 上周最后一天
        return [start, end]
      }
    },
    {
      text: '本周',
      value: () => {
        const start = getCurrentWeekFirstDay() // 本周第一天
        const end = getCurrentWeekLastDay() // 本周最后一天
        return [start, end]
      }
    },
    {
      text: '本月',
      onClick: picker => {
  		// 这里的日期需要用 dayjs 包一层,emit 才能生效(调试源码知道的)
        const start = dayjs(getCurrentMonthFirstDay()) // 本月第一天
        const end = dayjs(getCurrentMonthLastDay()) // 本月最后一天
        picker.emit('pick', [start, end])  
  
  		// 这里可以做一些自定义操作 ...
      }
    }
  ]

b、快捷方式高亮控制

<el-date-picker
        v-if="disabledDate"
        v-model="params.date"
        :default-value="params.date"
        :disabled="disabled"
        popper-class="popper-select-date"
        class="select-item-date"
        type="datetimerange"
        :shortcuts="shortcuts"
        :disabled-date="disabledDate"
        :editable="false"
        start-placeholder="开始时间"
        end-placeholder="结束时间"
        format="YYYY-MM-DD"
        value-format="x"
        @change="handleChange('date', $event)"
        @visible-change="handleVisibleChange"
      />
// 控制快捷选项的高亮状态 
const handleVisibleChange = (isReset = false) => {
  const btns = Array.from(
    document.querySelectorAll('.popper-select-date .el-picker-panel__shortcut')
  )
  for (let i = 0; i < btns.length; i++) {
    const el = btns[i]
    if (isReset) {
      el.classList.remove('active')
      return
    }
    el.addEventListener('click', e => {
      btns.forEach(el => {
        el.classList.remove('active')
      })
      e.target.classList.add('active')
    })
  }
}

36、配置 keep-alive 之后,对 DOM 的操作不会实时生效

有时候,我们需要通过 keep-alive 实现页面或组件的缓存,以此提升用户体验。但是,一旦使用了 keep-alive,它可能会导致一些潜在的问题,比如,对 DOM 的操作不会及时刷新。

场景:有一个带筛选的列表页面,它属于一个三级路由,通过里面的条目进入对应的详情页面,展示具体的数据,详情页也有一个筛选条(和列表页是同一个组件),此时我们只对列表页面添加了 keep-alive

但当我从列表页进入详情页后,对详情页中的一个 DOM 进行操作之后,页面上对应的元素并没有变化,需要我刷新页面才会改变(也就是没有重新渲染)。期初我以为直接给筛选组件加个 key 就可以修复这个问题的,但尝试无果,偶然间找到一篇文章说是要给 keep-alive 添加一个 key,试了一下,是可以的,只需要修改所有 keep-alive 相关的位置即可(我们可以使用当前完整路径 fullPath 来作为唯一 ‘key’),如下示例:

<keep-alive>
      <router-view v-if="$route.meta.keepAlive" :key="$route.fullPath">
      <!--要缓存的视图组件 -->
</keep-alive>
<router-view v-else  :key="$route.fullPath">
	<!-- 不缓存的视图组件 -->
</router-view>
  
 <!--也可以这样写 -->
 <router-view v-slot="{ Component }" :key="$route.fullPath">
      <keep-alive>
         <component :is="Component" v-if="$route.meta.keepAlive" />
      </keep-alive>
       <component :is="Component" v-if="!$route.meta.keepAlive" />
</router-view>

注意:
🈸 上面这种写法可能导致一些奇怪的问题(比如页面 DOM 渲染问题,重复请求等等)。经过多次尝试,建议使用官方提供的 include/exclude 来进行配置,基本不会再出现这些问题,同时避免了代码冗余。
如下代码:

<template>
    <router-view v-slot="{ Component }">
      <keep-alive :include="KEEP_ALIVE_PAGES">
         <component :is="Component" />
       </keep-alive>
    </router-view>
</template>
  
<script>
import { KEEP_ALIVE_PAGES } from '@/enum/index'
</script>
// @/enum/index
export const KEEP_ALIVE_PAGES = ['AimMake'] // 需要缓存的页面的 name 集合

由于我的项目中有 3 级路由,而 vue 的 keep-alive 不支持超过两级路由的缓存,所以我需要给每一级的 router-view 包一层 keep-alive 来实现第三级路由的缓存功能(但是这样写的话,我这边出现了组件重复渲染导致重复请求的问题,还有 DOM 未更新的情况,最后我尝试去掉第一层的 keep-alive 后竟然好了,真坑!!),以下是相关代码:

<!--Layout 页面(一级路由)-->
 <template>
  <div id="main-container">
    <div id="aside-container">
      <AsidePortal />
      <router-view name="aside" />
    </div>
    <div id="content-container">
      <router-view name="topbar" />
      <router-view v-slot="{ Component }">
        <!-- holy:多层路由最外层不套keep-alive竟然解决了内层缓存冗余问题,2里面就不行 -->
        <component :is="Component" />
      </router-view>
    </div>
  </div>
</template>
  
<!--Content 页面(二级路由)-->
<template>
  <div id="content-area">
    <router-view v-slot="{ Component }">
      <keep-alive>
        <component :is="Component" v-if="route.meta.keepAlive" />
      </keep-alive>
      <component :is="Component" v-if="!route.meta.keepAlive" />
    </router-view>
  </div>
</template>

<!--业务页面(三级路由)-->  
<template>
  <router-view v-slot="{ Component }">
    <keep-alive>
      <component :is="Component" v-if="route.meta.keepAlive" />
    </keep-alive>
    <component :is="Component" v-if="!route.meta.keepAlive" />
  </router-view>
</template>

可以看到我只在最后两级路由上添加了 keep-alive,具体原因未知,大家遇到这种问题还是多加尝试吧!。

参考:

37、setup 语法糖中使用 beforeRouteEnter

众所周知,在使用了 keep-alive 之后,我们需要对它进行控制,比如从列表进详情,详情返回的时候使用缓存,从其它页面进列表页的时候不走缓存,这就需要用到 beforeRouteEnter 这个钩子。
然而在 vue3 的 setup 语法中并不能使用这个钩子,因为它晚于 beforeRouteEnter 这个钩子,setup 内部只能用 beforeRouteUpdatebeforeRouteLeave,这样的话,我们就需要写 2 个 script 来支持,(还面临一个问题:一个 script 标签中无法直接引用另一个 script 标签内的方法 ),就像下面这样:

<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
  beforeRouteEnter(to, from, next) {
    // getData() // 报错xxx
    next()
  }
})
</script>
  
<script lang="ts" setup>
const getData = () => {}
</script>

经过大量搜索资料,发现了如下一些解决方案(按实现成本从低到高排序):

1、通过 defineExpose 把指定方法暴露出去,通过组件实例来引用

<script lang="ts">
import { defineComponent, ComponentPublicInstance } from 'vue'

interface IInstance extends ComponentPublicInstance {
  getData(from: string): void
}

export default defineComponent({
  beforeRouteEnter(to, from, next) {
    next((vm) => {
      const instance = vm as IInstance
      instance.getData(from.path)
    })
  }
})
</script>

<script lang="ts" setup>
const getData = (from: string) => {}

defineExpose({ getData })
</script>

2、通过引入 unplugin-vue-define-options 实现在 setup 内部定义组件的 options
a、安装 unplugin-vue-define-options 插件

yarn add unplugin-vue-define-options -D

b、vite.config.ts 中配置插件

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import DefineOptions from 'unplugin-vue-define-options/vite'

export default defineConfig({
  plugins: [vue(), DefineOptions()]
})

c、在组件中使用 defineOptions 来定义配置

<script lang="ts" setup>
defineOptions({
  name: '组件名',
  beforeRouteEnter(to, from, next) {
    next((vm) => {
      const instance = vm as any
      instance.getData(from.path)
    })
  }
})

const getData = (from: string) => {}
</script>

3、把 setup 的写法改为传统的 defineComponent 写法

<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
  setup() {
    const getData = () => {}
    return { getData }
  },
  beforeRouteEnter(to, from, next) {
    next(instance => {
      instance.getData(from.path)
    })
  }
})
</script>

😁 提示(一劳永逸型):如果你的逻辑很复杂,可以结合方法 1 和方法 2 ,最后只用写 1 个 script 标签即可,如下代码:

<script lang="ts" setup>
import { ComponentPublicInstance } from 'vue'

interface IInstance extends ComponentPublicInstance {
  getData(from: string): void
}

defineOptions({
  name: '组件名',
  beforeRouteEnter(to, from, next) {
    next((vm) => {
      const instance = vm as IInstance
      instance.getData(from.path)
    })
  }
})

const getData = (from: string) => {}

defineExpose({ getData })
</script>

参考:

参考文章

29
广告 广告

评论区