🗓️ 2024-01-20 🏷️ #前端#

目录

    • 1 工具
      • 1.1编辑器
      • 1.2 格式化
      • 1.3 插件
        • 1.3.1 语言支持
        • 1.3.2 规则检查
      • 1.4 调试工具
    • 2 组件(选项式)
      • 设计参考
      • 规范
        • 结构顺序
        • 命名
        • 模板
        • 属性顺序
        • Props
        • v-for设置键值
        • 样式
      • 分类
      • 拆分
        • 为什么拆分
        • 怎么拆分
        • 评价拆分
    • 3 路由
    • 4 目录
    • 5 其他
      • 保持代码整洁
    • 参考

写在前面

公司前端开发上一直没有统一的规范,不同部门,甚至部门内,每个人都有不同的代码风格,在项目开发上,造成了一些显而易见的问题。再者,我平时要写Python,C#,HTML,JS等代码,它们之间的编程风格差异很大,我很容易将其他语言的风格作用到另一个语言上,尤其是Vue项目,因为我对它接触时间短,对其了解还很浅显,没有形成自身的规范。所以我查找了多个业内前辈的总结,结合我的实际工作经历,带着拼凑性质编写出了下面这个针对Vue项目的开发规范(或者称守则),今后我将尽可能以该规范作为自己的编程行动指南,可能其中一些规范(守则)不适用于其他人,如果有人看到这篇记录时有其他更好的规范或是修改建议,欢迎补充。

[TOC]

1 工具

工欲善其事,必先利其器。

1.1编辑器

推荐使用VSCode,一款由微软开发且跨平台的免费源代码编辑器。该软件以扩展的方式支持语法高亮、代码自动补全、代码重构功能,并且内置了命令行工具和Git 版本控制系统。用户可以更改主题和键盘快捷方式实现个性化设置,也可以通过内置的扩展程序商店安装其他扩展以拓展软件功能。1

1.2 格式化

推荐使用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的代码格式进行校验
}

1.3 插件

1.3.1 语言支持

推荐使用Vue Language Features (Volar) ,一个VSCode上为Vue3打造的语言支持的插件,可以实现原生 TypeScript 语言服务级别的性能。3

安装好Varlo之后,编写Vue组件时,编辑器就能实现==代码高亮==,语法提示,代码跳转,class追溯等功能。

image-20231212232316779

1.3.2 规则检查

推荐使用ESLint来检查JavaScript代码及Vue组件是否符合规则。 规则可参考Vue官方文档

image-20231212232149817

1.4 调试工具

推荐使用Vue.js devtools 进行调试,它是一个Vue官方浏览器调试插件,开发模式下,可以定位,检查Vue组件及其属性状态(props,data,injecgt,provide,computed等)。4

vuejsdevtool

2 组件(选项式)

组件是 Vue 的一个重要概念,它是一种抽象,允许我们使用小型、独立和通常可复用的组件构建大型应用。几乎任意类型的应用界面都可以抽象为一个组件树。组件化能提高开发效率,方便重复使用,简化调试步骤,提升项目可维护性,便于多人协同开发。5

设计参考

如何发挥Vue组件的优点,以期实现更好的复用、可维护性、扩展性,在设计上可参考如下几点:5

  • 无副作用:设计的一个组件不应该对父组件产生副作用,从而达到引用透明(引用多次不影响结果)。
  • 容错处理/默认值:极端情况要考虑,不能少传一个或者错传一个参数就抛出异常。
  • 颗粒化合适,适度抽象:合理对组件拆分,单一职责原则。
  • 可配置/可扩展:实现多场景应用,提高复用性。
  • 详细文档或注释/易读性:代码不仅仅是给计算机执行的,也是要给人看的。方便你我他。
  • 规范化:变量、方法、文件名命名规范,通俗易懂,最好做到代码即注释。
  • 利用框架特性:Vue框架本身有一些特性,如果利用的好,可以帮我们写出效率更高的代码。

规范

结合上述参考,为使代码易读性、规范性,减少沟通成本,我们需要设计一些规范。

结构顺序

单文件组件应该总是让标签顺序保持为 <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>

原因有:

一,顺序由上到下,正好和组件生命周期一致,可以更方面一览其生命周期内的活动变化。

二,属性结构更清晰。

image-20240115150903838

第一组,name组件名称,属基本信息。

第二组,components,props为外部依赖注入;emits,expose为组件对外暴露的事件及方法;data内则定义组件私有响应式数据。上述属性可以理解为vue组件在运行期间所需的各种内外部依赖,按照变量先定义后使用的原则,将其列在第二位(这些我称之为环境属性)。

第三组,computed和watch ,前一个依赖响应式数据进行计算,另一个监控响应式数据变化,它们共同的特点是存在依赖其他数据,列第三。

第四组是各种生命周期选项及钩子,从created到unMounted(仅列出部分)。

第五组,最后methods单独成组,大部分业务代码集中在methods,导致其庞大臃肿,置于前位,往往将环境属性压到代码深处,将其放于尾部呼应第二组的“用”。

Props

在编写代码时,prop 的定义应该尽量详细,至少需要指定其类型。在声明的时候,其命名应该始终使用 camelCase。6

细致的 prop 定义有两个好处:

  • 它们写明了组件的 API,所以很容易看懂组件的用法;
  • 在开发环境下,如果向一个组件提供格式不正确的 prop,Vue 将会告警,以帮助你捕获潜在的错误来源。6

建议:

==必要时解释该props属性==

==可选项必须设置默认值==

// 可选
props:{
    name:{
    type: String,
    default: "名称"
    }
}
// 不可选
props:{
    name:{
    type: String,
    require: true //必须
    }
}

v-for设置键值

在组件上总是必须用 key 配合 v-for,以便维护内部组件及其子树的状态。6

<ul>
  <li v-for="(task, index) in tasks" :key="index">{{ task.name }}</li>
</ul>

样式

  1. css class命名推荐使用英语,非必要不用汉语拼音,单词同样要求有意义。 ​
  2. 单词命名时推荐不要缩写,除非其形式广而告之。 ​
  3. 规则命名中,一律采用小写加中划线的方式,不允许使用大写字母或 _ 例如(header-list)。
  4. 不允许通过1、2、3等序号进行命名 ​。
  5. 避免class与id重名。

css属性编写顺序推荐,

  1. 位置,定位,层级(position,top,bottom,left,right,z-index,display,float) ​
  2. 大小(width,height,padding,margin) ​
  3. 文字系列(font, line-height, letter-spacing, color- text-align) ​
  4. 背景(background,border) ​
  5. 其他(animation,transition)

仅对当前组件生效的样式,必须设置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

image.png
  • 基础组件:一些业务无关的、原子的基础组件。我们常见的 antd-design, element-ui、iview 这些组件库提供的组件就属于基础组件。它的特征如下:

    • 提供了构建一个页面所需的基础组件。它们是原子的,通常不能再往下拆分
    • 符合设计规范的基础组件。它奠定了应用的整体设计风格。也就是说,如果我们需要对不同的客户定义不同的主题,应该在这一层去做,不要再上层耦合太多样式相关的定义。 通常我们有一个统一的主题配置文件去配置风格。如果上层组件需要定义样式,应该引用这个配置。
  • 模式组件:模式组件依旧是业务无关的,这些组件通常是一些最佳实践和设计模式,旨在将一些重复的事情固化下来,提高开发的效率。比如一些通用的页面布局、表单、表格、导入导出。

    它们往往是基础组件的封装,将重复的代码和流程固化/标准化下来,开发者使用这些模式组件时只需要编写少量代码,关注业务和接口调用。

  • 业务组件: 业务组件,顾名思义就是耦合了我们自己的业务。它只能适用于某些特定的领域。比如用户选择器、素材选择器等等。

  • 页面:最终呈现到用户的界面,它的复用性最差。

按使用范围:

  • 公共组件,在不同项目(至少同一个项目内),模块内共享使用。
  • 私有组件,仅在某个功能模块/页面级内使用。

按是否与业务耦合:

  • 业务组件。
  • 通用/基础组件。

拆分

为什么拆分

  • 为可复用性。当多个页面具有相同或相似的UI及逻辑时,便有了复用的可能性,此时应将该部分拆分成公共组件,在其他项目或模块里也能使用,防止重复造轮子。比如页面上的标题,页脚,弹窗等。
  • 为可维护性。考虑到部分业务功能复杂,代码量大,将所有代码集中于一个组件里,三四千行的强耦合代码,会牵一发而动全身,不易于维护,理想状态下应该对业务分而治之,形成多个私有组件,负责单个细分业务。
  • 为可协作性。业务功能分而治之,将其模块化(包含UI和逻辑),使得每个细分业务对应的子功能可由团队内部不同成员开发实现,然后再将多个子功能组合成复杂功能。

怎么拆分

推荐几个拆分方法,

1.最小功能单元拆分。从整体到局部,分析当前页面是否存在两个及以上相同/相似UI且其功能不可再分(原子性)的地方,判断该部分是否需要拆分封装。一般而言,按照该原则拆分出来的组件为通用组件,颗粒度小,与业务代码几乎没有耦合,具有最高的自由度。

以CSDN登陆页为例,很容易划分得到最小功能单元如下:

  • 蓝区: 均为一排文字按钮,功能为点击
  • 绿区:均为输入框,功能为输入文字
  • 橙区: 均为一排图标按钮,功能为点击
image-20240115163517704

这样,我们就可以拆分出三个组件,文字按钮,图标按钮,输入框。此外还可以更进一步,前两个皆是按钮,虽然UI存在差异,但这种差异我们可以将通过添加配置化,比如props、slot来控制,

注意:

==拆分颗粒度==

2.页面布局拆分。整体到局部,观察当前页面,按页面结构拆分组件,如骨架、布局、分栏等。

image.png

3.视图-逻辑分离。将视图显示与逻辑控制分别拆分成渲染组件和控制组件。渲染组件不应存在过多逻辑,仅接收数据渲染展示,复杂逻辑应让渡给父级/上级组件统一调度响应。如下图工具栏,每个工具按钮触发不同功能的逻辑应该集中在父级组件上。

image-20240116112821738

评价拆分

组件拆分之后,那如何判断是否合理呢?我们可以通过如下几个原则判断,8

  1. 一个组件应该只包含单一的职责(职责单一化)9,并且该职责被完整地封装在一个组件中。
  2. A 因为使用了 B,而变得更简单,更易于实现。
  3. B 不能使用 A,即不允许 A、B 之间存在循环依赖。
  4. B 可以作为独立单元被其他组件使用。
  5. 去除 B 以后,A 的功能没有实际意义。

3 路由

推荐写法,

[
    {
        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: '首页'}
        }
    ]
)

4 目录

推荐目录结构如下: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: '注册'}
	    ]
	}
	//……
]

5 其他

保持代码整洁

1.删除替代注释。应业务变更导致原代码无法适用,需更改或另写,此时最好不要将旧代码注释,而是将其删除。这么做的考虑是:

  • 注释影响程序可读性。别人阅读你的代码,还需要越过注释代码的崇山峻岭,这显然是不合适。
  • 注释影响程序美感;一连串的注释,无论再优雅的代码,也仿佛一给件华丽的衣服打满补丁,乱涂乱画的感觉。
  • 注释增加维护负担。所有可维护的代码遵守的都是一个原则:降低维护时所需要载入的工作记忆量。由于人是有生理局限性的,因此越少的记忆量就代表着越好的可维护性。大量的记忆很容易出错而且学习成本很高。

2.移除无效代码。

针对单文件组件,既没有在内部调用也不暴露给外部组件,不影响组件运行,而实际处于隔离状态的属性应该删除干净,因为无效代码不仅增加了阅读者的心智负担,而且还会影响Vue性能。

推荐使用vue-hook-optimizer将被隔离的属性提取出来。

参考