前端工程化及相关工具总结

116 阅读16分钟

前言

如果你开发过前端项目,开发的时候你大概率会用到下面一些库:

  • vue 或 react(JavaScript 框架):高效构建交互式的前端应用;
  • ES6+(ES2015+)中的 JavaScript 新特性(let & const、promise、aysnc、await等);
  • TypeScript: 为 JS 添加静态类型系统,在编译时进行类型检查,减少运行时错误;
  • Sass 或 Less(CSS 预处理器):引入变量、嵌套、运算等特性来高效开发CSS样式;
  • ESLint、Prettier(代码检查和格式化工具):保证代码质量和风格统一,便于团队协作和代码维护;

最后上线的代码只有.js、.css 以及图片等静态资源。

那这中间发生了什么?前端工程化又在讲什么?

什么是工程化?什么是前端工程化?

随着发展的逐步推进,作为工程师除了需要关注要写的页面、样式和逻辑之外,还需面对日益复杂的系统性问题,比如模块化文件的组织、ES6 JS文件的编译、打包压缩所有的JS代码、优化和合并图片静态资源等等事情。

这也就是说,我们的项目需要以一种合理的方式进行组合,以应对在团队协作、需求迭代中保持项目的稳定发展。这种方式就是工程化系统去运行我们的项目。

例如如下图所示就是前端工程化中一次打包的过程,左侧就是我们项目中出现的源文件,例如我们会通过sass这种预处理样式来更好的组织我们的样式代码,使用各类其他语言比如typescript、coffescript等来书写我们的逻辑脚本。在最终上线时,我们需要把我们的这类文件,转化为能够在线上直接被浏览器识别的css和js。

image.png

这种使用工程化思维,以工具的形式来进行上述过程的,就是前端方面的工程化。

一句话总结就是,前端工程化,就是在使用工具处理那些与实际业务无关的内容,比如处理JS编译、打包、压缩、图片合并优化等各个方面的工程性代码。

前端工程化具体类目

包管理工具

对于一门成熟的语言来说,在有语言规范的同时,社区或者制定语言规范的组织也会有模块化的规范和模块存储到平台,每个人都能将自己写好的模块化代码发布到平台上,同时任何人也可以下载公共平台上其他人的模块化代码。这种模块化的代码我们一般称之为包(package),平台我们称之为包管理平台,这种行为我们称之为包管理(package manager)。

对于 JS 来说,现阶段大家比较熟悉的就是 Node.js 环境自带的 npm 工具,npm 全称是 node package manager,就是 Node.js 的包管理工具。对于一个符合规范的包来说,我们可以通过 npm publish 发布包,同样的,也可以通过 npm install 来下载别人发布的包,实现大家的模块复用。

社区中常见的包管理工具有 Bower、npm、Yarn 和 pnpm。目前还在经常使用的是 npm、Yarn 和 pnpm。

Bower

Bower 最早出现是使用在浏览器项目中。安装 Bower 命令后,通过 bower install jquery,就可以将 jQuery 下载到 bower_components 目录中,我们就可以在 HTML 文件加上 <script src="bower_components/jquery/dist/jquery.min.js"></script>,免去了我们直接从官网下载然后挪动到项目中的烦恼。

Bower 也支持一些配置,我们只需要在项目根目录下增加 .bowerrc 配置即可:

{
    "directory": "app/components/",
    "timeout": 120000,
    "registry": {
        "search": [
            "http://localhost:8000",
            "https://registry.bower.io"
        ]
    }
}

上面的配置主要配置了我们下载后的模块存储目录、下载超时时间和下载的地址等等。

如果想要发布一个 Bower 模块的话,需要我们这个项目下配置 bower.json,然后通过 bower register 命令发布。

Bower 内容的介绍比较简单,现在新项目中使用已经不多,包管理工具这一小节的重点是 npm、Yarn 和 pnpm。

npm

npm 是伴随着 Node.js 下载会一同安装的一个命令,它的作用与 Bower 一样,都是下载或发布一些 JS 模块。

我们可以使用 npm --version 查看安装的 npm 版本,不同版本带有不同功能。

同时,我们可以通过 npm install 来安装一个模块,例如上面的例子中,我们就可以通过 npm install jquery 来安装 jQuery,npm 会将模块安装到 node_modules 目录中。

一个合格的 npm 包,必须拥有 package.json 这个文件,里面有以下几个常见字段:

  • name:包或者模块的名称
  • version:版本
  • main(重要):默认加载的入口文件
  • scripts:定义一些脚本
  • dependencies:运行时需要的模块
  • devDependencies:本地开发需要运行的模块
  • optionalDependencies:可选的模块,即使安装失败安装进程也会正常退出
  • peerDependencies:必要依赖的版本号

其中 dependenciesdevDependencies 里面的版本通过“大版本号.次版本号.小版本号”的格式规定。我们通过 npm install 安装的模块,最终都会在里面进行记录。同时,我们通过 npm install 时,还会安装这里记录但是 node_modules 中没有的模块。

如果前面带有波浪号(~),则以大版本号和次版本号为主,例如“~1.3.2”的版本,最终安装时就会安装 1.3.x 的最新版本。

如果前面带有插入号(^),则以大版本号为主,例如“^1.3.2”的版本,最终安装就会安装 1.x.x 的最新版本。

高版本的 npm 会将所有依赖进行「打平」操作,这样能保证尽可能少的安装相同的模块。

所有下载的模块,最终都会记录在 package-lock.json 完全锁定版本,下次我们再 npm install 时,就会先下载 package-lock 里面的版本。

Yarn

Yarn 是一个新兴的包管理工具,它与 npm 有着相似的功能,最大的优势就是并发和快。

Yarn 在设计上对安装速度进行了优化,采用并行下载的方式,能够同时处理多个包的下载,大大缩短了安装时间。它还引入了 yarn.lock 文件来锁定依赖版本,确保在不同环境下安装的依赖版本一致。

我们可以通过 yarn add 来安装一个模块,通过 yarn xxx 来运行 scripts 中的脚本。例如:

yarn add react
yarn start

pnpm

pnpm(Performant NPM)是一个快速、节省磁盘空间的包管理工具。它基于硬链接和符号链接的技术,在安装依赖时能够复用已经下载过的包,从而避免了重复下载和磁盘空间的浪费。

特点
  • 高效的磁盘空间利用:pnpm 在磁盘上维护一个全局的包存储(通常位于 ~/.pnpm-store),当项目安装依赖时,会从全局存储中创建硬链接到项目的 node_modules 中。如果多个项目使用相同版本的依赖,它们会共享全局存储中的同一个副本,大大节省了磁盘空间。
  • 快速的安装速度:由于避免了重复下载,pnpm 的安装速度通常比 npm 更快。特别是在项目更新依赖时,pnpm 可以快速判断哪些包已经存在于全局存储中,只需创建相应的链接即可,无需重新下载。
  • 严格的依赖结构:pnpm 的依赖结构更加清晰,每个包的依赖都被明确地安装在其自身的 node_modules 子目录中,避免了幽灵依赖(项目中未明确声明但可以被使用的依赖)的问题。
使用方式
  • 安装依赖:使用 pnpm install 命令来安装项目的所有依赖,或者使用 pnpm add <package-name> 来安装单个依赖。
pnpm install
pnpm add vue
  • 移除依赖:使用 pnpm remove <package-name> 命令来移除项目中的某个依赖。
pnpm remove vue
  • 运行脚本:与 npm 和 Yarn 类似,使用 pnpm <script-name> 来运行 package.json 中定义的脚本。
pnpm start

包管理工具中常见面试问题

devDependencies、dependencies、optionalDependencies 和 peerDependencies 区别

  • devDependencies:是指使用本地开发时需要使用的模块,而真正的业务运行时不用的内容,比如代码打包工具(如 Webpack)、代码检查工具(如 ESLint)等。
  • dependencies:是指业务运行时需要的模块,例如项目中使用的 React、Vue 等前端框架,Express 等后端框架。
  • optionalDependencies:可选模块,安不安装均可,即使安装失败,包的安装过程也不会报错。如果某个依赖对于项目来说不是必需的,或者有替代方案,就可以将其列为可选依赖。
  • peerDependencies:一般用在大型框架和库的插件上,例如我们写 webpack--xx-plugin 的时候,对于使用者而言,他一定会先有 Webpack 再安装我们的这个模块,这里的 peerDependencies 就是约束了这个例子中 Webpack 的版本。

npm 中 --save-dev 和 --save 之间的区别

  • 对于大型项目来说,它们的界限实际上并不清晰。真正会有差异的地方是究竟我们使用哪种方式来进行安装所有的依赖。

  • --save-dev--save 都会把模块安装到 node_modules 目录下,但 --save-dev 会将依赖名称和版本写到 devDependencies 下,而 --save 会将依赖名称和版本写到 dependencies 下。如果我们使用 npm --production install 这样的命令安装模块的话,就只会安装 --save 安装的包。

npm 与 pnpm 的区别

  • 依赖安装机制:

    • npm把依赖尽量放node_modules顶层,避免重复安装,但会导致依赖嵌套乱、有幽灵依赖问题。不同项目用相同依赖会单独装,浪费磁盘空间。
    • pnpm:用硬链接和符号链接,在磁盘设全局包存储。项目安装依赖时从存储创建链接,多个项目可共享,节省空间,依赖结构清晰无幽灵依赖。
  • 安装速度

    • npm:早期串行下载慢,后来有并行下载,但整体还是可能没 pnpm 快。
    • pnpm:复用已安装包,避免重复下载,更新依赖时只需创建链接,速度通常更快。
  • 依赖版本锁定

    • npm:用package - lock.json锁定版本,但版本范围设置可能让安装版本有差异。
    • pnpm:用pnpm - lock.yaml,保证版本精确一致,锁文件简洁,能清晰展示依赖关系。
  • 工作空间支持

    • npm:v7 开始支持工作空间,可管理多子项目,但功能不成熟,处理复杂项目易出问题。
    • pnpm:原生支持工作空间,处理多包项目出色,命令和配置选项强大,方便多项目开发维护。

源代码静态检查和格式化工具

静态检查指的是我们在本地写源代码时,我们使用的编辑器对我们所写代码的提示,检查和格式化。在大型项目中,提示这一步因人而异大部分不做共同要求,检查和格式化一般会对团队使用的内容进行约束,以保证大家写出「正确」的代码和统一的代码风格。

对于代码的检查和格式化,前端发展中比较经典的是jslint,jshint,eslint和prettier。

他们基本上都是一类的工具,再细分的话,jslint,jshint和eslint是一类,他们专门处理JS格式化和静态语法检查,prettier是另一类,他能处理多语言的格式化。

eslint

eslint的官网:eslint.org/ 我们以eslint为例,只需要在项目中通过npm install --save-dev eslint安装他,通过配置.eslintrc(runtime config)我们就可以使用了。配合eslint的编辑器插件,我们就可以在编辑代码时eslint对我们的代码进行提示和修复。

通过配置eslint index.js这样的脚本,就可以对脚本文件进行静态校验。

注意这里是--save-dev因为我们只需要在项目开发过程中使用它而不是运行过程中使用这个模块。

prettier

prettier官网:prettier.io,同样的我们可以配置prettier的配置,.prettierrc里面也可以进行配置,最终搭配prettier的编辑器插件,我们同样能够实现代码编辑状态下的提示和修复。

ES6及其他泛JS语言的编译

大部分时候,我们不能直接在线上使用ES6语法规范的JS代码,我们就需要通过工具对JS进行编译。同时,有些项目我们可能会使用coffeescript、typescript、flow、elm、ocaml等可以编译为JS语言的泛JS语言书写代码,这就需要在调试或发布时,使用编译工具将对应代码编译为JS代码才能直接运行。

在编译过程中,JS比较常见的工具是babel,而其他的语言则对应有自己的编译器,例如coffeescript使用coffeescript编译器进行编译为js,typescript使用typescript编译器编译为js。

babel

babel官网为babeljs.io/,对于一个项目来说,我们可以通过npm install --save-dev @babel/core @babel/cli\来安装babel所需要的工具。

  • @babel/core是babel内部核心的编译和生成代码的方法。
  • @babel/cli(command line tool)是babel命令行工具内部解析相关方法。

安装了这两个包之后,我们就能够使用babel相关方法对代码进行操作,接下来我们需要配置,告诉babel我们需要将代码变成什么。

增加一个babel的preset,preset代表的是我们希望编译的结果的预设值。在最新的babel工具链中,统一使用了@babel/preset-env作为环境预设值。我们安装npm install --save-dev @babel/preset-env之后,新建.babelrc里面,通过配置:

{
    "presets": ["@babel/preset-env"]
}

scripts内定义一个脚本执行babel index.js -o output.js,我们在index.js中写的ES6语法就会被编译。 这一步只是编译语法层面的内容,如果我们使用了一些新的方法的话,还需要增加一个polyfill。使用npm install @babel/polyfill安装了所有符合规范的polyfill之后,我们需要在入口文件引入这个模块,就能正常的使用规范中定义的方法了。

JS打包工具

对于JS这门语言的不同环境来说,有CommonJS、AMD和ESModule这几种常见的模块化规范,这几种规范都有自身的缺点。

CommonJS不经处理只能运行在node.js,AMD不经处理无法运用在各个平台,需要搭配符合AMD规范的其他库例如require.js一起使用。ESModule虽然从语言层面上解决了规范问题,但是即使经过babel编译,也会将import,export之类的关键词编译为CommonJS的require和exports,我们还是无法直接在浏览器中使用。

为了能使任何一个模块都能自由的切换所使用的环境,例如在浏览器使用CommonJS封装好的模块,我们就需要经过打包这个步骤。

browserify、rollup等等工具都是处理诸如此类内容。

browserify

地址:browserify.org/ 我们通过npm install --save-dev browserify安装browserify,我们写一个简单的commonJS模块,通过browserify index.js -o output.js命令就可以将CommonJS模块化的包转化为通用的任何环境均可以加载的模块化规范。

rollup

rollup是一个新兴的打包工具,它最先提出一个概念叫tree shaking,他可以移除我们代码中无用的其他代码。

通过ESModule写的模块,在经过rollup处理之后,会对未使用的导出内容进行标记,在压缩过程就会将这类未使用的内容移除。

JS压缩工具

经过编译和打包的JS代码,最终要在线上经过压缩处理之后,才能最终在网站上面向用户显示。对于JS压缩工具来说,目前有非常多,但用的最多的还是uglify系列,uglify最新是版本3,不同uglify的实现原理和性能都有极大的不同。

uglify

uglify3地址:github.com/mishoo/Ugli…

安装成功之后非常简单,只需要通过uglifyjs index.js -o output.js就可以输出压缩的结果。同时我们可以通过添加--source - map在运行时生成sitemap文件,方便我们进行debug。

其他类目工具

任务处理工具(gulp/grunt)

上面我们说的所有工具都是针对某一个垂直领域来说的,比如编译、打包、压缩等等,我们需要通过不同的命令去运行和操作我们的JS文件。

本小节说的任务处理工具,就是这一类脚本工具,他们能通过脚本的形式将不同的工具进行组合输出。流式处理工具比较常说的两个是grunt和gulp。本小节我们分别介绍一下这两个工具。

  • grunt:grunt官网gruntjs.com/ 。首先通过npm install --save-dev grunt\安装grunt工具,新建gruntfile.js通过gruntfile.js中的配置来让grunt做不同的操作。

这里我们安装npm --save-dev @babel/core @babel/preset-env grunt-babel grunt-contrib-uglify来完整的进行一个项目的构建,通过配置gruntfile脚本,我们分别执行了编译、压缩的过程生成最后js脚本。

  • gulp:gulp官网gulpjs.com/。同样的我们使用npm install --save-dev gulp\安装gulp工具,新建gulpfile.js配置。

我们同样实现相同的功能来重新配置一下gulp任务。gulp相比于grunt来说,配置更加清晰,是一个链式调用的写法。

通用处理工具(fis3/webpack)

通用处理工具这里我们的分类是从功能上来讲,具备上面列举类目的多种功能的集合。这里我们列举的几个工具是fis3和webpack。

fis是国内百度公司在早期发布的一款前端通用处理工具(比webpack早),fis3是它的第三代,使用node.js重写了。

fis3和webpack他们有个最大的特点就是,他们已经不再是一个普通工具,而是一个具有插件化的系统,有着丰富和完善的社区环境,他们属于前端解决方案这么一个领域。理论上他们可以做非常多的事情,而不像上面介绍的大部分工具,只能处理某一个垂直分类下的内容。

webpack实际上和gulp grunt这类的任务处理工具有些类似,但是它本身具有打包的功能,同时也支持通过中间件和插件实现其他领域的功能,最终通过一个命令就能处理完成所有操作。

webpack通过webpack.config.js配置,配置loader中间件来对不同文件进行操作,同时通过插件化的配置,支持例如压缩等等操作。

使用fis3和webpack,他们更多的是将前面我们讲的所有其他工具融合起来,以一种插件的形式进行加载,从而达到了通用的目的。