likes
comments
collection
share

前端工程化漫谈

作者站长头像
站长
· 阅读数 24

一、什么是前端工程化

没有看到标准的定义,不同的人有不同的解释

工程化,可以理解为使用一些方式,去改良然后提高行业中现有的步骤、设计、应用方式。前端工程化,就是指对前端进行一些流程的标准化,让开发变得更有效率,且更好地做产品交付。

工程化是使用软件工程的技术和方法对项目的开发、上线和维护进行管理

前端工程可以定义为,将工程方法系统化地应用到前端开发中,以系统、严谨、可量化的方法开发、运营、维护前端应用程序

将前端开发流程,标准化、规范化、工具化、自动化、简单化。通过规范和工具来提高前端应用质量及开发效率

前端工程化是贯穿前端应用生命周期的一系列工程设施,用来保障前端应用的开发体验、质量和交付速度

但大家对目的看法是一样的,提升前端应用的开发效率和质量

二、前端工程化出现以前

前端工程化是近几年出现的概念,在十多年以前,前端页面不复杂,开发前端页面和工程化没有关系,一般有下面的开发方式

1、前后端不分离

页面DOM一般是写在服务端的模板引擎里,Java端塞进来数据做动态渲染,一些交互效果,就通过script标签把相关js引进来,每个js文件一般包成个自执行函数,尽量不污染全局

要改变DOM,就通过jQuery直接操作,要调用其他文件对象,一般是以全局变量的形式,比如window.WindVane.xxx

前端工程化漫谈

这个方式下,前端和后端边界模糊、耦合严重

2、html、css、js的目录结构

工程化出现以前,html + css + js是一个经典的目录结构,在html里构造dom,在css里编写样式,在js里处理交互,写完代码后,手动去点下浏览器的刷新...

也不是不能用...

前端工程化漫谈

随着项目变大,很多问题变得严重起来,主要矛盾是:

1、js文件不敢拆太多,手动维护引入顺序很头疼,顺序错了就Uncaught ReferenceError: xxx is not defined。拆少了,单个js代码量过大,维护也很困难

前端工程化漫谈

2、容易污染全局环境,js里的代码若要给其他文件用,就需要暴露到全局,为避免冲突命名心智负担大

前端工程化漫谈

js在最初设计的时候,是为了写小脚本程序,没有考虑让它来写复杂项目,现在急需引入新的技术方案,让更多js文件来和谐协作

三、前端工程化演进

1、模块化

社区先出马了,2009年,CommonJS和Node横空出世,requireJS(AMD)和seaJS(CMD)紧随其后,把前端带入模块化的世界,开启了前端工程化的起点

CommonJS

CommonJS是一套模块化的规范,用require()来引入其他模块,module.exports来导出,导出的内容只会影响导入它的模块,不会污染全局,每个js文件自己写require去依赖其它模块,被依赖的模块没被加载时,会同步等待加载,不需要手动维护js加载顺序,很好解决了上面的问题,示例代码:

// hello.js
function sayHello(){
  console.log('sayHello');
}
module.exports = { sayHello };

// main.js
var hello = require('./hello.js');
hello.sayHello();

不过由于CommonJS是同步加载依赖文件的,适合服务端,不适合浏览器端,同步加载依赖js会让页面卡住

AMD

2010年,RequireJS带着AMD(Asynchronous Module Definition - 异步模块定义)来了,带来了definerequire两个API,define定义一个模块,require真正执行模块。上面的示例代码成了这样:

// hello.js
define(function() {
  function sayHello(){
    console.log('sayHello');
  }
  return { sayHello };
});

// main.js
require(['./hello.js'], function(hello) {
  hello.sayHello();
});

AMD推崇依赖前置,通过依赖数组的方式提前声明当前模块的依赖

CMD

2011年,玉伯开发了使用更简洁的seaJS,在推广中形成了CMD(Common Module Definition - 通用模块定义)规范,CMD推崇依赖就近,写法更贴近CommonJS规范,示例代码成了这样:

// hello.js
define(function() {
  function sayHello(){
    console.log('sayHello');
  }
  module.exports = { sayHello };
});

// main.js
define(function(require) {
    var hello = require('./hello');
  hello.sayHello();
});

ES Module

2015年6月,ECMAScript 6正式发布,提出了很多新的特性,重点加强了模块、类声明、词法块定界、迭代器和生成器、异步编程的回调、模式解析以及合适的尾调用,另外还扩展了ECMAScript的内置库以支持更多的抽象数据结构。其目的就是使JavaScript可以用来编写复杂的应用程序,成为企业级开发语言。

2015年ES6发布了,带来了官方的模块化规范ES Module,写法和CommonJS规范比较像(机制有差异,详见 es6.ruanyifeng.com/#docs/modul…),使用import导入,export导出,示例代码成了这样:

// hello.js
export function sayHello(){
  console.log('sayHello');
}

// main.js
import { sayHello } from './hello';
sayHello();

可以看到,ES Module的方式用起来比AMD/CMD更方便,虽然浏览器支持需要一段时间,但通过babel编译,可以让我们用ES Module的方式进行模块化开发,编译成浏览器支持的代码去运行。AMD/CMD的时代宣告结束,ES6编程的时代到来。

babel之前叫6to5,名字简单直接

2、包管理

复杂的项目模块会很多,还会依赖许多外部模块(笔者负责项目是3800+),在包管理器出现之前,画风是这样的:

"引用一个外部模块,会去官网把文件下载下来放到项目中,同时在入口html中通过 script标签引用它,每个模块都要重复这个过程"

需要用到jQuery,去 jQuery 官网下载 jQuery库,导入到项目中

需要用到lodash,去lodash官网下载lodash库,导入到项目中

需要用到某个BootStrap,去BootStrap官网官网下载BootStrap库,导入到项目中

...

哪天若要把这些库升级一遍,上面的过程还要重复一次,这是非常低效和麻烦的

NPM

2010年,Node自带的模块管理工具NPM发布了,它搞了个远程代码仓库和配套CLI,开发者通过npm publish可以发布自己的模块,使用者通过npm install可以安装模块,解决了这个问题。上述导入模块的过程,变成了:npm install jquery --save

且模块和对应的版本信息会存在package.json里,管理很方便,共享和复用代码变得简单,大力促进了前端的发展和繁荣,到2016年,已经有超过30万个npm包

早期的npm 1.0比较粗暴,依赖包会一直嵌套,node_modules体积很大,比如我们项目依赖a、b,a和b都依赖c,node_modules是这样的:

- node_modules/
  - library-a/
    - node_modules/
      - library-c   @1.0.0
  - library-b/
    - node_modules/
      - library-c   @1.0.0

2015年,npm 3.0发布,对node_modules做了扁平化处理,做了hoist(依赖提升) ,上述的node_modules成了这样:

- node_modules/
  - library-a/
  - library-b/
  - library-c/

Yarn

随着npm的广泛应用和前端项目进一步复杂,大家遇到了一些新问题

比如经典的"在我电脑上是好的"....由于语义化版本号的广泛使用(示例:^2.2.1),同一份package.json,在不同时间安装,可能安装到不同的版本

We've used the npm client successfully at Facebook for years, but as the size of our codebase and the number of engineers grew, we ran into problems with consistency, security, and performance.

多年来,我们在 Facebook 成功使用了 npm 客户端,但随着代码库规模和工程师数量的增长,我们遇到了一致性、安全性和性能方面的问题。

2016年,Facebook推出了Yarn - a fast, reliable, and secure alternative npm client,比当时npm 3.x的主要优势是:

  • 一致性:引入yarn.lock和确定性的安装算法,保证了在每个机器上的安装结果一致
  • 速度快
    • yarn.lock里已经有了版本和下载地址,不用再去注册中心查询,减少了网络请求
    • 把安装过程设计成Resolution(解析)、Fetching、Linking三个步骤,每个步骤内能够并行
    • 设计了全局缓存,同一个包,只会下载一次

Yarn的设计确实优秀,后面npm也有跟进,推出了npm5.0,引入package-lock.json保障一致性,和大幅提升了性能

不过还有些问题,Yarn和npm都没有解决,比如幻影依赖、依赖分身、磁盘占用大

  • 幻影依赖:package.json没有指定,但作为次级依赖(依赖的依赖)因为依赖提升出现在node_modules根目录,我们的项目还引用了它...容易出现不符合预期的错误(比如我们使用了lodash,但package.json里只依赖了antd,antd可能会升级lodash的版本,甚至不用lodah了,我们代码可能就出错了)

    • 不兼容的版本:依赖包的dependencies变化,幻影依赖版本升级了,编译/使用就可能出错了...
    • 缺少依赖:依赖包的dependencies变化,可能导致我们用的这个幻影依赖没被安装,编译就报错了

笔者所在团队的组件包就存在这个问题,组件打包后的代码依赖 @babel/runtime ,但package.json里没声明,这样会往上级目录查找,使用到对方项目里装的@babel/runtime版本,容易出现不兼容问题(比如经典的.js文件找不到)

  • 依赖分身:Yarn/npm的依赖提升,只会提升第一个版本,后面的其它版本,会继续在树中,比如下图的library-f,会存在2个1.0版本,叫做"分身",这导致了多个问题:
    • 更慢的安装时间
    • 重复的types相互影响:Duplicate identifier 'XXX',TS 2.x已解决
    • 破坏单例模式:同时运行着多个相同的模块实例
    • 不符合预期的运行/编译时错误:不同分身所处不同的目录,加载到的幽灵依赖版本可能不一样,导致虽然是同样的模块,却在不同的环境下运行,可能触发神奇的错误(比如下面的library-f,若依赖了lodash,会加载到不同的版本)
- node_modules/
  - lodash      @4.x
  - library-b/
    - node_modules/
      - lodash  @3.x
      - library-f  @1.0.0
  - library-c/
    - node_modules/
      - library-f  @1.0.0
  - library-d
  - library-e
  - library-f      @2.0.0
  • 磁盘占用大:Yarn/npm安装的依赖,会从全局缓存中复制一份到node_modules下,项目多时磁盘压力大,从下面这个图可见一斑。但也侧面说明了前端生态的繁荣

前端工程化漫谈

大项目依赖分身问题是比较严重的,比如下图的项目的lodash一共存在112份...

前端工程化漫谈

然后目前大约90%的企业级项目都存在幻影依赖,这些作为一颗颗雷,在未来可能出现问题,加深了复杂前端项目的"灵异"特质。社区也出现了一些工具,比如depcheck可以检测是否有幻影依赖,但不治本

后来pnpm彻底解决了这些问题

pnpm

前端工程化漫谈

2017年,pnpm 1.0发布,利用软链接 + 硬链接的方式,通过三层结构设计很好解决了上面的问题:

  • 项目里声明的依赖,才会出现在node_modules根目录,让我们无法使用幻影依赖
  • node_modules下有.pnpm文件夹,以平铺的方式存储着所有的包,避免了依赖分身问题
  • 所有包都安装在磁盘全局目录~/.pnpm-store,项目的node_modules都硬链接到这,不重复占空间,磁盘占用空间下降,还避免了拷贝,安装更快

pnpm优势很大,缺点很小,vue配套生态已经全面使用pnpm

后面yarn 2推出了pnp模式,大幅提升了安装速度和节约了磁盘空间,某些场景下速度比pnpm更快,但它移除了node_modules目录,改动很大,生态上还有一些工具没兼容pnp模式

蓬勃发展的前端领域,不允许你说"学不动了",大佬们还在继续造 深入浅出 tnpm rapid 模式 - 如何比 pnpm 快 10 秒

3、构建工具

把视线再拉回到AMD/CMD时代,当时受限于HTTP1.1的队头阻塞问题(同一个TCP连接里,可以发多个请求,但服务端需要按顺序返回,队头的请求慢会阻塞后续请求的返回),为了缓解服务器的压力,浏览器限制了每个域名最多并发6-8个请求。因此为了更好的加载体验,我们的script标签不能太多,但我们又需要模块化来组织我们的代码,我们的js文件是很多的(笔者团队某个复杂页面首屏2000+ js文件)

然后还有个问题,amd/cmd框架js和页面入口js加载之后,才去在线进行依赖分析,确定加载顺序和执行顺序,这个也会延长页面的加载时间

怎么解决呢?答案是预编译 + 打包

browserify

2011年,browserify诞生了,它允许我们使用CommonJS的规范编写代码,通过调用配套CLI,它会自动分析依赖关系并进行打包,打包的指令很简单直接:

browserify main.js -o bundle.js

前端工程化漫谈

通过打包大大降低了请求个数,提升了页面加载速度


具体是怎么实现的呢?打包工具是前端工程化的核心,和我们的日常工作息息相关,非常值得学习它的原理,先来感受一个简化版的模块打包器的实现

1、从入口文件开始,收集模块间的依赖关系,并编译模块,构造依赖图 2、实现require方法,让模块可以互相调用,再用个函数包裹模块避免污染全局 3、用require方法和包裹后的模块代码来构造产物,生成bundle文件,自动执行入口文件的模块

const fs = require('fs');
const path = require('path');
const babylon = require('babylon');
const traverse = require('babel-traverse').default;
const { transformFromAst } = require('babel-core');

let ID = 0;

// 接受一个文件路径,读取内容并提取它的依赖关系
function createAsset(filename) {
  const content = fs.readFileSync(filename, 'utf-8');
  // 把文件转成AST (抽象语法树)
  const ast = babylon.parse(content, { sourceType: 'module' });
  // 保存这个模块依赖的模块的相对路径
  const dependencies = [];
  // 遍历`ast`来收集这个模块依赖哪些模块
  traverse(ast, {
    ImportDeclaration: ({node}) => {
      dependencies.push(node.source.value);
    },
  });
  // 通过递增简单计数器为此模块分配唯一标识符
  const id = ID++;
  // 使用babel,把我们写的代码,编译成浏览器支持的语法
  const { code } = transformFromAst(ast, null, { presets: ['env'] });
  // 返回有关此模块的所有信息,ID、文件路径、依赖的模块数组、编译后的代码
  return { id, filename, dependencies, code };
}

// 从入口文件entry开始,分析每个模块间的依赖关系,生成依赖图
function createGraph(entry) {
  // 首先解析入口文件
  const mainAsset = createAsset(entry);
  // 使用队列来存放要分析依赖的模块
  const queue = [mainAsset];
  for (const asset of queue) {
    // 用mapping记录当前文件依赖模块的路径和ID
    asset.mapping = {};
    // 遍历当前文件的依赖模块,对所有依赖模块调createAsset处理一遍
    asset.dependencies.forEach(relativePath => {
      const absolutePath = path.join(path.dirname(asset.filename), relativePath);
      const child = createAsset(absolutePath);
      asset.mapping[relativePath] = child.id;
      // 将依赖模块推入队列,这样依赖的依赖也将被遍历和解析
      queue.push(child);
    });
  }
  // 到这一步,队列就是一个包含目标应用中每个模块的数组,也就是依赖图
  return queue;
}

// 打包方法,通过依赖图,生成一个可以在浏览器中运行的js文件
// 将只有一个自我调用函数: `(function() {})()`
function bundle(graph) {
  let modules = '';
  graph.forEach(mod => {
    // babel编译后,模块代码是CommonJS规范: 需要一个require, 一个module和exports对象
    // 那些在浏览器中通常不可用,所以需要将它们实现并注入
    modules += `${mod.id}: [
      function (require, module, exports) { ${mod.code} },
      ${JSON.stringify(mod.mapping)},
    ],`;
  });
  // 构造打包产物内容
  const result = `
    (function(modules) {
      // 1、创建一个require()函数,根据模块ID去找对应的模块
      function require(id) {
        // 1.1 从modules里,拿到模块的包裹函数和依赖关系
        const [fn, mapping] = modules[id];
        function localRequire(name) { 
          return require(mapping[name]);
        }
        const module = { exports : {} };
        // 1.2 执行模块代码,传入require、module、exports
        fn(localRequire, module, module.exports); 
        // 1.3 模块可以改变exports对象来暴露模块的值,require函数最后会返回exports对象
        return module.exports;
      }
      // 2、自动执行第一个模块,也就是入口文件
      require(0);
    })({${modules}})
  `;
  return result;
}

// 1、从入口文件构造依赖图
const graph = createGraph('./example/entry.js');
// 2、打包,生成最终产物
const result = bundle(graph);

短短几十行代码,功能虽简陋,但已经是一个可以work的极简版打包工具,配合minipack仓库,可以自行打印其中各参数的值,吸收效果更佳

打包后我们的产物只有一个js文件

但这样就好了吗?还是不够好,单个产物js的方式,随着项目变大,产物也会变大,网络加载的耗时也不少,特别是在移动设备上,想想那时候的2G网络...

webpack

2012年,webpack带着它的Code Splitting出现了,同时支持CommonJS和AMD规范的模块,会自动把产物分成多个文件,需要的时候才加载。通过一份webpack.config.js来使用,除了普通的入口、输出基础概念外,还设计了loader、plugin机制,允许开发者自由组合拓展webpack的能力,为后续的生态繁荣奠定了基础

module.exports = {
  entry: "./src/index.js",    # 编译的入口
  output: './dist/bundle.js', # 打包的输出
  module: {
    loaders: [  # loader配置,使用不同loader预处理其它静态资源文件,资源转换器
      { test: /.js/, loader: 'babel-loader'}
    ]
  },
  plugins: [    # 插件配置,监听构建过程对应广播,拓展webpack的能力,压缩、混淆、拆包、修改产物等
    new HtmlwebpackPlugin({
      template: './src/index.html'
    })
  ]
}

随后也诞生了许多其他优秀的构建工具,编程式的grunt、gulp,专注ES6模块的Rollup,限于篇幅,不具体展开

此后的两年,webpack并没有很火。2014 OSCON 大会,Instagram 的前端团队分享了他们对前端页面加载性能优化,其中很重要的一件就是用到的 webpack 的 Code Splitting,webpack于是🔥了,之后大家纷纷使用,并贡献了无数的loader、plugins,生态开始繁荣,webpack也加强了插件系统设计,在2015年的webpack 2.0-beta版readme里,作者加了这句话:

Highly modular plugin system to do whatever else your application requires 高度模块化的插件系统,可满足您的应用程序所需的任何其他功能

那几年,前端风云变幻,TypeScript、React、Vue、babel、ES6等发布,大家在日常开发中不断使用新技术来提升效率和质量,对构建工具有了更高的要求,因此webpack也一直在进化,带来了很多我们熟悉的功能

2014年正式发布的webpack1.0,带来了sourceMapHMR(Hot Module Replacement - 模块热替换)等功能

2017年1月,从beta版走了一年多才正式发布的webpack2,核心代码采用ES6重写,带来了一些重磅更新:

  • 支持ES Module模块
  • 受益于ES Module的静态特性,支持了ES Module的Tree Shaking
  • 带来了强力的webpack-dev-server

2017年6月,webpack3发布,没有break change,没有迁移指南,根据金主爸爸的诉求,做了两个新功能:

import(/* webpackChunkName: "my-chunk-name" */ 'module');

2018年2月,webpack4发布,邀请了最大的金主爸爸trivago为这个版本冠名...这次更新比V3大:

  • 构建速度大幅提升
  • 新增mode配置项,默认production,可选development,内部会根据mode选择不同的默认配置(说明配置项比较多了)
  • 新增optimization.splitChunks,废弃CommonsChunkPlugin,拆包能力大幅增强
  • 支持WebAssembly模块

2020年10月,webpack5发布,绝对是个大版本,内部做了较多重构,为未来的功能做准备,重磅更新有:

  • 使用持久缓存提高构建性能: 涉及resolve、build、代码生成、sourceMap等各个阶段
  • 使用更好的算法和默认值改进长期缓存
  • 使用更好的Tree Shaking改进包大小:可跟踪对导出的嵌套属性的访问、支持Deep Scope Analysis、支持export *、根据静态分析对模块做自动的无副作用标记、支持CommonJS模块
  • 优化产物代码以改进包大小:更短的代码、可以生成ES6的代码
  • lazyCompilation - 懒编译:开启后,entries和动态imports可以在使用时才编译,不过还是实验性的
  • Module Federation - 模块联邦: 允许多个webpack构建一起工作,模块可以指定从远程构建中加载,共同构成一个巨大的连接模块图。跨项目复用代码变得十分方便,不用再安装npm包了

前端工程化漫谈

经过多年发展,webpack凭借强大的功能、和React/Vue视图框架更好的结合性,成长发构建工具里的王者,在打包方面做到了极致。但是,性能饱受诟病...

esbuild、swc

前端项目规模还在持续变大,打包性能慢的问题困扰许多开发者,大项目启动耗时甚至达几分钟,热更新达十几秒

没有一个程序员能拒绝在启动一个webpack项目的时候冲一杯咖啡☕️

前端工程化漫谈

很多人意识到,js并不擅长CPU密集型的工作,开始尝试高性能的语言来打造新一代的工具链,2015年Rust 1.0发布,由于其高性能、高可靠的特点,陆续出现了许多基于Rust编写的前端领域的工具:

  • Deno:Node.js作者开发的简单、现代、安全的JavaScript 和 TypeScript 运行时,比Node.js更安全和拥有更好的包管理系统(最初是用Go写的,后面改为了Rust)
  • swc:一个可扩展的基于 Rust 的前端构建工具,目前核心功能相当于 Babel,在单线程上比 Babel 快 20 倍,在四核上快 70 倍
  • dprint:比 Prettier 快 30x 倍
  • postcss-rs:比 Postcss 快 20x 倍
  • parcel:零配置构建工具,使用 SWC 作为编译器,再实现了依赖项收集、捆绑、摇树优化、热重载等

esbuild则选择了使用Go来编写,充分利用多线程 + 运行机器码,在语言上有性能优势,架构设计上充分利用多核CPU优势、高效利用内存,性能表现十分惊人

前端工程化漫谈

不过还没发布1.0版,重要功能还在持续开发中,特别是代码分隔、和CSS处理方面,因此作为生产构建器还有一段路要走

esbuild是在打包上去超越webpack,vite则是换了个思路 - bundleless(不打包)

Bundleless - snowpack、 vite

为什么可以bundleless?是因为随着环境变化,打包最初要解决的问题,没那么明显了

  • 2015年HTTP/2发布,头部压缩、传输效率提升、多路复用(不再有队头堵塞问题) ,对于支持HTTP/2的服务器,浏览器的并发请求数限制从6~8调大到了100,script标签可以多一些了
  • 现代主流浏览器基本已经充分支持ES6了,ES Module可以直接被浏览器加载执行

2019年,snowpack发布,一年后,vite发布,他们是bundleless构建工具的代表,通过无打包构建,加上使用esbuild这种新一代构建工具,启动性能提升了10倍,热更新也是快到飞起

不过,对于超大项目,也没有那么美好,请求js文件数上千后,即使是HTTP/2,页面的打开速度也慢了很多。因此bundleless大多用在开发阶段,上生产还是要打包(vite是基于RollUp)

前端工程化漫谈

但不是很影响开发者们的热情,目前vite的star数5W+,仅比webpack少1W,很多人认为vite是下一代构建工具

不过,webpack凭借强大的生态和出色的webpack5,也不是吃素的。根据umi的MFSU实践,在webpack里使用esbuild,再基于webpack5的持久缓存 + 模块联邦能力,把node_modules和src并行构建,可以实现比vite更快的速度

Turbopack

2022年10月,Vercel 发布了Turbopack,webpack作者亲自操刀,用Rust再写了一个webpack的后继者 - Turbopack,基于Rust的高度优化的机器代码函数级别的内存增量计算引擎(执行过的函数,不会再次执行) ,实现了更快的速度...

不过目前还未发布正式版本,预计今年会发布1.0

前端工程化漫谈

如作者所说:

It's time for a new beginning in compiler infrastructure for the entire web ecosystem 整个web生态系统编译器基础架构的新开端开始了

构建工具之争,尘埃还没有落定...

4、代码规范

JavaScript是一种动态、弱类型的语言,编译检查相对宽松,容易把一些错误带到运行环境。且随着开发人数增加,代码风格容易不一致,增加了阅读代码的负担

ESLint

2013年,ESLint诞生了,它通过静态分析代码的AST(抽象语法树) 来检查代码的规范性,通过修改AST和再次生成代码,可以自动完成修复。ESLint提供了大量内置的规则,允许开发者通过.eslintrc配置来调整,也支持增加自定义规则,通过一个例子大概能看明白它是怎么工作的:

module.exports = {
  meta: {             // 规则的元数据配置
    type: "problem",  // 规则类型,可选problem | suggestion | layout
    docs: {           // 对规则的解释文档
      description: "Description of the rule",
    },
    fixable: "code",  // 若是可自动修复的规则,则配置该条,可选 code | whitespace
  },             
  create(context) {   // 返回一个带有AST访问者函数的对象,ESLint遍历AST过程中会调用
    const sourceCode = context.getSourceCode()
    return {
      VariableDeclaration(node) { // 访问到VariableDeclaration节点时,会调这个方法
        if (node.kind === 'var') {   
          context.report({
            node,
            message:'不能用var',   // 给开发者的提示
            fix(fixer) {          // 自动修复的逻辑
              const varToken = sourceCode.getFirstToken(node)
              return fixer.replaceText(varToken, 'let')
            }
          })
        }
      }
    };
  },
};

ESLint有效保障了代码质量,一般结合husky使用,通过设置pre-commit门禁,让通过ESLint检查的代码才能提交

Prettier

ESLint是高度可配置的,有众多格式化规则可选择(目前内置的就有60+),出发点是好的,但在实践中,大家发现,团队里经常存在关于格式化规则的讨论,有时有效果,更多的时候是浪费时间。相互觉得对方的规则"奇怪"...

2016年,Prettier诞生了,它专注对代码进行格式化,不关心质量检测

格式化规则默认好用、更少的配置选项(Prettier觉得你不需要配,去干活吧) 、更少的讨论、更专注去干活,Prettier迅速流行起来

小孩子才做选择,大人全都要。用ESLint做代码检查、用Prettier做格式化,效果更佳

5、持续集成、持续交付/部署

在多人协同开发的过程中,我们有时会遇到这样的问题:

  • 大家分别开发某个模块,开发完后一集成发现许多问题,改动代码量大定位麻烦
  • 有段时间没集成,一集成发现很多冲突,合并过程费时又痛苦

为了快速发现错误防止分支大幅偏离主干,软件开发流程开始实践持续集成(Continuous integration - CI),倡导开发者频繁提交代码到共享仓库。代码提交到仓库后,通过CI服务器自动执行一些任务,比如构建、单元测试、代码检查、集成测试,快速反馈结果

下图的CHECK-IN,就是push代码

前端工程化漫谈

在CI的基础上,将集成后的代码依次部署到测试/预发环境,这个就是持续交付(Continuous Delivery – CD

前端工程化漫谈

上图右下角的部署到生产,如果是自动执行的,就是持续部署(Continuous Deployment - CD),这个比较理想

通过引入CI/CD,我们每次提交代码,就会触发自动构建、测试、交付,提高了交付速度,也解放了双手。其中的核心技术是CI服务端,典型的代表是Jenkins、Travis CI,github在2019年11月正式推出了Github Actions,功能更强大,除了CI/CD外,也可加入任意的自动化流程,把重复性的工作交给机器

四、总结

本文从早期的前端页面开发方式切入,讲述了随着代码规模上升、协作人数增多,遇到的各种问题,引出了模块化、包管理、构建工具、代码规范、CICD五个方面的内容。前端工程化领域不断出现的新工具,核心目的是提高开发效率和质量。走过这十四年,前端应用的开发方式已经发生了很大的变化,有些优秀的工具已经完成了使命,淹没在了历史的浪潮中

前端工程化的蓬勃发展,大大提高了前端开发的效率,伴随着硬件和网络的升级,web性能也有极大提高,前端边界越来越广,曾经火爆的iOS、Android在优胜劣汰的市场选择下份额不断缩小,相比于iOS的"没人要了",前端的"学不动了"还是要幸福一些...

随着前端工程规模愈加庞大,工程化设施容易遇到一些问题,通过了解前端工程化的诞生背景、演进过程、各个工具/方案的优劣,能让我们对复杂前端工程有更多的理解和掌控力,深入相关的技术细节,可以帮助我们解决遇到的问题,提升开发效率和幸福感,和大家共勉