vue + typescript

1,137 阅读3分钟

vue + typescript

以下过程基于vue2.XX,如果使用vue-cli3以上版本,请忽略该过程,不需要配置。。。。直接翻到最后,了解在项目中使用上的出入

安排:

  • 安装vue-cli
  • 安装ts依赖
  • 配置 webpack
  • 添加 tsconfig.json
  • 添加 tslint.json
  • 让 ts 识别 .vue
  • 改造 .vue文件
先贴一段最后配置完毕的.vue代码 ,template 和 style 跟以前的写法保持一致,只有 script 的变化
<template>
  <div>
    <div class="card" ref="hello2">
      <div class="title">今天是:{{ date }}</div>
      <p class="red">父子组件传参---on和emit</p>
      <hello-world :msg="msg" @changeMsg="changeMsg" ref="hello"></hello-world>
    </div>
  </div>
</template>

<script lang='ts'>
import Vue from "vue";
import Component from "vue-class-component";
import moment from "moment";
import helloWorld from "../components/HelloWorld.vue";

@Component({
  name: "indexA",
  components: {
    helloWorld
  }
})
export default class extends Vue {
  // 初始化数据
  private date = moment().format("YYYY-MM-DD");
  msg = "你好么????";
  // 声明周期钩子
  mounted() {
    // alert('你好')
  }

  public changeMsg(val) {
    this.msg = "我不好!!!!" + val;
  }
}
</script>
<style></style>

1、Vue 引入 TypeScript

需要安装一些必要/以后需要的插件

npm i vue-class-component vue-property-decorator --save

npm i ts-loader typescript tslint tslint-loader tslint-config-standard --save-dev

说明:

  • ==vue-class-component==:强化 Vue 组件,使用 TypeScript/装饰器 增强 Vue 组件
  • ==vue-property-decorator==:在 vue-class-component 上增强更多的结合 Vue 特性的装饰器
  • ==ts-loader==:TypeScript 为 Webpack 提供了 ts-loader,其实就是为了让webpack识别 .ts .tsx文件
  • ==tslint-loader== 跟 ==tslint==:我想你也会在.ts .tsx文件 约束代码格式(作用等同于eslint)
  • ==tslint-config-standard==:tslint 配置 standard风格的约束

2、配置 webpack

webpack.base.conf.js 文件

  • entry.app 将main.js 改成 main.ts, 顺便把项目文件中的main.js也改成main.ts
entry: {
  app: './src/main.ts'
}
  • 找到resolve.extensions 里面加上.ts 后缀 (是为了之后引入.ts的时候不写后缀)
resolve: {
    extensions: ['.js', '.vue', '.json', '.ts'],
    alias: {
      '@': resolve('src')
    }
  }
  • 找到module.rules 添加webpack对.ts的解析
{
  test: /\.ts$/,
  exclude: /node_modules/,
  enforce: 'pre',
  loader: 'tslint-loader'
},
{
  test: /\.tsx?$/,
  loader: 'ts-loader',
  exclude: /node_modules/,
  options: {
    appendTsSuffixTo: [/\.vue$/],
  }
}

说明: ts-loader 会检索当前目录下的 tsconfig.json 文件,根据里面定义的规则来解析.ts文件(就跟.babelrc的作用一样)

3、添加 tsconfig.json

根路径下创建tsconfig.json文件 官方文档配置说明

{
  "include": [
    "src/**/*"
  ],
  "exclude": [
    "node_modules"
  ],
  "compilerOptions": {
    "allowSyntheticDefaultImports": true,
    "experimentalDecorators": true,
    "allowJs": true,
    "module": "esnext",
    "target": "es5",
    "moduleResolution": "node",
    "isolatedModules": true,
    "lib": [
      "dom",
      "es5",
      "es2015.promise"
    ],
    "sourceMap": true,
    "pretty": true
  }
}

4、添加 tslint.json

在根路径下创建tslint.json文件,就是 引入 ts 的 standard 规范 github--说明文件

{
  "extends": "tslint-config-standard",
  "globals": {
    "require": true
  }
}

5、让 ts 识别 .vue

由于 TypeScript 默认并不支持 *.vue 后缀的文件,所以在 vue 项目中引入的时候需要创建一个 vue-shim.d.ts 文件,放在项目项目对应使用目录下,例如 src/vue-shim.d.ts

declare module "*.vue" {
  import Vue from "vue";
  export default Vue;
}

意思是告诉 TypeScript *.vue 后缀的文件可以交给 vue 模块来处理。

而在代码中导入 *.vue 文件的时候,需要写上 .vue 后缀。原因还是因为 TypeScript 默认只识别 *.ts 文件,不识别 *.vue 文件:

import Component from 'components/component.vue'

6、改造 .vue 文件

在这之前先让我们了解一下所需要的插件(下面的内容需要掌握es7的装饰器, 就是下面使用的@符号

vue-class-component

vue-class-component 对 Vue 组件进行了一层封装,让 Vue 组件语法在结合了 TypeScript 语法之后更加扁平化:

<template>
  <div>
    <input v-model="msg">
    <p>msg: {{ msg }}</p>
    <p>computed msg: {{ computedMsg }}</p>
    <button @click="greet">Greet</button>
  </div>
</template>

<script lang="ts">
  import Vue from 'vue'
  import Component from 'vue-class-component'

  @Component
  export default class App extends Vue {
    // 初始化数据
    msg = 123

    // 声明周期钩子
    mounted () {
      this.greet()
    }

    // 计算属性
    get computedMsg () {
      return 'computed ' + this.msg
    }

    // 方法
    greet () {
      alert('greeting: ' + this.msg)
    }
  }
</script>

上面的代码跟下面的代码作用是一样的

export default {
  data () {
    return {
      msg: 123
    }
  }

  // 声明周期钩子
  mounted () {
    this.greet()
  }

  // 计算属性
  computed: {
    computedMsg () {
      return 'computed ' + this.msg
    }
  }

  // 方法
  methods: {
    greet () {
      alert('greeting: ' + this.msg)
    }
  }
}

vue-property-decorator

vue-property-decorator 是在 vue-class-component 上增强了更多的结合 Vue 特性的装饰器,新增了这 7 个装饰器:

  • @Emit
  • @Inject
  • @Model
  • @Prop
  • @Provide
  • @Watch
  • @Component (从 vue-class-component 继承)

在这里列举几个常用的@Prop/@Watch/@Component, 更多信息,详见官方文档

import { Component, Emit, Inject, Model, Prop, Provide, Vue, Watch } from 'vue-property-decorator'

@Component
export class MyComponent extends Vue {
  
  @Prop()
  propA: number = 1

  @Prop({ default: 'default value' })
  propB: string

  @Prop([String, Boolean])
  propC: string | boolean

  @Prop({ type: null })
  propD: any

  @Watch('child')
  onChildChanged(val: string, oldVal: string) { }
}

上面的代码相当于:

export default {
  props: {
    checked: Boolean,
    propA: Number,
    propB: {
      type: String,
      default: 'default value'
    },
    propC: [String, Boolean],
    propD: { type: null }
  }
  methods: {
    onChildChanged(val, oldVal) { }
  },
  watch: {
    'child': {
      handler: 'onChildChanged',
      immediate: false,
      deep: false
    }
  }
}

7、开始修改App.vue文件

  1. 在script 标签上加上 lang="ts", 意思是让webpack将这段代码识别为typescript 而非javascript
  2. 修改vue组件的构造方式( 跟react组件写法有点类似, 详见官方 ), 如下图
  3. 用vue-property-decorator语法改造之前代码

踩坑记

1、对webpack版本要求4.0以上

webpack@3.6.0升级至webpack@4.37.0
相关webpack插件都需要升级 webpack-dev-server webpack-cli

Failed to compile.

./src/main.ts
Module build failed: Error: You may be using an old version of webpack; please check you're using at least version 4
    at Object.initializeInstance (E:\myDemos\vue\vueandts\node_modules\ts-loader\dist\instances.js:175:19)
    at successLoader (E:\myDemos\vue\vueandts\node_modules\ts-loader\dist\index.js:27:17)
    at Object.loader (E:\myDemos\vue\vueandts\node_modules\ts-loader\dist\index.js:24:5)
 @ multi (webpack)-dev-server/client?http://localhost:7998 webpack/hot/dev-server ./src/main.ts

如果仍然报错,请手动修改package.json中相关webpack版本,然后删除node_modules,重新install

"webpack": "^4.37.0",
    "webpack-bundle-analyzer": "^3.7.0",
    "webpack-cli": "^3.3.11",
    "webpack-dev-server": "^3.10.3",
    "webpack-merge": "^4.2.2"

2、compilation.mainTemplate.applyPluginsWaterfall

building for production.../Users/xyz_persist/front_end/ts/vue-ts-init2/node_modules/html-webpack-plugin/lib/compiler.js:81
        var outputName = compilation.mainTemplate.applyPluginsWaterfall('asset-path', outputOptions.filename, {
                                                  ^

TypeError: compilation.mainTemplate.applyPluginsWaterfall is not a function
————————————————
  • 原因:html-webpack-plugin未升级版本导致
  • 解决:升级 html-webpack-plugin 版本
npm i html-webpack-plugin@3.2.0
npm i vue-loader@15.7.1

4、TypeError: Cannot read property ‘eslint’ of undefined

TypeError: Cannot read property 'eslint' of undefined
    at Object.module.exports (/Users/xyz_persist/front_end/ts/vue-ts-init2/node_modules/eslint-loader/index.js:148:18)


TypeError: Cannot read property 'vue' of undefined
    at Object.module.exports (/Users/xyz_persist/front_end/ts/vue-ts-init2/node_modules/vue-loader/lib/loader.js:61:18)
 @ ./src/main.js 4:0-24 13:21-24
  • 原因:eslint-loader、vue-loader版本问题
  • 解决:版本升级
npm i eslint-loader@2.2.1 -D
npm i vue-loader@15.7.1 -D

==npm run build==

5、Error:webpack.optimize.CommonsChunkPlugin has been removed, please use config.optimization.splitChunks instead

  • 原因:CommonsChunkPlugin已被webpack4废弃,推荐使用SplitChunkPlugin抽离公共模块
  • 解决:找到 /build/webpack.prod.conf.js ,去掉如下配置
new webpack.optimize.CommonsChunkPlugin({
  name: 'vendor',
  minChunks (module) {
    // any required modules inside node_modules are extracted to vendor
    return (
      module.resource &&
      /\.js$/.test(module.resource) &&
      module.resource.indexOf(
        path.join(__dirname, '../node_modules')
      ) === 0
    )
  }
}),
new webpack.optimize.CommonsChunkPlugin({
  name: 'manifest',
  minChunks: Infinity
}),
new webpack.optimize.CommonsChunkPlugin({
  name: 'app',
  async: 'vendor-async',
  children: true,
  minChunks: 3
})

更改配置如下,optimization与output同级

optimization: {
    splitChunks: {
      cacheGroups: {
        commons: {
          chunks: "all",
          minChunks: 2,
          maxInitialRequests: 5, // The default limit is too small to showcase the effect
          minSize: 0 // This is example is too small to create commons chunks
        },
        vendor: {
          test: /node_modules/,
          chunks: "all",
          name: "vendor",
          priority: 10,
          enforce: true
        }
      }
    }
}

6、Error: Path variable [contenthash] not implemented in this context: static/css/[name].[contenthash].css

  • 原因:webpack4.x中提取CSS文件应该使用mini-css-extract-plugin,废弃extract-text-webpack-plugin
  • 解决:找到 /build/webpack.prod.conf.js ,更改如下配置
const MiniCssExtractPlugin = require('mini-css-extract-plugin')

plugins: [
    // new ExtractTextPlugin({
    //   filename: utils.assetsPath('css/[name].[contenthash].css'),
    //   allChunks: true,
    // }),
    new MiniCssExtractPlugin({
      filename: 'css/app.[name].css',
      chunkFilename: 'css/app.[contenthash:12].css'  // use contenthash *
    })
]

再修改/build/utils.js文件

const MiniCssExtractPlugin = require('mini-css-extract-plugin')
if (options.extract) {
  // return ExtractTextPlugin.extract({
  //   use: loaders,
  //   fallback: 'vue-style-loader'
  // })
  return [MiniCssExtractPlugin.loader].concat(loaders)
} else {
  return ['vue-style-loader'].concat(loaders)
}

用法出入

1、生命周期

计算属性

<p>computed msg: {{ computedMsg }}</p>


get computedMsg() {
    return "computed " + this.msg;
}
beforeCreate() {
    console.log("index-----beforeCreate");
}
created() {
    console.log("index-----creatd");
}
beforeMount() {
    console.log("index-----beforeMount");
}
mounted() {
    console.log("index-----mounted");
}
beforeUpdate() {
    console.log("index-----beforeUpdate");
}
updated() {
    console.log("index-----updated");
}
beforeDestroy() {
    console.log("index-----beforeDestroy");
}
destroyed() {
    console.log("index-----destroyed");
}

2、路由钩子

需要在app.vue或者main.ts文件中声明路由钩子

import { Component } from 'vue-property-decorator'
Component.registerHooks([
  'beforeRouteEnter',
  'beforeRouteLeave',
  'beforeRouteUpdate'
])

然后在页面中使用

beforeRouteUpdate(to: any, from: any, next: () => void): void {
    console.log("beforeRouteUpdate111", to, from, this);
    this.go(to);
    next();
  }

  beforeRouteEnter(to: any, from: any, next: () => void): void {
    console.log("beforeRouteEnter111", to, from);
    next();
  }

  beforeRouteLeave(to: any, from: any, next: () => void): void {
    console.log("beforeRouteLeave111");
    next();
  }

或者 监听路由变化

// vue-shim.d.ts 文件
import VueRouter, {Route} from 'vue-router'
declare module 'vue/types/vue' {
	interface Vue {
        $router: VueRouter; // 这表示this下有这个东西
        $route: Route;
	}
}
// index.vue
@Watch('$route')
private onChildChanged(val, oldVal) {
   console.log(val, oldVal);
}

3、父子组件传参

跟正常一样使用

// 父组件
<template>
  <div>
    <div class="card">
      <p class="red">父子组件传参---on和emit</p>
      <hello-world :msg="msg" @changeMsg="changeMsg" ref="hello"></hello-world>
    </div>
  </div>
</template>

<script lang='ts'>
import Vue from "vue";
import Component from "vue-class-component";
import helloWorld from "../components/HelloWorld.vue";

@Component({
  name: "indexA",
  components: {
    helloWorld
  }
})
export default class extends Vue {}
</script>
// 子组件
<template>
  <div class="hello">
    <h1 @click="changeSay">{{ msg }}</h1>
  </div>
</template>

<script lang='ts'>
import Vue from "vue";
import { Component, Prop,Emit } from "vue-property-decorator";

@Component({
  name: "helloWorld"
})
export default class extends Vue {
  // 初始化数据
  @Prop() msg;
  num = 6;
  
  changeSay() {
    this.num += 1;
    this.$emit("changeMsg", this.num);
  }
  
  @Emit()
  addToCount('changeMsg') {
    
  }
}
</script>

4、插件引入

// main.ts
import share from './tool/share.ts'
import echarts from 'echarts'

Object.defineProperty(Vue.prototype, '$moment', {
    value: moment
})
Vue.use(share)
Vue.prototype.$echarts = echarts
// vue-shim.d.ts
declare module 'vue/types/vue' {
	interface Vue {
        share: any;
        $echarts: any;
	}
}

5、使用ref

<div class="card" ref="hello2"></div>


// 断言as  就是对this不做校验,表明我知道自己是谁,你不用管
mounted() {
    console.log("----11111", (this as any).$refs.hello2.offsetTop);
}