记录使用 Vue3 + Vite + Typescript 所踩的坑。
更新
[2021-3-31]
[2021-4-1]
Added
[2021-4-2]
Added
[2021-4-9]
Added
[2021-4-12]
Added
[2021-4-15]
Added
初始化项目
1
| yarn create @vitejs/app vue3-vite-example-todo --template vue-ts
|
[vite] Internal server error: Failed to resolve import “@/api/api” from “src/main.ts”. Does the file exist?
问题概述
已经在 vite.config.ts
中配置了路径别名:
1 2 3 4 5 6 7 8
| export default defineConfig({ plugins: [vue()], resolve: { alias: { '@/': path.resolve(__dirname, './src'), }, }, });
|
并且于 tsconfig.json
中配置了路径映射:
1 2 3 4 5 6 7
| { "compilerOptions": { "paths": { "@/*": ["src/*"] } }, }
|
还是出现了无法识别路径别名的错误。
解决方案
查阅 issue#2316 发现,vite 其实是使用 rollup 作为构建工具的,rollup 配置路径别名的语法与 webpack 有所不同,采用的是如下方式:
1 2 3 4 5 6 7 8 9
| export default defineConfig({ plugins: [vue()], resolve: { - alias: { - '@/': path.resolve(__dirname, './src'), - }, + alias: [{ find: '@', replacement: path.resolve(__dirname, './src') }], }, });
|
vue3挂载全局属性
问题概述
在 vue2.x
中,我们一般直接在 Vue.prototype 上挂载一个方法或属性:
1 2 3 4
| import Vue from 'vue'; import * as api from '@/api.js'
Vue.prototype.$api = api
|
之后,为了更好的 ts 代码提示,我们需要声明类型:
1 2 3 4 5 6 7
| import * as api from '@/api/api';
declare module "vue/types/vue" { interface Vue { $api: typeof api } }
|
解决方案
在 vue3.x
中,可以通过如下方式挂载全局属性
1 2 3 4 5 6 7 8 9 10 11 12 13
| import { createApp } from 'vue'; import App from './App.vue'; import * as api from '@/api/api';
const app = createApp(App);
app.config.globalProperties['$api'] = api;
app.mount('#app');
|
接着,如果我们需要编写类型声明文件,vue3.x
和 vue2.x
所采取的方式不太一样。
src/@types/
目录下新建 properties-vue.d.ts
,接着引入并声明自定义的各种方法和属性:
1 2 3 4 5 6 7
| import api from '@/api/api';
declare module '@vue/runtime-core' { interface ComponentCustomProperties { $api: typeof api } }
|
记住,编写声明文件之后,需要重新启动编辑器!
最后,我们可以在 Composition API 中这么使用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| import { defineComponent, getCurrentInstance, ComponentInternalInstance, } from 'vue';
export default defineComponent({ async setup() { const app = (getCurrentInstance() as ComponentInternalInstance).proxy;
if (app) { const res = await app.$api.fetchUserInfo('xxx'); } }, });
|
如果你依旧想用 Options API 的话,那么你可以这么做,如下图所示:
异步的setup
问题概述
在编写组件的时候,遇到了一个问题,如果在组件中定义了异步的 setup
,那么该组件就无法被渲染,例如 src/components/Todo/index.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| import { defineComponent } from 'vue';
export default defineComponent({ name: "Todo", async setup() { const app = (getCurrentInstance() as ComponentInternalInstance).proxy; const res = await app?.$api.getTodoList();
return { todoList: reactive(res) } } });
|
解决方案
由于 async 返回的 Promise 使得 setup 函数被挂起,也就是该组件会被异步渲染,解决方式很简单,如果定义了异步的 setup,那么需要在其父组件中包裹一层 <Suspense>
组件:
1 2 3
| <Suspense> <Todo /> </Suspense>
|
权限路由的重置
问题概述
在 vue2 + vue-router3 中,我们想实现权限菜单,就需要后端来动态的返回一个菜单列表,大体格式如下:
1 2 3 4 5 6 7 8 9 10 11
| const dynamicRoutes = [ { action: 'PageA', permissions: ['按钮1', '按钮2', ...] }, { action: 'PageB', permissions: [] } ]
|
然后我们根据后端返回的权限菜单来与本地的静态路由表作对比,筛选出有权限展现的页面,静态路由表如下格式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| const staticRoutes = [ { path: '/pageA', name: 'PageA' meta: { hidden: false }, component: PageA, }, { path: '/pageB', name: 'PageB', meta: { hidden: false }, component: PageA, }, ]
|
最终需要重置 Router
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| const finalRoutes = staticRoutes.map(outerV => { const foundMenu = dynamicRoutes.find(innerV => { return innerV.action === outerV.name; });
return { ...outerV, meta: { ..outerV.meta, hidden: !foundMenu, }, }; });
router.matcher = createRouter(finalRoutes).matcher;
|
解决方案
vue-router4 抛弃了 router.matcher
属性,所以我们只能通过 router.addRoute()
来添加路由,引用 vue-route4 官方文档里的一句话:
所以,最终的代码是这样的:
1 2 3 4 5 6 7 8
| + import { useRouter } from 'vue-router';
// 替换现存的 Router - router.matcher = createRouter(finalRoutes).matcher; + finalRoutes.forEach(v => { + // addRoute() 第一个参数代表要插入的路由的父级路由 + router && router.addRoute('Home', v); + });
|
404路由配置
问题概述
在 vue2 中,我们通常使用通配符 *
来匹配所有不存在的路由页,从而跳转到自定义的 404 页,我们的本地静态路由表可能是这样的:
1 2 3 4 5 6 7 8 9 10 11 12
| import { createRouter, createWebHashHistory } from 'vue-router';
const router = createRouter({ history: createWebHashHistory(), routes: [ { path: '*', name: 'NotFound', component: () => import('@/views/404/index.vue'), }, ], });
|
解决方案
但是在 vue-router4 中,取消了通配符式匹配,须按如下方式来匹配 404 路由:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| const router = createRouter({ history: createWebHashHistory(), routes: [ - { - path: '*', - name: 'NotFound', - component: () => import('@/views/404/index.vue'), - }, + { + path: '/404', + name: 'NotFound', + component: () => import('@/views/404/index.vue'), + }, + { + path: '/:pathMatch(.*)*', + redirect: '/404', + }, ], });
|
父子传值的方式-props
问题概述
起初,翻了下 defineComponent 的类型定义,我以为直接通过以下的方式来定义 props 并接收:
index.d.ts
:
1 2 3 4 5 6 7 8 9 10 11 12
| export type IButtonType = | 'primary' | 'success' | 'danger' | 'default' | 'warning'; export type IButtonNativeType = 'button' | 'submit' | 'reset' | 'menu';
export interface IChildProps { type?: IButtonType; nativeType?: IButtonNativeType; }
|
Child.vue
:
1 2 3 4 5 6 7 8
| export default defineComponent<IChildProps>({ setup(props) { console.log('props :>> ', props); }, });
|
Parent.vue
:
1 2 3
| <template> <Child type="primary" nativeType="button">主按钮</Child> </template>
|
但是在 Child.vue
接收到的 props 总是空对象。
解决方案
vue3 目前依旧是采用 PropType 的形式来定义 props,所以上面的方式暂时是不可取的,变通一下:
Child.vue
:
1 2 3 4 5 6 7 8 9 10 11 12
| import * as TYPES from './index.d.ts';
-export default defineComponent<IChildProps>({ +export default defineComponent({ + props: { + type: String as () => TYPES.IButtonType, + nativeType: String as () => TYPES.IButtonNativeType, + }, setup(props) { console.log('props :>> ', props); }, });
|
click执行两次
问题概述
由于在编写组件库的时候,子组件(Button
)需要将 click 事件交给外界(父组件
)处理,具体代码如下:
子组件(Button)
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| <template> <button @click="handleClick"></button> </template>
<script lang="ts"> export default defineComponent({ setup(props, context) { function handleClick(e) { context.emit('click', e); }
return { handleClick, } } }); </script>
|
外界组件
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| <template> <V3Button @click="handleClick"></V3Button> </template>
<script lang="ts"> import V3Button from './components/Button.vue';
export default defineCompoent({ components: { V3Button, }, setup() { function handleClick(e) { console.log(e); } } }); </script>
|
上面的代码会产生一个问题,外界组件中的 handleClick
会执行两次。
解决方案
在 vue3 中,子组件的根节点会默认接收父组件上的所有 v-on
监听事件,所以如果需要在子组件的根元素上触发事件的话,不用在子组件中定义相应的处理函数,比如:
子组件(Button)
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| <template> - <button @click="handleClick"></button> + <button></button> </template>
<script lang="ts"> export default defineComponent({ setup(props, context) { - function handleClick(e) { - context.emit('click', e); - } - - return { - handleClick, - } } }); </script>
|
集成jest单测
问题概述
由于编写组件库,需要在项目中集成 jest
单元测试。
解决方案
首先列出所需依赖:
依赖名 |
环境 |
概述 |
jest |
dev |
官方的 jest 库 |
ts-jest |
dev |
用来编译 *.ts 文件 |
vue-jest |
dev |
用来编译 *.vue 文件 |
@types/jest |
dev |
ts 类型支持 |
接着配置 tsconfig.json
:
1 2 3 4 5 6 7 8 9 10 11 12 13
| { "compilerOptions": { + # 此处的 types 表明:只需要在本项目的 node_modules 寻找 ts 声明文件,不去父级向上一次查找 + "types": ["vite/client", "jest", "node"] }, "include": [ "src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue", + "__tests__/**/*" ] }
|
然后配置终端命令(package.json
):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| { "scripts": { "dev": "vite", "build": "vue-tsc --noEmit && vite build", "serve": "vite preview", + "test": "jest" }, "dependencies": { "icon": "^0.0.3", "vue": "^3.0.5" }, "devDependencies": { "@types/jest": "^26.0.22", "@types/node": "^14.14.37", "@vitejs/plugin-vue": "^1.2.1", "@vue/compiler-sfc": "^3.0.5", "@vue/test-utils": "^1.1.4", "jest": "^26.6.3", "sass": "^1.32.8", "ts-jest": "^26.5.4", "typescript": "^4.1.3", "vite": "^2.1.5", "vue-jest": "^3.0.7", "vue-tsc": "^0.0.15" } }
|