浅谈前端工程化

185 阅读8分钟

前端工程化是根据业务特点,将前端开发流程规范化、标准化,主要涉及:开发流程、代码规范、构建发布等。目的是提升前端工程师的开发效率,及创造扩展性更高、维护性更好的web应用程序

从实践出发,可以归纳为以下四个方面:规范化、组件化、模块化、自动化。

1. 编码规范化

规范化指的是对开发项目时制定的相关规范,所有的开发都需要遵守。一套合理完善的规范能够帮助我们很好的去管理和开发项目。

其中包含:

1.1 项目目录结构 - 自定义cli工具

├─api (接口)
├─assets (静态资源)
├─components (公共组件)
├─styles (公共样式)
├─router (路由)
├─store (vuex 全局数据)
├─utils (工具函数)
└─views (页面)

1.2 编码规范

使用统一配置的eslint、stylelint、prettier

1.3 命名规范

  • 文件夹命名:以-间隔
  • 文件命名:驼峰式
  • css类命名:BEM

1.4 Git规范

  • 分支规范

  • git commit规范: | feat | 新功能 | | --- | --- | | fix | 修复bug | | docs | 文档相关 | | perf | 性能优化 | | style | 代码格式修改,包括空格、分号等 | | refactor | 代码重构 | | test | 测试 | | ci | 构建相关文件修改,如package.json种的script内容 |

  • 提交前验证git规范: husky

2. 页面组件化

组件化是指在代码设计阶段从UI层面出发,对页面功能进行拆分,将重复的功能封装成可复用的通用组件,以提高开发和维护效率。

组件的设计和拆分是很重要的,需要对细粒度通用性这两个方面进行考虑。那么,在组件设计的时候,应该如何进行?
参考文章:
浅谈前端组件设计
面试官(6): 写过『通用前端组件』吗? - 掘金

2.1组件设计

首先,组件设计的原则是:** 单一职责 + 通用性**

  • 单一职责

单一职责是指组件只专注做一件事情,可以保证组件是最细的粒度,且利用复用;
但是过细的粒度又会造成组件的碎片化,导致过度抽象,因此单一职责要建立在可复用的基础上;
对于不可复用的单一职责组件,仅作为独立组件的内部组件即可。
如徽章组件右上角的数字组件,它满足了单一职责,但是不满足复用性(不可能被单独重复使用)

  • 通用性

组件的形态(DOM结构)永远是千变万化的,但其行为逻辑是固定的,因此组件通用性其实是一定意义上放弃对DOM的掌控,而将DOM结构的控制权交给使用者,组件只负责行为和最基本的DOM结构
如select组件,在使用时可能会存在其他非默认情况的业务场景,比如在下拉框的底部出现可自定义添加备选项的功能,如果重新去封装或者去改写组件那就太麻烦了,说明之前组件的设计通用性是不够的。在这种情况下,我们可以在设计的时候就把这种自定义的功能开放出去(提供dropdownRender方法供使用者自定义),并提供默认结构,保证在使用者不传的情况下能够渲染出默认功能的组件

2.2 编写规范的API

首先,一个易用的组件,使用者无需阅读文档或快速浏览文档即可快速使用,并且应该在使用过程中提供清晰的注释和代码提示。主要可以从以下几个方面考虑:

  1. 减少必填的API项,尽可能多的提供默认值,降低组件使用成本;
  2. 使用通用且有意义的API命名;
    • onXXX 监听事件方法名
    • renderXXX 渲染方法名
    • beforeXXX 前置动作方法名
    • afterXXX 后置动作方法名
    • XXXProps 子组件属性名称
    • 优先使用常见单词命名,如:label、value、visible、type、size等
  3. 单独维护类型组件,并将其打包至组件产物中,以便使用者在开发过程中能够实时看到类型的提示;
  4. 在类型文件中,为其编写详细注释。

2.3 合理使用Props和Slot

  • 布局类组件优先使用Slot,能够为开发者提供更灵活的使用方式,如Card、Layout等
  • 内容复杂、定制化程度高的组件更适合用Slot
  • 其余情况可以使用Props

3. 代码模块化

模块化是指将复杂代码按功能的不同拆分为不同的模块单独开发和维护,最后再进行统一的打包和加载,主要就是用来抽离公共代码,隔离作用域,避免变量冲突,这样能够提高协作效率,降低维护成本。

3.1 JS模块化

目前流行的JS模块化规范有AMD、CMD、CommonJS、ESModule

3.1.1 AMD/CMD

  • 核心API
    • define - 定义模块;
    • require - 使用模块
  • 代表工具库
    • AMD - requireJS;
    • CMD - Sea.js
  • 区别
    • AMD - 依赖前置,需要提前引用
    • CMD - 支持动态引入,需要时再引用
  • 原理:requireJS的会对每一个模块创建一个

虽然AMD/CMD的出现解决了原生js组织模块的一些问题,但是也必须等用户下载了require.js和sea.js文件之后,才能进行模块依赖的分析,这样会影响性能。同时在加载过程中突然生出了很多的script标签http请求也影响页面性能。
目前这两种模式已被抛弃。

3.1.2 CommonJs

  • CommonJs是Node.js使用的模块化规范。

在Node.js模块系统中,每个文件都被视为独立的模块,模块的本地变量是私有的,因为在执行模块代码之前,Node.js将使用IIFE的方式对其进行封装。

(function(exports, require, module, __filename, __dirname) {
  // 模块代码实际存在于此处
});
  • 导出: 通过module.exports导出一个对象;
  • 导入: 通过require来引用。

特点

  • 运行时加载;
  • 同步加载并执行模块文件;
  • 输出的是一个值的拷贝,可以直接修改;
  • 可以通过require()动态加载,对每一个加载都做了缓存,有效解决了循环引用;

3.1.3 ESModule

  • ES6模块的设计思想是尽量将其静态化,其模块依赖关系是确定的,和运行时状态无关。 :::tips 静态分析是什么?
    静态分析就是不执行代码,从字面量上对代码进行分析。
    JS引擎的编译过程:

    • 词法分析:将语句转化为有意义的词法单元;
    • 语法分析:将词法单元转化为AST;
    • 代码生成:将AST转化为可执行的代码。 :::
  • 导出: 通过export导出属性、export default默认导出;

  • 导入: 通过import来引用。

特点

  • 是导出是静态的,在编译时输出接口,让tree-shaking成为可能;
  • 提前加载并执行模块,在预处理阶段分析模块依赖,在执行阶段执行模块;
  • 输出的是一个值的引用,不能直接修改,但是可以通过导出方法执行修改;
  • 可以通过import()动态/懒加载,能够实现代码分割;

3.2 CSS模块化

CSS一直缺乏模块化的概念,经常被命名冲突的问题困扰,所以也衍生出了一些CSS模块化方案。

3.2.1 CSS命名方法论

为了避免CSS命名冲突的问题,延伸出了一些命名方法论

  • BEM

通过组件名的唯一性来保证选择器的唯一性,从而保证样式不会被污染到组件外。
命名规则:模块名(block)__ 元素名(element)-- 修饰器名(modifier)

<div class="card">
  <div class="card__head">
    <ul class="card__menu">
      <li class="card__menu-item">menu item 1</li>
      <li class="card__menu-item">menu item 2</li>
      <li class="card__menu-item card__menu-item--active">menu item 3</li>
      <li class="card__menu-item card__menu-item--disable">menu item 4</li>
    </ul>
  </div>
  <div class="card__body"></div>
  <div class="card__foot"></div>
</div>
  • OOCSS 面向对象的CSS

借鉴了面向对象的思想,主张将元素的样式抽离成多个独立的小样式类,来提高样式的灵活性和可复用性。
🌰 有一个块,宽度是100px,高度是100px,背景是灰色,左右12px的间距,8px的上边距,16px的下边距,边框为1px的黑色实线,如果不采用OOCSS的思想,我们一般会这样写:

<div class="box"></div>

<style>
  .box {
    width: 100px;
  	height: 100px;
    margin: 8px 12px 16px;
    background: grey;
    border: 1px solid black;
  }
</style>

但如果采用OOCSS的思想,需要为这个块创建更多的原子类,并且每个样式对应一个类,这样可以在之后继续重复使用这些类,避免写重复的样式:

<div class="size100 bgBlue solidGray mt-5 ml-10 mr-10 mb-10"></div>

<style>
  .size10 { width: 100px; height: 100px; }
  .bgGrey { background: grey; }
  .solidBlack { border: 1px solid black; }
  .mt-8 { margin-top: 8px; }
  .mr-12 { margin-right: 12px }
  .mb-16 { margin-bottom: 16px; }
  .ml-12 { margin-left: 12px; }
</style>

OOCSS的优点是可以最大限度的复用样式,也能够在整体上减少CSS的代码量;
但是缺点也很明显,我们需要为每个元素增加非常多的类名。

3.2.2 CSS Module

CSS Module允许我们使用CSS时可以采用import导入一个CSS文件,每个CSS文件都是一个独立的模块,CSS Module在打包时会自动增加hash值,以避免命名冲突。
CSS Module通常是和webpack等工具一起使用,配合预处理器的话书写体验更好(能够使用变量、嵌套等功能)。

3.2.3 CSS in JS

可以在JS中写CSS样式,全部变为内联样式。代表库是styled-components,在react中使用。

3.2.4 拓展:如何在vue中更好的写css

  • 使用Scoped CSS

在区块中添加scoped属性开启组件样式作用域vue会为该组件内所有的元素添加一个全局唯一的属性选择器,如[data-v-123],这样保证了组件内的CSS只应用于当前组件中的元素,避免了全局样式污染。这个属性选择器是vue-template-compiler编译时动态添加的父组件引入子组件的情况下,父组件的DOM上全部会加上一个唯一的选择器,包括传入子组件的插槽,子组件的最外层也会被加上,但是内层DOM不会被加上所以如果需要在父组件中去写子组件的样式,是不会生效的,需要加上深度选择器,可以用deep写法:

<template>
  <header class="header">header</header> 
</template>

<style scoped>
  .header {
    background-color: green;
  }
</style>

编译后:

<header class="header" data-v-5298c6bf>header</header>

<style>
  .header[data-v-5298c6bf] {
    background-color: green;
  }
</style>
  • 使用CSS Modules

为区块添加module属性即可以开启CSS Modulesvue会为组件注入一个名为$style的计算属性

<template>
  <header :class="$style.header">header</header>
</template>

<style module>
  .header {
    background-color: green;
  }
</style>

编译结果:

<header class="App__header--382G7">header</header>

<style>
  .App__header--382G7 {
    background-color: green;
  }
</style>

4. 构建自动化

自动化是指通过一些自动化工具,如Webpack、Rollup、Vite等,在构建、打包方面代替人工操作, 能够节省很多成本;除此之外,前端自动化还包含持续集成、自动化测试等方面。

  • 自动化构建、打包:Webpack、Rollup、Vite
  • 持续集成(CI):Jenkins
  • 自动化部署
  • 自动化测试