写在前面
公司前端开发上一直没有统一的规范,不同部门,甚至部门内,每个人都有不同的代码风格,在项目开发上,造成了一些显而易见的问题。再者,我平时要写Python,C#,HTML,JS等代码,它们之间的编程风格差异很大,我很容易将其他语言的风格作用到另一个语言上,尤其是Vue项目,因为我对它接触时间短,对其了解还很浅显,没有形成自身的规范。所以我查找了多个业内前辈的总结,结合我的实际工作经历,带着拼凑性质编写出了下面这个针对Vue项目的开发规范(或者称守则),今后我将尽可能以该规范作为自己的编程行动指南,可能其中一些规范(守则)不适用于其他人,如果有人看到这篇记录时有其他更好的规范或是修改建议,欢迎补充。
[TOC]
工欲善其事,必先利其器。
推荐使用VSCode,一款由微软开发且跨平台的免费源代码编辑器。该软件以扩展的方式支持语法高亮、代码自动补全、代码重构功能,并且内置了命令行工具和Git 版本控制系统。用户可以更改主题和键盘快捷方式实现个性化设置,也可以通过内置的扩展程序商店安装其他扩展以拓展软件功能。1
推荐使用Prettier插件,它是一个自动代码格式化工具,可以解析代码并按照给定的规则重新打印代码,通过引入最大行长度,在必要时对代码进行换行,从而强制执行一致的风格。2在进行团队开发时,遵守一套格式相同的代码有助于代码阅读,减小维护难度。
规则示例,
{
/* prettier的配置 */
"prettier.printWidth": 100, // 超过最大值换行
"prettier.tabWidth": 2, // 缩进字节数
"prettier.useTabs": false, // false缩进不使用tab,使用空格
"prettier.semi": false, // true句尾添加分号
"prettier.singleQuote": true, // true使用单引号代替双引号
"prettier.proseWrap": "preserve", // 默认值。因为使用了一些折行敏感型的渲染器(如GitHub comment)而按照markdown文本样式进行折行
"prettier.arrowParens": "avoid", // 在对象,数组括号与文字之间加空格 "{ foo: bar }"
"prettier.disableLanguages": ["vue"], // 不格式化vue文件,vue文件的格式化单独设置
"prettier.endOfLine": "auto", // 结尾是 \n \r \n\r auto
"prettier.eslintIntegration": false, //不让prettier使用eslint的代码格式进行校验
"prettier.htmlWhitespaceSensitivity": "ignore",
"prettier.jsxBracketSameLine": false, // 在jsx中把'>' 是否单独放一行
"prettier.jsxSingleQuote": false, // 在jsx中使用单引号代替双引号
"prettier.parser": "babylon", // Require a 'prettierconfig' to format prettier
"prettier.stylelintIntegration": false, // 在对象或数组最后一个元素后面是否加逗号(在ES5中加尾逗号)
"prettier.tslintIntegration": false, // 不让prettier使用tslint的代码格式进行校验
}
推荐使用Vue Language Features (Volar) ,一个VSCode上为Vue3打造的语言支持的插件,可以实现原生 TypeScript 语言服务级别的性能。3
安装好Varlo之后,编写Vue组件时,编辑器就能实现==代码高亮==,语法提示,代码跳转,class追溯等功能。
推荐使用ESLint来检查JavaScript代码及Vue组件是否符合规则。 规则可参考Vue官方文档
推荐使用Vue.js devtools 进行调试,它是一个Vue官方浏览器调试插件,开发模式下,可以定位,检查Vue组件及其属性状态(props,data,injecgt,provide,computed等)。4
组件是 Vue 的一个重要概念,它是一种抽象,允许我们使用小型、独立和通常可复用的组件构建大型应用。几乎任意类型的应用界面都可以抽象为一个组件树。组件化能提高开发效率,方便重复使用,简化调试步骤,提升项目可维护性,便于多人协同开发。5
如何发挥Vue组件的优点,以期实现更好的复用、可维护性、扩展性,在设计上可参考如下几点:5
结合上述参考,为使代码易读性、规范性,减少沟通成本,我们需要设计一些规范。
单文件组件应该总是让标签顺序保持为 <template> 、<script>、 <style>
单文件组件的文件名应该要么始终是单词大写开头 (PascalCase),要么始终是横线连接 (kebab-case)。6
复合组件(由多个单/复合组件组合)应将其所有私有组件置于同一目录下,目录命名应该始终使用 camelCase或kebab-cas,其出口文件名推荐命名为index.vue。
Vue组件名应该与其文件名保持或趋于一致,同时二者应该由含有具体语意或表象意义的单词组成,体现组件的功能/业务。
==必须设置组件名== ==项目内必须仅存在camelCase或kebase-case中一种==
// 推荐
export default {
name: "TaskList"
//...
}
// 不推荐
export default {
name: 'tasklist'
}
// 不推荐
export default {
name: 'component'
}
// 不推荐
export default{
name: 'newTask1'
}
和父组件紧密耦合的子组件文件名应该以父组件文件名作为前缀命名,组件名同理,参考:
components
|- todo-list.vue
|- todo-list-item.vue
|- todo-list-item-button.vue
|- user-profile-options.vue (完整单词)
所有代码变量名统一命名为camelCase,名称应该简洁易懂,推荐优先使用语义化英文单词,在不得已时辅以中文拼音,若包含数字,其必须有意义;若简写/缩写单词,其形式应该为大众熟知,或者在团队内部预先约定好单词简写/缩写规则,尽量做到在缺少注释的情况下仅凭代码也能理解逻辑。
// 不推荐使用拼音
let jianchaguize = \/\d{1, 3};
// 更不推荐使用简写拼音
let jcgz = \/\d{1, 3};
// 不推荐,难道还有checkRule2, checkRule3……?
let checkRule1 = \/d{1, 3};
// 推荐
let checkRule = \/\d{1, 3};
// 不推荐
class infomationClass{
//……
}
// 推荐
class InfomationClass{
//……
}
// 大众熟知的简写形式
let info = new InformationClass();
export default {
data(){
return {
cityName: "",
userName: "",
}
}
methods:{
// 不推荐,指向cityName或userName不明确
getName(){
return this.cityName;
},
// 推荐
getCityName(){
return this.cityName;
}
}
}
组件内容为空,其标签应当自闭和。6
<-- 不推荐-->
<MyComponent></MyComponent>
<-- 推荐-->
<MyComponent />
组件模板应该只包含简单的表达式,复杂的表达式则应该重构为计算属性或方法。复杂表达式会让模板变得不那么声明式。我们应该尽量描述应该出现的是什么,而非如何计算那个值,而且计算属性和方法使得代码可以重用。6
推荐
<template>
<p>{{ normalizedFullName }}</p>
</template>
// 复杂表达式已经移入一个计算属性
computed: {
normalizedFullName: function () {
return this.fullName.split(' ').map(function (word) {
return word[0].toUpperCase() + word.slice(1)
}).join(' ')
}
}
不推荐
<template>
<p>
{{
fullName.split(' ').map(function (word) {
return word[0].toUpperCase() + word.slice(1)
}).join(' ')
}}
</p>
</template>
包含多个 attribute 的元素应分行撰写,每个 attribute 一行。
<-- 推荐 -->
<MyComponent foo="a" bar="b" baz="c"
foo="a" bar="b" baz="c"
foo="a" bar="b" baz="c"
/>
<-- 不推荐 -->
<MyComponent foo="a" bar="b" baz="c" foo="a" bar="b" baz="c" foo="a" bar="b" baz="c" foo="a" bar="b" baz="c"/>
推荐按如下顺序编写单文件选项式组件,
<template>
<div></div>
</template>
<script>
export default {
name: 'component-name',
components: {},
props: {},
emits: [],
expose: [],
data() {
return {}
},
computed: {},
watch: {},
created(){},
beforeMount(){},
mounted() {},
beforeUnmount(){},
unmounted() {},
methods: {},
}
</script>
<style scoped>
</style>
原因有:
一,顺序由上到下,正好和组件生命周期一致,可以更方面一览其生命周期内的活动变化。
二,属性结构更清晰。
第一组,name组件名称,属基本信息。
第二组,components,props为外部依赖注入;emits,expose为组件对外暴露的事件及方法;data内则定义组件私有响应式数据。上述属性可以理解为vue组件在运行期间所需的各种内外部依赖,按照变量先定义后使用的原则,将其列在第二位(这些我称之为环境属性)。
第三组,computed和watch ,前一个依赖响应式数据进行计算,另一个监控响应式数据变化,它们共同的特点是存在依赖其他数据,列第三。
第四组是各种生命周期选项及钩子,从created到unMounted(仅列出部分)。
第五组,最后methods单独成组,大部分业务代码集中在methods,导致其庞大臃肿,置于前位,往往将环境属性压到代码深处,将其放于尾部呼应第二组的“用”。
在编写代码时,prop 的定义应该尽量详细,至少需要指定其类型。在声明的时候,其命名应该始终使用 camelCase。6
细致的 prop 定义有两个好处:
建议:
==必要时解释该props属性==
==可选项必须设置默认值==
// 可选
props:{
name:{
type: String,
default: "名称"
}
}
// 不可选
props:{
name:{
type: String,
require: true //必须
}
}
在组件上总是必须用 key
配合 v-for
,以便维护内部组件及其子树的状态。6
<ul>
<li v-for="(task, index) in tasks" :key="index">{{ task.name }}</li>
</ul>
css属性编写顺序推荐,
仅对当前组件生效的样式,必须设置scoped属性,否则会污染全局样式,而如果想让当前样式作用于子组件,那么需要穿透,若是想作用于整个Vue App或是想将该组件内样式作为公共样式在多个组件间共享的话,我们应该将样式剥离出该组件,前者设置为全局样式引用后者单独引入公共样式。
建议:
==总是开启scoped==
<html>
<span class="name">{{ name }}</span>
</html>
//...
<style scoped>
.name{
color: #fff;
}
</style>
// main.js
// 全局引入
import 'path_to_public_css'
<html>
<span class="name">{{ name }}</span>
</html>
//...
<style lang="scss">
//单独引入公共样式
@import url('path_to_shared_css');
</style>
按照不同的角度,我们可以将组件划分为不同的类别, 按职责划分,大概可以拆分为 4 层,越往下通用性越强,业务耦合度也就越低:7
基础组件
:一些业务无关的、原子的基础组件。我们常见的 antd-design, element-ui、iview 这些组件库提供的组件就属于基础组件。它的特征如下:
模式组件
:模式组件依旧是业务无关的,这些组件通常是一些最佳实践和设计模式,旨在将一些重复的事情固化下来,提高开发的效率。比如一些通用的页面布局、表单、表格、导入导出。
它们往往是基础组件的封装,将重复的代码和流程固化/标准化下来,开发者使用这些模式组件时只需要编写少量代码,关注业务和接口调用。
业务组件
: 业务组件,顾名思义就是耦合了我们自己的业务。它只能适用于某些特定的领域。比如用户选择器、素材选择器等等。
页面
:最终呈现到用户的界面,它的复用性最差。
按使用范围:
按是否与业务耦合:
推荐几个拆分方法,
1.最小功能单元拆分。从整体到局部,分析当前页面是否存在两个及以上相同/相似UI且其功能不可再分(原子性)的地方,判断该部分是否需要拆分封装。一般而言,按照该原则拆分出来的组件为通用组件,颗粒度小,与业务代码几乎没有耦合,具有最高的自由度。
以CSDN登陆页为例,很容易划分得到最小功能单元如下:
这样,我们就可以拆分出三个组件,文字按钮,图标按钮,输入框。此外还可以更进一步,前两个皆是按钮,虽然UI存在差异,但这种差异我们可以将通过添加配置化,比如props、slot来控制,
注意:
==拆分颗粒度==
2.页面布局拆分。整体到局部,观察当前页面,按页面结构拆分组件,如骨架、布局、分栏等。
3.视图-逻辑分离。将视图显示与逻辑控制分别拆分成渲染组件和控制组件。渲染组件不应存在过多逻辑,仅接收数据渲染展示,复杂逻辑应让渡给父级/上级组件统一调度响应。如下图工具栏,每个工具按钮触发不同功能的逻辑应该集中在父级组件上。
组件拆分之后,那如何判断是否合理呢?我们可以通过如下几个原则判断,8
推荐写法,
[
{
path: '/',
name: 'index',
component: () => import('@/views/index/index.vue'),
meta: {title: '首页'}
}
]
不推荐写法,
import Home from '@/views/index/index.vue'
let router = createRouter(
[
{
path: '/',
name: 'index',
component: Home,
meta: {title: '首页'}
}
]
)
推荐目录结构如下:10
main.js
主入口。router.js
路由划分。views
所有路由页面。原则:轻page,重component。components
所有组件。包括原子组件、业务公用组件、页面独有组件。api
api引入入口。assets
sass、图片资源入口,不常修改数据。utils
工具文件夹, 非必须。store
标准vuex格式或响应式数据,非必须。project-name
└───src
│ |____assets // css、image、svg等资源
│ | |____css // 所有sass资源
| | | | reset.scss // 重置浏览器
| | | | variable.scss // sass变量和function等
| | |
│ | |____img // image图片库
| |
| |___api // 接口层
| | | index.js
| | | request.js // axios二次封装
| |
| |___store // 全局状态数据
| | | index.js
| |
| |___icons svg图标资源(搭配svg-sprite-loader使用)
| |
| |____components // 全局公共组件
| | C.vue
| |
| |____views // 页面
| |
| |____utils // 工具
| |
│ │ app.vue // 主页面
│ │ main.js // 主入口
| | router.js // 所有路由
│
└───public // 公用文件,不经过webpack处理
│ │ favicon.ico
│ │ index.html
│ vue.config.js // vue-cli3主配置
│ babel.config.js// babel配置
│ .eslintrc.js // eslint配置
│ .prettierrc.js // perttier配置
│ package.json // npm配置
│ README.md // 项目说明
页面目录结构映射路由,
____views
| index.vue
|————index
| index.vue
|————login
| index.vue
| |_____register
| | index.vue
对应路由
[
{
path: '/',
component: () => import('@/views/index/index.vue'),
meta: { title: '首页' }
},
{
path: '/login',
component: () => import('@/views/login/index'),
meta: { title: '登陆' },
children:[
path:'register',
component: () => import('@/views/login/register/index.vue'),
meta: { title: '注册'}
]
}
//……
]
1.删除替代注释。应业务变更导致原代码无法适用,需更改或另写,此时最好不要将旧代码注释,而是将其删除。这么做的考虑是:
2.移除无效代码。
针对单文件组件,既没有在内部调用也不暴露给外部组件,不影响组件运行,而实际处于隔离状态的属性应该删除干净,因为无效代码不仅增加了阅读者的心智负担,而且还会影响Vue性能。
推荐使用vue-hook-optimizer将被隔离的属性提取出来。
https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode ↩︎
https://marketplace.visualstudio.com/items?itemName=Vue.volar ↩︎
https://microsoftedge.microsoft.com/addons/detail/vuejs-devtools/olofadcdnkkjdfgjcmjaadnlehnnihnl) ↩︎
https://v2.cn.vuejs.org/v2/style-guide/index.html ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎
https://everfind.github.io/posts/2021/08/25/how-to-decompose-component.html#%E6%8B%86%E5%88%86%E5%8E%9F%E5%88%99 ↩︎
https://lq782655835.github.io/blogs/team-standard/recommend-vue-project-structure.html ↩︎