俗话说【工欲善其事,必先利其器】,在咱们的前端领域也是同样的道理,想作好一个项目,必需要要有一个完整的项目架构作为支撑,才能更好的进行团队合做、为业务作嫁衣。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 文件
效果以下:
以上只是简单的概述,详细内容请看 源码地址
【笔记不易,如对您有帮助,请点赞,谢谢】