如何使用 TypeScript 写 Vue

发布于:5/22/2019, 10:36:16 PM @孙博
技术分享 | TypeScript,Vue
许可协议:署名-非商业性使用(by-nc)

近期有些小伙伴向我咨询了如何更好的用typescript去写Vue,我作为一个后端工程师表示很难通过口头说明白,于是准备了下面这个小案例。但我毕竟只是个后端工程师,有些不够“前端”的写法还请前端大佬多多理解。

以下例子的代码已托管在 Github:https://github.com/LuckyStarry/typescript-vue-sample

准备工作

npm init

首先我们使用上述命令将工作目录初始化,或手动在工作目录中创建package.json文件,类似于下述形式。

{
  "name": "typescript-vue-sample",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "repository": {
    "type": "git",
    "url": "git+https://github.com/LuckyStarry/typescript-vue-sample.git"
  },
  "author": "",
  "license": "ISC",
  "bugs": {
    "url": "https://github.com/LuckyStarry/typescript-vue-sample/issues"
  },
  "homepage": "https://github.com/LuckyStarry/typescript-vue-sample#readme"
}

安装 Visual Studio Code 的建议插件

  • Vetur
    • Vue 开发必备插件
  • Beautify
    • 代码格式化美化工具
  • Path Intellisense
    • 引用路径智能提示
  • Prettier - Code formatter
    • 代码格式化美化工具

安装必备组件

  • 代码运行依赖的必备组件
npm i vue vue-property-decorator typescript -S
  • 代码开发过程依赖的组件
npm i webpack webpack-merge webpack-dev-server webpack-encoding-plugin webpack-cleanup-plugin webpack-cli vue-loader vue-template-compiler html-webpack-plugin mini-css-extract-plugin uglifyjs-webpack-plugin optimize-css-assets-webpack-plugin babel-loader tslint-loader url-loader ts-loader css-loader less-loader less tslint tslint-config-standard @babel/core babel-preset-env -D

代码结构

├─.vscode
│   └─settings.json
├──build
│   ├─webpack.config.js
│   ├─webpack.debug.config.js
│   └─webpack.release.config.js
├──output
│   ├─debug
│   │  └─index.html
│   └─release
├──src
│   ├─views
│   │  ├─app
│   │  │  ├─app.less
│   │  │  ├─app.ts
│   │  │  ├─app.vue
│   │  │  └─index.ts
│   │  └─index.ejs
│   ├─main.ts
│   └─vendors.ts
├─.gitignore
├─.jsbeautifyrc
├─.prettierrc
├──package.json
├──tsconfig.json
└──tslint.json

定义配置文件

TypeScript 相关配置

  • 基础配置:tsconfig.json
{
  "include": ["src/**/*"],
  "exclude": ["node_modules"],
  "compilerOptions": {
    "allowSyntheticDefaultImports": true,
    "experimentalDecorators": true,
    "allowJs": false,
    "jsx": "preserve",
    "module": "esnext",
    "target": "es5",
    "moduleResolution": "node",
    "isolatedModules": false,
    "lib": ["dom", "es5", "es2015"],
    "sourceMap": true,
    "pretty": true,
    "baseUrl": "src"
  }
}
  • 代码检查:tslint.json
{
  "extends": "tslint-config-standard",
  "globals": {
    "require": true
  },
  "rules": {
    "class-name": false,
    "space-before-function-paren": false,
    "member-ordering": false,
    "no-return-await": false,
    "ter-func-call-spacing": false
  }
}

调试及编译配置文件

  • 基础配置文件 webpack.config.js
const path = require("path");
const EncodingPlugin = require("webpack-encoding-plugin");
const VueLoaderPlugin = require("vue-loader/lib/plugin");

module.exports = {
  entry: {
    main: "./src/main",
    vendors: "./src/vendors"
  },
  output: {
    // eslint-disable-next-line no-undef
    path: path.join(__dirname, "../output/dist")
  },
  module: {
    rules: [
      {
        enforce: "pre",
        test: /\.ts(x?)$/,
        exclude: /node_modules/,
        loader: "tslint-loader"
      },
      {
        test: /\.ts(x?)$/,
        exclude: /node_modules/,
        use: ["babel-loader", "ts-loader"]
      },
      {
        test: /\.(gif|jpg|png|woff|svg|eot|ttf)\??.*$/,
        loader: "url-loader?limit=8192"
      }
    ]
  },
  plugins: [new EncodingPlugin({ encoding: "utf-8" }), new VueLoaderPlugin()],
  resolve: {
    extensions: [".ts", ".js", ".vue"],
    alias: { views: path.resolve(__dirname, "../src/views") }
  }
};
  • 调试配置文件 webpack.debug.config.js
const path = require("path");
const webpack = require("webpack");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const WebpackMerge = require("webpack-merge");
const webpackConfig = require("./webpack.config");
module.exports = WebpackMerge(webpackConfig, {
  devtool: "#source-map",
  mode: "development",
  output: {
    // eslint-disable-next-line no-undef
    path: path.join(__dirname, "../output/debug/dist"),
    publicPath: "/dist/",
    filename: "[name].js",
    chunkFilename: "[name].chunk.js"
  },
  module: {
    rules: [
      { test: /\.vue$/, loader: "vue-loader" },
      { test: /\.css$/, use: [MiniCssExtractPlugin.loader, "css-loader"] },
      {
        test: /\.less$/,
        use: [MiniCssExtractPlugin.loader, "css-loader", "less-loader"]
      }
    ]
  },
  plugins: [
    new MiniCssExtractPlugin({ filename: "[name].css" }),
    new webpack.HotModuleReplacementPlugin()
  ],
  externals: { vue: "Vue" },
  devServer: {
    contentBase: "./output/debug/",
    compress: true,
    port: 30020,
    host: "0.0.0.0",
    disableHostCheck: true,
    hot: true,
    inline: true,
    historyApiFallback: true
  }
});
  • 生产配置文件 webpack.release.config.js
const path = require("path");
const package = require("../package.json");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const UglifyJsPlugin = require("uglifyjs-webpack-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin");
const WebpackCleanupPlugin = require("webpack-cleanup-plugin");
const WebpackMerge = require("webpack-merge");
const webpackConfig = require("./webpack.config");

module.exports = WebpackMerge(webpackConfig, {
  mode: "production",
  output: {
    // eslint-disable-next-line no-undef
    path: path.join(__dirname, "../output/release/dist"),
    publicPath: `/dist`,
    filename: "[name].[hash].js",
    chunkFilename: "[name].[hash].chunk.js"
  },
  module: {
    rules: [
      {
        test: /\.vue$/,
        loader: "vue-loader",
        options: { loaders: { ts: ["babel-loader", "ts-loader"] } }
      },
      { test: /\.css$/, use: [MiniCssExtractPlugin.loader, "css-loader"] },
      {
        test: /\.less$/,
        use: [MiniCssExtractPlugin.loader, "css-loader", "less-loader"]
      }
    ]
  },
  optimization: {
    splitChunks: {
      cacheGroups: {
        default: false,
        common: false,
        styles: {
          name: "main",
          test: /\.(css|less)$/,
          chunks: "all",
          minChunks: 1,
          enforce: true
        }
      }
    },
    minimizer: [
      new UglifyJsPlugin({
        cache: true,
        parallel: true,
        sourceMap: true
      }),
      new OptimizeCSSAssetsPlugin({})
    ]
  },
  plugins: [
    new WebpackCleanupPlugin(),
    new MiniCssExtractPlugin({ filename: "[name].[hash].css" }),
    new HtmlWebpackPlugin({
      // eslint-disable-next-line no-undef
      filename: path.join(__dirname, "../output/release/index.html"),
      template: "./src/views/index.ejs",
      inject: false
    })
  ],
  externals: { vue: "Vue" }
});
  • 修改 package.json 中的脚本节点
{
  "scripts": {
    "dev": "webpack-dev-server --mode development --config build/webpack.debug.config.js",
    "build": "webpack --progress --config build/webpack.release.config.js"
  }
}

Babel 配置

Babel 规则文件.babelrc

{
  "presets": [["env"]]
}

代码案例

除了将代码文件后缀由 .js 改为 .ts 之外,使用 TypeScript 代码稍微还有些要注意的地方。
首先就是为了能够让编译器正常的识别 .vue 文件,我们需要在 src 目录下新增一个 vue-shim.d.ts 文件,其内容为

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

小技巧

.vue 文件除了可以写 HTML 模板外,往往我们还会把 <style><script> 一起写入这个文件,虽然 Vue 模板编译器可以正常的运行代码,但是代码格式化功能往往无法正常运行。

所以大家可以尝试将一个 .vue 文件分解为 三个文件 —— 代码 .ts、样式 .less、模板 .vue。就像例子中的 App

/* app.less */
h1 {
  color: red;
}
// app.ts
import { Vue } from "vue-property-decorator";
export default class App extends Vue {}
<!-- app.vue -->
<style lang="less">
  @import url("./app.less");
</style>
<template>
  <h1>Hello World</h1>
</template>
<script type="ts" src="./app.ts" />