Vue3+TS+Vite2+ElementPlus+Eslint项目实践遇到的技巧/问题汇总
技巧/问题汇总
Volar插件
推荐一款 VSCode
插件,Volar
是一款针对 Vue
的打造的官方插件,在 第四届VueConf 中尤雨溪大大专门做了推荐。
用 VSCode
的铁汁们就有福了,虽然现在只有 21000
左右的下载量,但我觉得以后肯定会增加的。因为它是真的非常强大的,特别在对 Vue3
和一些 Vue
新的 RFC
都有很好的适配和及时的更新。
安装方式很简单,直接在 vscode
的插件市场搜索 Volar
,然后点击安装就可以了,具体使用就慢慢自己去体会啦。
Vue3中对props进行TS类型规范
对一块组件中要求的数据进行数据类型的规范是非常重要的,这样有利于组件的可维护,下面我们尝试来规范比较常见的对象
、数组
、函数
这三种情况。
写法一
在使用 Vue3+TS
对 props
进行复杂类型验证的时候,可以直接用 Vue
提供的 PropType
属性进行强制转换:
// 子组件Goods.vue <script lang='ts'> import {defineComponent, PropType} from 'vue' interface IGoods { id: number; goodsName: string; } interface IFun { (): IGoods } export default defineComponent({ props: { // 对象 goods: { required: true, type: Object as PropType<IGoods> }, // 数组 list: { required: true, type: Object as PropType<IGoods[]> }, // 函数 getInfo: { required: true, type: Function as PropType<IFun> } } }) </script>
写法二
也能以函数的形式来编写:
// 子组件Goods.vue <script lang='ts'> import {defineComponent} from 'vue' interface IGoods { id: number; goodsName: string; } interface IFun { (): IGoods } export default defineComponent({ props: { goods: { required: true, type: Object as () => IGoods }, list: { required: true, type: Array as () => IGoods[] }, getInfo: { required: true, type: Function as unknown as () => IFun } } }) </script>
Vue3中如何挂载全局方法
在 Vue2
的项目中,我们能经常见到这样子的场景:
// 挂载全局方法 Vue.prototype.$dateFormat = () => { console.log('日期转换方法') }; // 调用全局方法,在任何.vue文件中都能直接使用 this.$dateFormat();
几乎所有 Vue2
项目都会把一些全局性的变量、方法直接挂载在 Vue
的原型上,这样子通过 this
调用,确实挺方便我们使用的。
但是在:
Vue3
中是没有 this
!!!
Vue3
中是没有 this
!!!
Vue3
中是没有 this
!!!
(重要的事情说三遍)
globalProperties
虽然话是怎么说,但是 Vue3
是兼容 Vue2
的,我们其实依旧也能使用 this
,但只能在原来的 Option API
中使用,不能在新的 Composition API
使用哦。而且 Vue3
也并不推荐在原型上做文章了,但尤雨溪大大为了让一些想要把 Vue2
升级为 Vue3
项目的用户过渡平滑,也推出了代替 Vue.prototype
原型的 globalProperties 方案。
如何挂载全局方法:
// main.ts import { createApp } from 'vue' import App from './App.vue' const app = createApp(App) app.config.globalProperties.$dateFormat = () => { console.log('日期转换方法') }; app.mount('#app')
具体使用:
<script lang="ts"> import {defineComponent, onMounted, getCurrentInstance} from 'vue' export default defineComponent({ // Options API mounted() { this.$dateFormat() }, // Composition API setup() { const { proxy } = getCurrentInstance()!; onMounted(() => { proxy.$dateFormat() }) return {} } }) </script>
上面分别展示 Option API
和 Composition API
获取全局方法的方式。
getCurrentInstance() 方法是用来访问内部组件实例,它身上挂载着很多方法,我们可以通过它的 proxy
属性访问到 globalProperties
身上进而访问到挂载的全局方法, 对于访问 globalProperties
我们也能换一种方式:
const { $dateFormat } = getCurrentInstance()!.appContext.config.globalProperties; $dateFormat();
这里有个需要注意的地方,我看网上很多文章会以下面的形式去访问
globalProperties
身上挂载的东西:
const { ctx } = getCurrentInstance();
ctx.$dateFormat();
但这种方式只能在开发环境下使用,生产环境下的ctx
将访问不到globalProperties
,也就是打包后访问ctx.$dateFormat();
是会报错。(Uncaught TypeError: ctx.$dateFormat is not a function
)
(但现在好像Vue
改动了,开发环境也直接访问不了,YES,挺好!)
虽然上面通过 globalProperties
的方式挂载全局方法挺好用的,又能替代 Vue.prototype
也能在 Composition API
中使用,但看尤雨溪大大的意思好像是不建议这么用了,具体可以查看这个 vue/rfcs 。
虽然 Vue3
不推荐怎么挂载一些变量或方法,但是推荐使用依赖注入的 provide()和inject() 形式。
provide()/inject()
使用 project()
绑定依赖:
import { createApp } from 'vue' import App from './App.vue' const app = createApp(App) app.provide('$dateFormat', () => { console.log('日期转换方法') }) app.mount('#app')
使用 inject()
获取:
<script lang="ts"> import {defineComponent, inject} from 'vue' export default defineComponent({ setup() { const $dateFormat: (() => void) | undefined = inject('$dateFormat') $dateFormat && $dateFormat() return {} } }) </script>
provide/inject
的用法很简单,但需要注意只能在当前活动实例的 setup()
期间才能调用这两者,更多妙用可以自己去细细尝试一下哦。 点我
Vite中动态HTML内容的插件
在使用 vue-cli
构建的项目中,默认为我们提供以 <%= htmlWebpackPlugin.options.title %>
的形式来动态为 HTML
插入内容。其过程是利用 webpack
提供的 HtmlWebpackPlugin
插件,在编译时,把模板变量替换为实际的 title
值。
而在 Vite
中要实现这样的功能也非常简单,我们借助 vite-plugin-html 就能轻松实现。
安装:
npm install vite-plugin-html -D
配置:
// vite.config.ts import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' import { injectHtml } from 'vite-plugin-html' export default defineConfig({ plugins: [ vue(), injectHtml({ injectData: { title: '用户管理系统' } }) ], })
具体使用,动态变量语法简单了许久
// index.html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <link rel="icon" href="/favicon.ico" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title><%= title %></title> </head> <body> <div id="app"></div> <script type="module" src="/src/main.ts"></script> </body> </html>
更多配置细节就好好看 vite-plugin-html文档 吧。(-^〇^-)
Vite中配置项目别名“@”的问题
配置项目别名现在也是一个项目必不可少的环节了,因为项目使用了 Vite
来构建,Vite文档 也有介绍别名的配置,我们先按文档老老实实配置一下。
import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' import { resolve } from 'path' export default defineConfig({ plugins: [vue()], resolve: { alias: { '@': resolve(__dirname, 'src') } } })
resolve: { alias: { '@': fileURLToPath(new URL('./src', import.meta.url)) // 上面不起效果,使用此写法 } }
简简单单,是不是?(^ω^)
配置好了,我们就直接在项目中使用,我们假设引入一个CSS文件和一个TS文件:
呃...??? 报红了!但是引入的样式生效了,引入的TS文件中的方法也能正常调用。
因为项目用到了 eslint
,怀疑这应该是 eslint
不识别项目别名报的红了,那我们修改下它的配置应该就可以啦! 解决 eslint
不识别项目别名的方式有很多种,这里我们先直接选择最暴力的,关闭它相关的 ESLint rules
:
// .eslintrc.js module.exports = { env: { browser: true, es2021: true }, extends: ['plugin:vue/essential', 'airbnb-base', 'plugin:prettier/recommended'], parserOptions: { ecmaVersion: 12, parser: '@typescript-eslint/parser', sourceType: 'module' }, plugins: ['vue', '@typescript-eslint'], rules: { 'import/no-unresolved': 'off', // 关闭对别名(@)审查 'import/no-extraneous-dependencies': 'off' // 关闭内置模块审查 } }
修改完 eslint
的配置后,我们能发现引入的CSS文件已经不报红了,但TS文件还是报红的,这又是为什么呢?
这里应该就已经不是 eslint
的问题了,可能是 VSCode
编辑器审查语法的问题了。VSCode
语法检查、tsc编译
都需要依赖 tsconfig.json
文件中的配置,那我们就再配置一下 TS
中的别名看看。
// tsconfig.json { "compilerOptions": { "target": "esnext", "module": "esnext", "moduleResolution": "node", "strict": true, "jsx": "preserve", "sourceMap": true, "resolveJsonModule": true, "esModuleInterop": true, "lib": ["esnext", "dom"], "paths": { "@/*": ["./src/*"], // 配置ts别名 } }, "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"] }
修改完 tsconfig.json
就没有报红了,大功告成,再也没有报错了。(-^〇^-)
Vite中全局引入CSS变量
Less变量
安装
npm install less less-loader -D
配置
// vite.config.ts import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' export default defineConfig({ plugins: [vue()], css: { preprocessorOptions: { less: { modifyVars: { hack: `true; @import (reference) "${resolve('src/assets/lessVar.less')}";` }, javascriptEnabled: true } } } })
具体使用
// lessVar.less @primary: #43AFFF;
// 在任意.vue文件中可以直接使用变量 <style lang="less" scoped> h1 { color: @primary; } </style>
Scss变量
安装
npm install sass sass-loader node-sass@4.14.1 -D
配置
// vite.config.ts import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' export default defineConfig({ plugins: [vue()], css: { preprocessorOptions: { scss: { additionalData: '@import "src/assets/scssVar.scss";' } } } })
具体使用
// scssVar.less $primary: #43AFFF;
// 在任意.vue文件中可以直接使用变量 <style lang="scss" scoped> h1 { color: $primary; } </style>
在使用 scss
踩了两个坑:
在
Vite
中使用scss
强制要求下载sass
,要不就一直报错。
我使用的
win7
系统,对于node-sass
依赖只能安装 5 以下的版本,主要原因是node-sass
对node
版本有要求,要求node15
以上,但是node
从 14 左右就开始不支持win7
的安装了。
Vite中代理功能rewrite属性的改变
为开发服务器配置自定义代理也是一个老操作了,Vite
同样也提供了一个 server.proxy 来配置代理,其背后和 webpack
一样也是使用了 http-proxy 做为底层。
在使用 webpack
很多时候我们可能都是如下做配置:
proxy: { '/api': { target: '代理的服务地址', secure: true, // 配置https changeOrigin: true, // 跨域, 本地会虚拟一个服务器接收并代转发你的请求 pathRewrite: { // 忽略前缀, 也就是不会加上/api这一层上去 '^/api': '' } } }
但是在使用 Vite
构建的项目如此配置却报错了!!!
查了一下 Vite文档 才知道是 pathRewrite
属性改名称了,现在它变成了 rewrite
且接收一个函数形式,正确的配置:
// vite.config.ts export default defineConfig({ plugins: [vue()], server: { proxy: { '/api': { target: '代理的服务地址', secure: true, changeOrigin: true, rewrite: (path) => path.replace(/^\/api/, '') } } }, })
Vite中的环境变量
在使用 vue-cli
的时候,我们经常会使用到环境变量,用它来判断程序运行的环境:
const BASE_URL = process.env.NODE_ENV === 'production' ? 'http://production.com' : 'http://development.com';
Vite
也提供了它自己的 环境变量
Vite
通过 import.meta.env
来访问相关环境变量,我们先把 import.meta
打印来看看: 可以看到它包含挺多东西的,但我们重点看 env
属性,它身上内置了一些环境变量:
import.meta.env.BASE_URL
:string类型,部署应用时的基本URL,他由 base配置项 决定。import.meta.env.DEV
:boolean类型,应用是否运行在开发环境(永远与import.meta.env.PROD
相反)。import.meta.env.MODE
:string类型,应用运行的模式,它由 mode配置项 决定。import.meta.env.PROD
:boolean类型,应用是否运行在生产环境。import.meta.env.SSR
:boolean类型,是否是SSR应用,更多详情。
注意在生产环境中,这些环境变量会在构建时被静态替换,因此,在引用它们时请使用完全静态的字符串。动态的 key 将无法生效。例如,动态 key 取值 import.meta.env[key] 是无效的。
如何创建额外的环境变量
我们能通过创建不同模式下的配置文件来加载额外的变量,比如,我们在项目根目录下创建 .env.development
与 .env.production
文件。
// .env.development VITE_TITLE = 'XXX-开发环境' OTHER_TITLE = '其他名称-开发环境'
文件内容如上,再打印 import.meta
查看,记得重启哦。
我们能发现已经读取到这些额外的变量了,因为我们执行的是 npm run dev
的命令启动项目,所以读取的会是 .env.development
文件的内容,如果执行 npm run build
则就会读取.env.production
文件。
而且 Vite
为了防止意外地将一些环境变量泄漏到客户端,只有将以 VITE_
为前缀的变量才会暴露给经过 Vite
处理的代码。
当然,我们不仅仅能创建 development
与 production
这两种模式的文件,如果你配置了 其他模式, 如 test
测试模式,你也可以创建 .env.test
文件来加载额外的变量。
.env # 所有情况下都会加载
.env.local # 所有情况下都会加载,但会被 git 忽略
.env.[mode] # 只在指定模式下加载
.env.[mode].local # 只在指定模式下加载,但会被 git 忽略
Vite中用globEager替代require.context
原来在 vue-cli
的项目中,webpack
为我们提供了 require.context() 方法,让我们可以很方便的将一个文件夹下的所有文件一并导入。但该方法是 webpack
提供的,在 Vite
中自然不能用了,不过不用慌,vite
为我们提供了 Glob 模式的模块导入,一样能实现文件的一并导入功能。
下面我们来分别使用二者来实现将 @/components/
文件下的文件全部导入,并注册为全局组件,让我们来看看两者有什么不同。
require.context
// main.js const allCom = require.context('@/components/', true, /\.vue/); allCom.keys().forEach(key => { const fullName = key.substr(key.lastIndexOf('/') + 1) const comName = fullName.split('.')[0].toUpperCase() Vue.component(comName, allCom(key).default || allCom(key)) })
globEager
// main.ts const allCom = import.meta.globEager('./components/*.vue') Object.keys(allCom).forEach((key) => { const files = key.substr(key.lastIndexOf('/') + 1) const name = files.split('.')[0].toUpperCase() app.component(name, allCom[key].default) })
该 Glob 模式会被当成导入标识符:必须是相对路径(以 ./ 开头)或绝对路径(以 / 开头,相对于项目根目录解析),无法使用项目别名
导入全部文件的常见场景,除了动态注册全局组件外,还有一种就是注册路由的场景了,但注册路由的情况就不推荐使用 import.meta.globEager
了,推荐使用 import.meta.glob
,因为它默认具有懒加载性质,并会在构建时分离为独立的 chunk
。
Eslint常见的配置
去掉console的warn提示
用一下 console
大法打印点东西都能有黄线,哎,难受。(T_T)
解决:
// .eslintrc.js module.exports = { ... rules: { // 开发环境不对console进行审查 'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off' } }
允许使用自增('++')自减('--')符号
个人觉得自增自减还是挺好用的,看网上很多建议是可以写成 number.value += 1
这样子的形式,呃,看个人喜好吧。
解决:
// .eslintrc.js module.exports = { ... rules: { // 允许使用自增自减符号 'no-plusplus': [ 'off', { allowForLoopAfterthoughts: true } ] } }
去掉alert()的warn提示
有时候为了方便想直接用系统的提示框,这也被提示黄线了。
解决:
// .eslintrc.js module.exports = { ... rules: { // 去调用使用alert/confirm/prompt的warn 'no-alert': 0 } }
允许对函数参数再赋值
这是我在把上篇 完整的Axios封装-单独API管理层、参数序列化、取消重复请求、Loading、状态码... 文章改造成 TS
形式遇到的一个问题。
大致问题就是 eslint
默认不允许对函数参数再进行赋值操作,要不就会报红提示。
确实对函数参数中的变量进行赋值可能会误导读者,导致混乱,也会改变 arguments 对象。这确实是个有点危险性的操作,但有时候无可奈何,我就是想改(T_T),就如我在上面提到的文章中,我封装了一个取消重复请求的方法:
为了方便,我就是想在这个 addPending
方法中修改 cancelToken
属性。但是报红提示了,那有没有办法去掉这个提示呢?答案当然是有的,我们需要配置一下 no-param-reassign 规则。
解决:
// .eslintrc.js module.exports = { ... rules: { // 允许修改函数的入参 'no-param-reassign': [ 'error', { props: true, ignorePropertyModificationsFor: [ 'config', ] } ], } }
配置后, config
就不报红啦,如果有更多其他参数名要配置,可以在 ignorePropertyModificationsFor
属性继续添加。
ElementPlus中的新组件 - SelectV2 - 虚拟列表选择器
在我写这篇文章的时候,偶然在 ElementPlus 官网上发现了它竟然新出了一块组件,这里就顺便写上来推荐推荐,感兴趣的小伙伴可以更新最新版的库下来玩一玩。
ElementPlus中使用Loading组件的TS类型
在我们用 TS
二次封装 ElementPlus
的组件的时候,直接选择用它的 type
类型更方便一点哦。
<template> <div> <button @click="clickEvent">点击</button> </div> </template> <script lang="ts"> import { defineComponent } from 'vue' import { ElLoading } from 'element-plus' import { ILoadingOptions } from 'element-plus/lib/el-loading/src/loading.type' import 'element-plus/lib/theme-chalk/index.css' export default defineComponent({ setup() { // 封装loading方法 function openLoading(loadingOptions?: ILoadingOptions) { ElLoading.service(loadingOptions) } function clickEvent() { openLoading({ fullscreen: true }) } return { clickEvent } } }) </script>
其他组件的类型规范也对应在源码中找到相关的 type
引入即可。
TS中的可索引的类型
这是一个可能容易被忽略的特性,它大概的作用就是:可以使用它来描述那些能够“通过索引得到”的类型,比如 a[10]
或 ageMap["daniel"]
。 文档
之所以讲到这个特性,主要是在项目中看到其他成员写了一些这样子的代码:
// 大致模样 interface Goods { goodsName: string } let goods: Goods = { goodsName: '一号商品' } goods = { goodsName: '二号商品', aliasGoodsName: '商品别名' } as Goods;
上面代码通过 as
断言语句,绕开编辑器检查,让商品对象增加了一个 aliasGoodsName
属性,但是 Goods
接口确没有相关描述,这有时会让其他项目成员很捉摸不透,因为商品对象是用 Goods
接口规范的,所有属性应该都很明确才对,并不存在什么不确定性。
下面我们先介绍一下,比较正确的方式来解决这种情况,直接使用 可选属性 :
interface Goods { // 接口定义类型 goodsName: string aliasGoodsName?: string } let goods: Goods = { goodsName: '一号商品' } goods = { goodsName: '二号商品', aliasGoodsName: '商品别名' };
可选属性 能够很好的应对这种情况,但是增加个别属性还好,如果所规范的对象存在的不确定性非常大,增加的是十个?二十个呢(极端情况)? 如果我们还使用这种方式,就每次都要改 Goods
接口,这就变麻烦了。
懒是激发潜力的重要动力,这有没有一种一劳永逸的方式呢?答案当然是用的,且看:
interface Goods { goodsName: string [proName: string]: any } let goods: Goods = { goodsName: '一号商品' } goods = { goodsName: '二号商品', aliasGoodsName: '商品别名' } goods = { goodsName: '三号商品', price: 100 }
利用 可索引的类型
特性,我们就能实现规范的类型属性随便增加了,再也不用操心了。
呃,但是呢!!!这上面使用了 any
而且你有没有突然觉得整个接口类型都变得虚有图表,意义不是很大了?
哈哈哈,我告诉你.................这是错觉,不要在意这些细节,我们的写代码的第一目标就是代码能跑,也不是不能用就行,这是我们的宗旨。