Vue + TypeScript + Element-ui + Axios 搭建前端項目基礎框架

俗話說【工欲善其事,必先利其器】,在咱們的前端領域也是同樣的道理,想作好一個項目,必需要要有一個完整的項目架構作爲支撐,才能更好的進行團隊合做、爲業務作嫁衣。javascript

往往新建一個項目都須要進行一些基礎的配置、當咱們沒有作一個基礎模版的時候都須要從其它的項目複製過來,甚至須要本身重寫一些工具類,這時候咱們就急須要這個基礎模版。css

源碼地址html

有新項目的👩‍🎓,而且想嘗試 Vue + TypeScript 開發的可用這套模板
複製代碼

基礎功能列表

  • 目錄結構的劃分
  • 環境的區分(開發、測試、生產)
  • 路由自動化管理、按需加載
  • 頁面加載進度提示
  • api 管理
  • Vuex / 自定義的狀態管理
  • axios 的封裝(重複請求取消,多個請求發送時只出現一個loading,token 失效從新刷新)
  • 通用的工具函數(防抖、截流等)
  • 常見指令的封裝(動畫指令、圖片懶加載、複製指令等)
  • Web Workers 的引入(開啓一個線程、分擔主線程的計算壓力、在處理特別耗時的任務中特別有用)
  • WebSocket 的嵌入(雙向通信)
  • 多頁面配置
  • Element-ui(表格、搜索、分頁組件的封裝、主題、國際化等)
  • git commit 提交記錄的優化
  • 移動、pc端的適配
  • 權限的處理(按鈕權限, 根據權限動態添加路由
  • 自動化測試
  • 埋點

【紅色部分還未完成】前端

建立項目

選擇 [Vue Cli](https://cli.vuejs.org/zh/guide/) 腳手架 快速建立

`vue create xxx`
複製代碼

目錄結構

mac 下安裝 brew 
複製代碼

/bin/zsh -c "$(curl -fsSL https://gitee.com/cunkai/HomebrewCN/raw/master/Homebrew.sh)"vue

安裝 tree
複製代碼

brew install treejava

tree 列出目錄結構node

├── README.md			說明文件
├── babel.config.js		bable 配置文件
├── jest.config.js		單元測試配置文件
├── package.json		項目信息文件
├── public				
│   ├── favicon.ico
│   ├── index.html
│   ├── other.html
│   └── static			靜態資源文件
│       ├── css			
│       │   └── reset.css
│       └── worker		Web Workers 文件夾(根據 Web Workers 的特殊性、須要放在在服務器)
│           └── test.worker.js
├── src
│   ├── api				api 管理,按多頁面分文件夾
│   │   ├── default-page
│   │   │   ├── index.ts			導出 api
│   │   │   └── testModule.api.ts	頁面下的小模塊 api
│   │   └── other-page
│   │       ├── index.ts
│   │       └── newsModule.api.ts
│   ├── assets			靜態資源文件,但會通過 webpack 進行編譯,不須要編譯的能夠放到 public 目錄下
│   │   └── styles		公共樣式(基礎樣式、其它公用樣式)
│   │       ├── common.scss
│   │       └── pageAnimate.scss
│   ├── components
│   │   ├── business	業務組件
│   │   │   └── xw-list
│   │   │       ├── index.ts
│   │   │       ├── index.type.ts
│   │   │       └── index.vue
│   │   ├── common		基礎組件
│   │   │   ├── xw-pagination
│   │   │   │   ├── index.type.ts
│   │   │   │   └── index.vue
│   │   │   ├── xw-search
│   │   │   │   ├── generateEl.vue
│   │   │   │   ├── index.type.ts
│   │   │   │   └── index.vue
│   │   │   └── xw-table
│   │   │       ├── coustomColumn.vue
│   │   │       ├── generateElTable.ts
│   │   │       ├── generateElTableColumn.ts
│   │   │       ├── index.type.ts
│   │   │       └── index.vue
│   │   └── example		例子文件
│   │       ├── langExample.vue
│   │       ├── requestExample.vue
│   │       ├── vuexExample.vue
│   │       ├── workerExample.vue
│   │       └── wsExample.vue
│   ├── directive		指令
│   │   ├── animate.directive.ts
│   │   ├── copy.directive.ts
│   │   ├── debounce.directive.ts
│   │   ├── draggable.directive.ts
│   │   ├── emoji.directive.ts
│   │   ├── index.ts
│   │   ├── longpress.directive.ts
│   │   └── permissions.directive.ts
│   ├── i18n			國際化
│   │   ├── index.ts
│   │   └── lang
│   │       ├── en.ts
│   │       └── zh.ts
│   ├── layout			項目佈局
│   │   ├── base.layout.vue
│   │   └── other.layout.vue
│   ├── mock			mock 數據
│   │   └── index.js
│   ├── plugins			項目插件
│   │   ├── config.ts
│   │   ├── index.ts
│   │   └── lazyLoad.plugin.ts
│   ├── router			路由管理,按多頁面分文件夾
│   │   ├── config.ts
│   │   ├── default
│   │   │   └── module1.router.ts	頁面下的小模塊 api
│   │   ├── globalHook.ts			全局路由鉤子
│   │   ├── index.ts				導出全部路由
│   │   └── other
│   │       └── module1.router.ts
│   ├── shims-tsx.d.ts
│   ├── shims-vue.d.ts
│   ├── store			Vuex管理,按多頁面分文件夾
│   │   ├── common					基礎的 Vuex 模塊
│   │   │   ├── permissions.vuex.ts
│   │   │   └── user.vuex.ts
│   │   ├── default
│   │   │   └── home.vuex.ts
│   │   └── index.ts				導出基礎的 Vuex 模塊
│   ├── theme			主題
│   │   ├── fonts
│   │   │   ├── element-icons.ttf
│   │   │   └── element-icons.woff
│   │   └── index.css
│   ├── types			類型控制文件(提供語法提示)
│   │   └── vue.d.ts
│   ├── utils			工具函數文件夾
│   │   ├── common.ts				通用的 js 函數
│   │   ├── dom.ts					dom 操做相關的
│   │   ├── eventCenter.ts			發佈訂閱者模式(事件管理中心)
│   │   ├── progressBar.ts			頁面進度條
│   │   ├── readyLocalStorage.ts	讀取本地存儲數據(用戶信息、token、權限等)並存到 Vuex 中
│   │   ├── request					Ajax 請求封裝
│   │   │   ├── index.ts
│   │   │   ├── index.type.ts
│   │   │   └── request.ts
│   │   ├── requestInstance.ts		Ajax 實例
│   │   ├── useElement.ts			按需使用 Element-ui
│   │   └── ws.ts					WebSocket 通信
│   └── views			按多頁面分文件夾
│       ├── 404.vue
│       ├── default-page	
│       │   ├── App.vue
│       │   ├── main.ts
│       │   └── test-module			小模塊
│       │       ├── home			具體頁面
│       │       │   └── index.vue
│       │       └── home2
│       │           └── index.vue
│       ├── login.vue
│       └── other-page
│           ├── App.vue
│           ├── main.ts
│           └── news-module			小模塊
│               ├── news1			具體頁面
│               │   ├── components	頁面內組件
│               │   │   └── coustomColumnHeader.vue
│               │   └── index.vue
│               └── news2
│                   ├── components
│                   │   └── coustomColumnHeader.vue
│                   └── index.vue
├── tests
│   └── unit
│       └── example.spec.ts
├── tsconfig.json
├── vue.config.js		webpack 配置文件
├── yarn-error.log
└── yarn.lock
└── .env.development 	本地環境配置
└── .env.production		生產環境配置
└── .env.staging		測試環境配置
複製代碼

環境區分

經過 webpack 提供的模式來實現不一樣的 環境變量linux

  • 根目錄下分別新建一下三個文件

.env.developmentwebpack

# 指定模式
NODE_ENV = "development"
# Ajax 地址
VUE_APP_REQUEST_URL = 'http://localhost:8080'
複製代碼

.env.productionios

NODE_ENV = "production"
VUE_APP_REQUEST_URL = 'http://prod.com'
複製代碼

.env.staging

NODE_ENV = "production"
VUE_APP_REQUEST_URL = 'http://staging.com'
複製代碼
  • 添加編譯命令

package.json

"scripts": {
  "serve": "vue-cli-service serve --mode development",		// 開發
  "build:stage": "vue-cli-service build --mode staging",	// 測試
  "build": "vue-cli-service build"							// 生產
}
複製代碼

路由自動化管理、按需加載

按需加載

import() 方式 【推薦】

const App = () => import(/* webpackChunkName: app */ './app.vue')

/* webpackChunkName: app */ 組件分塊

異步組件的方式

異步組件

const App = resolve => require(["./app.vue"], resolve)

路由懶加載官方文檔

自動化路由

經過 webpackrequire.context 來進行遞歸式模塊引入

require.context(
  directory: String,
  includeSubdirs: Boolean /* 可選的,默認值是 true */,
  filter: RegExp /* 可選的,默認值是 /^\.\/.*$/,全部文件 */,
  mode: String  /* 可選的, 'sync' | 'eager' | 'weak' | 'lazy' | 'lazy-once',默認值是 'sync' */
)
複製代碼
require.context 的參數不能接收變量
複製代碼

【好處:減小多人開發的衝突、新模塊忘記引入】

api 管理

api 統一管理,項目中咱們一個頁面的【增刪改查】url 都是統一的,只會改變請求方式,因此將 url 集中管理

Vuex / 自定義的狀態管理

Vuex

  • 命名空間

    解決不一樣模塊之間 actions mutions 之間的命名衝突

  • 動態註冊模塊 除基礎模塊以外、其它模塊動態註冊及卸載

自定義的狀態管理(發佈訂閱者模式)

【思路:】 一個集合中存儲了不一樣類型的事件函數,並功過監聽、取消、派發等方法來處理這個集合

其中處理單次監聽用到了閉包,來存貯是否已經執行過

once(eventName: string, cb: CbType) {
    const { eventStack } = this
    const eventValue = eventStack[eventName]
    const tempCb = () => {
      let isOutOfDate = false

      return (data: object) => {
        if (isOutOfDate) return
        cb(data)
        isOutOfDate = true
      }
    }

    eventValue ? eventValue.push(tempCb()) : eventStack[eventName] = [tempCb()]
  }
複製代碼

axios 的封裝

【功能列表】

  • 請求地址的處理

    主要處理路由的規範性

    /** * 處理路徑 * @param url 路徑 * @param isBaseURL 是不是根路徑 */
    private transformUrl(url = "", isBaseURL = false) {
      if (!url) return url;
    
      if (isBaseURL) {
        if (!/\/$/.test(url)) {
          return `${url}/`;
        }
    
        return url;
      }
    
      if (/^\//.test(url)) {
        return `${url.substr(1)}`;
      }
    
      return url;
    }
    複製代碼
  • 是否須要 loading,多個請求串行時只出現一個 loading

    用一個變量記錄請求的個數,有新的請求的時候數量 +1, 當數量爲 0 而且須要 loading 的時候開啓 loading,當請求完成以後 -1,並關閉 loading

    /** * Loading 的開啓關閉 * @param customConfig 自定義配置項 * @param isOpen 是否開啓 */
      private handleLoading(customConfig: CustomConfigType, isOpen: boolean) {
            if (!customConfig.isNeedLoading) return;
            // 不重複開啓 Loading
            if (this.requestCount !== 0) return;
    
            if (isOpen) {
                console.log("開啓 Loading");
                return
            }
    
            console.log("關閉 Loading");
      }
    複製代碼
    /** * 發起請求 * @param config 配置項 * @param customConfig 自定義配置 */
    private async transfromRquest(
      config: AxiosRequestConfig,
      customConfig: CustomConfigType = {}
    ): Promise<AxiosResponse> {
      customConfig = { ...this.defaultCustomConfig, ...customConfig };
    
      this.transformUrl(config.url);
      this.handleLoading(customConfig, true);
      this.addToken(config, customConfig);
      this.requestCount++
    
      try {
        const result = await this.axios.request(config);
        return result;
      } catch (error) {
        // ...
      } finally {
          this.requestCount--
    		this.handleLoading(customConfig, false);
      }
    }
    複製代碼
  • 是否須要 token

/** * token 處理 * @param config 配置項 * @param customConfig 自定義配置項 */
  private addToken(config: AxiosRequestConfig, customConfig: CustomConfigType) {
    if (customConfig.isNeedToken) {
      config.headers = {
        token: store.getters['userStore/getToken'] || ''
      };
    } else {
      config.headers = {};
    }
  }
複製代碼
  • 請求錯誤的處理,當出現 token 失效的時候,從新刷新 token 再發送失敗的請求
/** * 發起請求 * @param config 配置項 * @param customConfig 自定義配置 */
  private async transfromRquest(
    config: AxiosRequestConfig,
    customConfig: CustomConfigType = {}
  ): Promise<AxiosResponse> {
    customConfig = { ...this.defaultCustomConfig, ...customConfig };

    this.transformUrl(config.url);
    this.handleLoading(customConfig, true);
    this.addToken(config, customConfig);
		this.requestCount++

    try {
      const result = await this.axios.request(config);
      return result;
    } catch (error) {
      const { code, config } = error

      if (code === 401) {
        // 解決 token 失效的

        // 方案一 跳轉至登陸頁
        // store.commit('userStore/setToken', '')
        // store.commit('permissionsStore/setPermissions', {})
        // router.replace({ path: '/login', query: {
        // redirectUrl: router.currentRoute.fullPath
        // } })

        // 方式二 自動刷新 token 並從新發起失敗的請求
        const res = await this.transfromRquest({
          method: 'post',
          url: '/refresh-token'
        })
        console.log(res, '/refresh-token')
        store.commit('userStore/setToken', res.data.token)
        return this.transfromRquest(config)

        // 方式三 在請求攔截裏面先校驗 token 是否過時 再發起請求
      }

      this.handleError(customConfig, error);
      return Promise.reject(error);
    } finally {
			this.requestCount--
      this.handleLoading(customConfig, false);
		}
  }
複製代碼
  • 取消請求

    利用 Axios 提供的 CancelToken 結合隊列來實現。(隊列中存放的是當前請求的信息(自定義的一些規則,來判斷是不是同一個請求)和取消函數)

[缺點:]

相似這種取消請求,其實服務端是有收到的,只是瀏覽器層面作了一層處理等不到響應而已。

當須要作防止數據的重複提交的時,這種方式的實現是不許確的,能夠考慮防抖、變量控制函數的執行、變量控制按鈕的點擊狀態等

Web Workers 的引入

postMessage 不能發送函數

WebSocket 的嵌入

封裝一個簡單的 WebSocket 庫

Element-ui 列表組件的封裝

[思路:]

  • 劃分組件,頭部、內容、底部
<header class="list-header animate__animated animate__fadeIn">
      <slot name="head" />
      <xw-search v-if="searchOption" :searchOption="searchOption" :searchParams="searchParams" @onSearch="getList" >
        <slot name="search" />
      </xw-search>
    </header>

    <main class="list-main">
      <slot name="main" />
      <xw-table :tableOption="tableOption" />
    </main>

    <footer class="list-footer">
      <slot name="footer" />
      <xw-pagination v-if="paginationOption" :paginationOption.sync="paginationOption" @onPagination="getList" />
    </footer>
複製代碼
  • 搜索結果由列表組件保管

searchParams: SearchParams = {};

  • 表格數據的組裝

    爲了方便開發過程當中減小模版的編寫,將表格的全部相關操做都封裝成配置項的形式。

import { Component } from 'vue'

export interface TableOption {
  // element-ui 表格的配置屬性
  tableAttribute: TableAttribute
  // 列的配置屬性
  tableColumn: TableColumn[]
}

export interface TableAttribute {
  // 屬性
  props: {
    data: object[]
    [index: string]: any
  }
  // 事件
  on: { [key: string]: Function | Function[] }
}

export interface TableColumn {
  // 屬性
  props: {
    label?: string
    prop?: string
    [index: string]: any
  },
  // 插槽
  slots?: {
    [index: string]: {
      // 屬性
      options?: object
      // 自定義組件
      component: Component
    }
  }
  // 多級表頭
  columnChild?: TableColumn[]
}
複製代碼

複雜例子

當前行的編輯、根據權限展現不一樣的按鈕、按鈕的加載跟禁用狀態

git commit 提交記錄的優化

使用 commitizen 替代你的 git commit , commitizen 還須要適配器的配合,官方推薦 cz-conventional-changelog

  • 安裝

npm install -D commitizen cz-conventional-changelog

  • 配置

package.json中配置:

"scripts": {
    "commit": "git-cz"
  },
"config": {
    "commitizen": {
      "path": "node_modules/cz-conventional-changelog"
    }
  }
複製代碼
  • 使用

npm run commit

  • 自定義適配器

    • 安裝 npm i -D cz-customizable @commitlint/config-conventional @commitlint/cli
    • 配置
"config": {
    "commitizen": {
      "path": "node_modules/cz-customizable"
    }
  }
複製代碼

同時在項目目錄下建立 .cz-config.js .commitlintrc.js 文件

效果以下:

指令的封裝

  • 結合 animate.css 的自定義動畫指令
  • 複製粘貼指令
  • 防抖指令
  • 拖拽指令
  • 禁止表情及特殊字符指令
  • 長按指令
  • 權限控制指令

源碼地址

以上只是簡單的概述,詳細內容請看 源碼地址

博文推薦

【筆記不易,如對您有幫助,請點贊,謝謝】

相關文章
相關標籤/搜索