Typescript+Vue大型後臺管理系統實戰

項目預覽

分享不易,喜歡的話必定別忘了點💖javascript

只關注不點💖的都是耍流氓css

只收藏也不點💖的也同樣是耍流氓html

簡介

ts香不香這裏我就不作過多吹捧了,基礎api官方文檔有比較清晰的介紹,本篇博客主要分享Vue+typescript+element-ui,後臺管理系統實戰篇。前端

簡單來講,ts屬於強類型語言,它的優點在於靜態類型檢查,歸納來講主要包括如下幾點:vue

  • 靜態類型檢查
  • IDE 智能提示
  • 代碼重構
  • 可讀性

1、技術棧

  • Typescript
  • vue-cli 3.x+
  • Yarn
  • Sass
  • Element-ui
  • Vuex
  • ...

2、項目說明

yarn install  //依賴安裝
yarn run serve //項目啓動
yarn run build:prod //打包
複製代碼

3、ts用法介紹

本次項目基礎框架爲Vue,跟正常的Vue項目仍是或多或少有很多差距的。衆所周知,js是一門弱類型的語言,尤爲是在變量賦值時,永遠都是給變量直接賦值各類類型值來初始化,線上一些隱藏的bug就冷不防會暴露出來。把這種錯誤扼殺在項目開發編譯階段而非上線階段,全部就有了typescript超集的出現。java

那Vue中是怎麼引用typescript項目的呢,項目開始以前,先大體介紹一番ts在Vue項目中的基礎用法node

vue-property-decoratorwebpack

vue-property-decoratorvue-class-component的基礎上增長了更多與Vue相關的裝飾器,使Vue組件更好的跟TS結合使用。這二者都是離不開裝飾器的,(decorator)裝飾器已在ES提案中。Decorator是裝飾器模式的實踐。裝飾器模式呢,它是繼承關係的一個替代方案。動態地給對象添加額外的職責。在不改變接口的前提下,加強類的性能。ios

vue-property-decorator是這個Vue項目文件中徹底依賴的庫,它是Vue官方推薦的而且依賴於vue-class-component,先介紹下它在項目中的常見用法。git

  • @Component
  • @Emit
  • @Provice @Inject
  • @Prop
  • @Watch
  • @Model
  • @Minxins

@Component 類裝飾器

首先,Vue頁面中的script部分要加一個lang=ts,這樣安裝好typescript正能引用

<script lang="ts">
    import {Vue, Component} from 'vue-property-decorator';
    import BaseHeader from '@/components/BaseHeader'; 
    
    //公共頭部組件
    @Component({
        components: {
            BaseHeader
        }
    })
    export default class extends Vue {
        private stateA:boolean = true
        private stateB:string = ''
        private stateC:number = 0
        private stateD:any = {}
        stateE:any[] = []
    }
</script>
複製代碼

等同於

<script>
    import Vue from 'vue';
    import BaseHeader from '@/components/BaseHeader'; //公共頭部組件

    export default {
        components: {
            BaseHeader
        },
        
        data(){
            return {
                stateA: true,
                stateB: '',
                stateC: 0,
                stateD: {},
                stateE: []
            }	
        }
    }
</script>
複製代碼

vue-property-decorator在項目中的應用最主要是起一個裝飾器的做用,差別化的話看對比就很是直觀了

data變量的定義比較多元化,這裏區別有加private,不加就是public,當變量標記爲private時,它就不能在聲明它的類的外部訪問。

@Component裝飾器屬性名必須得寫上


@Prop

父子組件之間的屬性傳值

export default class extends Vue {
    @Prop({ default: 0 }) private propA!: number
    @Prop({ default: () => [10, 20, 30, 50] }) private propB!: number[]
    @Prop({ default: 'total, sizes, prev, pager, next, jumper' }) private propC!: string
    @Prop({ default: true }) private propD!: boolean,
    @prop([String, Boolean]) propE: string | boolean;
}    
複製代碼

等同於

export default {
  props: {
    propA: {
	    type: Number
    },
    propB: {
    	type: Array,
    	default: [10, 20, 30, 50]
    },
    propC: {
    	type: String,
    	default: 'total, sizes, prev, pager, next, jumper'
    },
    propD: {
    	type: String,
    	default: 'total, sizes, prev, pager, next, jumper'
    },
    propE: {
    	type: [String, Boolean]
    }
  }
}
複製代碼

這裏有兩個經常使用修飾符!``?!和可選參數?是相對的, !表示強制解析(也就是告訴typescript編譯器,我這裏必定有值),你寫?的時候再調用,typescript會提示可能爲undefined


@Emit

Component
export default class YourComponent extends Vue {
  count = 0

  @Emit('reset')
  resetCount() {
    this.count = 0
  }

  @Emit()
  returnValue() {
    return 10
  }

  @Emit()
  onInputChange(e) {
    return e.target.value
  }
}
複製代碼

等同於

export default {
  data() {
    return {
      count: 0
    }
  },
  
  methods: {
    resetCount() {
      this.count = 0
      this.$emit('reset')
    },
    
    returnValue() {
      this.$emit('return-value', 10)
    },
    
    onInputChange(e) {
      this.$emit('on-input-change', e.target.value, e)
    }
  }
}
複製代碼

@Emit裝飾器的函數會在運行以後觸發等同於其函數名(駝峯式會轉爲橫槓式寫法)的事件, 並將其函數傳遞給$emit

@Emit觸發事件有兩種寫法

  • @Emit()不傳參數,那麼它觸發的事件名就是它所修飾的函數名.
  • @Emit(name: string),裏面傳遞一個字符串,該字符串爲要觸發的事件名

@Watch 觀察屬性裝飾器

@Watch裝飾器主要用於替代Vue屬性中的watch屬性,監聽依賴的變量值變化而作一系列的操做

@Component
export default class YourComponent extends Vue {
  @Watch('child')
  onChildChanged(val: string, oldVal: string) {}

  @Watch('person', { immediate: true, deep: true })
  onPersonChanged(val: Person, oldVal: Person) {}
}
複製代碼

等同於

export default {
  watch: {
	child(val, oldVal) {},
	person: {
		handler(val, oldVal) {},
		immediate: true,
		deep: true
	}
  }
}
複製代碼

watch 是一個對象,對象就有鍵,有值。

  • 第一個handler:其值是一個回調函數。即監聽到變化時應該執行的函數。
  • 第二個是deep:其值是true或false;確認是否深刻監聽。deep的意思就是深刻觀察,監聽器會一層層的往下遍歷,給對象的全部屬性都加上這個監聽器(受現代 JavaScript 的限制 (以及廢棄 Object.observe),Vue 不能檢測到對象屬性的添加或刪除)
  • 第三個是immediate:其值是true或false;immediate:true表明若是在 wacth 裏聲明瞭以後,就會當即先去執行裏面的handler方法,若是爲 false就跟咱們之前的效果同樣,不會在綁定的時候就執行

@Watch使用很是簡單,接受第一個參數爲要監聽的屬性名, 第二個屬性爲可選對象。@Watch所裝飾的函數即監聽到屬性變化以後應該執行的函數。

@Watch裝飾的函數的函數名並不是如上onStateChanged嚴格命名,它是多元化的,你能夠爲所欲爲的命名,固然,能按照規範化的命名會使你的代碼閱讀性更好。


@Minxins

// myMixin.ts

@Component
export default class MyMixin extends Vue {
  mixinValue:string = 'Hello World!!!'
}
複製代碼
// 引用mixins
import MyMixin from './myMixin.js'

@Component
export default class extends mixins(MyMixin) {
  created () {
    console.log(this.mixinValue) // -> Hello World!!!
  }
}
複製代碼

而後我又偷學到了另一種mixins寫法,記錄一下

先改造一下myMixin.ts,定義vue/type/vue模塊,實現Vue接口

// myMixin.ts
import { Vue, Component } from 'vue-property-decorator';


declare module 'vue/types/vue' {
    interface Vue {
        mixinValue: string;
    }
}

@Component
export default class myMixins extends Vue {
    mixinValue: string = 'Hello World!!!'
}
複製代碼

引用

import { Vue, Component, Prop } from 'vue-property-decorator';
import MyMixin from './myMixin.js'

@Component({
    mixins: [MyMixin]
})
export default class extends Vue{
    created(){
        console.log(mixinValue) // => Hello World!!!
    }
}
複製代碼

兩種方式不一樣在於定義mixins時若是沒有定義vue/type/vue模塊, 那麼在混入的時候就要繼承該mixins; 若是定義vue/type/vue模塊,在混入時能夠在@Componentmixins直接混入。


@Model

@Model裝飾器容許咱們在一個組件上自定義v-model,接收兩個參數:

  • event: string 事件名。
  • options: Constructor | Constructor[] | PropOptions 與@Prop的第一個參數一致。
import { Vue, Component, Model } from 'vue-property-decorator'

@Component
export default class MyInput extends Vue {
  @Model('change', { type: String, default: 'Hello world!!!' }) readonly value!: string
}
複製代碼

等同於

<template>
  <input
    type="text"
    :value="value"
    @change="$emit('change', $event.target.value)"
  />
</template>

export default {
  model: {
    prop: 'value',
    event: 'change'
  },
  props: {
    value: {
      type: String,
      default: 'Hello world!!!'
    }
  }
}
複製代碼

@Provide @Inject

@Provide 聲明一個值 , 在其餘地方用 @Inject 接收,在實戰項目中用得很少,通常用於不依賴於任何第三方狀態管理庫(如vuex)的組件編寫


@Ref(refKey?: string)

@Ref裝飾器接收一個可選參數,用來指向元素或子組件的引用信息。若是沒有提供這個參數,會使用裝飾器後面的屬性名充當參數

import { Vue, Component, Ref } from 'vue-property-decorator'
import { Form } from 'element-ui'

@Componentexport default class MyComponent extends Vue {
  @Ref() readonly loginForm!: Form
  @Ref('changePasswordForm') readonly passwordForm!: Form

  public handleLogin() {
    this.loginForm.validate(valide => {
      if (valide) {
        // login...
      } else {
        // error tips
      }
    })
  }
}
複製代碼

等同於

export default {
  computed: {
    loginForm: {
      cache: false,
      get() {
        return this.$refs.loginForm
      }
    },
    passwordForm: {
      cache: false,
      get() {
        return this.$refs.changePasswordForm
      }
    }
  }
}
複製代碼

使用時切記要引入修飾器

import {
	Vue,
	Component,
	Prop,
	Component,
	Emit,
	Provice,
	Inject,
	Watch,
	Model,
	Minxins,
} from 'vue-property-decorator'
複製代碼

鉤子函數

如下的public、private在引入tslint後是必寫的,不然會有警告,若是沒有引的話是能夠不寫的

Ts Js 說明
public created() {} created() {} 初始化
public mounted() {} mounted() {} 掛載完畢
private _getInitData() {} methods: { _getInitData() {} } 方法
private get _userName() {} computed: { _userName() {} } 計算屬性
public destroyed() {} destroyed() {} 銷燬生命週期

搭建環境

安裝

安裝vue-cli最新版

npm install -g @vue/cli
# OR
yarn global add @vue/cli

//查看是否安裝正確
vue --version


複製代碼

建立項目

? Please pick a preset:(使用上下箭頭)
 ◯ default (babel, eslint)        //默認配置
❯◉ Manually select features       //手動選擇
複製代碼
? Check the features needed for your project:
 ◉ Babel                                    // javascript轉譯器
 ◉ TypeScript                               // 使用 TypeScript 書寫源碼
 ◯ Progressive Web App (PWA) Support        // 漸進式WEB應用
 ◉ Router                                   // 使用vue-router
 ◉ Vuex                                     // 使用vuex
 ◉ CSS Pre-processors                       // 使用css預處理器
❯◉ Linter / Formatter                       // 代碼規範標準
 ◯ Unit Testing                             // 單元測試
 ◯ E2E Testing                              // e2e測試
複製代碼

是否使用class風格的組件語法: 使用前:home = new Vue()建立vue實例 使用後:class home extends Vue{}

? Use class-style component syntax? (Y/n) Y

// 使用Babel與TypeScript一塊兒用於自動檢測的填充
? Use Babel alongside TypeScript (required for modern mode, auto-detected polyfills, transpiling JSX)? (Y/n) Y

// 路由
? Use history mode for router? (Requires proper server setup for index fallback in production) (Y/n) Y

// 預處理器
? Pick a CSS pre-processor (PostCSS, Autoprefixer and CSS Modules are supported by default): (Use arrow keys)
❯◉ Sass/SCSS (with dart-sass)    // 保存後編譯
 ◯ Sass/SCSS (with node-sass)    // 實時編譯 
 ◯ Less
 ◯ Stylus

// 代碼格式化檢測
? Pick a linter / formatter config: (Use arrow keys)
 ◯ ESLint with error prevention only     // 只進行報錯提醒
 ◯ ESLint + Airbnb config                // 不嚴謹模式
 ◯ ESLint + Standard config              // 正常模式
 ◯ ESLint + Prettier                     // 嚴格模式
❯◉ TSLint(deprecated)                    // typescript格式驗證工具

// 代碼檢查方式
? Pick additional lint features: (Press <space> to select, <a>
to toggle all, <i> to invert selection)
❯◉ Lint on save             // 保存檢查
 ◯ Lint and fix on commit   // commit時fix

// 文件配置
? Where do you prefer placing config for Babel, ESLint, etc.? (
Use arrow keys)
❯ In dedicated config files // 配置在獨立的文件中
  In package.json
  
// 保存上述配置,保存後下一次可直接根據上述配置生成項目
? Save this as a preset for future projects? (y/N) N

// 建立成功
🎉  Successfully created project vue-typescript-admin-demo.
複製代碼

yarn run serve運行項目以後會報一堆莫名的錯誤,這都是 tslint.json 搞的鬼,配置一下從新運行便可

// tsconfig.json
Error: Calls to 'console.log' are not allowed.

Error: 去除行尾必加';'

Error: 禁止自動檢測末尾行必須使用逗號,always老是檢測,never從不檢測,ignore忽略檢測

"rules": {
    "no-console": false,
    "semicolon": [
        false,
        "always"
    ],
    "trailing-comma": [true, {
        "singleline": "never",
        "multiline": {
            "objects": "ignore",
            "arrays": "ignore",
            "functions": "never",
            "typeLiterals": "ignore"
        }
    }]
}
複製代碼

至此,整個項目算是正常運行起來了。But... 這仍是傳統的Vue項目,咱們要開發的是Vue+ts實戰項目,因此須要改造一番,詳細的目錄結構,等改造完以後再附上吧。


改形成typescript項目

按照如下的目錄結構改造項目

.
├─ public/             # 模板文件
├─ dist/               # build 生成的生產環境下的項目
├─ src/                # 源碼目錄(開發都在這裏進行)
│   ├─ api/            # 服務(SERVICE,統一Api管理)
│   ├─ assets/         # 靜態資源文件
│   ├─ components/     # 組件
│   ├─ filters/        # 全局過濾器
│   ├─ icons/          # svg轉ts格式的icon
│   ├─ lang/           # 國際化語言
│   ├─ layout/         # 架構佈局
│   ├─ router/         # 路由(ROUTE)
│   ├─ store/          # 模塊化狀態管理vuex
│   ├─ styles/         # 公共樣式
│   ├─ utils/          # 工具庫
│   ├─ views/          # 視圖頁(pages)
│   ├─ App.vue         # 啓動文件
│   ├─ main.ts         # 主入口頁
│   ├─ permission.ts   # 路由鑑權
│   ├─ shims-tsx.d.ts   # 相關 tsx 模塊注入
│   ├─ shims-vue.d.ts   # Vue 模塊注入
│   ├─ .env.development  # 開發環境默認API屬性配置
│   ├─ .env.production   # 線上環境默認API屬性配置
│   ├─ babel.config.js   # babel配置
複製代碼

主要涉及 shims-tsx.d.tsshims-vue.d.ts 兩個文件

  • shims-tsx.d.ts ,容許你以 .tsx 結尾的文件,在 Vue 項目中編寫 jsx 代碼
  • shims-vue.d.ts 主要用於 TypeScript 識別 .vue 文件, ts 默認並不支持導入 .vue 文件,這個文件告訴 ts 導入 .vue 文件都按 VueConstructor<Vue> 處理。

① 刪除多餘的文件

把vue-cli攜帶的home about文件刪除,清空components文件夾的內容,把路由的指向到主入口App.vue文件,修改App.vue的內容

<!--App.vue-->
<template>
  <div id="app">
    <p>hello world!!!</p>
    <img alt="Vue logo" src="@/assets/logo.png" />
  </div>
</template>

<script lang="ts">
import { Component, Vue } from 'vue-property-decorator'

@Component({
  name: 'app'
})
export default class extends Vue {
  private created() {
    console.log(12345)
  }
}
</script>
複製代碼

第一個ts的文件就這麼搞定了,接下來大刀闊斧的改造吧。

修改路由,新增login登陸頁和主骨架搭建頁面、以及dashboard首頁

路由對象都以懶加載的形式引入,而且備註webpackChunkName,便於查找。

根路由/重定向到/dashboard

const routes = [
  {
    // webpackChunkName:懶加載後的文件名
    component: () => import(/* webpackChunkName: "login" */ '@/views/login/index.vue'),
    ...
  }
]
複製代碼

② 按需引入element-ui

安裝normalize.css初始化css

yarn add normalize.css

安裝element-ui最新版

按需引入

配置babel.config.js

"plugins": [
    [
      "component",
      {
        "libraryName": "element-ui",
        "styleLibraryName": "theme-chalk"
      }
    ]
]
複製代碼

運行後報錯,須要安裝babel-plugin-component解析

ERROR : Cannot find module 'babel-plugin-component' from '/Users/zeng/Desktop/hello/vue-typescript-admin-demo'
複製代碼

yarn add babel-plugin-component --dev

main.ts主入口文件裏面引用normalize.csselement-ui

// main.ts
import 'normalize.css'
import ElementUI from 'element-ui'

Vue.use(ElementUI)
複製代碼

如今去Vue頁面裏面就能爲所欲爲的引用element-ui的組件了,並且不須要在任何頁面裏再次import

③ 定製主題

定製主題本想在搭建後管理後臺的主骨架的時候寫的,但轉眼一想,要開始搭建頁面UI了,到時候再回頭改一遍太折騰了,so...就先來個定製主題吧

每一個項目都有本身風格的主題,因爲在項目中應用的是sass預處理器,定製化修改UI顏色背景,固然要結合sass來完成

.
├─ src/                # 源碼目錄(開發都在這裏進行)
│   ├─ styles/
│   ├─── element-variables.scss  
│   ├─── element-variables.scss.d.ts 
   
複製代碼
/* Element Variables */

// Override Element UI variables
$--color-primary: #1890ff;
...

// Icon font path, required
$--font-path: '~element-ui/lib/theme-chalk/fonts';

// Apply overrided variables in Element UI
@import '~element-ui/packages/theme-chalk/src/index';

// The :export directive is the magic sauce for webpack
// https://mattferderer.com/use-sass-variables-in-typescript-and-javascript
:export {
  theme: $--color-primary;
}
複製代碼

因爲這裏引用到了:export,你沒看錯,確實是:export,具體的解釋等大佬指教吧.... 官方是這麼解釋的 :export directive is the magic sauce for webpack`,充滿魔法,webpack的語法糖???

在ts的規則裏,你必須按照它的規範來進行每一步的開發,所以你要加上element-variables.scss.d.ts對應的.d.ts文件

// element-variables.scss.d.ts
export interface IScssVariables {
    theme: string
}

export const variables: IScssVariables

export default variables
複製代碼

好了,如今你能夠在主入口文件裏面引用定製主題的文件配置了。

// main.ts
import '@/styles/element-variables.scss'
複製代碼

簡單的框架主題定製就這麼完成了,後面會加深擴展,加一個換膚的功能,先繼續下一步。定義全局的統一sass變量,方便統一規範css顏色

爲了防止後期再次聲明這個統一變量,這裏暫且把以後的菜單欄的變量也加上吧

// variables.scss

// Base color
$blue:#324157;
...

// Sidebar
$subMenuBg:#1f2d3d;
...

// Login page
$loginBg: #2d3a4b;
...

// The :export directive is the magic sauce for webpack
// https://mattferderer.com/use-sass-variables-in-typescript-and-javascript
:export {
  menuBg: $menuBg;
}
複製代碼

同上,對應的解析文件

// variables.scss.d.ts
export interface IScssVariables {
    menuBg: string
}

export const variables: IScssVariables

export default variables
複製代碼

全局變量定義好了,那如何加入到項目中呢?直接在主入口文件main.ts裏面引用是不生效的,因此這裏咱們要換個思路,放在style-resources-loader裏面去引入,在整個項目的配置文件裏去配置。

style-resources-loader是個什麼鬼,搜索了個全局都沒發現這個配置項,vue-li 3.0+的配置乾乾淨淨的,不想老版本vue-li 2.0,有對應的config文件夾對應各類不一樣的環境來作選項配置,那怎麼配置vue-li 3.0+

就不賣關子了,直接上代碼吧。首先咱們要在根目錄的同級新建一個vue.config.js,也就是說,對應vue-li 2.0版本乾的事,在vue-li 3.0裏面就須要咱們本身按需配置了

新建vue.config.js文件

把統一的scss變量引入到插件選項卡中去,這裏須要安裝兩個類庫,不然會沒法解析

yarn add vue-cli-plugin-style-resources-loader yarn add style-resources-loader --dev // 開發環境loader編譯

// vue.config.js
module.exports = {
    pluginOptions: {
        'style-resources-loader': {
            preProcessor: 'scss',
            patterns: [
                path.resolve(__dirname, 'src/styles/variables.scss'),
                path.resolve(__dirname, 'src/styles/mixins.scss')
            ]
        }
    },
}

複製代碼

除了variables.scss,對應還新增了一個mixins.scss,即全局混入的mixins混合變量。

好了,公共的sass變量就完成了,能夠應用到項目中去了。

具體的全局各個樣式設定能夠參考@/styles/目錄下的文件。

如今能夠正式開始搭建頁面了。

Warn 警告修復

tslint添加以下配置

// tslint.json
// 不檢測隱式類型
"arrow-parens": [
	false,
	"as-needed"
]
複製代碼

登陸頁面開發

登陸頁面比較簡潔,沒有什麼酷炫的樣式

再強調一遍,開發新頁面,必定要轉換觀念,不要把原始的寫法風格引入進來,那就達不到引入ts的目的了

template模塊是幾乎沒有變化,script須要加上lang="ts"

<script lang="ts">
import { Component, Vue } from 'vue-property-decorator'

//類組件@Component 必寫
@Component({
  name: 'login'
})
export default class extends Vue {
  private loading:bollen = false // data變量

  private get pageSize() { // 計算屬性
	return 10
  }

  private created() { ... }
  
  private mounted() { ... }

  private handleLogin() { ... } // methods方法 
  

  public destroyed() {} // 銷燬聲明週期
}
</script>
複製代碼

登陸頁用了element-uiForm表單組件。雖然template模板部分沒有什麼變化,可是js Form表單部分差別化仍是比較大的

!!!關閉tslint

這個tslint真的是讓有強迫症的我無法在繼續下去了,關了吧,一堆的warn....

// tslint.json
{
	"defaultSeverity": "none", // 值爲warn時爲警告
	"rules": {
		...
	}
}

複製代碼

世界頓時清淨了~~~ 有硬須要的朋友能夠自行打開,前提是必定要配置好tslint規則,不然仍是有點痛苦不堪的,畢竟warn多了看着難受。告辭

迴歸主題。

這裏要提別提一下表單校驗,對,就是el-formrules屬性值,經過Form-Item Attributesprop達到表單校驗的功能

import { Form as ElForm } from 'element-ui'

export default class extends Vue {
	private validateMobilePhone = (
	  rule: any,
	  value: string,
	  callback: Function
	) => {
	  if (!value.trim()) {
	    callback(new Error('請輸入手機號'))
	  } else {
	    callback()
	  }
	}
	
	private validatePassword = (rule: any, value: string, callback: Function) => {
	  if (value.length < 6) {
	    callback(new Error('密碼長度不能小於6位'))
	  } else {
	    callback()
	  }
	}
	
	private loginRules = {
	  mobilePhone: [{ validator: this.validateMobilePhone, trigger: 'blur' }],
	  password: [{ validator: this.validatePassword, trigger: 'blur' }]
	}
	
	private handleLogin() {
	  (this.$refs.loginForm as ElForm).validate(async (valid: boolean) => {
	    if (valid) {
	      ...
	    }
	  });
	}
}

複製代碼

以上即爲表單登陸校驗,須要特別提醒的就是,登陸事件是(this.$refs.loginForm as ElForm).validate() => {},而非this.$refs.loginForm.validate() => {},與原始有着較爲明顯的差別。

直接用後者的話,是會直接報錯的

因此這裏你要先引用element-uiForm組件,而後再使用this.$refs.loginForm as ElForm,這樣你就有Form組件的validate方法了,自此登陸頁表單校驗就搭建完成了

接下來是api請求

axios api

本項目使用axios api請求工具庫

yarn add axios

封裝api工具庫

/**
    src->untils->request.ts
**/
import axios from 'axios'

const service = axios.create({
  baseURL: process.env.VUE_APP_BASE_API,
  timeout: 5000
})

// Request interceptors
service.interceptors.request.use(
  (config) => {
    // Add Authorization header to every request, you can add other custom headers here
    config.headers['Authorization'] = 'token信息配置'
    return config
  },
  (error) => {
    Promise.reject(error)
  }
)

// Response interceptors
service.interceptors.response.use(
  (response) => {
    攔截操做...
  },
  (error) => {
    return Promise.reject(error)
  }
)

export default service
複製代碼

總的來講,跟原始版本的工具庫封裝沒什麼明顯差別,主要就是配置一下幾個點

  • 響應超時時間timeout
  • api公共請求頭baseURL
  • 請求攔截Request interceptors
  • 響應攔截Response interceptors
  • config.headers token權限配置

具體的api工具類能夠參考源碼

這裏有個process.env.VUE_APP_BASE_API變量值,你能夠直接在當前的工具類根據環境配置,也能夠根據vue-cli 3.0內置的文件配置,第二種方案在根目錄下

.
├─ src/   
├─ .env.development   // 開發環境配置
├─ .env.production    // 線上環境配置   
...
複製代碼

VUE_APP_BASE_API 變量名爲固定命名,沒法修改

// .env.development
VUE_APP_BASE_API = '192.168.1.1:8090'   

// .env.production
VUE_APP_BASE_API = 'https://www.baidu.con' 
複製代碼

注意:

在這裏配置好VUE_APP_BASE_API,前提是服務端已經作好跨域處理,若是服務端接口沒有作跨域處理,那這裏配置api的請求頭就無效了。你能夠在vue.config.js裏面配置跨域代理屬性,本項目接口已作跨域處理,因此這裏註釋了,須要能夠自行打開

const mockServerPort = 8090

module.exports = {
  devServer: {
    proxy: {
        [process.env.VUE_APP_BASE_API]: {
            target: `http://localhost:${mockServerPort}/mock-api/v1`,
            changeOrigin: true, // 若是接口跨域,須要進行這個參數配置
            // ws: true,// proxy websockets
            pathRewrite: { // pathRewrite方法重寫url
                ['^' + process.env.VUE_APP_BASE_API]: ''
            }
        }
    }
  }
}
複製代碼

配置好請求頭後,後面全部的接口調用都是

url = VUE_APP_BASE_API + url

好了,接下來按照順序應該安利一下使用,爲了體驗性和可維護性更友好點,api接口模塊會分頁面定義

.
├─ src/                # 源碼目錄
│   ├─ api/
│   ├─── pageA.ts  
│   ├─── pageB.ts
...
複製代碼

定義api接口

import request from '@/utils/request'

export const login = (data: any) =>
    request({
        url: '/api/login',
        method: 'post',
        data
    })
複製代碼

api工具類封裝完成,在開始使用以前,先了解一波vuex+ts版的狀態管理吧,由於調用login接口成功後會存儲用戶和token信息,會用到狀態管理


狀態管理Vuex

傳統的vuex在vue+ts的項目裏面是行不通的,vue 2.0版本對ts的兼容性自己並非特別友好,因此要達到狀態管理的效果,這裏要額外引用一個類庫vuex-module-decorators,它是基於vue-class-component 所作的拓展,它提供了一系列的裝飾器,讓vue+ts結合的項目達到狀態管理的做用。

vue-class-component 主要提供瞭如下的裝飾器,接下來讓咱們一一的瞭解一遍吧

import { VuexModule, Module, Action, Mutation, getModule, State } from 'vuex-module-decorators'

先來看看要完成的模塊化管理的目錄結構

.
├─ src/        
│   ├─ store/
│   ├─── modules/
│   │ 		├─ app.ts 
│   │ 		├─ user.ts
│   ├─── index.ts   
複製代碼

動手改造index.ts

import Vue from 'vue'
import Vuex from 'vuex'
import { IAppState } from './modules/app'
import { IUserState } from './modules/user'

Vue.use(Vuex)

export interface IRootState {
    app: IAppState
    user: IUserState
}

// Declare empty store first, dynamically register all modules later.
export default new Vuex.Store<IRootState>({})
複製代碼

等同於

import Vue from 'vue'
import Vuex from 'vuex'
import app from './modules/app'
import user from './modules/user'
Vue.use(Vuex)

const store = new Vuex.Store({
  modules: {
    app,
    user
  }
})

export default store
複製代碼

這樣,模塊化狀態管理的雛形就完成了。對比來看,只是語法風格的變化,其它的變化不大。ts版的狀態管理最大的改變體如今各個功能功能函數上

先看一看原始的vuex配置,輕車熟路

export default new Vuex.Store({
    state: {
    },
    mutations: {
    },
    actions: {
    },
    modules: {
    }
});
複製代碼

爲了顯得不那麼囉嗦,直接上版ts版的狀態管理吧,能夠有個直觀的對比

// user.ts
import { VuexModule, Module, Action, Mutation, getModule } from 'vuex-module-decorators'
import store from '@/store'

export interface IUserState {
    id_token: string
}

@Module({ dynamic: true, store, name: 'user' })
class User extends VuexModule implements IUserState {
    public id_token = ''
    
    @Mutation
    private SET_TOKEN(token: string) {
        this.id_token = token
    }
    
    @Action
    public async Login(params: any) {
        this.SET_TOKEN(`token!!!`)
    }
}

export const UserModule = getModule(User)

複製代碼

解析:

咱們看到了一堆的@開頭的裝飾器函數@Mutation @Mutation @Module...

先來一張表格對比一下差別化吧

Ts Js
public State state
@Mutations mutations
@Action action
get getters

定義module

定義一個modules,直接使用裝飾器@Module

注意:原始的vuex一樣有一個名爲Module的類,但它不是一個裝飾器,因此別用混淆了

@Module({ dynamic: true, store, name: 'user' })

從上面能夠看到,咱們定義modules不僅僅用了裝飾器,還帶了參數值,這個是代表是經過命名空間的形式來使用module,如上,這裏的namespaced值即爲user

詳細vuex命名空間的說明,能夠參考vuex命名空間

除了namespaced,咱們看到還有另一個參數值store,它即爲主入口頁對應的整個vuex模塊的store

import store from '@/store'

若是去掉它的話,瀏覽器會報如下錯誤

state

這裏全部的state屬性由於加了tslint都會添加上public修飾,其它的用法都是類似的

Getters

原始的getters計算函數,在這裏對應的即便get方法,即

@Module
export default class UserModule extends VuexModule {
  countsNum = 2020
  
  get calculatCount() {
    return countsNum / 2
  }
}
複製代碼

等同於

export default {
  state: {
    countsNum: 2
  },
  getters: {
    calculatCount: (state) => state.countsNum / 2
  }
}
複製代碼

Mutations

@Mutation
private SET_TOKEN(token: string) {
    this.token = token
}

@Mutation
...
複製代碼

等同於

mutations: {
    SET_TOKEN: (state, token) => {
        state.token = token
    },
    ...
}
複製代碼

說明:

  • 二者的區別其實就是語法糖,原始的Mutation同步方法都是定義在mutations內,而ts版的每個Mutation都要加上裝飾器@Mutation修飾

注意: 一旦使用@Mutation裝飾某一函數後, 函數內的this上下文即指向當前的state,因此想引用state的值,能夠直接this.token訪問便可。

Muation函數不可爲async函數, 也不能使用箭頭函數來定義, 由於在代碼須要在運行從新綁定執行的上下文

Action

@Action
public async Login(userInfo: { username: string, password: string}) {
    ...
    this.SET_TOKEN(data.accessToken)
}
複製代碼

等同於

actions: {
    async Login({ commit }, data) {
        ...
        commit('SET_TOKEN', data.accessToken)
    }
}
複製代碼

說明:

異步函數Action和同步函數Mutation使用方法大同小異,區別就是一個是同步,一個是異步,只要作好區分便可

注意:

  • 若是須要在action函數中運行耗時很長的任務/函數, 建議將該任務定義爲異步函數*(async methods)*
  • 千萬不要使用箭頭函數=>來定義action函數, 由於在運行時須要動態綁定this上下文

vuex+ts版的配置搭建成功,接下來咱們把它運用到項目中來吧,這裏抽一個登錄頁面的模塊作介紹

import {
  VuexModule,
  Module,
  Action,
  Mutation,
  getModule
} from 'vuex-module-decorators'
import { login } from '@/api/users' //調用api方法
import store from '@/store'

//聲明user模塊的state變量類型
//export interface 只是對一個東西的聲明(不能具體的操做)
//export class 導出一個類 類裏面可有參數 ,函數,方法(幹一些具體的事情)
export interface IUserState {
  id_token: string
}

@Module({ dynamic: true, store, name: 'user' })
class User extends VuexModule implements IUserState {
  public id_token = ''

  @Mutation
  private SET_TOKEN(token: string) {
    //同步存儲id_token變量
    this.id_token = token
  }

  @Action
  public async Login(params: any) {
    let { mobilePhone, password } = params
    const { data } = await login({ mobilePhone, password })
    this.SET_TOKEN(`Bearer ${data.id_token}`)
  }
}

export const UserModule = getModule(User)
複製代碼

在login頁面中調用

import { UserModule } from '@/store/modules/user'

await UserModule.Login({
  ...this.loginForm,
  router: this.$router
})
複製代碼

把路由對象做爲參數傳過去是爲了根據不一樣的響應狀態作判斷,當請求成功後,能夠直接應用傳過來的路由對象參數跳轉頁面。

router.push('/')

注意:

這一步操做實際上是調用了vuex的Action操做,即原始的this.$store.commit('action'),可是在vuex+ts項目中,調用異步函數Action,不須要再用this.$store.commit('action')這種方法,引用模塊後,直接調用裏面的Action方法就行了,一樣的,同步的Mutation也是這樣調用。這些都要歸功於vuex-module-decorators類庫的封裝

好了,調用Action後粗發Mutation同步操做,保存好token令牌,由於登陸以後全部的請求都要把token值放在header頭中發起請求

除了vuex狀態管理,在項目中可能咱們還會結合工具類js-cookie一塊兒使用,管理各類變量的值,具體用法跟原始版沒有什麼區別,最主要的是安裝類庫的過程當中,還得安裝一個開發ts編譯版

yarn add js-cookie // dependencies yarn add @types/js-cookie --dev // devDependencies(必裝)

登陸頁面至此就完成了,它會直接重定向到首頁。接下來讓咱們來搭建項目的的骨架導航菜單

layout 導航菜單

這裏其實沒太多可介紹的,直接用element-uiNavMenu 導航菜單組件便可

Error:

Cannot find module '@/assets/401-images/401.gif'

解決: 加上模塊定義

//shims-vue.d.ts

declare module '*.png'
declare module '*.jpg'
declare module '*.gif'
複製代碼

Error:

在.vue文件裏 "import path from 'path' ’" 報錯 Cannot find module 'path'

解決: 在tsconfig.json的type項添加"node"便可。

"types": [
  "node",
]
複製代碼

Error:

SassError: expected selector

解決:

yarn add node-sass --dev

若是安裝了node-sass以後還有報錯,那有多是你的sass類庫安裝錯誤,檢查下package.json中是否在devDependencies開發依賴中安裝了sass,若是有的話yarn remove sass便可,留一個sass-loader就行了

Error: 在使用path-to-regexp中出現以下錯誤警告

解決:

主要是 import 使用的問題:在引入path-to-regexp時應使用如下的方法

import * as pathToRegexp from 'path-to-regexp'

layout 步驟:

定義菜單路由結構

export const constantRoutes: RouteConfig[] = [
]

export const asyncRoutes: RouteConfig[] = [
]

const createRouter = () =>
  new Router({
    // scrollBehavior功能只在 HTML5 history 模式下可用,當切換到新路由時,想要頁面滾到頂部,或者是保持原先的滾動位置,就像從新加載頁面那樣
    // mode: 'history',
    scrollBehavior: (to, from, savedPosition) => {
      if (savedPosition) {
        // 當且僅當 popstate 導航 (經過瀏覽器的 前進/後退 按鈕觸發) 時纔可用
        return savedPosition
      } else {
        return { x: 0, y: 0 }
      }
    },
    base: process.env.BASE_URL,
    routes: constantRoutes
  })

const router = createRouter()

export default router
複製代碼

這裏有兩個路由數組對象asyncRoutesconstantRoutes,字面量理解意思就好了

路由守衛

爲了讓項目體驗性更友好,固然要增長路由守衛功能,與此同時,會添加一個路由進度條nprogress類庫

yarn add nprogress yarn add @types/nprogress --save

// @/src/permission.ts

import router from './router'
import NProgress from 'nprogress' // Progress 進度條
import 'nprogress/nprogress.css'// Progress 進度條樣式
import { Message } from 'element-ui'
import { PermissionModule } from '@/store/modules/permission'
import { UserModule } from '@/store/modules/user'
import { Route } from 'vue-router'

const whiteList = ['/login'] // 不重定向白名單
router.beforeEach(async(to: Route, _: Route, next: any) => {
    NProgress.start()
    if (UserModule.id_token) {
        if (to.path === '/login') {
            next({ path: '/' })
            NProgress.done()
        } else {
            // Check whether the user has obtained his permission roles
            if (UserModule.roles.length === 0) {
                try {
                    // Note: roles must be a object array! such as: ['admin'] or ['developer', 'editor']
                    await UserModule.GetUserInfo()
                    const roles = UserModule.roles
                    // Generate accessible routes map based on role
                    PermissionModule.GenerateRoutes(roles)
                    // Dynamically add accessible routes
                    router.addRoutes(PermissionModule.dynamicRoutes)
                    // Hack: ensure addRoutes is complete
                    // Set the replace: true, so the navigation will not leave a history record
                    next({ ...to, replace: true })
                } catch (err) {
                    // Remove token and redirect to login page
                    UserModule.ResetToken()
                    Message.error(err || 'Has Error')
                    next(`/login?redirect=${to.path}`)
                    NProgress.done()
                }
            } else {
                next()
            }
        }
    } else {
        if (whiteList.indexOf(to.path) !== -1) {
            next()
        } else {
            next('/login')
            NProgress.done()
        }
    }
    // next()
    NProgress.done()
})

router.afterEach(() => {
    NProgress.done() // 結束Progress
})
複製代碼

固然,要在主入口文件引入這個路由守衛文件

import '@/permission' // main.ts

在這個路由守衛工具類中能夠經過角色控制來限制菜單欄的路由地址,各類不一樣的角色配置不一樣的路由

接下來構建骨架,比較中規中矩

404 錯誤路徑

該有的功能仍是得有的,體驗性要友好。具體的就不細講了,能夠參照源碼,在這裏,你能夠爲所欲爲的搭建你想要的UI風格。

路由Icon配置

本項目中的Icon,都是svg類型的矢量圖標通過vue-svgicon類庫轉化成的組件,用法較爲靈活,方便

一、安裝依賴

yarn add vue-svgicon

二、配置svg圖標目錄路徑(任意目錄)和輸出路徑

// package.json

"scripts": {
    ...
    "svg": "vsvg -s ./src/icons/svg -t ./src/icons/components --ext ts --es6"
}
複製代碼

三、在主入口main.ts中引入svgIcon組件,而且全局註冊

// main.ts

import SvgIcon from 'vue-svgicon'

Vue.use(SvgIcon, {
  tagName: 'svg-icon',
  defaultWidth: '1em',
  defaultHeight: '1em'
})
複製代碼

四、執行命令,生成Icon 組件引用

yarn run svg

五、使用參考SvgIcon

好了,至此,vue+ts的整個模板後臺就搭建好了。由於每一個人的業務需求確定不同,因此到這裏算是一個比較乾淨的模板框架了。

固然,你覺得到這裏就結束了嗎,固然不是,確定要多幾個功能,否則,辛辛苦苦玩一次ts項目,確定要玩得盡興一點

言歸正傳。

國際化

國際化對於大廠或者某些有國際化業務的公司來講,是必不可少的,那咱們就先擴展個國際化的功能玩玩吧。

一、首先,安裝vue-i18n

yarn add vue-i18n

二、定製國際化語言包,這裏暫且就只搞中英兩種語言,其它都是共通的

值得注意的是,所謂的國際化語言包,在前端部分,你只能體如今一些固定的位置,好比菜單欄的頁面名稱,表單的label值,placeholder屬性值...即全部非服務端響應的數據,由於服務端響應的數據是不固定的,你是無法作成語言包翻譯成對應語言的。因此,真正的國際化項目,應該是先後端協調好,當切換到對應語言後,傳值對應的國際化參數值,而後服務端同時也配置國際化的數據響應,從而達到整個項目的國際化

├─ src/                # 源碼目錄
│   ├─ lang/
│   ├─── en.ts  
│   ├─── zh.ts
│   ├─── index.ts
...
複製代碼

英文版

// en.ts

export default {
    route: {
        dashboard: 'Dashboard',
        commonTable: 'Common Table',
        helpCenter: 'Help Center',
        salary: 'Salary',
        firstStep: 'First Step',
        secondStep: 'Second Step',
        sendRecord: 'Send Record'
    },
    login: {
        mobilePhone: 'Please enter your mobile number',
        password: 'Please enter your Password',
        btn: 'login'
    }
}
複製代碼

中文版

export default {
    route: {
        dashboard: '首頁',
        commonTable: '表格',
        helpCenter: '幫助中心',
        salary: '工資條',
        firstStep: '第一步',
        secondStep: '第二步',
        sendRecord: '發送記錄'
    },
    login: {
        mobilePhone: '請輸入手機號碼',
        password: '請輸入密碼',
        btn: '登陸'
    }
}
複製代碼

總的來講,就是取他們所表達意義的label值作成可配置化的對象

三、綜合國際化語言包

import Vue from 'vue'
import Cookies from 'js-cookie'
import VueI18n from 'vue-i18n'

// element-ui built-in lang
import elementEnLocale from 'element-ui/lib/locale/lang/en'
import elementZhLocale from 'element-ui/lib/locale/lang/zh-CN'

// User defined lang
import enLocale from './en'
import zhLocale from './zh'

Vue.use(VueI18n)

const getLanguage = () => Cookies.get('language')

const messages = {
    en: {
        ...enLocale,
        ...elementEnLocale
    },
    zh: {
        ...zhLocale,
        ...elementZhLocale
    },
    ... // 這裏若是有其它語言包繼續按照規則添加便可
}

export const getLocale = () => {
    const cookieLanguage = getLanguage()
    if (cookieLanguage) {
        return cookieLanguage
    }

    const language = navigator.language.toLowerCase()
    const locales = Object.keys(messages)
    for (const locale of locales) {
        if (language.indexOf(locale) > -1) {
            return locale
        }
    }

    // Default language is english
    return 'en'
}

const i18n = new VueI18n({
    locale: getLocale(),
    messages
})

export default i18n
複製代碼

四、主入口main.ts注入

import i18n from '@/lang'
Vue.use(ElementUI, {
  i18n: (key: string, value: string) => i18n.t(key, value)
})

new Vue({
  ...
  i18n,
}).$mount('#app')
複製代碼

注意:

Vue.use裏面的i18n只是element-ui的框架屬性配置,真正全局化的國際化配置,必定要跟路由和狀態管理同樣注入到new Vue()

國際化到這裏就配置完成了,那怎麼使用呢

五、國際化配置應用

① 登陸頁應用

登陸頁就一個表單,手機號和密碼,在這個頁面中的國際化就是對他們的labelplaceholder屬性名的值作配置

<template>
    <div class="login-container">
        <lang-select />
           <el-form>
	           <!--手機號-->
               <el-form-item>
                   <el-input :placeholder="$t('login.mobilePhone')"
                   >
                   </el-input>
               </el-form-item>
               <!--密碼-->
               <el-form-item>
                   <el-input :placeholder="$t('login.password')"
                   >
                   </el-input>
               </el-form-item>
               <footer>
                   <el-button>
                    {{ $t('login.btn') }}
                   </el-button>
               </footer>
           </el-form>
    </div>
</template>

<script lang="ts">
import LangSelect from '@/components/LangSelect/index.vue'

@Component({
    name: 'login',
    components: {
        LangSelect
    }
})
export default class extends Vue {}

</script>

複製代碼

語法糖: 具體國際化語言切換,就是經過這種變量值取值方式根據以前的國際化語言包取值的$t('...')

固然,這裏還有個語言切換工具,以前全部的配置都是怎麼取值,那這個語言切換工具呢就是設值語言值得工具,即上面components中的LangSelect

@Component
export default class extends Vue {
    get language() { // 把language語言值存進vuex,這裏是取值
        return AppModule.language
    }

    private handleSetLanguage(lang: string) {
        this.$i18n.locale = lang
        AppModule.SetLanguage(lang)
        this.$message({
            message: 'Switch Language Success',
            type: 'success'
        })
    }
}
複製代碼

切換後報如下錯誤

解決:

// src/shims-vue.d.ts

declare module 'element-ui/lib/locale/lang/*' {
  export const elementLocale: any
}
複製代碼

擴展內容

換膚

換膚和前面的定製主題仍是有區別的,概念的話就不作過多介紹了,能夠自行體會

這裏就不細說了,能夠查看@/components/ThemePicker,更換的全部主題色在@/styles/element-variables.scss配置

輪播圖和echarts

安裝依賴

yarn add echarts yarn add @types/echarts --dev

輪播圖和echarts都在首頁Dashboard頁面裏,能夠自行參考

表格分頁、搜索

此外,還新加了一個表格分頁和搜索的頁面供參考

詳細請參考表格所在的路由頁面

這裏,爲了體驗性友好,表格的分頁切換引進了一個滾動到最頂端的小動畫

登陸背景動效圖

到這裏差很少這個項目就要結束了,整個的項目架構來講,越輕量級越好,因此就不作太多擴展了,儘可能開箱即用

在此,爲了登陸頁不顯得那麼寒磣,仍是給加個動效吧

好了,大功告成。撒花撒花~~~

分享不易,喜歡的話必定別忘了點💖!!!

只關注不點💖的都是耍流氓,只收藏也不點💖的也同樣是耍流氓

結束👍👍👍

源碼

vue-typescript-admin-element-ui typescript+vue實戰

uni-app 小程序 uni-app小程序手把手項目實戰

React+antd+Redux-saga實戰 手把手帶你搭建React16+Router+Redux-saga+Antd後臺管理系統

參考

vue-typescript-admin-template

相關文章
相關標籤/搜索