俗話說【工欲善其事,必先利其器】,在咱們的前端領域也是同樣的道理,想作好一個項目,必需要要有一個完整的項目架構作爲支撐,才能更好的進行團隊合做、爲業務作嫁衣。javascript
往往新建一個項目都須要進行一些基礎的配置、當咱們沒有作一個基礎模版的時候都須要從其它的項目複製過來,甚至須要本身重寫一些工具類,這時候咱們就急須要這個基礎模版。css
源碼地址html
有新項目的👩🎓,而且想嘗試 Vue + TypeScript 開發的可用這套模板
複製代碼
根據權限動態添加路由
)自動化測試
埋點
【紅色部分還未完成】前端
選擇 [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 tree
java
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.development
webpack
# 指定模式
NODE_ENV = "development"
# Ajax 地址
VUE_APP_REQUEST_URL = 'http://localhost:8080'
複製代碼
.env.production
ios
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" // 生產
}
複製代碼
const App = () => import(/* webpackChunkName: app */ './app.vue')
/* webpackChunkName: app */
組件分塊
const App = resolve => require(["./app.vue"], resolve)
經過
webpack
的require.context
來進行遞歸式模塊引入
require.context(
directory: String,
includeSubdirs: Boolean /* 可選的,默認值是 true */,
filter: RegExp /* 可選的,默認值是 /^\.\/.*$/,全部文件 */,
mode: String /* 可選的, 'sync' | 'eager' | 'weak' | 'lazy' | 'lazy-once',默認值是 'sync' */
)
複製代碼
require.context 的參數不能接收變量
複製代碼
【好處:減小多人開發的衝突、新模塊忘記引入】
api 統一管理,項目中咱們一個頁面的【增刪改查】url 都是統一的,只會改變請求方式,因此將 url 集中管理
命名空間
解決不一樣模塊之間 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()]
}
複製代碼
【功能列表】
請求地址的處理
主要處理路由的規範性
/** * 處理路徑 * @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 結合隊列來實現。(隊列中存放的是當前請求的信息(自定義的一些規則,來判斷是不是同一個請求)和取消函數)
[缺點:]
相似這種取消請求,其實服務端是有收到的,只是瀏覽器層面作了一層處理等不到響應而已。
當須要作防止數據的重複提交的時,這種方式的實現是不許確的,能夠考慮防抖、變量控制函數的執行、變量控制按鈕的點擊狀態等
postMessage 不能發送函數
[思路:]
<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[]
}
複製代碼
當前行的編輯、根據權限展現不一樣的按鈕、按鈕的加載跟禁用狀態
使用
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 文件
效果以下:
以上只是簡單的概述,詳細內容請看 源碼地址
【筆記不易,如對您有幫助,請點贊,謝謝】