原有vue項目接入typescript

javascript因爲自身的弱類型,使用起來很是靈活。javascript

這也就爲大型項目、多人協做開發埋下了不少隱患。若是是本身的私有業務倒無所謂,主要是對外接口和公共方法,對接起來很是頭疼。主要表如今幾方面:css

  1. 參數類型沒有校驗,怎麼傳都有,有時會出現一些因爲類型轉換帶來的未知問題。
  2. 接口文檔不規範,每次都要經過讀代碼才能知道傳什麼,怎麼傳
  3. 接口編寫符合規範,可是公共庫中有大量的處理類型校驗的代碼

這就很是不利於工程標準化。因而咱們決定引入typescript進行代碼層面的強校驗。html

概覽

原有vue項目接入ts主要包含下面幾大步驟:vue

  1. 安裝typescript相關npm包
  2. 修改webpack和ts配置文件
  3. 項目公共庫和vue文件改造

ok,咱們開始

1. 安裝typescript相關npm包

這塊有個很是重要的點須要注意:java

就是要根據你本地的環境,去升級對應版本的typescriptnode

這塊是不少初次使用的同窗都會遇到的問題。webpack

由於只是看到了官網的教程,一步一步安裝完發現各類報錯。主要問題就是webpack版本不匹配,或者其餘一些npm包版本不匹配web

以我本地爲例:

我本地環境是webpack3,因此直接安裝最新版本的typescript,控制檯會報錯webpack版本太低的問題。算法

因此你要不把本身的webpack升級到webapck4.要不就採用與之相匹配的typescript版本。typescript

我選擇的是後者,由於直接給本身的項目升級到webapck4,會花費更長的時間。咱們用的腳手架是公司內部統一的。裏面集成了不少底層通用的基礎服務。冒然升級webpack4會帶來更大的麻煩,更況且項目時間比較緊迫,你懂得。

下面是我安裝的包和對應的版本:

  • "typescript": "^3.1.4" (這個是必須的,ts庫)
  • "ts-loader": "^3.5.0" (識別ts的laoder)
  • "tslint": "^5.11.0" (tslint校驗庫)
  • "tslint-loader": "^3.5.4" (tslint的loader)
  • "tslint-config-standard": "^8.0.1" (用於tslint默認校驗規則)
  • "vue-property-decorator": "^7.2.0" (用於在.vue文件中使用ts語法)

2. 修改配置文件

  • 修改webpack配置文件(加入ts的相關配)
base: {
  entry: {
    ...
    app: resolve('src/main.ts') // 把main.js改成main.ts
  }
...
resolve: {
  ...
  extensions: ['vue', '.js', '.ts']
}
module: {
  rules: [
    ...,
    {                           // 加入對文件的ts識別         
      test: /\.ts$/,
      exclude: /node_modules/,
      enforce: 'pre',
      loader: 'tslint-loader'
    }, {
      test: /\.tsx?$/,
      loader: 'ts-loader',
      exclude: /node_modules/,
      options: {
        appendTsSuffixTo: [/\.vue$/],
      }
    }
  ]
}
複製代碼

注意: main.js改爲main.ts後,還要作一些改造,

main.ts

import Vue from 'vue'
import { legic } from '@/lib/legic.ts'
...

// 給vue對象添加自定義方法
declare module 'vue/types/vue' {
  interface Vue {
    $legic: Function
  }
}

// 統計上報方法注入
Vue.prototype.$legic = legic
...
;(window as any).vm = new Vue({
  el: '#app',
  router,
  store,
  template: '<App/>',
  components: { App }
})

複製代碼

這裏有幾個注意的點

  1. 給Vue的prototpye注入新屬性和方法的話,直接用Vue.prototype.xxx = xxx ts校驗是不經過的。須要經過declare聲明一下
  2. 在main.ts中用window對象須要寫成(window as any)這種方式
  • 在根目錄下建立tslint.json(相似eslint,這裏設定一個校驗標準)
{
  "extends": "tslint-config-standard",
  "globals": {
    "require": true
  }
}
複製代碼
  • 在根目錄建立tsconfig.json(typescript配置文件)
{
  "compilerOptions": {
    // 編譯目標平臺
    "target": "es5",
    // 輸出目錄
    "outDir": "./dist/",
    // 添加須要的解析的語法,不然TS會檢測出錯。
    "lib": ["es2015", "es2016", "dom"],
    // 模塊的解析
    "moduleResolution": "node",
    // 指定生成哪一個模塊系統代碼
    "module": "esnext",
    // 在表達式和聲明上有隱含的any類型時報錯
    "noImplicitAny": false,
    // 把 ts 文件編譯成 js 文件的時候,同時生成對應的 map 文件
    "sourceMap": true,
    // 容許編譯javascript文件
    "allowJs": true,
    // 指定基礎目錄
    "baseUrl": "./",
    // 啓用裝飾器
    "experimentalDecorators": true,
    // 移除註釋
    "removeComments": true,
    "pretty": true,
    // 是相對於"baseUrl"進行解析
    "paths": {
      "vue": ["node_modules/vue/types"],
      "@/*": ["src/*"]
    }
  },
  "include": [
    "src/**/*"
  ],
  "exclude": [
    "node_modules"
  ]
}
複製代碼
  • 在src目錄下建立sfc.d.ts(用來聲明全局變量、class、module、function、命名空間)

咱們在這裏主要是讓ts識別.vue文件、window對象和一些module

具體declare的使用方式請看這裏

/**
 * 告訴 TypeScript *.vue 後綴的文件能夠交給 vue 模塊來處理
 * 而在代碼中導入 *.vue 文件的時候,須要寫上 .vue 後綴。
 * 緣由仍是由於 TypeScript 默認只識別 *.ts 文件,不識別 *.vue 文件
 */
declare module "*.vue" {
  import Vue from 'vue'
  export default Vue
}
/**
 * 告訴 TypeScript window是個全局對象,直接可用,這樣就不會在window.xx = 123時報錯
 */
declare var window: any
/**
 * 引入部分第三方庫/本身編寫的模塊的時候須要額外聲明文件
 * 引入的時候,須要使用相似 import VueLazyLaod from 'vue-lazyload' 的寫法
 */
declare module 'vue-lazyload'
declare module '@zz/perf/vue'
declare module 'raven-js'
declare module 'raven-js/plugins/vue'

複製代碼

將src/main.js改成main.ts

項目改造

這個部分是最麻煩的,主要有幾大塊

  • 基礎庫改造

    若是你的基礎庫引用了大量的npm包,那麼恭喜你,這部分你的改形成本會低不少。

    若是你的lib庫有至關一部分都是本身手寫的,那麼,我也得恭喜你。。。

    咱們本身的lib庫裏,有大量的本身維護的js文件。那麼若是你要進行ts改造的話,統統都要改。

    舉個例子: lib/url.js 中的getParam (算法並不高級,就是易讀、兼容性好)

export default class URL{
  /**
   * @memberOf URL
   * @summary 獲取當前頁面鏈接中指定參數
   * @type {function}
   * @param {string} param1                     - 若是param2爲undefined,param1是指從當前頁面url中獲取指定參數的key, 若是param2不爲空,param1爲指定的url
   * @param {string} param2                     - 可選參數,若是param2存在,則從指定的param1鏈接中獲取對應參數的key
   * @return {string|null}
   */
  static getParam (param1, param2) {
    let url = ''
    let param = null;
    // 若是隻有一個參數,默認從當前頁面連接獲取參數
    if (typeof param2 === 'undefined') {
      url = window && window.location.href || ''
      param = param1
    // 從指定url中獲取參數
    } else {
      url = param1
      param = param2
    }
    // 排除hash的影響
    url = url.split('#')[0]
    if (url.indexOf('?') > -1) {
      url = url.split('?')[1]
    }
    const reg = new RegExp('(^|&)' + param + '=([^&]*)[&#$]*', 'i')
    const rstArr = url.match(reg)
    if (rstArr !== null) {
      return decodeURIComponent(rstArr[2])
    }
    return null
  }
  ...
}
複製代碼

改造後的文件爲:lib/url.ts

export default class URL {
  /**
   * @memberOf URL
   * @summary 獲取url中指定參數
   * @type {function}
   * @param {string} param1                     - 若是param2爲undefined,param1是指從當前頁面url中獲取指定參數的key, 若是param2不爲空,param1爲指定的url
   * @param {string} param2                     - 可選參數,若是param2存在,則從指定的param1鏈接中獲取對應參數的key
   * @return {string|null}
   */
  static getParam (param1: string, param2?: string): string {
    let url: string = ''
    let param = null
    // 若是隻有一個參數,默認從當前頁面連接獲取參數
    if (typeof param2 === 'undefined') {
      url = window && window.location.href || ''
      param = param1
    // 從指定url中獲取參數
    } else {
      url = param1
      param = param2
    }
    url = url.split('#')[0]
    if (url.indexOf('?') > -1) {
      url = url.split('?')[1]
    }
    const reg = new RegExp('(^|&)' + param + '=([^&]*)[&#$]*', 'i')
    const rstArr = url.match(reg)
    if (rstArr !== null) {
      return decodeURIComponent(rstArr[2])
    }
    return null
  }
  ...
}
複製代碼

對於一個方法多種調用方式,若是你想徹底改爲typescript推薦的方式,你能夠用到方法重載

我沒有用是由於我不但願改變原有頁面的使用方式。

注:對於一個大型項目來說,咱們並不建議上來就對所有的文件進行ts改造。

咱們更建議採用漸進式改造方案,在不影響原有頁面的狀況下,逐一改造。具體方案後面會介紹

  • vue文件改造

src/components/helper/newUser/index.vue

<template>...</template>
<script>
import { LEGO_ATTR, initLegoData, legic } from '@/lib/legic'
import { getMyProfile } from '@/api/helper'
import { toast } from '@/lib/ZZSDK'
import myComponent from './myComponent.vue'
let flag = false // 是否發送視頻點擊埋點
export default {
  components: {
    // 自定義組件
    myComponent
  },
  data () {
    return {
      // 用戶頭像
      portrait: '',
      // 用戶名稱
      nickName: '',
      // 是否點擊播放
      isPlay: false
    }
  },
  mounted () {
    this.initData()
    initLegoData({
      type: 'newUserGuide'
    });
    legic(LEGO_ATTR.newUserGuide.SHOW);
  },
  methods: {
    initData () {
      getMyProfile().then(data => {
        console.log('data', data)
        const { respData } = data
        this.portrait = respData.portrait || ''
        this.nickName = respData.nickname || ''
      }).catch(err => {
        toast({ msg: err })
      })
    },
    goPageClick (type) {
      switch (type) {
        case 'SUN':
          legic(LEGO_ATTR.newUserGuide.SUNVILLAGECLICK)
          break
        case 'FOOTBALL':
          legic(LEGO_ATTR.newUserGuide.FOOTBALLCLICK)
          break
        case 'SIGN':
          legic(LEGO_ATTR.newUserGuide.SIGNCLICK)
          break
        default:
          return
      }
    },
    videoClick () {
      if (flag) {
        return
      } else {
        flag = true
        legic(LEGO_ATTR.newUserGuide.SIGNCLICK)
        this.isPlay = true
        this.$refs.video.play()
      }
    }
  }
}
</script>
<style lang="scss" scoped>...</style>
複製代碼

改造後

<template>...</template>
<script lang="ts">
import { LEGO_ATTR, initLegoData, legic } from '@/lib/legic'
import { getMyProfile } from '@/api/helper.ts'
import { toast } from '@/lib/ZZSDK'
import { Component, Vue } from 'vue-property-decorator'
import test from './test.vue'

let flag: boolean = false // 是否發送視頻點擊埋點
@Component({
  components: {
    test
  }
})
export default class NewUser extends Vue {
  // 用戶頭像
  portrait = ''
  // 用戶名稱
  nickName = ''
  // 是否點擊播放
  isPlay = false

  mounted (): void {
    this.initData()
    initLegoData({
      type: 'newUserGuide'
    });
    legic(LEGO_ATTR.newUserGuide.SHOW)
  }

  initData () {
    // 獲取profile信息
    getMyProfile().then((data: any) => {
      console.log('data', data)
      const { respData } = data
      this.portrait = respData.portrait || ''
      this.nickName = respData.nickname || ''
    }).catch((err: string) => {
      toast({ msg: err })
    })
  }

  goPageClick (type: string) {
    switch (type) {
      case 'SUN':
        legic(LEGO_ATTR.newUserGuide.SUNVILLAGECLICK)
        break
      case 'FOOTBALL':
        legic(LEGO_ATTR.newUserGuide.FOOTBALLCLICK)
        break
      case 'SIGN':
        legic(LEGO_ATTR.newUserGuide.SIGNCLICK)
        break
      default:
        return
    }
  }

  videoClick () {
    if (flag) {
      return
    } else {
      flag = true
      legic(LEGO_ATTR.newUserGuide.SIGNCLICK)
      this.isPlay = true
      this.$refs.video['play']()
    }
  }
}
</script>
<style lang="scss" scoped>...</style>
複製代碼

myComponent.vue改造前略,這裏只展現改造後的組件

<template>
  <div class="main">{{title}}{{name}}</div>
</template>
<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator'

@Component
export default class MyComponent extends Vue {

  @Prop({ type: String, default: '' })
  name: string

  title: string = '您好'
}
</script>
<style lang="scss" scoped>
  .main{
    display: none;
  }
</style>
複製代碼

這裏須要注意的是:

  • ts默認不會識別.vue文件,因此須要在sfc.d.ts文件中聲明,同時在引入vue組件時,要加.vue後綴
  • 引入vue-property-decorator插件。採用修飾符的方式進行組件註冊,這樣裏面的data、prop和function都經過扁平化方式調用(這也是官方推薦的方式)
  • ts中import引入文件,若是不寫後綴,默認是js文件。若是js文件沒有,則才識別ts文件
  • 關於router的運用,正常的push這種操做是ok的,可是history是識別不了的。如:this.router.history.current.query.xxx,ts就會報錯,須要改爲this.router['history'].current.query.xxx這種方式。(咱們目前這麼用的,有更好的方式你們能夠指正)

如今說下前面提到的改造方案:

這裏其實主要涉及.vue文件和lib庫的改造,vue文件沒啥可說的,一個個改就能夠了。主要說lib裏面的文件,這裏我建議:

  • 一開始保留原來的js文件,並不刪除。這樣目前還沒有改造的文件能夠繼續使用
  • 新建對應的ts文件,好比lib中有util.js,新建立util.ts
  • 新改造的vue文件統統引入lib庫中xx.ts(要加.ts後綴),如import Util from '@/lib/util.ts' 這樣能夠一點點改造整個項目,同時未改造的頁面照樣能夠運行。

ok以上就是咱們改造的所有過程。 有什麼問題能夠指正,你們互相學習

相關文章
相關標籤/搜索