├── .browserslistrc ├── .gitignore ├── README.md ├── babel.config.js ├── package-lock.json ├── package.json ├── postcss.config.js ├── public ├── favicon.ico ├── img │ └── icons │ │ ├── android-chrome-192x192.png │ │ ├── android-chrome-512x512.png │ │ ├── apple-touch-icon-120x120.png │ │ ├── apple-touch-icon-152x152.png │ │ ├── apple-touch-icon-180x180.png │ │ ├── apple-touch-icon-60x60.png │ │ ├── apple-touch-icon-76x76.png │ │ ├── apple-touch-icon.png │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── msapplication-icon-144x144.png │ │ ├── mstile-150x150.png │ │ └── safari-pinned-tab.svg ├── index.html ├── manifest.json └── robots.txt ├── src ├── App.vue ├── assets │ └── logo.png ├── components │ └── HelloWorld.vue ├── main.ts ├── registerServiceWorker.ts ├── router │ └── index.ts ├── store │ ├── home │ │ ├── index.ts │ │ └── interface.ts │ └── index.ts ├── types │ ├── ajax.d.ts │ ├── shims-qs.d.ts │ ├── shims-tsx.d.ts │ └── shims-vue.d.ts ├── utils │ ├── api.ts │ └── config.ts └── views │ ├── About.vue │ └── Home.vue ├── tsconfig.json ├── tslint.json ├── vue.config.js └── yarn.lock /.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | not ie <= 8 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | # local env files 6 | .env.local 7 | .env.*.local 8 | 9 | # Log files 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | 14 | # Editor directories and files 15 | .idea 16 | .vscode 17 | *.suo 18 | *.ntvs* 19 | *.njsproj 20 | *.sln 21 | *.sw* 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # typescript-vue 2 | 3 | 使用的命令说明,建议`yarn` 4 | 5 | ``` 6 | yarn install // Project setup 7 | yarn run serve // Compiles and hot-reloads for development 8 | yarn run build // Compiles and minifies for production 9 | yarn run lint // Lints and fixes files 10 | ``` 11 | 12 | vue-config的自定义属性说明 See [Configuration Reference](https://cli.vuejs.org/config/). 13 | 14 | 15 | 发现网上很多ts+vue的脚手架介绍的文章虽然多,但往往有所缺憾,经常让新人没法进行下去,经过总结,在我的`github`仓库中探索了一份脚手架,具体地址为:[typescript-vue](https://github.com/wlx200510/typescript-vue) 有个示例的组件和接口配置,也是参考各家大佬资料来完成的,下面就跟着笔者一起来探索脚手架的搭建过程和一些当下的最佳实践的代码写法。`go~go~go` 16 | 17 | 18 | ### 项目之初 19 | 20 | 在`vue-cli 3.0`上,已经有了对ts语言的全面支持,终于不用对着`webpack.config.js`根据`ts-loader`说明一堆魔改了,站在巨人的肩膀上才是王道。所以第一步就是在电脑上安装`vue-cli 3.0`,为搭建起`ts-vue`框架做好保障工作。 21 | 22 | 安装方法: 23 | ```shell 24 | npm install @vue/cli -g 25 | # or 26 | yarn global add @vue/cli 27 | 28 | vue --version 29 | ``` 30 | 31 | 安装没啥好说的,就是记得不要装成2.x版本了,具体看参考[这里](https://cli.vuejs.org/guide/installation.html),注意文档中对`node`版本的要求,如果不满足需要提前升级哦。 32 | 33 | 创建基本`ts-vue`框架(可以用GUI,不过个人习惯CLI): 34 | 35 | ```shell 36 | # 选择自己手动配置 37 | ? Please pick a preset: Manually select features 38 | # 选择本项目需要的预处理器和工具 39 | ? Check the features needed for your project: Babel, TS, PWA, Router, Vuex, CSS Pre-processors, Linter 40 | # 选择是否要用类继承方式的组件写作风格 41 | ? Use class-style component syntax? Yes 42 | # babel和ts一起用 43 | ? Use Babel alongside TypeScript for auto-detected polyfills? Yes 44 | # 选择css的预处理器,这里笔者选了Less 45 | ? Pick a CSS pre-processor (PostCSS, Autoprefixer and CSS Modules are supported by default): LESS 46 | # 选择代码格式检查器 选TSLint 47 | ? Pick a linter / formatter config: TSLint 48 | # 选择在commit代码时修复代码格式,保存时检查格式 49 | ? Pick additional lint features: Lint on save, lint and fix on commit 50 | # 选择把各个工具的配置列为单独文件放到项目目录下 方便维护 51 | ? Where do you profer placing config for Babel, PostCSS, ESLint, etc.? In dedicated config files 52 | # 不需要把内容保存为一个模板 53 | ? Save this as a preset for future projects? No 54 | # 选用安装这些包的工具,可选npm和yarn 55 | ? Pickthe package manager to use when installing dependecies: YARN 56 | ``` 57 | 58 | 然后就是等待安装了,经过漫长的时间之后,顺利的话就完成了初步的脚手架搭建。此时的项目目录如下: 59 | 60 | ```shell 61 | ├── public // 静态页面 62 | ├── src // 主目录 63 | ├── assets // 静态资源 64 | ├── components // 组件 65 | ├── views // 页面 66 | ├── App.vue // 页面主入口 67 | ├── main.ts // 脚本主入口 68 | ├── registerServiceWorker.ts // PWA 配置 69 | ├── router.ts // 路由 70 | ├── shims-tsx.d.ts // 相关 tsx 模块注入 71 | ├── shims-vue.d.ts // Vue 模块注入 72 | └── store.ts // vuex 配置 73 | ├── .postcssrc.js // postcss 配置 74 | ├── package.json // 依赖 75 | ├── tsconfig.json // ts 配置 76 | └── tslint.json // tslint 配置 77 | ``` 78 | 79 | ### 结构改造 80 | 81 | 为了更好地满足业务开发的要求,参考某位大佬的分享做了结构改造,完成后的结构如下所示: 82 | 83 | ```shell 84 | ├── public // 静态页面 85 | ├── scripts // 相关脚本配置 86 | ├── src // 主目录 87 | ├── assets // 静态资源 88 | ├── filters // 过滤 89 | ├── lib // 全局插件 90 | ├── router // 路由配置 91 | ├── store // vuex 配置 92 | ├── styles // 样式 93 | ├── types // 全局注入 94 | ├── utils // 工具方法(axios封装,全局方法等) 95 | ├── views // 页面 96 | ├── App.vue // 页面主入口 97 | ├── main.ts // 脚本主入口 98 | ├── registerServiceWorker.ts // PWA 配置 99 | ├── .editorconfig // 编辑相关配置 100 | ├── .npmrc // npm 源配置 101 | ├── .postcss.config.js // postcss 配置 102 | ├── babel.config.js // preset 记录 103 | ├── package.json // 依赖 104 | ├── README.md // 项目 readme 105 | ├── tsconfig.json // ts 配置 106 | ├── tslint.json // tslint 配置 107 | └── vue.config.js // webpack 配置 108 | ``` 109 | 上面的项目结构把各个模块拆分的比较细致,便于项目的迭代和维护,同时方便了分模块开发的需求,通过`views`文件夹维护各自的页面逻辑,公共的方法和路由配置都交给单独的文件夹模块来负责,相信有经验的小伙伴都懂的~ 110 | 111 | 在介绍具体改造之前,先介绍几个概念,主要是针对某些对ts不熟悉的小伙伴的: 112 | 113 | - 把项目所有的`.d.ts`文件放到了types文件夹中统一维护,那这种`.d.ts`文件是干啥的呢 114 | - 怎样让`ts`识别到`.vue`后缀的组件呢?用纯js来编写组件是不受这个限制的,但直接用单文件组件是需要配置点东西的 115 | 116 | 其实上面两个问题本质是同一个问题,就我自己的理解,`.d.ts`文件是用于告诉`typescript`什么文件可用ts来解析或者某些引入的模块的接口结构,为了方便大家理解,项目中有配置`ajax.d.ts`和`shims-qs.d.ts`两个文件作为示例。 117 | `shims-vue.d.ts`这个文件,主要用于`TypeScript`识别`.vue`文件,`Ts`默认并不支持导入`vue`文件,这个文件告诉`ts`导入`.vue`文件都按`VueConstructor`处理,因此导入`vue`文件必须写`.vue`后缀。这个文件是脚手架搭建好后自带的,所以不需要我们自己来写。 118 | `shims-tsx.d.ts`文件,这个文件主要是方便你使用在`ts`中使用`jsx`语法,前提是需要用`vue render function`从而在渲染函数中使用`jsx`来用上`t`s的静态类型提示。 119 | `ajax.d.ts`文件用于接口请求返回的`res`的接口格式,写在这个文件中就定义到了全局,可以直接通过`(res: Ajax.AjaxResponse)`这样来使用,可参照后面的`Home.vue`中请求数据的写法,相信会理解不少。 120 | 理论上在ts中通过`import`引入的模块都要指定其数据结构,否则ts-lint就会报错,好在大部分常用模块都有@types的编写,这个具体介绍放在下面,现在只需对`.d.ts`有个概念上的认识即可,让我们继续。 121 | 122 | 123 | 路由管理:在`router/index.ts`中配置了路由懒加载的配置,但这一块跟普通js相比没啥区别,具体可参考相关文件的写法,不做重点介绍 124 | 125 | vuex的模块化管理:这个牵扯到`vuex`在`typescript`的加成下,如何更好地应用于项目,因此要展开说一波。 126 | store的示例结构如下所示: 127 | ```shell 128 | ├── home // 主目录 129 | ├── index.ts // vuex state getters mutations action 管理 130 | ├── interface.ts // 接口管理 131 | └── index.ts // vuex 主入口 132 | ``` 133 | `store`下面一个文件夹对应一个模块,每一个模块都有一个`interface`进行接口管理,这也是规范化模块的样板。需要注意的一点是:之前模块化是不需要`interface.ts`这个文件的,这个新增接口声明文件的作用是指定当前模块中的`State`接口类型,从而明确该模块的`State`中有哪些变量,可以从这个文件里面揣摩下ts中类型接口定义的写法和组织技巧。 134 | ```ts 135 | export interface HomeContent { 136 | name: string, 137 | m1?: boolean 138 | } 139 | export interface State { 140 | count: number, 141 | message: string, 142 | test1?: HomeContent[] 143 | // test1?: Array 144 | } 145 | ``` 146 | 然后可以在`home/index.ts`下写相应的模块内容: 147 | ```js 148 | import request from '@/utils/api' 149 | import { State } from './interface' 150 | import { Commit } from 'vuex' 151 | 152 | // 请求的data需要定义一个接口来约束 153 | interface GetTodayWeatherParam { 154 | city: string 155 | } 156 | 157 | const state: State = { 158 | count: 0, 159 | message: '', 160 | test1: [], 161 | } 162 | 163 | const getters = { 164 | count: (state: State) => state.count, 165 | message: (state: State) => state.message, 166 | } 167 | 168 | const mutations = { 169 | INCREMENT(state: State, num: number) { 170 | state.count += num 171 | }, 172 | DECREMENT(state: State, num: number) { 173 | state.count -= num 174 | }, 175 | MESSAGE(state: State, payload: any) { 176 | state.message = payload.message 177 | }, 178 | } 179 | 180 | const actions = { 181 | // request.get可以理解为返回了一个promise,具体封装可参考仓库中的api.ts文件 182 | getTodayWeather(context: { commit: Commit }, params: GetTodayWeatherParam) { 183 | return request.get('/weather_mini', { params }) 184 | }, 185 | } 186 | 187 | export default { 188 | namespaced: true, 189 | state, 190 | getters, 191 | mutations, 192 | actions, 193 | } 194 | ``` 195 | 以上模块比较完整的表示了在`typeScript`中`state|getters|mutations|actions`的写法,抛砖引玉,请多指教。 196 | 接下来我会用`home.vue`这个文件来完整介绍一下使用`typescript`如何优雅地写`Vue`组件,这也是最核心的内容。 197 | 198 | ### Vue组件的TS写法探索 199 | 200 | > 经过各种资料翻阅,得知需要使用如下两个插件`vue-property-decorator`和`vuex-class`来辅助编写Vue组件 201 | 202 | 先介绍对外暴露Vue组件的写法,通常一种是`Vue.extend()`,另一种是`@Components` + `class XXX extends Vue`,关于这两者的比较,推荐[文章](https://juejin.im/post/5bd698c7f265da0ae8015f12),虽然函数式组件只能用前者,但论优雅程度和方便程度,我还是推崇后者,具体问题具体分析,这两种组件又不是不能混用,具体看各人喜好,下面的讲解都是基于`vue-property-decorator`来介绍的。 203 | 204 | #### vue-property-decorator的用法 205 | [vue-property-decorator](https://github.com/kaorun343/vue-property-decorator) 完全依赖于官方库 [vue-class-component](https://github.com/vuejs/vue-class-component),不用担心维护问题,可以放心大胆使用。其中包含了 8 个装饰符解决写的问题。 206 | - `@Emit` 指定事件 emit,可以使用此修饰符,也可以直接使用 `this.$emit()` 207 | - `@Inject` 指定依赖注入 208 | - `@Mixins` mixin 注入 209 | - `@Model` 指定 model(少) 210 | - `@Prop` 指定 Prop 211 | - `@Provide` 指定 Provide(少) 212 | - `@Watch` 指定 Watch 213 | - `@Component` 组件修饰符 214 | 215 | 上一个带`mixin`和外部组件的用法的例子: 216 | `fooMixin.ts`: 217 | ```typescript 218 | import { Vue, Component } from 'vue-property-decorator' 219 | @component 220 | export default class FooMixin extends Vue { 221 | mixinValue = 'Hello' 222 | } 223 | ``` 224 | 组件的写法例子: 225 | ```typescript 226 | import { Vue, Component, mixins, Prop, Watch } from 'vue-property-decorator' 227 | import FooMixin from './fooMixin' 228 | import Hello from './Hello.vue' 229 | 230 | interface Person { 231 | name: string, 232 | age: number 233 | } 234 | 235 | @Component({ 236 | componsnts: { 237 | Hello 238 | } 239 | }) 240 | export class MyComp extends mixins(FooMixin) { // 可传入多个mixin 241 | @Prop({ default: 0 }) 242 | propA: number 243 | 244 | @Watch('foo') 245 | onChildChanged (val: string, oldVal: string) {} 246 | @Watch('bar', { immediate: true, deep: true }) 247 | onPersonChanged (val: Person, oldVal: Person) {} 248 | 249 | created () { 250 | console.log(this.mixinValue) // -> Hello 251 | }, 252 | // dynamic component this.$refs.helloComponent.sayHello() 253 | $refs!: { 254 | helloComponent: Hello 255 | } 256 | } 257 | ``` 258 | #### vuex-class的用法 259 | 260 | 提供了 4 个修饰符和方便模块索引的`namespace`,解决`Vuex`的使用问题 261 | - @State 262 | - @Getter 263 | - @Mutation 264 | - @Action 265 | - namespace 266 | 267 | 官方仓库的使用例子如下,可观摩学习: 268 | ```typescript 269 | import Vue from 'vue' 270 | import Component from 'vue-class-component' 271 | import { 272 | State, 273 | Getter, 274 | Action, 275 | Mutation, 276 | namespace 277 | } from 'vuex-class' 278 | 279 | const someModule = namespace('path/to/module') 280 | 281 | @Component 282 | export class MyComp extends Vue { 283 | @State('foo') stateFoo 284 | @State(state => state.bar) stateBar 285 | @Getter('foo') getterFoo 286 | @Action('foo') actionFoo 287 | @Mutation('foo') mutationFoo 288 | @someModule.Getter('foo') moduleGetterFoo 289 | 290 | // If the argument is omitted, use the property name 291 | // for each state/getter/action/mutation type 292 | @State foo 293 | @Getter bar 294 | @Action baz 295 | @Mutation qux 296 | 297 | created () { 298 | this.stateFoo // -> store.state.foo 299 | this.stateBar // -> store.state.bar 300 | this.getterFoo // -> store.getters.foo 301 | this.actionFoo({ value: true }) // -> store.dispatch('foo', { value: true }) 302 | this.mutationFoo({ value: true }) // -> store.commit('foo', { value: true }) 303 | this.moduleGetterFoo // -> store.getters['path/to/module/foo'] 304 | } 305 | } 306 | ``` 307 | 在上整体的例子之前有几个注意点要提前说明: 308 | 309 | - 使用了ts后,接口层面很多数据结构都可以预先定义出来,能不用any就不用,方便排错 310 | - 初次使用,可能会报一堆`lint`的错误,可以好好研究下`ts-config`和`ts-lint`,通过配置解决细节问题 311 | - `$refs`的报错问题,需要用到类型断言,来使用其上的dom方法,这个下面会提到,不要急 312 | - 定义`props`的变量类型时,要用感叹号表示非空断言,否则报错让人抓狂。 313 | 314 | `home.vue`这个组件的具体写法如下所示: 315 | ```typescript 316 | import { Component, Prop, Watch, Vue } from 'vue-property-decorator' 317 | import { State, Getter, Mutation, Action, namespace } from 'vuex-class' 318 | 319 | const homeModule = namespace('home') 320 | interface WeatherContent { 321 | low: string, 322 | high: string, 323 | type: string 324 | } 325 | 326 | @Component 327 | export default class Home extends Vue { 328 | // 原data中的属性可以直接写 329 | public city: string = '邯郸' 330 | public content: WeatherContent = { 331 | low: '', 332 | high: '', 333 | type: '', 334 | } 335 | 336 | @Prop({ default: 0 }) 337 | public propA!: number 338 | 339 | @homeModule.State('message') public message!: string 340 | @homeModule.Getter('count') public count!: number 341 | @homeModule.Mutation('INCREMENT') public INCREMENT!: (num: number) => void 342 | @homeModule.Mutation('DECREMENT') public DECREMENT!: (num: number) => void 343 | @homeModule.Mutation('MESSAGE') public MESSAGE!: (data: {message: string}) => void 344 | @homeModule.Action('getTodayWeather') public getTodayWeather!: (payload: {city: string}) => Promise 345 | 346 | @Watch('content', { immediate: true, deep: true }) 347 | public onPersonChanged(val: WeatherContent): void { 348 | console.log(val) 349 | this.INCREMENT(1) 350 | } 351 | // 代替computed的写法 352 | get lowTemperature(): string { 353 | return this.content.low 354 | } 355 | 356 | public created() { 357 | console.log(this.message) // -> store.home.state.message 358 | console.log(this.count) // -> store.home.getters.count 359 | this.INCREMENT(2) // -> store.commit('home/INCREMENT', 2) 360 | this.getCityWeather(this.city) 361 | } 362 | 363 | // 方法可以直接定义,不需要用methods包裹起来 364 | public getCityWeather(city: string): void { 365 | this.getTodayWeather({ city }).then((res: Ajax.AjaxResponse) => { // -> store.dispatch('home/foo', { city: city }) 366 | const { low, high, type } = res.data.forecast[0] 367 | this.MESSAGE({ message: type }) 368 | this.content = { low, high, type } 369 | }) 370 | } 371 | } 372 | ``` 373 | 374 | #### 解决$refs的报错 375 | 376 | $refs 报错这个问题相信基本都遇到,除非你真没用到这个,如图: 377 | 378 | ![](build-ts-vue/refs.png) 379 | 380 | - 第一种解决方案: 381 | 把这个变量改成定义成HTMLInputElement就好,这里需要用到类型断言 382 | ```javascript 383 | test() { 384 | let inputBox: HTMLInputElement = this.$refs.inputBox as HTMLInputElement 385 | inputBox.blur() 386 | } 387 | ``` 388 | - 第二种解决方案(刚才上面代码示例有提到): `$refs!: {}` 389 | 390 | ![](build-ts-vue/refs2.png) 391 | 392 | 这样编辑器还会提示组件里面有什么方法,当你打出`this.$refs.header.`时候,编辑器有提示`Header`这个组件里面的属性和方法 393 | 394 | ### 建议的组件写作顺序 395 | 396 | > 参考工作组内的最佳实践,给出建议如下 397 | 398 | 组件引用,mixins,filters 等放在`@Component`里面,放在首行,装饰器一定要放在顶部。 399 | `class`内部的顺序: 400 | 401 | - data 402 | 403 | - @Prop 404 | 405 | - @State 406 | 407 | - @Getter 408 | 409 | - @Action 410 | 411 | - @Mutation 412 | 413 | - @Watch 414 | 415 | - 生命周期钩子 416 | 417 | - beforeCreate(按照生命周期钩子从上到下) 418 | 419 | - created 420 | 421 | - beforeMount 422 | 423 | - mounted 424 | 425 | - beforeUpdate 426 | 427 | - updated 428 | 429 | - activated 430 | 431 | - deactivated 432 | 433 | - beforeDestroy 434 | 435 | - destroyed 436 | 437 | - errorCaptured(最后一个生命周期钩子,这个用的较少) 438 | 439 | - 路由钩子 440 | 441 | - beforeRouteEnter 442 | 443 | - beforeRouteUpdate 444 | 445 | - beforeRouteLeave 446 | 447 | - computed 448 | 449 | - methods 450 | 451 | ### 总结 452 | 一开始学习了微软在VUE项目中引入TS的过程,参考了[TypeScript Vue Starter](https://github.com/Microsoft/TypeScript-Vue-Starter#typescript-vue-starter),建立了初步概念,在学习了相关TypeScript理念和一些常用实践后探究了大型Vue项目引入TypeScript的姿势。希望能给大家一些技巧和想法,最后再推荐一下按照本教程建立好的[仓库](https://github.com/wlx200510/typescript-vue),欢迎star&fork。我的微信号是`kashao3824`,如果有前端问题想要探讨的,欢迎加好友哦~ 453 | 454 | Reference: 455 | - [合格前端第十二弹-TypeScript + 大型项目实战](https://zhuanlan.zhihu.com/p/40322215) 456 | - [Vue with TypeScript](https://juejin.im/post/5bd698c7f265da0ae8015f12) 457 | - [TypeScript+Vue实例完全教程](https://zhuanlan.zhihu.com/p/32122243) -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/app' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "typescript-vue", 3 | "version": "0.1.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@types/node": { 8 | "version": "10.12.10", 9 | "resolved": "https://registry.npmjs.org/@types/node/-/node-10.12.10.tgz", 10 | "integrity": "sha512-8xZEYckCbUVgK8Eg7lf5Iy4COKJ5uXlnIOnePN0WUwSQggy9tolM+tDJf7wMOnT/JT/W9xDYIaYggt3mRV2O5w==", 11 | "dev": true 12 | }, 13 | "axios": { 14 | "version": "0.18.0", 15 | "resolved": "http://registry.npmjs.org/axios/-/axios-0.18.0.tgz", 16 | "integrity": "sha1-MtU+SFHv3AoRmTts0AB4nXDAUQI=", 17 | "requires": { 18 | "follow-redirects": "1.5.10", 19 | "is-buffer": "1.1.6" 20 | } 21 | }, 22 | "debug": { 23 | "version": "3.1.0", 24 | "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", 25 | "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", 26 | "requires": { 27 | "ms": "2.0.0" 28 | } 29 | }, 30 | "follow-redirects": { 31 | "version": "1.5.10", 32 | "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz", 33 | "integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==", 34 | "requires": { 35 | "debug": "3.1.0" 36 | } 37 | }, 38 | "is-buffer": { 39 | "version": "1.1.6", 40 | "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", 41 | "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" 42 | }, 43 | "ms": { 44 | "version": "2.0.0", 45 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 46 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "typescript-vue", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build", 8 | "lint": "vue-cli-service lint" 9 | }, 10 | "dependencies": { 11 | "axios": "^0.18.0", 12 | "qs": "^6.6.0", 13 | "register-service-worker": "^1.0.0", 14 | "vue": "^2.5.17", 15 | "vue-router": "^3.0.1", 16 | "vuex": "^3.0.1" 17 | }, 18 | "devDependencies": { 19 | "@types/node": "^10.12.10", 20 | "@types/webpack-env": "^1.13.6", 21 | "@vue/cli-plugin-babel": "^3.1.0", 22 | "@vue/cli-plugin-pwa": "^3.1.0", 23 | "@vue/cli-plugin-typescript": "^3.1.0", 24 | "@vue/cli-service": "^3.1.0", 25 | "less": "^3.0.4", 26 | "less-loader": "^4.1.0", 27 | "lint-staged": "^7.2.2", 28 | "typescript": "^3.0.0", 29 | "vue-template-compiler": "^2.5.17", 30 | "vuex-class": "^0.3.1", 31 | "vue-class-component": "^6.0.0", 32 | "vue-property-decorator": "^7.0.0" 33 | }, 34 | "gitHooks": { 35 | "pre-commit": "lint-staged" 36 | }, 37 | "lint-staged": { 38 | "*.ts": [ 39 | "vue-cli-service lint", 40 | "git add" 41 | ], 42 | "*.vue": [ 43 | "vue-cli-service lint", 44 | "git add" 45 | ] 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | autoprefixer: {} 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wlx200510/typescript-vue/7d9c74d20745bc561ee0a46ea0973f8876418e83/public/favicon.ico -------------------------------------------------------------------------------- /public/img/icons/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wlx200510/typescript-vue/7d9c74d20745bc561ee0a46ea0973f8876418e83/public/img/icons/android-chrome-192x192.png -------------------------------------------------------------------------------- /public/img/icons/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wlx200510/typescript-vue/7d9c74d20745bc561ee0a46ea0973f8876418e83/public/img/icons/android-chrome-512x512.png -------------------------------------------------------------------------------- /public/img/icons/apple-touch-icon-120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wlx200510/typescript-vue/7d9c74d20745bc561ee0a46ea0973f8876418e83/public/img/icons/apple-touch-icon-120x120.png -------------------------------------------------------------------------------- /public/img/icons/apple-touch-icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wlx200510/typescript-vue/7d9c74d20745bc561ee0a46ea0973f8876418e83/public/img/icons/apple-touch-icon-152x152.png -------------------------------------------------------------------------------- /public/img/icons/apple-touch-icon-180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wlx200510/typescript-vue/7d9c74d20745bc561ee0a46ea0973f8876418e83/public/img/icons/apple-touch-icon-180x180.png -------------------------------------------------------------------------------- /public/img/icons/apple-touch-icon-60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wlx200510/typescript-vue/7d9c74d20745bc561ee0a46ea0973f8876418e83/public/img/icons/apple-touch-icon-60x60.png -------------------------------------------------------------------------------- /public/img/icons/apple-touch-icon-76x76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wlx200510/typescript-vue/7d9c74d20745bc561ee0a46ea0973f8876418e83/public/img/icons/apple-touch-icon-76x76.png -------------------------------------------------------------------------------- /public/img/icons/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wlx200510/typescript-vue/7d9c74d20745bc561ee0a46ea0973f8876418e83/public/img/icons/apple-touch-icon.png -------------------------------------------------------------------------------- /public/img/icons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wlx200510/typescript-vue/7d9c74d20745bc561ee0a46ea0973f8876418e83/public/img/icons/favicon-16x16.png -------------------------------------------------------------------------------- /public/img/icons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wlx200510/typescript-vue/7d9c74d20745bc561ee0a46ea0973f8876418e83/public/img/icons/favicon-32x32.png -------------------------------------------------------------------------------- /public/img/icons/msapplication-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wlx200510/typescript-vue/7d9c74d20745bc561ee0a46ea0973f8876418e83/public/img/icons/msapplication-icon-144x144.png -------------------------------------------------------------------------------- /public/img/icons/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wlx200510/typescript-vue/7d9c74d20745bc561ee0a46ea0973f8876418e83/public/img/icons/mstile-150x150.png -------------------------------------------------------------------------------- /public/img/icons/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.11, written by Peter Selinger 2001-2013 9 | 10 | 12 | 148 | 149 | 150 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | typescript-vue 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "typescript-vue", 3 | "short_name": "typescript-vue", 4 | "icons": [ 5 | { 6 | "src": "./img/icons/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "./img/icons/android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "start_url": "./index.html", 17 | "display": "standalone", 18 | "background_color": "#000000", 19 | "theme_color": "#4DBA87" 20 | } 21 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: 3 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 30 | -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wlx200510/typescript-vue/7d9c74d20745bc561ee0a46ea0973f8876418e83/src/assets/logo.png -------------------------------------------------------------------------------- /src/components/HelloWorld.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 42 | 43 | 44 | 60 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import App from './App.vue'; 3 | import router from './router'; 4 | import store from './store'; 5 | import './registerServiceWorker'; 6 | 7 | Vue.config.productionTip = false; 8 | 9 | new Vue({ 10 | router, 11 | store, 12 | render: (h) => h(App), 13 | }).$mount('#app'); 14 | -------------------------------------------------------------------------------- /src/registerServiceWorker.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable:no-console */ 2 | 3 | import { register } from 'register-service-worker'; 4 | 5 | if (process.env.NODE_ENV === 'production') { 6 | register(`${process.env.BASE_URL}service-worker.js`, { 7 | ready() { 8 | console.log( 9 | 'App is being served from cache by a service worker.\n' + 10 | 'For more details, visit https://goo.gl/AFskqB', 11 | ); 12 | }, 13 | cached() { 14 | console.log('Content has been cached for offline use.'); 15 | }, 16 | updated() { 17 | console.log('New content is available; please refresh.'); 18 | }, 19 | offline() { 20 | console.log('No internet connection found. App is running in offline mode.'); 21 | }, 22 | error(error) { 23 | console.error('Error during service worker registration:', error); 24 | }, 25 | }); 26 | } 27 | -------------------------------------------------------------------------------- /src/router/index.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Router from 'vue-router'; 3 | 4 | Vue.use(Router); 5 | 6 | export default new Router({ 7 | mode: 'history', 8 | base: process.env.BASE_URL, 9 | routes: [ 10 | { 11 | path: '/', 12 | name: 'home', 13 | component: () => import(/* webpackChunkName: "home" */ '../views/Home.vue'), 14 | }, 15 | { 16 | path: '/about', 17 | name: 'about', 18 | // route level code-splitting 19 | // this generates a separate chunk (about.[hash].js) for this route 20 | // which is lazy-loaded when the route is visited. 21 | component: () => import(/* webpackChunkName: "about" */ '../views/About.vue'), 22 | }, 23 | ], 24 | }); 25 | -------------------------------------------------------------------------------- /src/store/home/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: wanglixing 3 | * @Date: 2018-11-26 15:34:20 4 | * @Last Modified by: wanglixing 5 | * @Last Modified time: 2018-11-26 15:47:07 6 | */ 7 | import request from '@/utils/api' 8 | import { State } from './interface' 9 | import { Commit } from 'vuex' 10 | 11 | // 请求的data需要定义一个接口来约束 12 | interface GetTodayWeatherParam { 13 | city: string 14 | } 15 | 16 | const state: State = { 17 | count: 0, 18 | message: '', 19 | test1: [], 20 | } 21 | 22 | const getters = { 23 | count: (state: State) => state.count, 24 | message: (state: State) => state.message, 25 | } 26 | 27 | const mutations = { 28 | INCREMENT(state: State, num: number) { 29 | state.count += num 30 | }, 31 | DECREMENT(state: State, num: number) { 32 | state.count -= num 33 | }, 34 | MESSAGE(state: State, payload: any) { 35 | state.message = payload.message 36 | }, 37 | } 38 | 39 | const actions = { 40 | getTodayWeather(context: { commit: Commit }, params: GetTodayWeatherParam) { 41 | return request.get('/weather_mini', { params }) 42 | }, 43 | } 44 | 45 | export default { 46 | namespaced: true, 47 | state, 48 | getters, 49 | mutations, 50 | actions, 51 | } 52 | -------------------------------------------------------------------------------- /src/store/home/interface.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: wanglixing 3 | * @Date: 2018-11-26 15:32:38 4 | * @Last Modified by: wanglixing 5 | * @Last Modified time: 2018-11-26 15:40:00 6 | * 主要用于定义home模块中相关模块的接口 7 | */ 8 | export interface HomeContent { 9 | name: string, 10 | m1?: boolean 11 | } 12 | export interface State { 13 | count: number, 14 | message: string, 15 | test1?: HomeContent[] 16 | } 17 | -------------------------------------------------------------------------------- /src/store/index.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Vuex from 'vuex'; 3 | import home from './home' 4 | 5 | Vue.use(Vuex); 6 | 7 | export default new Vuex.Store({ 8 | modules: { 9 | home, 10 | }, 11 | }); 12 | -------------------------------------------------------------------------------- /src/types/ajax.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace Ajax { 2 | // axios 返回数据 3 | export interface AxiosResponse { 4 | data: AjaxResponse; 5 | } 6 | 7 | // 请求接口数据 8 | export interface AjaxResponse { 9 | status: number; 10 | data: any; 11 | desc: string; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/types/shims-qs.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace QueryString { 2 | interface IStringifyOptions { 3 | delimiter?: string; 4 | strictNullHandling?: boolean; 5 | skipNulls?: boolean; 6 | encode?: boolean; 7 | filter?: any; 8 | arrayFormat?: any; 9 | indices?: string; 10 | } 11 | interface IParseOptions { 12 | delimiter?: string; 13 | depth?: number; 14 | arrayLimit?: number; 15 | parseArrays?: boolean; 16 | allowDots?: boolean; 17 | plainObjects?: boolean; 18 | allowPrototypes?: boolean; 19 | parameterLimit?: number; 20 | strictNullHandling?: boolean; 21 | } 22 | function stringify(obj: any, options?: IStringifyOptions): string; 23 | function parse(str: string, options?: IParseOptions): any; 24 | } 25 | declare module 'qs' { 26 | export = QueryString; 27 | } 28 | -------------------------------------------------------------------------------- /src/types/shims-tsx.d.ts: -------------------------------------------------------------------------------- 1 | import Vue, { VNode } from 'vue'; 2 | 3 | declare global { 4 | namespace JSX { 5 | // tslint:disable no-empty-interface 6 | interface Element extends VNode {} 7 | // tslint:disable no-empty-interface 8 | interface ElementClass extends Vue {} 9 | interface IntrinsicElements { 10 | [elem: string]: any; 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/types/shims-vue.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.vue' { 2 | import Vue from 'vue'; 3 | export default Vue; 4 | } 5 | -------------------------------------------------------------------------------- /src/utils/api.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import config from './config'; 3 | import { AxiosRequestConfig } from 'axios'; 4 | 5 | // 取消重复请求 6 | const pending: Array<{ 7 | url: string, 8 | cancel: () => void, 9 | }> = []; 10 | const cancelToken = axios.CancelToken; 11 | const removePending = (axiosConfig: AxiosRequestConfig) => { 12 | for (const p in pending) { 13 | if (pending.hasOwnProperty(p)) { 14 | const item: any = p; 15 | const list: any = pending[p]; 16 | // 当前请求在数组中存在时执行函数体 17 | if (list.url === axiosConfig.url + '&' + axiosConfig.method) { 18 | // 执行取消操作 19 | list.cancel(); 20 | // 从数组中移除记录 21 | pending.splice(item, 1); 22 | } 23 | } 24 | } 25 | }; 26 | 27 | const service = axios.create(config); 28 | 29 | // 添加请求拦截器 30 | service.interceptors.request.use( 31 | (axiosConfig: AxiosRequestConfig) => { 32 | removePending(axiosConfig); 33 | axiosConfig.cancelToken = new cancelToken((c) => { 34 | pending.push({ url: axiosConfig.url + '&request_type=' + axiosConfig.method, cancel: c }); 35 | }); 36 | return axiosConfig; 37 | }, 38 | (error) => { 39 | return Promise.reject(error); 40 | }, 41 | ); 42 | 43 | // 返回状态判断(添加响应拦截器) 44 | service.interceptors.response.use( 45 | (res) => { 46 | removePending(res.config); 47 | return res.data; 48 | }, 49 | (error) => { 50 | console.log(error) 51 | return Promise.reject(error); 52 | }, 53 | ); 54 | 55 | export default service; 56 | -------------------------------------------------------------------------------- /src/utils/config.ts: -------------------------------------------------------------------------------- 1 | import http from 'http'; 2 | import https from 'https'; 3 | import qs from 'qs'; 4 | import { AxiosResponse, AxiosRequestConfig } from 'axios'; 5 | 6 | const axiosConfig: AxiosRequestConfig = { 7 | baseURL: '/', 8 | transformResponse: [(data: AxiosResponse) => data], 9 | paramsSerializer(params: any) { 10 | return qs.stringify(params); 11 | }, 12 | timeout: 8000, 13 | withCredentials: true, 14 | responseType: 'json', 15 | xsrfCookieName: 'XSRF-TOKEN', 16 | xsrfHeaderName: 'X-XSRF-TOKEN', 17 | maxRedirects: 5, 18 | maxContentLength: 3000, 19 | validateStatus(status: number) { 20 | return status >= 200 && status < 300; 21 | }, 22 | // 用于node.js, 可以删除 23 | httpAgent: new http.Agent({ keepAlive: true }), 24 | httpsAgent: new https.Agent({ keepAlive: true }), 25 | }; 26 | 27 | export default axiosConfig; 28 | -------------------------------------------------------------------------------- /src/views/About.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /src/views/Home.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 74 | 75 | 88 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "strict": true, 6 | "jsx": "preserve", 7 | "importHelpers": true, 8 | "moduleResolution": "node", 9 | "experimentalDecorators": true, 10 | "esModuleInterop": true, 11 | "allowSyntheticDefaultImports": true, 12 | "sourceMap": true, 13 | "baseUrl": ".", 14 | "types": [ 15 | "webpack-env", 16 | "node" 17 | ], 18 | "paths": { 19 | "@/*": [ 20 | "src/*" 21 | ], 22 | "*": [ 23 | "node_modules/*", 24 | "src/types/*" 25 | ] 26 | }, 27 | "lib": [ 28 | "esnext", 29 | "dom", 30 | "dom.iterable", 31 | "scripthost" 32 | ] 33 | }, 34 | "include": [ 35 | "src/**/*.ts", 36 | "src/**/*.tsx", 37 | "src/**/*.vue", 38 | "tests/**/*.ts", 39 | "tests/**/*.tsx" 40 | ], 41 | "exclude": [ 42 | "node_modules" 43 | ] 44 | } 45 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "warning", 3 | "extends": [ 4 | "tslint:recommended" 5 | ], 6 | "linterOptions": { 7 | "exclude": [ 8 | "node_modules/**" 9 | ] 10 | }, 11 | "rules": { 12 | "quotemark": [true, "single"], 13 | "indent": [true, "spaces", 2], 14 | "interface-name": false, 15 | "ordered-imports": false, 16 | "object-literal-sort-keys": false, 17 | "no-consecutive-blank-lines": false, 18 | "no-namespace": [true, "allow-declarations"], 19 | "semicolon": false, 20 | "no-console": [ 21 | false 22 | ], 23 | "only-arrow-functions": [ 24 | false 25 | ], 26 | "no-shadowed-variable": false 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | devServer: { 3 | open: true, 4 | proxy: { 5 | "/weather_mini": { // 天气接口的proxy 6 | target: "http://wthrcdn.etouch.cn", 7 | changeOrigin: true 8 | } 9 | } 10 | } 11 | } --------------------------------------------------------------------------------