前言
本文讲解如何在 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