写在开始
我想写这个系列很久了,困窘于自身经验不足与与生俱来的拖延症,一直没有动笔,但是在 2024 的开始,我打算完成这个系列,算是对这两年工作的一个总结跟分享,这个系列是什么?是我依据工作经验与公司中后台项目中常出现的问题,复盘的一系列的中后台开发最佳实践。
技术栈的介绍
工欲善其事,必先利其器,init 项目前我们需要进行开发工具的选取。我个人将需要工具分为两类,一类是基本固定死的,基本是 Vue3 中后台开发的最佳实践的组合拳了,另外一类是可以进行考虑的。
固定工具栈
Vue3 + typescript + axios + pinia + vue-router + sass/less + vite + pnpm + reset/normalize
里面大多都是官方的工具生态包,关于是否引入 typescript,我个人认为是十分有必要的,尤其是在团队协同开发的中后台项目,能够合理地推断业务数据的类型,并且通过链式可选运算符访问到接下来的内容,不得不说是种很舒服的体验👍。pnpm 就不过多介绍,一个更加优秀的包管理工具。
其他工具栈
这里分享的都是一些有封装好的技术工具,这些工具并不影响项目主流程的进行,它们的服务对象并不是产品,而是开发者,用来提高我们的开发体验,可有可无。
tailwind
为什么不直接用行内样式呢?对比来看,tailwind 相比行内样式的表达能力更强,行内样式的冗余也不利于 HTML 的维护,同时行内样式增加的是 HTML 文件的大小,tailwind 增加的是样式文件的大小,HTML 的体积成本相比样式文件更加昂贵。
原子化 CSS 方案见仁见智,对我来说,tailwind 解决了 CSS 封装的命名负担,回首来看,我维护的大多项目中,可复用的 class 样式只占很小一部分,剩下的都只是为了 BEM 而去命名,因而我选择脱离这种命名模式转投 tailwind。
但使用 tailwind 仍有一些主要的注意点
- tailwind 固执己见的基础样式
tailwind 引入的样式不同于 normalize 跟 reset ,tailwind 在抹平不同浏览器样式不统一的基础上,还对一些元素的默认样式进行了覆盖。
比如 list 进行了无样式化处理
一些元素被重新定义为块级元素
如果你并不期望这种样式,可以通过修改配置文件关闭这种样式文件的引入。
- CSS 到 tailwind 快速转化技巧
一般情况下,Tailwind CSS IntelliSense 这个插件基本满足日常开发了,但是对一些你想批量转换的 CSS 样式,又不想一条一条手打的话,可以通过这个工具进行快速转换
https://transform.tools/css-to-tailwind
- tailwind 的字符串格式化技巧
使用原子化的过程中还容易导致的一个问题是,目前代码格式化工具对字符串之间的空格是无能为力的,这就导致了这样丑陋的 class 写在代码中,需要人工去调整空格间隙。
目前并社区并没有好的解决方案,虽然有相关提案的讨论,但我并不认为 ESlint 作为一个质量检测工具应该加入这些功能。
我目前解决方案是将原子化 class 填入数组,这样就可直接通过 Prettier 格式化了
- 不要为了原子化而去原子化
什么意思,但对于样式是否需要全局原子化,tailwind 提供了 apply 语法以供开发者封装一些自定义的原子化的样式,但是我并不建议这样做
apply 需要在样式文件里面注册 CSS 类
但是在 CSS 文件里面写一些原子化的 class 类名并不是一个很好的注意,与其这样,倒不如直接写样式属性封装 class。
使用 tailwind 并不意味着就不能再封装样式了,事实上,在表现一些父元素 hover、focus,子元素高亮的场景时,将这些表现单独封装到一个样式文件内部还是十分有必要的。
loadash
lodash 提供了日常开发常用的一些工具函数,例如 debounce、throttle、get 等,
跟 lodash 类似的工具包还有 ramda、underscorejs 等
我时常用的,比如对某些嵌套对象内部实现不清楚的,直接一个 get,也不用那么多条件判断与链式兜底了
虽然大部分函数大家自己手写也能实现,但是相比于其他同事实现的工具函数,用这种经过广泛的使用和测试的工具库更加值得信赖。
vue use
VueUse 是基于Composition API 的实用函数集合
比如 useFullscreen、useToggle、useStorage 都是一些十分好用的 hooks,这一些我也在我的开源项目 vue-tsx-admin 中进行了实践。
pinia-plugin-persistedstate
对于 pinia 下的某些全局数据,我既期望响应化,也期望持久化(浏览器 reload 全局状态不丢失),这样我就需要再使用 pinia 的时候还需要对某些状态进行持久化的管理,就以深色浅色主题色切换为例,除了写渲染状态切换逻辑外,还需要关注状态在 localStorage 的存取。
export enum LocalStorageKey {
localeKey = 'vtsc-locale',
applicationTheme='vtsc-theme'
}
export interface ApplicationState {
theme: ApplicationTheme
}
export enum ApplicationTheme {
lingt = 'light',
dark = 'dark'
}
const getSupportTheme = ():ApplicationTheme=>{
const isSupportTheme = (theme:string|null):theme is ApplicationTheme=>{
if(isNull(theme)) return false
return (Object.values(ApplicationTheme) as string[]).includes(theme)
}
const themeValue = localStorage.getItem(LocalStorageKey.applicationTheme)
if(isSupportTheme(themeValue)){
return themeValue
}
return ApplicationTheme.lingt
}
export default defineStore('theme', {
state(): ApplicationState {
return {
theme: getSupportTheme(),
}
},
getters:{
isDark(state:ApplicationState){
return state.theme === ApplicationTheme.dark
}
},
actions: {
updateApplicationTheme(theme:ApplicationTheme){
localStorage.setItem(LocalStorageKey.applicationTheme,theme)
this.theme = getSupportTheme()
},
toggleDarkLightMode() {
if (this.isDark) {
this.updateApplicationTheme(ApplicationTheme.lingt)
} else {
this.updateApplicationTheme(ApplicationTheme.dark)
}
},
}
})
但是安装这个 pinia 插件后,只需要进行 persist 注册,就可以借助插件自动化进行持久化管理。
vue i18n + i18n-ally
通常来说,项目引入国际化之后,对于开发人员来讲,是一件很影响开发体验的事情
因为对于开发者来说,难以从 i18n 的文本去关联到这个 key 值代表的翻译字段
i18n-ally 这个 IDE 插件可以很大程度上优化这种体验,不过这个插件目前文档并不容易入门,导致用的人不多
这里简单做一个教程
download 这个插件后
调用 VScode 命令行 ,选取自动配置语料库目录,这里目的是读取项目的翻译文本
之后在调用命令修改显示语言后,插件基本可以正常运行了
同时文件侧还有其他功能,修改翻译值、使用分析等等,就不细说了
javascript console utils
开发过程中我们经常需要 console 去打印一些数据信息进行调试,但是多出调用console 的话就会导致不确定哪一行使我们想要的打印值,这里就需要添加一些关键的 key 进行区分
javascript console utils 这个 IDE 插件就是将上述这些操作进行简化,直接通过选定想要打印的内容,按下Cmd+Shift+L 快捷键就可以快速构造出带有关键 key 值的 console.log 语句
change-case
维护翻译文本的时候经常需要批量进行一些变量命名的转换,对于这种命名转换的操作,也可以借助插件实现
这个插件基本满足日常命名 case 的转换
甚至可以利用光标选中做一些骚操作
响应式开发工具
前端进行响应式开发时需要适配不同的设备,这就需要我们打开浏览器通过设备工具栏进行调整
responsively 这个工具提供了一个功能,一个屏幕查看内进行多种设备的支持情况,然后进行适配
snipaste
切图工具,可以取色,截图,在屏幕上固定一些图片啊等等,真的好用,甚至一些测试同学都在问这个工具是什么。
一些技巧
- restart ide plugin
volar 跟 typescript 的使用中时常会出现因为编译报错导致的推断类型不正确,导致红色波浪线卡住一直不更新的问题,这里事实上不用重新打开 IDE ,只需要调出命令行,restart 这些插件进行重新编译即可。
- 自定义代码模板
无论是使用 SFC 还是 JSX ,写组件的时候都需要重复的写一些东西,
Vscode 也有一些插件是解决这些问题的,比如 Vue 3 Snippets
但使用起来还是不够灵活,虽然插件提供了大量的模板可以复用,但是注入片段之后还是需要或多或少的删减和添加一些代码,比如 less scss scoped ts等,插件也不会单门为某一个用户去定义一个代码片段。
但事实上这个功能 VScode 实现了,我们不用在借助插件,直接可以自定义化,然后通过代码提示调用
教程为调取 VScode 命令配置用户代码片段
然后写入你期望的代码片段前缀跟代码片段具体内容即可,这里 VScode 接受的代码内容格式需要修改,并不能将代码直接粘贴进去,大家可以使用 https://snippet-generator.app/ 这个工具快速生成,然后写入配置文件
之后键入一些关键字,然后根据提示就可以快速输入代码片段了🍺。
- 组件命名模式修改
大家在使用一些组件库的时候会发现,组件库下载后代码键入提示推荐都是短横线命名,比如 el-button
如果你想修改这种命名模式怎么办?
volar 提供了一个功能修改组件的默认命名模式,位置在 vscode 的底部状态栏
prop 和 自定义的 component 的命名模式跟 prop 提示的命名模式都可以通过这个配置修改
一些约定
一个项目内一定需要有固定约定跟规则,以此作为 CodeReview 的践行准则和约束团队成员开发的规范。
目录结构
- views/: 存放页面级别的组件
- components/: 存放业务级别的 UI 组件
- hooks/: 存放业务级别的逻辑逻辑处理
- utils/: 存放通用的功能性函数,如数据聚合、处理等
- styles/: 存放全局的 CSS 样式、变量、mixins 等
- assets/: 存放静态资源,如图标等
- locale/: 存放应用国际化需要的多语言文件
- api/: 存放接口调用函数
- router/: 存放路由守卫以及项目内路由表
- mock/: 存放 mock 的接口文件
i18n 翻译字段 前端路由表 跟 页面组件进行同构化处理
接口函数根据后端业务进行模块划分,类型单独存放一个文件进行管理控制。
这样做的目的就是进行模块划分,清晰化目录,避免随意命名以及多个文件聚合在 index.js 里面造成几千行的代码
typescript 规范
- 管理类型
引入 typescript 有点很多,但是也会出现一些问题,类型管理一直是项目中比较头疼的存在,一般有两种解决方案,中心化管理跟分散化管理,具体可以查看这个问题的相关讨论 how-to-organize-typescript-interfaces,很那说哪种方案更加优化。
我个人认为,团队开发中每个成员的水平参差不一,同时项目是死的,开发人员是流动的,随意地生产消费类型容易引发循环引用,同时团队开发时,很容易出现不同的地方生产出两种类似的类型导致项目中类型冗余的问题,因此我更喜欢将类型集中起来,需要时在固定的一个地方进行寻找,然后引入。
- type or interface
在 typescript 中,使用 type 还是 interface 是老生常谈的问题,这里我的个人建议是,尽量使用 type。
这里是两篇拓展阅读文章:
什么时候推荐用 type 什么时候用 interface ?
TypeScript type 和 interface 的区别
变量命名规范
我个人最喜欢的命名方式为
- 文件夹: 短横线命名
- 文件: 驼峰命名
- 变量:驼峰命名
同时项目中还需要求尽量不使用任何硬编码的值来保证项目样式的统一性。
什么是硬编码值,通俗来讲,我们经常说的魔法字符串,魔法数字,使用 typescript 我们可以通过使用as const
,enum
为这些硬编码值维护一个地方进行存取。
就以 vue-tsx-admin 来说,维护了那些常量呢?
像 router.push 的 name 名,localStorage 的 key 值,请求响应约定的 Code,这些通过 typescript 维护成了常量
export enum LocaleOptions {
cn = 'zh-CN',
en = 'en-US'
}
export enum LocalStorageKey {
localeKey = 'VUE_TSX_ADMIN_LOCALE',
loginFormKey = 'VUE_TSX_ADMIN_LOGIN_FORM_INFO',
tokenKey = 'VUE_TSX_ADMIN_TOKEN'
}
export enum AppTheme {
light = 'light',
dark = 'dark'
}
export enum ApplicationInfo {
appTitle = import.meta.env.VITE_APP_TITLE
}
// component name
// route name
// keepalive require component name to same
export enum ViewNames {
login = 'login',
redirect = 'redirect',
notFound = 'notFound',
// =============== DIVIDER ==================
dashboard = 'dashboard',
workplace = 'workplace',
monitor = 'monitor',
// =============== DIVIDER ==================
exception = 'exception',
_403 = '403',
_404 = '404',
_500 = '500',
// =============== DIVIDER ==================
form = 'form',
step = 'step',
group = 'group',
// =============== DIVIDER ==================
profile = 'profile',
// =============== DIVIDER ==================
list = 'list',
searchTable = 'searchTable',
cardList = 'cardList',
// =============== DIVIDER ==================
result = 'result',
success = 'success',
error = 'error',
// =============== DIVIDER ==================
user = 'user',
info = 'info',
setting = 'setting'
// =============== DIVIDER ==================
}
export const layoutStyleConfig = {
navbarHeight: 64,
breadcrumbHeight: 52,
tabHeight: 40,
footerHeight: 35
} as const
export enum ResCode {
success = 20000,
error = 50000,
illegalToken = 50008,
expiredToken = 50014,
// Other clients logged
otherLogin = 50012
}
使用时直接通过.
运算符进行访问
CSS 属性也同样需要注意避免硬编码,进行 CSS 变量维护的好处就是减少后期拓展主题切换时的开发成本,主题切换的原理是什么?就是不同的主题下呈现不同的表现形式,开发中应尽量避免直接拿切图后的 value 直接赋值,而需要考虑这些属性的统一性与复用性。
git 规范
git 事实上也需要一些规范以此对线上崩溃时,可以快速、高效的定位、回滚以及发布。
目前业内各种团队开发工作流都是基于 git flow 进行演化的,这点就不多细讲,
这里推荐实践的是另一方面的开发规范,原子化 commit,即团队中每个成员提交的 commit 的内容应该尽量干净且独立,并且与 message 描述的内容保持一致,提交具有尽可能小的大小。每个提交都会做一件,而且只做一件简单的事情,可以用一个简单的句子来概括,可以理解为少食多餐。
这样的好处就是项目 git history 干净且独立,同时进行 revert,cherry-pick 等操作时,心智负担会更小。
本文所涉及的技术在 vue-tsx-admin 中可以找到完整的实例,希望对你写 Vue 的项目有所帮助。欢迎 star 和提出不足。
系列文章: