前言: 使用过 vue-cli 3.x 的同学应该明白目前 vue-cli 3.x 已经对 typescript 有了很好的支持, 可以很基于 vue-cli 3.x 很方便地创建 vue + typescript 项目,但是具体操作以及常见的开发痛点文档中并没有做过多介绍。其中还是有不少坑的,本文就是基于自身使用给大家做一个关于 vue + typescript 项目基础创建的分享。

基于 vue-cli 3.x 新建typescript 项目流程

  1. 使用 vue-cli 3.x 命令行创建新项目 vue create projectname
  2. 修改 tsconfig 和 tslint 配置项
  3. 设置 vscode 用户代码片段 snippet
  4. 改写 router
  5. 修改 webpack 配置
  6. 书写一个典型的 vue-typescript 组件
  7. 起步demo展示

一、 基于 vue-cli 创建项目

vue create projectName
复制代码

项目设置设置中 Check the features needed for your project 记得勾选 typescript,Use class-style component syntax 选择Yes,其余按照个人习惯进行选择即可。

Vue CLI v3.1.0
? Please pick a preset: Manually select features
? Check the features needed for your project: Babel, TS, Router, Vuex, CSS Pre-processors, Linter, Unit
? Use class-style component syntax? Yes
? Use Babel alongside TypeScript for auto-detected polyfills? Yes
? Use history mode for router? (Requires proper server setup for index fallback in production) Yes
? Pick a CSS pre-processor (PostCSS, Autoprefixer and CSS Modules are supported by default): Less
? Pick a linter / formatter config: TSLint
? Pick additional lint features: Lint on save
? Pick a unit testing solution: Jest
? Where do you prefer placing config for Babel, PostCSS, ESLint, etc.? In package.json
? Save this as a preset for future projects? (y/N)
复制代码

二、 修改 tsconfig 和 tslint 配置项

进入项目打开 tsconfig.json 修正 compilerOptions 下的配置项, 方便引入js模块和静态资源。

"compilerOptions": {
    ...
    "allowSyntheticDefaultImports": true, // 允许从没有设置默认导出的模块默认倒入
    "noImplicitAny": false, // 在表达式和声明上有隐含的any类型时报错
    ...
}
复制代码

打开 tslint 根据个人习惯修正相关配置,以下是个人推荐配置

{
    "defaultSeverity": "warning",
    "extends": [
        "tslint:recommended"
    ],
    "linterOptions": {
        "exclude": [
            "node_modules/**"
        ]
    },
    "no-trailing-whitespace": false,
    "rules": {
        "quotemark": false,
        "indent": [true, "spaces", 4],
        "interface-name": false,
        "ordered-imports": false,
        "object-literal-sort-keys": false,
        "no-console": false,
        "no-debugger": false,
        // 提升可维护性
        "no-unused-expression": [true, "allow-fast-null-checks"], // 禁止无用的表达式 但是允许写法 e && e.fn()
        "no-unused-variable": false, // 定义过的变量必须使用
        "triple-equals": true, // 必须使用 === 或 !==,禁止使用 == 或 !=,与 null 比较时除外
        "no-parameter-reassignment": true, // 禁止对函数的参数重新赋值
        "no-conditional-assignment": true, // 禁止在分支条件判断中有赋值操作
        "no-construct": true, // 禁止使用 new 来生成 String, Number 或 Boolean
        "no-duplicate-super": true, // 禁止 super 在一个构造函数中出现两次
        "no-duplicate-switch-case": true, // 禁止在 switch 语句中出现重复测试表达式的 case
        "no-object-literal-type-assertion": true, // 禁止对对象字面量进行类型断言(断言成 any 是允许的)
        "no-return-await": true, // 禁止没必要的 return await
        "no-sparse-arrays": true,  // 禁止在数组中出现连续的逗号,如 let foo = [,,]
        "no-string-throw": true, // 禁止 throw 字符串,必须 throw 一个 Error 对象
        "no-switch-case-fall-through": true, // switch 的 case 必须 return 或 break
        "prefer-object-spread": true, // 使用 { ...foo, bar: 1 } 代替 Object.assign({}, foo, { bar: 1 }) 前者的类型检查更完善
        "radix": true, // parseInt 必须传入第二个参数
        "cyclomatic-complexity": [
            true,
            20
        ], // 禁止函数的循环复杂度超过 20
        "deprecation": true,  // 禁止使用废弃(被标识了 @deprecated)的 API
        "use-isnan": true, // 必须使用 isNaN(foo) 而不是 foo === NaN
        "no-duplicate-imports": true,  // 禁止出现重复的 import
        "no-mergeable-namespace": true, // 禁止一个文件中出现多个相同的 namespace
        "encoding": true, // 文件类型必须时 utf-8
        "import-spacing": true, // import 语句中,关键字之间的间距必须是一个空格
        "interface-over-type-literal": true, // 接口可以 implement extend 和 merge
        "new-parens": true, // new 后面只必须有一个空格
        "no-angle-bracket-type-assertion": true, // 类型断言必须使用 as Type,禁止使用 <Type>, <Type> 容易被理解为 jsx
        "no-consecutive-blank-lines": [
            true,
            3
        ]  // 禁止连续超过三行空行
    }
}
复制代码

其中有几项配置需单独进行说明

"no-unused-expression": [true, "allow-fast-null-checks"],
复制代码

打开此项目需要像上面的配置文件中一样,添加 "allow-fast-null-checks",不然类似 e && e.fn() 语法会报错 unused-expression。

"no-console": false,
"no-debugger": false,
复制代码

"no-console" 和 "no-debugger" 在调试时可以设置 false

"no-trailing-whitespace": false,
复制代码

"no-trailing-whitespace" 建议修改为 false

三、 设置 vscode 用户代码片段 snippet

这里在 vscode 中配置 snippet 方便我们新建 .vue 文件,如果是其他编辑器可按照配置自行修正。cmd + shift + P 打开设置查找,输入 snippet,新建 vuets.code-snippet.json 中进行编辑:

{
    "Print to console": {
        "prefix": "vuets",
        "body": [
            "<template>",
            "",
            "</template>",
            "<script lang='ts'>",
            "import { Component, Vue, Prop } from 'vue-property-decorator';",
            "import  from '@/components/ .vue'",
            "@Component({",
            "    components: {",
            "        ",
            "    }",
            "})",
            "export default class componentName extends Vue {",
            "    @Prop(type)private propName = propValue;",
            "    private variableName: typeName = variableValue;",
            "    public methodName() {",
            "        ",
            "    }",
            "}",
            "</script>",
            "<style lang='less' scoped >",
            "",
            "</style>"
        ],
        "description": "basic vue typescript template"
    }
}
复制代码

配置完成后,在 .vue 文件中输入 vuets + tap, 即可生成单 vue 文件的大体目录

<template>

</template>
<script lang='ts'>
import { Component, Vue, Prop } from 'vue-property-decorator';
import  from '@/components/ .vue'
@Component({
    components: {
        
    }
})
export default class componentName extends Vue {
    @Prop(type)private propName = propValue;
    private variableName: typeName = variableValue;
    public methodName() {
        
    }
}
</script>
<style lang='less' scoped >

</style>
复制代码

四、 改写 router

用 import 的方式引入对应组件

Vue.use(Router);
const About = () => import(/* webpackChunkName: "about" */ './views/About.vue');
...
routes: [
        {
            path: '/',
            name: 'home',
            component: Home,
        },
        {
            path: '/about',
            name: 'about',
            component: About,
        },
    ],
复制代码

五、 修改 webpack 配置

可能很多同学已经发现 vue-cli 3 默认已经没有了 webpackconfig 之类的文件,如果我们需要进行 http 请求 或者修正其他配置项,可以在根目录下创建 vue.config.js ,书写对应配置。

module.exports = {
    baseUrl: '/',
    devServer: {
        proxy: {
            '/api': {
                target: 'http://api.com',
                changeOrigin: true,
            },
            '/test': {
                target: 'http://test.com',
                changeOrigin: true
            },
            '/res': {
                target: 'http://res.com',
                changeOrigin: true,
            },
        },
    },
}
复制代码

六、 书写一个典型的 vue-typescript 组件

基础的写法中,style 部分和通常并无二致。

template 部分,在父组件中书写引入的子组件时,改为单标签书写。

<template>
<FirstPage propMessage="从home传入的值"
    @greet-emit="greetEmit"/>
</template>
<script lang="ts">
import Vue from "vue";
import { Component, Prop, Watch, Emit } from "vue-property-decorator";
import FirstPage from "@/components/FirstPage.vue";  // 引入组件

@Component({
    FirstPage, // 引入组件
})
export default class Home extends Vue {
    
}
...
</script>
复制代码

script 标签中,我们需要写 lang = 'ts' 来告诉编辑器我们使用的是 ts 来书写组件部分。常规的props,和生命周期书写方式如下:

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

@Component
export default class FirstPage extends Vue {
    // props
    @Prop([String, Boolean]) private propMessage;
    @Prop(Number) private propA!: number;
    @Prop({ default: 'default value' }) private propB!: string;
    // 初始化 data
    private msg: number = 123;
    private helloMsg = "Hello, " + this.propMessage;
    private emitMsg: string  = "This is emitMsg";

    // watch
    @Watch('msg')
    public onChildChanged(val: number, oldVal: number) {
        if (val > 6) {
            console.log('msg is changed');
        }
    }

    // computed
    get computedMsg() {
        return "computed " + this.msg;
    }

    // 生命周期钩子
    public beforeCreate() {
        console.log('beforeCreate');
    }

    public created() {
        this.sayMsg();
    }

    public beforeMount() {
        console.log('beforeMount');
    }

    public mounted() {
        this.greet();
    }

    public beforeUpdate() {
        console.log('beforeUpdate');
    }

    public updated() {
        console.log('updated');
    }

    public beforeDestroy() {
        console.log('beforeDestroy');
    }

    public destroyed() {
        console.log('destroyed');
    }

    // methods
    public sayMsg() {
        console.log('created');
    }

    public showEmit() {
        this.greetEmit(this.emitMsg);
    }

    @Emit()
    public greetEmit(msg: string) {
        console.log('emit');
    }

    public greet() {
        console.log("mounted - greeting: " + this.msg);
    }
}
</script>
复制代码
需要特别注意的点:
  1. 想要使用 watch,props,emit 需要单独引用:
import { Component, Prop, Watch, Emit } from "vue-property-decorator";
复制代码
  1. props 和 data 需要设定 private 类型以及类型断言:
@Prop([String, Boolean]) private propMessage;
private msg: number = 123;
复制代码
  1. watch 的函数名并无实际意义,不冲突即可:
@Watch('msg')
    public onChildChanged(val: number, oldVal: number) { // 此处函数名随意
        if (val > 6) {
            console.log('msg is changed');
        }
    }
复制代码
  1. emit 传值处理, 需要传入的值在调用已声明的 emit 中传入:
public showEmit() {
    this.greetEmit(this.emitMsg);
}

@Emit()
public greetEmit(msg: string) {
    console.log('emit');
}
复制代码

六、 起步 demo