前言
本文介绍的是有关于vue方面的前端工程化实践,主要通过实践操作让开发人员更好的理解整个前端工程化的流程。
本文通过开发准备阶段、开发阶段和开发完成三个阶段开介绍vue前端工程化的整体过程。
准备阶段
准备阶段我将其分为:框架选择、规范制定、脚手架搭建。
框架选择
vue的框架有很多,有脚手架框架例如vue提供脚手架、nuxt等,还有vue版本对应的版本;UI组件库可选择性更多,element、Ant Design、Naive等,还有自封的ui组件库。
本文的实践选择的脚手架框架为:vue3+vite+typescript。
使用的UI组件库为:element组件库。
框架选择:
脚手架:vue3+vite+typescript
UI组件库:element
规范制定
规范制定主要有一下几个点
结构规范
即项目的目录规范,每个项目的文件夹命名代表的含义,公共组件和页面组件应该存在在哪,工具类,静态文件应该存放在哪个文件夹,应该如何命名等。
1.规范一:功能分离结构
作者自己取得名,即各个文件夹功能分离。
如上图所示,每个文件夹又有其含义,每个有特定含义的文件都应该放到对应的文件夹内。
api文件夹
用于存放接口请求的文件夹,内部文件命名有两种规则(这个是根据作者自身开发经验总结的)
1.api文件夹内不允许子文件夹的存在,文件名应该以路由名+api作为文件名;
2.api文件夹内可以有子文件夹,子文件夹名称应该与路由文件名同名,文件名按照其含义进行命名+api,切文件夹结构应该只允许存在二级结构;
对于第一种规则,如下图所示:
页面路由home-page下的全部请求,应该放在home-page-api文件中。这种规则适合于项目接口较少的项目,一般一个页面路由全部请求不超过20个推荐使用这种方式。
对于第二种规则:如下图所示
在api文件夹内建一个与页面路由同名的文件夹,该文件夹命名规则为页面路由子路由文件夹名+api的形式,即该子路由内的全部请求方法放到对应的api中的文件内。这种规则适用于路由页面请求方法比较多的情况下使用。
assets文件夹
文件夹要求:建于路由页面同名的文件,用于存放该路由下的图片资源,文件夹不可有三级结构,对于组件所需的图片资源,应该统一存放在一个统一的文件夹内。
components文件夹
按照组件名命名文件夹
pages-view文件夹
用于存放页面组件,pages页面路由入口,pages-view存放对应页面。具有共性的页面组件可以统一放到一个文件夹或者提到components文件夹。
例如下图所示:红色框表示的是首页路由,则绿色框怎就是对应的页面组件(关系为路由组件为页面组件提供出口,路由组件只做简单props接收和简单逻辑运算)
router
用于存放路由配置,如果路由过多建议拆分路由。
store
用于存放vuex等配置,但是项目中贡献数据不建议使用vuex而是建议存放在缓存中。
utils
用于存放工具类和工具方法
2.规范二:模块分离结构
所谓的模块聚合结构,则表示的是将api请求,页面组件,路由配置都存放到view文件夹中,router作为主配置引用子路由配置。对于根路径下的components、assets和store作用和规范一的作用一样。
此处下的pages-views、api和router文件夹下的文件名要求不太高,只要文件名有特定含义即可,但是文件夹下目录不可找过三级。
3.总结
- 规范一:适用于小项目,协作人员少的项目,结构清晰明了,维护方便,弊端是协作性比较差。
- 规范二:适用于中大型项目,协作人员多的项目,开发人员只要在自己的目录下开发即可,不相互影响,提交代码不容易发生冲突。
命名规范
命名规范主要有:文件和文件夹命名规范、变量&方法&计算属性&类&接口命名规范、css类名命名规范、html组件引用规范&导入组件名和组件名规范。
1.文件和文件夹命名规范
文件和文件夹命名:全为小写字母,多单词之前用横线隔开,主要是为了适配window和mac,windows不区分大小写,mac区分大小写。
2.变量&方法&类&接口命名规范
变量&方法&计算属性:使用小驼峰命名法,其中方法末尾需要加Fn,计算属性末尾添加Computed(这个是作者个人习惯)。
类&接口:使用大驼峰命名法,其中接口末尾加Types(作者个人习惯,用于区分类)。
3.css类名命名规范——BEM
使用BEM命名规范。
BEM(Block,Element,Modifier)是一种基于组件的web开发思想。这种开发思想主张将用户界面划分为独立的块。这使得网页开发变得简单快捷,即使是复杂的用户界面,也可以重用现有的代码,而无需复制和粘贴。
Block - 一个功能独立的页面组件,可以重用。
- block名称描述了此模块的用途,它是什么?
- 各个模块可以相互嵌套,嵌套层级数量不受限
例如:
<!-- `header` block -->
<header class="header">
<!-- Nested `logo` block -->
<div class="logo"></div>
<!-- Nested `search-form` block -->
<form class="search-form"></form>
</header>
header 模块嵌套了logo模块和搜索表单模块
Element - 块的组成模块,不能与块分开使用,也不能自己单独使用。
- element名称描述了在这模块中的用途,它是什么?
- element名称的语法结构为 **block-name__element-name**,使用双下划线将block名称和element名称连接起来。
- element 元素彼此之间可以相互嵌套,嵌套层级数量不受限
- 一个element元素里可以嵌套包含一个block块,这就意味着,element名称定义不能为多层级结构,如 block__elem1__elem2 ,这种命名是不被允许的。
例如:
<form class="search-form">
<div class="search-form__content">
<input class="search-form__input">
<button class="search-form__button">Search</button>
</div>
</form>
search-forminput,search-formbutton,search-form__content 即为element元素。
search-form__content 元素中嵌套了element元素。
但是search-form__content__input 这种多层级命名element元素是不被允许的,类名过长,层级结构过多,不清晰。
Modifier - 定义block块或element元素的外观、状态或行为。
- modifier 的名称一般描述了block块或element元素的外观,它的大小?它的状态?它的颜色
- modifier 的名称语法结构为:block-name_modifier-name;block-name__element-name_modifier-name
- 一般使用单下划线将它跟block元素或者element元素连接起来;布尔形式,区分是或不是的状态,完整的语法结构为:block-name_modifier-name;block-name__element-name_modifier-name
例如:
<form class="search-form search-form_theme_islands">
<input class="search-form__input">
<!-- The `button` element has the `size` modifier with the value `m` -->
<button class="search-form__button search-form__button_disabled">Search</button>
</form>
search-form__button_disabled 这种命名结构是布尔形式。
key-value键值对的形式,区分不同的状态。完整的语法结构则为:
- block-name_modifier-name_modifier-value
- block-name__element-name_modifier-name_modifier-value
例如:
<form class="search-form search-form_theme_islands">
<input class="search-form__input">
<!-- The `button` element has the `size` modifier with the value `m` -->
<button class="search-form__button search-form__button_size_m">Search</button>
</form>
search-form__button_size_m 这种命名结构就是键值对的形式
- modifier 不能被单独使用,必须与block元素或者element元素联合使用。因为一个modifier就是用来描述此元素的外观、大小、一个实体的状态。
BEM的优点与缺点?
优点
- 结构简单,一目了然
- 组件化,代码复用
- 不使用标签选择器,避免父级元素内的标签的受影响。举个例子,商品详情页是允许商家自定义标签的,那么商家展示区域标签的祖先元素,一旦用标签选择器定义了样式,子子孙孙都要背负.
例如,将这个网页拆分成BEM的写法
无BEM写法:
<section>
<h1>Sterling Calculator</h1>
<form action="process.php" method="post">
<p>Please enter an amount: (e.g. 92p, £2.12)</p>
<p>
<input name="amount">
<input type="submit" value="Calculate">
</p>
</form>
</section>
BEM 写法:
<section class="widget">
<h1 class="widget__header">Sterling Calculator</h1>
<form class="widget__form" action="process.php" method="post">
<p>Please enter an amount: (e.g. 92p, £2.12)</p>
<p>
<input name="amount" class="widget__input widget__input_amount">
<input type="submit" value="Calculate" class="widget__input widget__input_submit">
</p>
</form>
</section>
元素清单:
- widget
- widget__header
- widget__form
- widget__input
这样就形成了一个可复用的块
注意其中的 widget__input_amount 和 widget__input_submit为Modifier
缺点
- 类名变的更长,一个元素可能拥有多个class
- id选择器无用武之地
- class命名不能重复
- Block的抽象至关重要
谁适用于BEM
项目复杂,复用模块较多,多人协作团队使用。
4.html组件引入规范&导入组件名和组件名规范
html组件引入的组件名称使用大驼峰,带结束标签
导入组件名使用大驼峰,
import TipsDialog from '../tips-dialog/index.vue'
组件名规范使用大驼峰
defineOptions({
name: 'CompModel'
})
代码规范
代码规范主要是代码校验和自动格式化以及git提交规范校验。
1.代码校验和自动格式化
代码校验和代码格式化两个是相辅相成的,通过插件对代码进行校验并自动格式化成符合要求的格式。要实现需要依赖两个插件eslint和prettier。
配置eslint
下载依赖
yarn add -D eslint eslint-plugin-vue
npx eslint --init
init
命令会自动生成 .eslintrc.js
修改配置为:
module.exports = {
root: true,
ignorePatterns: ['node_moduls/*'],
env: {
browser: true,
es2021: true,
node: true,
commonjs: true
},
extends: ['eslint:recommended', 'plugin:vue/essential', 'prettier'],
overrides: [],
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module'
},
plugins: ['vue', 'prettier'],
rules: {
'linebreak-style': ['error', 'unix'],
'no-multiple-empty-lines': [1, { max: 2 }], //空行最多不能超过2行
'vue/multi-word-component-names': 'off' //vue组件名去掉多单词限制
}
}
配置文档
规则 | Rules_Eslint_参考手册_非常教程 (verydoc.net)
配置代码风格工具prettier
安装
yarn add -D prettier eslint-config-prettier eslint-plugin-prettier
创建 .prettierrc
{
"useTabs": false,
"tabWidth": 4,
"printWidth": 80,
"singleQuote": true,
"trailingComma": "none",
"semi": false,
"endOfLine": "lf"
}
官方地址
Configuration File · Prettier 中文网
2.git提交规范
相关配置在这篇文章,内容有点多
git提交规范-CSDN博客
api和数据规范
这个部分需要后端配置,后端返回的code需要代表一定含义,这样才能做统一数据处理,例如token失效的code为多少,接口超时的code或者接口报错的返回code为多少。
1.api封装
这个部分比较灵活,需要按照公司的业务或者开发的个人习惯进行封装,以下是一个例子:
import axios, { AxiosRequestConfig } from 'axios'
import qs from 'qs'
import { getToken } from './userInfo/token'
import { subscribe, MetaDataHelper } from '@gogoal/fund-utils'
import { blob2File } from './tools'
import { isFileList, isObject } from './types'
const SUCCESS_CODE = 0
const service = axios.create({
// timeout: 200000, // request timeout
})
// response interceptor
service.interceptors.response.use(
(response) => {
const res = response.data
if (res.code === SUCCESS_CODE) {
return res.data
} else {
subscribe.notify('xhr-fail', res)
console.log('xhr-fail response', response)
return Promise.reject(res)
// if (res.code == '1100' || res.code == '1103') {
// dealLogout()
// } else {
// return Promise.reject(res)
// }
}
},
(error) => {
return Promise.reject({
code: 1,
message: '服务器错误',
error
})
}
)
interface RequestDataParams extends AxiosRequestConfig {
needToken?: boolean /* 是否需要token */
needToString?: boolean /* 是否需要将参数转成token */
data?: Record<string, any>
headers?: Record<string, any>
config?: Record<string, any>
}
// 请求接口数据
// interface ResponseData<T = any> {
// code: number
// data: T
// message: string
// }
const getData = function <T>({
url = '',
params = {},
data,
method = 'GET',
needToken = false,
needToString = false,
headers,
config
}: RequestDataParams): Promise<T> {
if (needToken) {
const token = getToken()
params.token = token
}
const _params = {
url,
method
}
// deleteEmptyProperty(params);
if (['POST', 'PUT', 'PATCH'].includes(method.toUpperCase())) {
Object.assign(_params, {
data: needToString ? qs.stringify(data) : data,
params: params
})
} else {
Object.assign(_params, {
params: params
})
}
if (headers) {
Object.assign(_params, {
headers
})
}
if (config) {
Object.assign(_params, {
config
})
}
return service(_params)
}
/**
* @description 公用 GET 请求
*/
const get = function <T>({
url,
params,
needToken = false,
needToString = false
}: RequestDataParams) {
return getData<T>({
url,
params,
needToken,
method: 'GET',
needToString
})
}
/**
* @description 公用 POST 请求
*/
const post = function <T>({
url,
params,
data,
needToken = false,
needToString = false
}: RequestDataParams) {
return getData<T>({
url,
params,
data,
needToken,
method: 'POST',
needToString
})
}
const upload = function ({ url, data, params = {}, needToken = false }) {
// debugger
const _url = url || '/file/comm_upload_file'
const _data = new FormData()
// 将得到的文件流添加到FormData对象
Object.keys(data).forEach((_k) => {
const _p = data[_k]
if (isObject(_p)) {
_data.append(_k, JSON.stringify(_p))
} else if (isFileList(_p)) {
Array.from(_p).forEach((res) => {
_data.append(_k, res)
})
} else if (isArray(_p)) {
_p.forEach((res) => {
getType(res) == 'File' && _data.append(_k, res)
})
} else {
_data.append(_k, _p)
}
})
return getData({
url: _url,
method: 'POST',
data: _data,
params,
needToken
})
}
function formatFile(query) {
const _params = new FormData()
// 将得到的文件流添加到FormData对象
Object.keys(query).forEach((_k) => {
const _p = query[_k]
if (isObject(_p)) {
// _params.append(_k, JSON.stringify(_p))
_params.append(_k, _p)
} else if (isFileList(_p)) {
_p.forEach((res) => {
_params.append(_k, res)
})
} else {
_params.append(_k, _p)
}
})
return _params
}
const downloadBlob = function (url, fileName) {
const xhr = new XMLHttpRequest()
xhr.open('get', url, true)
xhr.responseType = 'blob' // 返回类型blob
// 定义请求完成的处理函数,请求前也可以增加加载框/禁用下载按钮逻辑
xhr.onload = function () {
// 请求完成
if (this.status === 200) {
// 返回200
const blob = this.response
blob2File(blob, fileName)
}
}
// 发送ajax请求
xhr.send()
}
type File = {
url: string
query?: string
params?: string
fileName: string
config: any
}
const downloadFile = ({ url, query, params, fileName, config = {} }: File) => {
const _config: AxiosRequestConfig = Object.assign(
{
method: 'post',
headers: {
'Content-Type': 'application/x-www-form-urlencoded' // 请求的数据类型为form data格式
},
responseType: 'blob'
},
config
)
let _url = url || '/file_export/excel/common'
if (query) {
_url += `?${qs.stringify(query)}`
}
_config.url = _url
_config.headers['X-Pro'] = MetaDataHelper.getId()
if (params) {
_config.data = qs.stringify(params)
}
return new Promise<void>((resolve, reject) => {
axios(_config as AxiosRequestConfig)
.then((response) => {
const { data, message } = response
if (!data && message) {
reject(response)
return
}
blob2File(data, fileName)
resolve()
})
.catch((error) => {
reject(error)
})
})
}
export default getData
export {
service /* 导出方便修改默认参数 */,
get,
post,
upload,
downloadFile,
downloadBlob,
formatFile
}
使用:
const response = await service({
url: '/api/v1/zyfp_account_home/bind_mobile',
method: 'post',
data: {
source: 'login',
mobile: bindForm.value.mobile,
sms_code: bindForm.value.sms_code
}
})
2.数据规范
这个需要后端定,主要是接口返回的数据格式,例如返回code规范,返回对象规范等,这个需要根据具体情况具体分析。