前言
本文讲解如何在 Vue 项目中使用 TypeScript 来搭建并开发项目,并在此过程中踩过的坑 。
TypeScript 具有类型系统,且是 JavaScript 的超集,TypeScript 在 2018年 势头迅猛,可谓遍地开花。
Vue3.0 将使用 TS 重写,重写后的 Vue3.0 将更好的支持 TS。2019 年 TypeScript 将会更加普及,能够熟练掌握 TS,并使用 TS 开发过项目,将更加成为前端开发者的优势。
所以笔者就当然也要学这个必备技能,就以 边学边实践 的方式,做个博客项目来玩玩。
此项目是基于 Vue 全家桶 + TypeScript + Element-UI 的技术栈,且已经开源,github 地址 blog-vue-typescript 。
因为之前写了篇纯 Vue 项目搭建的相关文章 基于vue+mint-ui的mobile-h5的项目说明 ,有不少人加我微信,要源码来学习,但是这个是我司的项目,不能提供原码。
所以做一个不是我司的项目,且又是 vue 相关的项目来练手并开源吧。
1. 效果
效果图:
pc 端
移动端
完整效果请看:https://biaochenxuying.cn
2. 功能
已经完成功能
[x] 登录
[x] 注册
[x] 文章列表
[x] 文章归档
[x] 标签
[x] 关于
[x] 点赞与评论
[x] 留言
[x] 历程
[x] 文章详情(支持代码语法高亮)
[x] 文章详情目录
[x] 移动端适配
[x] github 授权登录
待优化或者实现
[ ] 使用 vuex-class
[ ] 更多 TypeScript 的优化技巧
[ ] 服务器渲染 SSR
3. 前端主要技术
所有技术都是当前最新的。
vue: ^2.6.6
typescript : ^3.2.1
element-ui: 2.6.3
vue-router : ^3.0.1
webpack: 4.28.4
vuex: ^3.0.1
axios:0.18.0
redux: 4.0.0
highlight.js: 9.15.6
marked:0.6.1
4. 5 分钟上手 TypeScript
如果没有一点点基础,可能没学过 TypeScript 的读者会看不懂往下的内容,所以先学点基础。
TypeScript 的静态类型检查是个好东西,可以避免很多不必要的错误, 不用在调试或者项目上线的时候才发现问题 。
类型注解
TypeScript 里的类型注解是一种轻量级的为函数或变量添加约束的方式。变量定义时也要定义他的类型,比如常见的 :
// 布尔值
let isDone: boolean = false; // 相当于 js 的 let isDone = false;
// 变量定义之后不可以随便变更它的类型
isDone = true // 不报错
isDone = "我要变为字符串" // 报错
// 数字
let decLiteral: number = 6; // 相当于 js 的 let decLiteral = 6;
// 字符串
let name: string = "bob";  // 相当于 js 的 let name = "bob";
// 数组
 // 第一种,可以在元素类型后面接上 [],表示由此类型元素组成的一个数组:
let list: number[] = [1, 2, 3]; // 相当于 js 的let list = [1, 2, 3];
// 第二种方式是使用数组泛型,Array<元素类型>:
let list: Array
 = [1, 2, 3]; // 相当于 js 的let list = [1, 2, 3];
// 在 TypeScript 中,我们使用接口(Interfaces)来定义 对象 的类型。
interface Person {
    name: string;
    age: number;
}
let tom: Person = {
    name: 'Tom',
    age: 25
};
// 以上 对象 的代码相当于 
let tom = {
    name: 'Tom',
    age: 25
};
// Any 可以随便变更类型 (当这个值可能来自于动态的内容,比如来自用户输入或第三方代码库)
let notSure: any = 4;
notSure = "我可以随便变更类型" // 不报错
notSure = false;  // 不报错
// Void 当一个函数没有返回值时,你通常会见到其返回值类型是 void
function warnUser(): void {
    console.log("This is my warning message");
}
// 方法的参数也要定义类型,不知道就定义为 any
function fetch(url: string, id : number, params: any): void {
    console.log("fetch");
}
以上是最简单的一些知识点,更多知识请看 TypeScript 中文官网
5. 5 分钟上手 Vue +TypeScript
vue-class-component 
vue-class-component 对 Vue 组件进行了一层封装,让 Vue 组件语法在结合了 TypeScript 语法之后更加扁平化:
  
    
    prop: {{propMessage}}
    msg: {{msg}}
    helloMsg: {{helloMsg}}
    computed msg: {{computedMsg}}
    
   
上面的代码跟下面的代码作用是一样的:
  
    
    prop: {{propMessage}}
    msg: {{msg}}
    helloMsg: {{helloMsg}}
    computed msg: {{computedMsg}}
    
   
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
    }
  }
}
vuex-class
vuex-class :在 vue-class-component 写法中 绑定 vuex 。
import Vue from 'vue'
import Component from 'vue-class-component'
import {
  State,
  Getter,
  Action,
  Mutation,
  namespace
} from 'vuex-class'
const someModule = namespace('path/to/module')
@Component
export class MyComp extends Vue {
  @State('foo') stateFoo
  @State(state => state.bar) stateBar
  @Getter('foo') getterFoo
  @Action('foo') actionFoo
  @Mutation('foo') mutationFoo
  @someModule.Getter('foo') moduleGetterFoo
  // If the argument is omitted, use the property name
  // for each state/getter/action/mutation type
  @State foo
  @Getter bar
  @Action baz
  @Mutation qux
  created () {
    this.stateFoo // -> store.state.foo
    this.stateBar // -> store.state.bar
    this.getterFoo // -> store.getters.foo
    this.actionFoo({ value: true }) // -> store.dispatch('foo', { value: true })
    this.mutationFoo({ value: true }) // -> store.commit('foo', { value: true })
    this.moduleGetterFoo // -> store.getters['path/to/module/foo']
  }
}
6. 用 vue-cli 搭建 项目
笔者使用最新的 vue-cli 3 搭建项目,详细的教程,请看我之前写的 vue-cli3.x 新特性及踩坑记,里面已经有详细讲解 ,但文章里面的配置和此项目不同的是,我加入了 TypeScript ,其他的配置都是 vue-cli 本来配好的了。详情请看 vue-cli 官网 。
6.1 安装及构建项目目录
安装的依赖:
安装过程选择的一些配置:
搭建好之后,初始项目结构长这样:
├── public                          // 静态页面
├── src                             // 主目录
    ├── assets                      // 静态资源
    ├── components                  // 组件
    ├── views                       // 页面
    ├── App.vue                     // 页面主入口
    ├── main.ts                     // 脚本主入口
    ├── router.ts                   // 路由
    ├── shims-tsx.d.ts              // 相关 tsx 模块注入
    ├── shims-vue.d.ts              // Vue 模块注入
    └── store.ts                    // vuex 配置
├── tests                           // 测试用例
├── .eslintrc.js                    // eslint 相关配置
├── .gitignore                      // git 忽略文件配置
├── babel.config.js                 // babel 配置
├── postcss.config.js               // postcss 配置
├── package.json                    // 依赖
└── tsconfig.json                   // ts 配置
奔着 大型项目的结构 来改造项目结构,改造后 :
├── public                          // 静态页面
├── src                             // 主目录
    ├── assets                      // 静态资源
    ├── filters                     // 过滤
    ├── store                       // vuex 配置
    ├── less                        // 样式
    ├── utils                       // 工具方法(axios封装,全局方法等)
    ├── views                       // 页面
    ├── App.vue                     // 页面主入口
    ├── main.ts                     // 脚本主入口
    ├── router.ts                   // 路由
    ├── shime-global.d.ts           // 相关 全局或者插件 模块注入
    ├── shims-tsx.d.ts              // 相关 tsx 模块注入
    ├── shims-vue.d.ts              // Vue 模块注入, 使 TypeScript 支持 *.vue 后缀的文件
├── tests                           // 测试用例
├── .eslintrc.js                    // eslint 相关配置
├── postcss.config.js               // postcss 配置
├── .gitignore                      // git 忽略文件配置
├── babel.config.js                 // preset 记录
├── package.json                    // 依赖
├── README.md                       // 项目 readme
├── tsconfig.json                   // ts 配置
└── vue.config.js                   // webpack 配置
tsconfig.json 文件中指定了用来编译这个项目的根文件和编译选项。
本项目的 tsconfig.json 配置如下 :
{
    // 编译选项
  "compilerOptions": {
    // 编译输出目标 ES 版本
    "target": "esnext",
    // 采用的模块系统
    "module": "esnext",
    // 以严格模式解析
    "strict": true,
    "jsx": "preserve",
    // 从 tslib 导入外部帮助库: 比如__extends,__rest等
    "importHelpers": true,
    // 如何处理模块
    "moduleResolution": "node",
    // 启用装饰器
    "experimentalDecorators": true,
    "esModuleInterop": true,
    // 允许从没有设置默认导出的模块中默认导入
    "allowSyntheticDefaultImports": true,
    // 定义一个变量就必须给它一个初始值
    "strictPropertyInitialization" : false,
    // 允许编译javascript文件
    "allowJs": true,
    // 是否包含可以用于 debug 的 sourceMap
    "sourceMap": true,
    // 忽略 this 的类型检查, Raise error on this expressions with an implied any type.
    "noImplicitThis": false,
    // 解析非相对模块名的基准目录 
    "baseUrl": ".",
    // 给错误和消息设置样式,使用颜色和上下文。
    "pretty": true,
    // 设置引入的定义文件
    "types": ["webpack-env", "mocha", "chai"],
    // 指定特殊模块的路径
    "paths": {
      "@/*": ["src/*"]
    },
    // 编译过程中需要引入的库文件的列表
    "lib": ["esnext", "dom", "dom.iterable", "scripthost"]
  },
  // ts 管理的文件
  "include": [
    "src/**/*.ts",
    "src/**/*.tsx",
    "src/**/*.vue",
    "tests/**/*.ts",
    "tests/**/*.tsx"
  ],
  // ts 排除的文件
  "exclude": ["node_modules"]
}
更多配置请看官网的 tsconfig.json 的 编译选项
本项目的 vue.config.js:
const path = require("path");
const sourceMap = process.env.NODE_ENV === "development";
module.exports = {
  // 基本路径
  publicPath: "./",
  // 输出文件目录
  outputDir: "dist",
  // eslint-loader 是否在保存的时候检查
  lintOnSave: false,
  // webpack配置
  // see https://github.com/vuejs/vue-cli/blob/dev/docs/webpack.md
  chainWebpack: () => {},
  configureWebpack: config => {
    if (process.env.NODE_ENV === "production") {
      // 为生产环境修改配置...
      config.mode = "production";
    } else {
      // 为开发环境修改配置...
      config.mode = "development";
    }
    Object.assign(config, {
      // 开发生产共同配置
      resolve: {
        extensions: [".js", ".vue", ".json", ".ts", ".tsx"],
        alias: {
          vue$: "vue/dist/vue.js",
          "@": path.resolve(__dirname, "./src")
        }
      }
    });
  },
  // 生产环境是否生成 sourceMap 文件
  productionSourceMap: sourceMap,
  // css相关配置
  css: {
    // 是否使用css分离插件 ExtractTextPlugin
    extract: true,
    // 开启 CSS source maps?
    sourceMap: false,
    // css预设器配置项
    loaderOptions: {},
    // 启用 CSS modules for all css / pre-processor files.
    modules: false
  },
  // use thread-loader for babel & TS in production build
  // enabled by default if the machine has more than 1 cores
  parallel: require("os").cpus().length > 1,
  // PWA 插件相关配置
  // see https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-pwa
  pwa: {},
  // webpack-dev-server 相关配置
  devServer: {
    open: process.platform === "darwin",
    host: "localhost",
    port: 3001, //8080,
    https: false,
    hotOnly: false,
    proxy: {
      // 设置代理
      // proxy all requests starting with /api to jsonplaceholder
      "/api": {
        // target: "https://emm.cmccbigdata.com:8443/",
        target: "http://localhost:3000/",
        // target: "http://47.106.136.114/",
        changeOrigin: true,
        ws: true,
        pathRewrite: {
          "^/api": ""
        }
      }
    },
    before: app => {}
  },
  // 第三方插件配置
  pluginOptions: {
    // ...
  }
};
6.2 安装 element-ui
本来想搭配 iview-ui 来用的,但后续还想把这个项目搞成 ssr 的,而 vue + typescript + iview + Nuxt.js 的服务端渲染还有不少坑, 而 vue + typescript + element + Nuxt.js 对 ssr 的支持已经不错了,所以选择了 element-ui 。
安装:
npm i element-ui -S
按需引入, 借助 babel-plugin-component,我们可以只引入需要的组件,以达到减小项目体积的目的。
npm install babel-plugin-component -D
然后,将 babel.config.js 修改为:
module.exports = {
  presets: ["@vue/app"],
  plugins: [
    [
      "component",
      {
        libraryName: "element-ui",
        styleLibraryName: "theme-chalk"
      }
    ]
  ]
};
接下来,如果你只希望引入部分组件,比如 Button 和 Select,那么需要在 main.js 中写入以下内容:
import Vue from 'vue';
import { Button, Select } from 'element-ui';
import App from './App.vue';
Vue.component(Button.name, Button);
Vue.component(Select.name, Select);
/* 或写为
 * Vue.use(Button)
 * Vue.use(Select)
 */
new Vue({
  el: '#app',
  render: h => h(App)
});
6.3 完善项目目录与文件
route
使用路由懒加载功能。
export default new Router({
  mode: "history",
  routes: [
    {
      path: "/",
      name: "home",
      component: () => import(/* webpackChunkName: "home" */ "./views/home.vue")
    },
    {
      path: "/articles",
      name: "articles",
      // route level code-splitting
      // this generates a separate chunk (articles.[hash].js) for this route
      // which is lazy-loaded when the route is visited.
      component: () =>
        import(/* webpackChunkName: "articles" */ "./views/articles.vue")
    },
  ]
});
utils
utils/utils.ts 常用函数的封装, 比如 事件的节流(throttle)与防抖(debounce)方法:
// fn是我们需要包装的事件回调, delay是时间间隔的阈值
export function throttle(fn: Function, delay: number) {
  // last为上一次触发回调的时间, timer是定时器
  let last = 0,
    timer: any = null;
  // 将throttle处理结果当作函数返回
  return function() {
    // 保留调用时的this上下文
    let context = this;
    // 保留调用时传入的参数
    let args = arguments;
    // 记录本次触发回调的时间
    let now = +new Date();
    // 判断上次触发的时间和本次触发的时间差是否小于时间间隔的阈值
    if (now - last < delay) {
      // 如果时间间隔小于我们设定的时间间隔阈值,则为本次触发操作设立一个新的定时器
      clearTimeout(timer);
      ti