前端工程化

了解前端工程化

本博客参考:前端工程化入门教程

刚接触前端的时候,做一个页面,都是用到的前端三件套(HTML + CSS + JS),需要几个页面,就需要创建几个HTML。而目前的前端开发工作,早已进入了前端工程化开发的时代,已经充满了各种现代化框架、预处理器、代码编译等等等,最终的产物也不再单纯是多个 HTML 页面,经常能看到 SPA / SSR / SSG 等词汇的身影。

传统开发的弊端

在传统的前端开发模式下,前端工程师大部分只需要单纯地写写页面,都是在 HTML 文件里直接编写代码,所需要的 JavaScript 代码是通过 script 标签以内联或者文件引用的形式放到 HTML 代码里的,当然 CSS 代码也是一样的处理方式。

  1. 可能存在同名的变量声明,引起变量冲突
  2. 引入多个资源文件时,比如有多个 JS 文件,在其中一个 JS 文件里面使用了在别处声明的变量,无法快速找到是在哪里声明的,大型项目难以维护
  3. 大部分代码缺乏分割,比如一个工具函数库,很多时候需要整包引入到 HTML 里,文件很大,然而实际上只需要用到其中一两个方法
  4. 由第 4 点大文件延伸出的问题, script 的加载从上到下,容易阻塞页面渲染
  5. 不同页面的资源引用都需要手动管理,容易造成依赖混乱,难以维护
  6. 如果要压缩 CSS 、混淆 JS 代码,也是要人力操作使用工具去一个个处理后替换,容易出错,等等

工程化带来的优势

开发优势

在开发层面,前端工程化有以下这些好处:

  1. 引入了模块化和包的概念,作用域隔离,解决了代码冲突的问题
  2. 按需导出和导入机制,让编码过程更容易定位问题
  3. 自动化的代码检测流程,有问题的代码在开发过程中就可以被发现
  4. 编译打包机制可以让使用开发效率更高的编码方式,比如 Vue 组件、 CSS 的各种预处理器
  5. 引入了代码兼容处理的方案( e.g. Babel ),可以让开发者自由使用更先进的 JavaScript 语句,而无需顾忌浏览器兼容性,因为最终会转换为浏览器兼容的实现版本
  6. 引入了 Tree Shaking 机制,清理没有用到的代码,减少项目构建后的体积

团队协作优势

  1. 统一的项目结构,通过脚手架创建的项目,拥有统一的目录结构。

  2. 统一代码风格,每个开发者可以通过自己喜好的方式编程,最后交给工具统一格式化。

  3. 可复用的模块和组件,通过npm打包和安装组件。

  4. 代码健壮性,Ts项目有严格的类型约束。

  5. 团队开发效率提升,Mock接口、CI/CD流水线部署等等。

Vue.js 与工程化

大部分前端开发者都学过Vue这门框架技术,Vue 3.0 版本还引入了组合式 API 的概念,更符合软件工程 “高内聚,低耦合” 的思想,Vue 的特色之一数据的双向绑定功能大幅度减少了开发过程的编码量以及操作DOM的过程,Vue 的编程方式,称之为 “数据驱动” 编程。

如果在一个页面上频繁且大量的操作真实 DOM ,频繁的触发浏览器回流( Reflow )与重绘( Repaint ),会带来很大的性能开销,从而造成页面卡顿,在大型项目的性能上很是致命。

而 Vue 则是通过操作虚拟 DOM ( Virtual DOM ,简称 VDOM ),每一次数据更新都通过 Diff 算法找出需要更新的节点,只更新对应的虚拟 DOM ,再去映射到真实 DOM 上面渲染,以此避免频繁或大量的操作真实 DOM 。

TIP:虚拟 DOM 是一种编程概念,是指将原本应该是真实 DOM 元素的 UI 界面,用数据结构来组织起完整的 DOM 结构,再同步给真实 DOM 渲染,减少浏览器的回流与重绘。

在 JavaScript 里,虚拟 DOM 的表现是一个 Object 对象,其中需要包含指定的属性(例如 Vue 的虚拟 DOM 需要用 type 来指定当前标签是一个 <div /> 还是 <span /> ),然后框架会根据对象的属性去转换为 DOM 结构并最终完成内容的显示。

传统的开发涉及到多个页面跳转时候,是通过 A 标签跳转到另外一个页面,在跳转期间会产生 “新页面需要重新加载资源、会有短暂白屏” 等情况,用户体验不友好。

Vue 提供了 Vue Router 实现路由功能,利用 History API实现单页面模式,在一个 HTML 页面里也可以体验 “页面跳转” 这样的体验,但如果页面很多,所有代码都堆积在一个 HTML 页面里,就很难维护。

现代化的开发概念

MPA 与 SPA

名词 全称 中文
MPA Multi-Page Application 多页面应用
SPA Single-Page Application 单页面应用

多页面应用

MPA 多页面应用是最传统的网站体验,当一个网站有多个页面时,会对应有多个实际存在的 HTML 文件,访问每一个页面都需要经历一次完整的页面请求过程:

从用户点击跳转开始:
—> 浏览器打开新的页面
—> 请求【所有】资源
—> 加载 HTML 、CSS 、 JS 、 图片等资源
—> 完成新页面的渲染

MPA 的优点

  • 首屏加载速度快
  • SEO 友好,容易被搜索引擎收录
  • 容易与服务端语言结合,比如PHP 、 JSP 、 ASP 、 Python 等非前端语言或技术栈来编写页面模板,最终输出 HTML 页面到浏览器访问

MPA 的缺点

  • 页面之间的跳转访问速度慢
  • 开发成本高,无法做到前后端分离

单页面应用

SPA 单页面应用是现代化的网站体验,与 MPA 相反,不论站点内有多少个页面,在 SPA 项目实际上只有一个 HTML 文件,也就是 index.html 首页文件。

它只有第一次访问的时候才需要经历一次完整的页面请求过程,之后的每个内部跳转或者数据更新操作,都是通过 AJAX 技术来获取需要呈现的内容并只更新指定的网页位置。

SPA 在页面跳转的时候,地址栏也会发生变化,主要有以下两种方式:

  1. 通过修改 Location:hash修改 URL 的 Hash 值(也就是 # 号后面部分),例如从 https://example.com/#/foo 变成 https://example.com/#/bar
  2. 通过 History API 的 pushState方法更新 URL ,例如从 https://example.com/foo 变成 https://example.com/bar

这两个方式的共同特点是更新地址栏 URL 的时候,均不会刷新页面,只是单纯的变更地址栏的访问地址,而网页的内容则通过 AJAX 更新,配合起来就形成了一种网页的 “前进 / 后退” 等行为效果。

TIP:Vue Router 默认提供了这两种 URL 改变方式的支持,分别是 createWebHashHistory 的 Hash 模式和 createWebHistory 对应的 History 模式。

SPA 的请求过程简化为如下步骤:

从用户点击跳转开始:
—> 浏览器通过 pushState 等方法更新 URL
—> 请求接口数据(如果有涉及到前后端交互)
—> 通过 JavaScript 处理数据,拼接 HTML 片段
—> 把 HTML 片段渲染到指定位置,完成页面的 “刷新”

SPA 的优点

  • 只有一次完全请求的等待时间(首屏加载)
  • 用户体验好,内部跳转的时候可以实现 “无刷切换”
  • 因为不需要重新请求整个页面,所以切换页面的时候速度更快
  • 因为没有脱离当前页面,所以 “页” 与 “页” 之间在切换过程中支持动画效果
  • 脱离了页面跳页面的框架,让整个网站形成一个 Web App ,更接近原生 App 的访问体验
  • 开发效率高,前后端分离,后端负责 API 接口,前端负责界面和联调,同步进行缩短工期

SPA 的缺点

  • 首屏加载相对较慢,由于 SPA 应用的路由是由前端控制, SPA 在打开首页后,还要根据当前的路由再执行一次内容渲染,相对于 MPA 应用从服务端直出 HTML ,首屏渲染所花费的时间会更长。
  • 不利于 SEO 优化,由于 SPA 应用全程是由 JavaScript 控制内容的渲染,因此唯一的一个 HTML 页面 index.html 通常是一个空的页面,只有最基础的 HTML 结构,不仅无法设置每个路由页面的 TDK ,页面内容也无法呈现在 HTML 代码里,因此对搜索引擎来说,网站的内容再丰富,依然只是一个 “空壳” ,无法让搜索引擎进行内容爬取。

尽管通过增加 Loading 过程,或者 Skeleton 骨架屏等优化方案,但也是治标不治本,又进一步催生出了更多实用的技术方案以适配更多的业务场景。

CSR 与 SSR

名词 全称 中文
CSR Client-Side Rendering 客户端渲染
SSR Server-Side Rendering 服务端渲染

客户端渲染

SPA 单页面应用正是基于 CSR 客户端渲染实现的(因此大部分情况下, CSR 等同于 SPA ,包括实现原理和优势),这是一种利用 AJAX 技术,把渲染工作从服务端转移到客户端完成,不仅客户端的用户体验更好,前后端分离的开发模式更加高效。

但随之而来的首屏加载较慢、不利于 SEO 优化等缺点,而 SPA 的这几个缺点,却是传统 MPA 多页面应用所具备的优势,但同样的, MPA 也有着自己开发成本高、用户体验差等问题。

既然原来的技术方案无法完美满足项目需求,因此在结合 MPA 的优点和 SPA 的优点之后,一种新的技术随之诞生,这就是 SSR 服务端渲染。

服务端渲染

和传统的 MPA 使用 PHP / JSP 等技术栈做服务端渲染不同,现代前端工程化里的 SSR 通常是指使用 Node.js 作为服务端技术栈。

传统的服务端渲染通常由后端开发者一起维护前后端代码,需要写后端语言支持的模板、 JavaScript 代码维护成本也比较高;而 SSR 服务端渲染则是交给前端开发者来维护,利用 Node 提供的能力进行同构渲染,由于本身前后端都使用 JavaScript 编写,维护成本也大大的降低。

SSR 技术利用的同构渲染方案( Isomorphic Rendering ),指的是一套代码不仅可以在客户端运行,也可以在服务端运行,在一些合适的时机先由服务端完成渲染( Server-Side Rendering )再直出给客户端激活( Client-Side Hydration ),这种开发模式带来了:

  • 更好的 SEO 支持,解决了 SPA 单页面应用的痛点
  • 更快的首屏加载速度,保持了 MPA 多页面应用的优点
  • 和 SPA 一样支持前后端分离,开发效率依然很高
  • 有更好的客户端体验,当用户完全打开页面后,本地访问过程中也可以保持 SPA 单页面应用的体验
  • 统一的心智模型,由于支持同构,因此没有额外的心智负担

那么,使用 Vue 开发项目时,应该如何实现 SSR 呢?

Vue 的 SSR 支持非常好, Vue 官方不仅提供了一个 Vue.js 服务器端渲染指南 介绍了基于 Vue 的 SSR 入门实践,还有基于 Vue 的 Nuxt.jsQuasar 框架帮助开发者更简单地落地 SSR 开发,构建工具 Vite 也有内置的 Vue SSR 支持。

Pre-Rendering 与 SSG

SSR 服务端渲染技术的开发成本总归比较高,如果本身项目比较简单,例如一个静态博客,或者静态官网、落地页等内容不多,仅需要简单的 SEO 支持的项目需求,是否有更简便的方案呢?

以下两种方案正是用于满足这类需求的技术:

名词 全称 中文
Pre-Rendering Pre-Rendering 预渲染
SSG Static-Site Generation 静态站点生成

预渲染

预渲染也是一种可以让 SPA 单页面应用解决 SEO 问题的技术手段。

预渲染的原理是在构建的时候启动无头浏览器( Headless Browser ),加载页面的路由并将访问结果按照路由的路径保存到静态 HTML 文件里,这样部署到服务端的页面,不再是一个空的 HTML 页面,而是有真实内容的存在,但由于只在构建时运行,因此用户每次访问的时候 HTML 里的内容不会产生变化,直到下一次构建。

预渲染和服务端渲染最大的区别在于,预渲染只在构建的时候就完成了页面内容的输出(发生在用户请求前),因此构建后不论用户何时访问, HTML 文件里的内容都是构建的时候的那份内容,所以预渲染适合一些简单的、有一定的 SEO 要求但对内容更新频率没有太高要求、内容多为静态展示的页面。 用 Vite 更简单的解决 Vue3 项目的预渲染问题

静态站点生成

SSG 静态站点生成是基于预渲染技术,通过开放简单的 API 和配置文件,就让开发者可以实现一个预渲染静态站点的技术方案。

常见的 SSG 静态站点生成器有:基于 Vue 技术的 VuePressVitePress ,自带了 Vue 组件的支持,还有基于 React 的 Docusaurus ,以及很多各有特色的生成器,例如 JekyllHugo 等等。

ISR 与 DPR

名词 全称 中文
ISR Incremental Site Rendering 增量式的网站渲染
DPR Distributed Persistent Rendering 分布式的持续渲染

当网站的内容体量达到一定程度的时候,从头开始构建进行预渲染所花费的时间会非常久,而实际上并不是所有页面的内容都需要更新,这两项技术的推出是为了提升大型项目的渲染效率。

ISR 增量式的网站渲染,通过区分 “关键页面” 和 “非关键页面” 进行构建,优先预渲染 “关键页面” 以保证内容的最新和正确,同时缓存到 CDN ,而 “非关键页面” 则交给用户访问的时候再执行 CSR 客户端渲染,并触发异步的预渲染缓存到 CDN 。

这样做的好处是,大幅度的提升了每次构建的时间,但由于只保证部分 “关键页面” 的构建和内容正确,所以访问 “非关键页面” 的时候,有可能先看到旧的内容,再由 CSR 刷新为新的内容,会丢失一部分用户体验。

更多 ISR 技术细节可以阅读 Netlify 的开发者体验总监 Cassidy Williams 的一篇文章: Incremental Static Regeneration: Its Benefits and Its Flaws

DPR 分布式的持续渲染则是为了解决 ISR 方案下可能访问到旧内容的问题,这也是由 Cassidy Williams 发起的一个提案,详情可在 GitHub 查看:Distributed Persistent Rendering (DPR)

由于目前这两项技术还在发展初期,能够支持的框架和服务还比较少,可作为知识储备了解。

工程化不止于前端

服务端开发

Node 本身是一个 JavaScript 的运行时,还提供了 HTTP 模块 可以启动一个本地 HTTP 服务,如果把 Node 项目部署到服务器上,就可以运行一个可对外访问的公网服务。

ExpressKoaFastify 为代表的轻量级服务端框架,这一类框架的特点是 “短平快” ,对于服务端需求不高,只是跑一些小项目的话,开箱即用非常地方便。

Nest (底层基于 Express ,可切换为 Fastify )、 Egg (基于 Koa )为代表的基于 MVC 架构的企业级服务端框架,这一类框架的特点是基于底层服务进行了更进一步的架构设计并实现了代码分层,还自带了很多开箱即用的 Building Blocks ,例如 TypeORM 、WebSockets 、Swagger 等等,同样也是开箱即用,对大型项目的开发更加友好。

App 开发

之前的前端开发者对 App 的作用通常是做一些活动页面、工具页面内嵌到 App 的 WebView 里。

而 Hybrid App 的出现,使得前端开发者也可以使用 JavaScript / TypeScript 来编写混合 App ,只需要了解简单的打包知识,就可以参与到一个 App 的开发工作中。

例如目前主流的有基于 Vue 的 uni-app 、基于 React 的 React Native 等等,这些 Hybrid 框架都具备了 “学习成本低、开发成本低、一套代码编译多个平台” 的特点。

桌面程序开发

得益于 Electron / Tauri 等技术栈的出现,其中 Electron 的成熟度最高、生态最完善、最被广泛使用,除了可以构建 Windows 平台支持的 .exe 文件之外,对 macOS 和 Linux 平台也提供了对应的文件构建支持。

Node.js

Node.js (简称 Node ) 是一个基于 Chrome V8 引擎构建的 JS 运行时( JavaScript Runtime )。

它让 JavaScript 代码不再局限于网页上,还可以跑在客户端、服务端等场景,极大的推动了前端开发的发展,现代的前端开发几乎都离不开 Node 。

Node 和浏览器的区别

虽然 Node 也是基于 Chrome V8 引擎构建,但它并不是一个浏览器,它提供了一个完全不一样的运行时环境,没有 Window 、没有 Document 、没有 DOM 、没有 Web API ,没有 UI 界面…

但它提供了很多浏览器做不到的能力,比如和操作系统的交互,例如 “文件读写” 这样的操作在浏览器有诸多的限制,而在 Node 则轻轻松松。

对于前端开发者来说, Node 的巨大优势在于,使用一种语言就可以编写所有东西(前端和后端),不再花费很多精力去学习各种各样的开发语言。

哪怕仅仅只做 Web 开发,也不再需要顾虑新的语言特性在浏览器上的兼容性( e.g. ES6 、 ES7 、 ES8 、 ES9 …), Node 配合构建工具,以及诸如 Babel 这样的代码编译器,可以帮转换为浏览器兼容性最高的 ES5 。

工程化的构建工具

目前已经有很多流行的构建工具,例如: GruntGulpWebpackSnowpackParcelRollupVite … 每一个工具都有自己的特色。

构建工具通常集 “语言转换 / 编译” 、 “资源解析” 、 “代码分析” 、 “错误检查” 、 “任务队列” 等非常多的功能于一身。

比如构建工具的“语言转换 / 编译”,新版本的 JS API非常多,由于浏览器版本不同,就会产生兼容性问题,就需要对新API进行转化,通常会让构建工具来自动化完成,常见的方案就有 Babel

除了 “语言转换 / 编译”,在实际的开发中,构建工具可以更好地提高开发效率、提供自动化的代码检查、规避上线后的生产风险,例如:

  • 项目好多代码可以复用,可以直接抽离成模块 、组件 ,交给构建工具去合并打包
  • TypeScript的类型系统和代码检查真好用,也可以放心写,交给构建工具去编译
  • CSS 写起来很慢,可以使用 Sass 、 Less 等CSS 预处理器 ,利用它们的变量支持、混合继承等功能提高开发效率,最终交给构建工具去编译回 CSS 代码
  • 海量的 npm 包开箱即用,剩下的工作交给构建工具去按需抽离与合并
  • 项目上线前代码要混淆,人工处理太费劲,交给构建工具自动化处理

Webpack

Webpack 是一个老牌的构建工具,前些年可以说几乎所有的项目都是基于 Webpack 构建的,生态最庞大,各种各样的插件最全面,对旧版本的浏览器支持程度也最全面。Webpack 官网,在Vue2项目中,一般默认使用Webpack作为构建工具。

Vite

基于 ESM 实现的构建工具,主打更轻、更快的开发体验,主要面向现代浏览器。Vite 官网

两者的区别

Webpack 会先打包,再启动开发服务器,访问开发服务器时,会把打包好的结果直接给过去,下面是 Webpack 使用的 bundler 机制的工作流程。

Vite 是基于浏览器原生的 ES Module ,所以不需要预先打包,而是直接启动开发服务器,请求到对应的模块的时候再进行编译,下面是 Vite 使用的 ESM 机制的工作流程。

所以当项目体积越大的时候,在开发启动速度上, Vite 和 Webpack 的差距会越来越大。

构建方面,为了更好的加载体验,以及 Tree Shaking 按需打包 、懒加载和 Chunk 分割利于缓存,两者都需要进行打包;但由于 Vite 是面向现代浏览器,所以如果项目有兼容低版本浏览器的需求的话,建议还是用 Webpack 来打包,否则, Vite 是目前的更优解。

开发环境和生产环境

开发环境

比如npm run dev启动前端工程,如果基于 Webpack 或者 Vite 这样的构建工具,测试环境提供了更多的功能,例如:

  • 可以使用 TypeScript 、 CSS 预处理器之类的需要编译的语言提高开发效率
  • 提供了热重载( Hot Module Replacement , 简称 HMR ),当修改了代码之后,无需重新运行或者刷新页面,构建工具会检测的修改自动帮更新
  • 代码不会压缩,并有 Source Mapping 源码映射,方便 BUG 调试
  • 默认提供局域网服务,无需自己做本地部署

生产环境

比如npm run build构建打包前端工程,TypeScript 代码编译成了 JavaScript ,这个时候 dist 文件夹下的代码文件就处于 “生产环境” 了,因为之后不论源代码怎么修改,都不会直接影响到它们,直到再次执行 build 编译。

  • 代码会编译为浏览器最兼容的版本,一些不兼容的新语法会进行 Polyfill
  • 稳定,除非重新发布,否则不会影响到已部署的代码
  • 打包的时候代码会进行压缩混淆,缩小项目的体积,也降低源码被直接曝光的风险

环境判断

在 Webpack ,可以使用 process.env.NODE_ENV 来区分开发环境( development )还是生产环境( production ),它会返回当前所处环境的名称。

在 Vite ,还可以通过判断 import.meta.env.DEVtrue 时是开发环境,判断 import.meta.env.PRODtrue 时是生产环境(这两个值永远相反)。