Vue2+typescript写法总结
之前不是ts写的老项目,想接入ts,首先使用vue命令安装typescript下面依次对安装过程中出现的选项进行解释最后这里回车过后就可以继续安装ts了。安装过程中,可能出现如下错误:ERRORError: Cannot find module ‘@vue/cli-plugin-router/generator/template/src/views/HomeView.vue’ from ’ xxx
老项目如何接入typescript
之前不是ts写的老项目,想接入ts,首先使用vue命令安装typescript
vue add typescript
下面依次对安装过程中出现的选项进行解释
E:\test>vue add typescript
📦 Installing @vue/cli-plugin-typescript...
+ @vue/cli-plugin-typescript@5.0.6
added 123 packages from 57 contributors and removed 11 packages in 15.57s
119 packages are looking for funding
run `npm fund` for details
✔ Successfully installed plugin: @vue/cli-plugin-typescript
// 是否使用class类风格编码,默认是yes,这里直接回车使用默认
? Use class-style component syntax? Yes
// 是否同时使用ts和babel编译代码。这是因为ts编译后的代码是es6代码,如果需要最终代码编译成es5的js代码,需要使用bable。也就是选择是否将ts和bable编译器结合使用
? Use Babel alongside TypeScript (required for modern mode, auto-detected polyfills, transpiling JSX)? Yes
//将js文件转为ts文件,默认yes ,这里同样选默认
? Convert all .js files to .ts? Yes
//允许编辑js代码,默认no,这里选择yes,允许编译
? Allow .js files to be compiled? Yes
// 是否跳过所有类型文件的检查,默认yes,这里使用默认。不建议检查所有类型,因为很多类型文件是外部库提供的
? Skip type checking of all declaration files (recommended for apps)? Yes
最后这里回车过后就可以继续安装ts了。
安装过程中,可能出现如下错误:
ERROR Error: Cannot find module ‘@vue/cli-plugin-router/generator/template/src/views/HomeView.vue’ from ’ xxx 或者 ERROR Error: Cannot find module ‘@vue/cli-plugin-router/generator/template/src/views/HellowWord.vue’ from ’ xxx 类似的错误。
出现这种错误,是版本问题造成。
🚀 Invoking generator for @vue/cli-plugin-typescript...
ERROR Error: Cannot find module '@vue/cli-plugin-router/generator/template/src/views/HomeView.vue' from 'E:\test\node_modules\@vue\cli-plugin-typescript\generator\template\src\views'
Error: Cannot find module '@vue/cli-plugin-router/generator/template/src/views/HomeView.vue' from 'E:\lecent\lecent-web-pedestal\node_modules\@vue\cli-plugin-typescript\generator\template\src\views'
at Function.resolveSync [as sync] (C:\Users\yanglian\AppData\Roaming\npm\node_modules\@vue\cli\node_modules\resolve\lib\sync.js:111:15)
at renderFile (C:\Users\yanglian\AppData\Roaming\npm\node_modules\@vue\cli\lib\GeneratorAPI.js:518:17)
at C:\Users\yanglian\AppData\Roaming\npm\node_modules\@vue\cli\lib\GeneratorAPI.js:303:27
at processTicksAndRejections (internal/process/task_queues.js:95:5)
at async Generator.resolveFiles (C:\Users\yanglian\AppData\Roaming\npm\node_modules\@vue\cli\lib\Generator.js:310:7)
at async Generator.generate (C:\Users\yanglian\AppData\Roaming\npm\node_modules\@vue\cli\lib\Generator.js:204:5)
at async runGenerator (C:\Users\yanglian\AppData\Roaming\npm\node_modules\@vue\cli\lib\invoke.js:111:3)
at async invoke (C:\Users\yanglian\AppData\Roaming\npm\node_modules\@vue\cli\lib\invoke.js:92:3)
就是在 module依赖包@vue/cli-plugin-router 里面,找不到HomeView.vue,本测试项目的@vue/cli-plugin-router版本如图
按住ctrl键点击版本号,进入真实版本依赖文件,显示为4.5.18
找到该文件所在的module包,查看包结构,如下,可以看到确实没有HomeView.vue,但是有Home.vue
这里顺便说下快速找包小诀窍(用webstorm的情况下),在当前文件打开时点开左边 风扇样的按钮即可
这问题估计出现的比较少,至少百度很难查到(没试过google)。那我们可以建一个新项目,注意不选ts,然后当做老项目来试试。建新项目的步骤省略,建好后,在新项目根目录执行vue add typescript,发现运行畅通,然后我们查看新项目的@vue/cli-plugin-router 版本,是5.0.0以上,实际版本是5.0.6,打开module文件夹,确实是有HomeView.vue
现在我们知道了问题所在,那么只要仿造新创建的项目,将@vue/cli-plugin-router版本升级到5.0.0以上,也许就解决了。
直接将我们要改造的项目的package.json里的@vue/cli-plugin-router改成如下,
然后 npm i 安装依赖,然后再运行 vue add typescript。顺利安装成功。
注意,如果之前运行过一次 vue add typescript,再重新运行时,会提示是否继续,默认否,这里需要选 是(y)
安装成功后,我们可能发现报了一大堆eslint的错,无妨。只要根据报错逐一改掉即可。
由于上面在安装的时候,我们在Convert all .js files to .ts? (Y/n) 这行选择了yes,所以安装完后,我们发现项目里面所有的js文件都被改成了s文件,仅仅改了文件后缀,里面的内容都得我们自己去改造,但这可能不是我们想要的,因为老项目改造可能牵扯过多,不宜一下子全部改造。因此我们可以在Convert all .js files to .ts? (Y/n)这里选择no。
安装后,main.js会改变成main.ts,
另外,会在views文件夹里面多个HomeView.vue,在components文件夹里面多个HelloWord.vue,删掉即可。
对于老项目,一下子改不完,我们需要允许js和TS同时存在,需要在tsconfig.json配置允许引入和编译js。
"resolveJsonModule": true,
"allowJs": true,
加上两个配置后,你会发现在main.ts里面引入的js模块文件,之前报的类似错误
TS7016: Could not find a declaration file for module ‘./router’. ‘E:/lecent/lecent-web-pedestal/src/router/index.js’ implicitly has an ‘any’ type.
就消失了
如何声明js模块
shims-vue.d.ts
shims-vue.d.ts 是模块声明文件,如果我们依赖的第三方包js写的,那就需要在这里声明模块,否则会报错。
js模块的声明示例
// vue 的模块,默认就有
declare module "*.vue" {
import Vue from "vue"
export default Vue
}
// 其他的根据需要自己加
declare module 'vue-cropper'
declare module "vue-clipboard2"
declare module "lodash/debounce"
declare module "file-saver"
declare module "*.less"
declare module "ant-design-vue" {
const Ant: any
export default Ant
}
在 ts 里面,.d.ts 文件声明的模块是全局的,因此不需要使用 declare global,在.d.ts里面使用declare.global也不起作用,如果在其他模块文件里面声明变量,想声明成全局变量,就要使用declare global,否则声明的变量就具有模块作用域,其他模块无法使用
下面我们先让项目跑起来:运行 npm run serve
结果发现有如下报错:
Syntax Error: TypeError: loaderContext.getOptions is not a function
@ multi (webpack)-dev-server/client?http://192.168.1.8:7590&sockPath=/sockjs-node (webpack)/hot/dev-server.js ./src/main.ts
关于这错误,开始比较蒙蔽,估计也是版本问题,但是却不知道是哪个包的问题。百度了一下,查出来的都说了less less-loader问题,但其实他们的问题不一样,没有loaderContext。
然后尝试将未安装ts的版本跑起来,却没问题。这说明这个问题很可能是ts插件的问题
无奈继续百度,终于看到有篇文章 https://blog.csdn.net/weixin_41610178/article/details/123292109 有类似问题,确实就是@vue/cli-plugin-typescript 插件版本的问题。按照文章版本改成4.5.15后,问题解决。
import 结构引入
现在我们在main.ts里面写一行代码
import { message } from "ant-design-vue"
首先因为 vue2 版本的ant-design-vue是js模块
这时候会报错,TS2614: Module ‘“ant-design-vue”’ has no exported member ‘message’. Did you mean to use ‘import message from “ant-design-vue”’ instead?
也就是 ant-design-vue 里面找不到 message ,这时候我们只需要对模块声明进行改写,添加 export const message:any 如下
declare module "ant-design-vue" {
const Ant: any
export const message:any
export default Ant
}
vue自带挂载到vue 原型上的模块数据声明
这里强调vue自带的,因为不是所有的挂载到vue原型链上的都出自vue本身,例如ant的this.
m
e
s
s
a
g
e
,
就
不
是
v
u
e
本
身
的
。
这
里
主
要
是
t
h
i
s
.
message,就不是vue本身的。这里主要是this.
message,就不是vue本身的。这里主要是this.route,this.
r
o
u
t
e
r
,
t
h
i
s
.
router,this.
router,this.store等。另外,this.$refs不适用。
示例如下
import VueRouter, { Route } from "vue-router"
import Store from "store"
declare module 'vue/types/vue' {
interface Vue {
$route: Route
$router: VueRouter
$store: Store
}
}
需要单独建.d.ts文件的
有些模块需要单独建.d.ts模块(具体原因待研究),比如video.js
我们直接在 shims-vue.d.ts中如下声明 declare module “video.js”,会发现一直报找不到,没有生效。但是我们建一个新的文件,video.d.ts,写上declare module “video.js”,ok,这样没问题。
关于d.ts文件的作用。这里还是不了解其机制,需要进一步学习了解。有知道的大神麻烦告知一下,谢谢。
全局变量声明
比如windows对象使用。当我们尝试在代码中使用window.xxx时,会报类似错误:TS2339: Property ‘xxx’ does not exist on type ‘Window & typeof globalThis’.
window全局变量和ant-design-vue不同,我们不能用声明模块方式,但是可以用声明接口方式。那么在shims-vue.d.ts中,增加window的声明
interface Window {
xxx:string,
}
declare let window: Window
这时候不报错了,而且还有提示(这里编辑器用webstorm)。如果有多个,我们都加上就行,比如
interface Window {
xxx:string,
// eslint-disable-next-line camelcase
umi_plugin_ant_themeVar:any,
showMessage:any
}
如果我们想让所有用window.xxx的都不报错,那就改写成如下形式
interface AnyWindow {
[k: string]: any
}
declare let window: AnyWindow
但是这样就没有提示了
如果我们想让写在Window类型里面的有提示,其他不知道的也不报错
使用interface,写在一起就好
interface Window {
[k: string]: any,
xxx:any,
// eslint-disable-next-line camelcase
umi_plugin_ant_themeVar:any,
showMessage:any,
}
当然我们也可以使用 interface的声明合并
interface Window {
[k: string]: any,
xxx:any,
// eslint-disable-next-line camelcase
umi_plugin_ant_themeVar:any,
showMessage:any,
}
interface Window {
[k: string]: any,
}
declare let window: Window
值得一提的是这里不能使用type,type不具有继承性
vue文件中使用ts
vue2 使用ts有两种形式,一是使用vue.extend,一是使用vue-class-component。vue.extend写法和js写法比较接近,这里先不做讨论。重点讲下vue-class-component的使用。
装饰器使用
首先,使用vue-class-component ,依赖与vue-property-decorator 提供的装饰器。该包提供的装饰器有 @Prop 、@PropSync、@Model、@ModelSync、@Watch、@Provide、@Inject、@ProvideReactive、@InjectReactive、@Emit、@Ref、@VModel
这里讲下实践过的几个:@Prop 、@Watch、@Provide、@Inject、@Ref、@Component
统一的引用方式:
import { Prop, Component, Vue, Watch, Provide, Inject , Ref} from "vue-property-decorator"
@Component
这个基本时vue组件的必选,否则编译报错。就算我们什么操作都不做,也要使用
import { Component, Vue,} from "vue-property-decorator"
@Component
export default class App extends Vue {
}
如果我们这个页面有子组件和使用了vuex
import { Component, Vue,} from "vue-property-decorator"
import {mapState} from 'vuex'
import Child from "../components/home/child"
@Component({
components:{
Child
},
computed:{
...mapState(["userInfo"])
}
})
export default class App extends Vue {
}
也就是这里面其实可以实现部分和不使用ts相同的功能。
@Prop
父子组件传递数据必备,用于非ts时和props一样的功能
<template>
<div class="hello">
prop测试
</div>
</template>
<script lang="ts">
import { Component, Prop, Vue } from "vue-property-decorator"
@Component
export default class HelloWorld extends Vue {
@Prop() readonly height?:number|string // 高度
@Prop({ type: Boolean, default: true }) readonly isEdit?:boolean // 是否是编辑状态
@Prop({ type: Object, default: () => { return {} } }) readonly spanStyle?:object // 查看状态下样式
@Prop() readonly value:string | number | any[] | undefined// 值
@Prop() readonly mode?:string // 多选单选
@Prop({ default: "100%" }) readonly width?:string // 宽度
@Prop() readonly placeholder?:string // 占位符
@Prop() readonly disabled?:boolean // 是否可见
@Prop() readonly isUp?:boolean // 是否需要展示无上级
@Prop() readonly string?:boolean // 使用字符串
}
</script>
@Prop函数里面,可以传递和平时使用prop时候一样的数据教育,比如
@Prop({ type: Boolean, default: true }) readonly isEdit?:boolean // 是否是编辑状态
@Prop({ type: Object, default: () => { return {} } }) readonly spanStyle?:object // 查看状态下样式
至于 函数后面,就是ts相关的数据定义和类型判断了
另外如果是布尔值的,一定要在Prop函数里面写上{type:Boolean},否则接收到的默认值就是undefind
@Watch
使用watch监听需要watch监视器,示例如下
@Watch("height")
onHeightChange(v: string | number):void {
console.log("heightChange", v)
}
这里需要说明的是,@Watch装饰器对紧挨着自己后面的函数起作用,函数名可以任意例如:
@Watch("height")
abc(v: string | number):void {
console.log("heightChange", v)
}
如果紧挨着@watch装饰器下面的不是函数,或者是生命周期函数,就会报错,例如如下代码会报错:[Vue warn]: Error in callback for watcher “height”: “TypeError: handler.apply is not a function”
@Watch("height")
text = "ddd"
abc(v: string | number):void {
console.log("heightChange", v)
}
@Watch装饰器第二个参数是watch的相关配置,如下:
@Watch("height", { immediate: true, deep: true })
onHeightChange(v: string | number):void {
console.log("heightChange", v)
}
@Provide、@Inject
parent.vue
<template>
<child/>
</template>
<script lang='ts'>
import Child from "./child.vue"
import { Vue, Component, Prop, Provide } from "vue-property-decorator"
@Component({
components: {
Child
}
})
export default class Parent extends Vue {
@Provide() testProvide="testProvide"
@Provide() testProvide2="testProvide2"
}
</script>
child.vue
<template>
<div>
<p>{{ testProvide }}</p>
<p>{{ testProvide2 }}</p>
</div>
</template>
<script lang='ts'>
import { Vue, Component, Prop, Inject } from "vue-property-decorator"
@Component
export default class Child extends Vue {
@Inject() readonly testProvide!: string
@Inject() readonly testProvide2!: string
mounted() {
console.log("this.testProvide", this.testProvide)
}
}
</script>
@Ref
使用在vue2+ts里面,想实现this.
r
e
f
s
调
用
时
,
有
两
种
方
式
,
一
是
将
t
h
i
s
设
为
a
n
y
,
如
(
t
h
i
s
a
s
a
n
y
)
.
refs调用时,有两种方式,一是将this设为any,如(this as any).
refs调用时,有两种方式,一是将this设为any,如(thisasany).refs.footer.close(),第二种就是使用@Ref装饰器,写法为:
@Ref() readonly refName!: RefComponent
其中 RefComponent 代表组件类型,如果是自定义组件,可以直接使用组件,如果是原生标签,例如span,为HTMLSpanElement,div 为HTMLDivElement,其他类似。自定义组件时RefComponent也可以用一个对象,里面写上想调用的方法。另外如果ref名和我们声明的ref变量(refName)不一致,可以在Ref()函数中指明。现在假设我们有三个组件,parent.vue,HelloWord.vue,并且在HelloWord.vue中有两个方法,show和show2示例如下
<template>
<div>
<hello-world is-edit ref='helloWord'/>
<parent ref="parent"/>
<span ref="customRefKey"></span>
</div>
</template>
<script lang="ts">
import { Component, Vue, Ref } from "vue-property-decorator"
import HelloWorld from "@/components/HelloWorld.vue"
import Parent from "@/components/parent.vue"
@Component({
components: {
HelloWorld,
Parent
},
})
export default class App extends Vue {
@Ref() readonly parent!: Parent
@Ref("customRefKey") readonly spanEl!: HTMLSpanElement
@Ref() readonly helloWord!: { show: (arg0: any) => void }
mounted() {
console.log("this.parent", this.parent)
console.log("this.spanEl", this.spanEl)
console.log("this.helloWord", this.helloWord)
this.helloWord.show(110)
this.helloWord.show2(120)
}
}
</script>
调用后我们发现,this.helloWord.show2会报错,TS2551: Property ‘show2’ does not exist on type ‘{ show: (arg0: any) => void; }’. Did you mean ‘show’,这是因为我们声明ref时只单独指明show函数,如果改成如下,不指明函数,就不会报错
@Ref() readonly helloWord!: HelloWorld
另外,声明spanEl时,我们用了和变量spanEl不一样的ref值,customRefKey,如果我们将customRefKey改成customRefKey2,那将拿不到span节点。
@Ref("customRefKey2") readonly spanEl!: HTMLSpanElement
然后就是调用,我们可以直接通过如this.helloWord访问组件里面的数据方法,可以直接通过如this.spanEl拿到dom节点
data 的使用与替代
在vue2+ts里面,可以像使用js时声明data,html里面也可以直接使用,不同的是,我们在生命周期、方法里面,不能直接通过this读取数据,需要使用(this as any)来调用
data() {
return {
test: "xxx"
}
}
mounted() {
console.log((this as any).test)
}
}
data的替代写法
由于我们使用class类书写方式,data完全可以如下替代,而且可以很方便的用this访问
test:string = "xxx"
count:number = 0
list:Array<string> = []
mounted() {
console.log(this.test)
console.log(this.count)
console.log(this.list)
}
methods
不需要methods对象了,直接写方法
<template>
<div class="hello">
测试
</div>
</template>
<script lang="ts">
import { Component,, Vue, } from "vue-property-decorator"
@Component
export default class HelloWorld extends Vue {
mounted() {
this.$emit("init", "init")
}
/**
*显示1函数
**/
show(index:number) {
console.log(index)
}
/**
*显示2函数
**/
show2(index:number) {
console.log(index)
}
}
</script>
mixins 混入
使用class模式,混入可以用Mixins处理。
如下,我们新建mixins-test.ts
import { Vue, Component, Prop } from "vue-property-decorator"
@Component
export default class MixinsTest extends Vue {
@Prop({ default: "1000px" }) readonly mixinHeight?:number|string // 混入高度
mixinText = "mixinText" // 测试文字
mixinMethodValue = ""
/**
* 混入测试方法
*/
mixinMethod() {
console.log("mixinMethod执行")
this.mixinMethodValue = "mixinMethod执行 后 mixinMethodValue"
}
}
然后在 HelloWord.vue里面使用Mixins
<template>
<div style="padding:40px;">
<p>混入高度测试:{{mixinHeight}}</p>
<p>混入文字测试:{{mixinText}}</p>
<p>混入方法调用测试:{{mixinMethodValue}}</p>
</div>
</template>
<script lang="ts">
import { Component, Prop, Mixins } from "vue-property-decorator"
import MixinsTest from "@/mixins/mixins-test"
@Component
export default class HelloWorld extends Mixins(MixinsTest) {
mounted() {
this.mixinMethod()
}
}
</script>
运行结果
假设我们需要在组件里面覆盖mixins-test.ts的内容,将HelloWord.vue修改如下
<template>
<div style="padding:40px;">
<p>混入高度测试:{{mixinHeight}}</p>
<p>混入文字测试:{{mixinText}}</p>
<p>混入方法调用测试:{{mixinMethodValue}}</p>
</div>
</template>
<script lang="ts">
import { Component, Prop, Mixins } from "vue-property-decorator"
import MixinsTest from "@/mixins/mixins-test"
@Component
export default class HelloWorld extends Mixins(MixinsTest) {
@Prop({ default: 200 }) readonly mixinHeight?:number|string // 混入高度
mixinText = "helloWordText" // 测试文字
mixinMethodValue = ""
/**
* 混入测试方法
*/
mixinMethod() {
console.log("helloWordMethod执行")
this.mixinMethodValue = "mixinMethod执行 后 helloWordValue"
}
mounted() {
this.mixinMethod()
}
}
</script>
这时候发现HelloWord.vue 里 prop 的 mixinHeight报错
TS2612: Property ‘mixinHeight’ will overwrite the base property in 'MixinsTest & Vue & object & Record '. If this is intentional, add an initializer. Otherwise, add a ‘declare’ modifier or remove the redundant declaration.
报错为mixinHeight将被改写,需要使用declare声明或者删除。这里我们加上declare
@Prop({ default: 200 }) declare readonly mixinHeight?:number|string // 混入高度
运行结果
结果已经被改写
除了使用Mixin实现混入效果,我们也可以直接使用继承,例如
<template>
<div style="padding:40px;">
<p>混入高度测试:{{mixinHeight}}</p>
<p>混入文字测试:{{mixinText}}</p>
<p>混入方法调用测试:{{mixinMethodValue}}</p>
</div>
</template>
<script lang="ts">
import { Component, Prop, Mixins } from "vue-property-decorator"
import MixinsTest from "@/mixins/mixins-test"
@Component
export default class HelloWorld extends MixinsTest {
mounted() {
this.mixinMethod()
}
}
</script>
运行效果一致。当然,如果同时有多个混入,那还是用Mixins,使用为Mixins(MixinA,MixinB,…)
全局组件注册,name的坑
ts组件在生产环境下找不到name:全局注册多个组件时,我们为了方便,都会使用类似如下方式注册
for(let i=0;i<components.length;i++){
Vue.use(components[i].name)
}
如果没有使用ts class模式,这么写,完全没问题
使用ts class 模式,开发环境下,这么写,也没问题。但是当我们打包到生产环境后,就会发现一个问题,组件注册没成功。控制台报注解未注册错误。这时候如果点开页面html,我们会发现组件没有被编译成原生标签,页面上完整的显示了我们定义的组件标签名。这时候如果我们在注册组件时打印组件name,开发环境下是正常的的,生产环境下,name被编译成a、b、c类似的变量。
使用ts编写的组件,在生产环境下,找不到name,需要使用 component.option.name来注册。
封装组件库并发布到npm使用tsx的坑
在使用比如基于ant-design-vue封装组件时,若不使用ts,比如我们在组件中使用了a-button标签,那么只要项目中引用了a-button,组件就可以正常使用。
如果组件使用tsx,则必须在组件库中引入a-button,否则报找不到a-button组件
更多推荐
所有评论(0)