前端工程化

随着项目复杂度不断提高,前端工程化已成为一种必然。

模块化

随着JS的快速发展,项目的代码量也逐渐提升,单纯将所有代码写在一个文件里已经难以维护 。 没有模块化,所有的JS变量都挂在windows中,命名冲突、全局变量污染等问题接踵而至。有了模块化,我们可以按照功能将JS代码拆成一个个独立的模块,一个文件就是一个模块,模块之间可以引入、使用,在提高代码独立性、复用性 的同时,也让模块之间的依赖关系变得更加明晰。

早期的模块化是通过立即执行函数(IIFE)来实现的,通过创建一个新的作用域,避免了命名冲突、全局变量污染等问题,但是它仍然无法解决模块之间的依赖问题,真正的模块化应运而生,包括AMD、CMD、CJS、ESM等,现在以CJS、ESM为主。 其中CJS主要用于node环境,ESM则是ECMA Script官方发布的模块化标准,早期主要用于浏览器环境,现在node也支持ESM模块化。

来一道工程化必问的面经看看~

CJS、ESM的区别

1.语法层面

  • CJS导入:const a = require('./a.js');
  • CJS导出:module.exports = { name: 'cjs', age: 10 };
  • ESM导入:import { add } from './math'
  • ESM导出:export default {add,name}

注意ESM只能有一个默认导出,CJS的module.exports是一个对象。

2.模块加载方式和时机

(1)CJS运行时同步加载、动态加载

require()本质上是一个函数,只有代码执行到这一行才会读取它引用的模块。

  • 同步加载:只有require()函数执行完了,才会执行后面的代码;
  • 动态加载:require()导入语句可以放在if条件判断语句中,根据条件判断是否执行。

(2)ESM编译时静态分析

在代码编译阶段,就会扫描所有代码,确定模块之间的依赖关系图。

  • 异步加载:import导入语句不阻塞后续代码的正常编译;
  • 静态加载:import语句必须写在最顶层,不能放在条件或者函数里,并且会自动提升到模块顶部执行

3.缓存机制不同

(1)CJS缓存的是module.export对象的值拷贝,即使模块内部后续修改了导出的值,通过require获取到的还是最初导出时缓存的结果。

(2)ESM缓存的是值的引用,导出的变量会和原模块保持动态关联,同步更新。

4.this指向不同

CJS 中:this 指向 module.exports

js 复制代码
console.log(this === module.exports); // true

ESM 默认使用严格模式"use strict",严格模式下全局 thisundefined

5.__dirname__filename

CJS中可以直接使用__dirname(当前模块所在目录) 和 __filename(当前模块的完整路径)。这是因为包裹函数提供了这两个参数:

js 复制代码
(function(exports, require, module, __filename, __dirname) {
  // __filename 和 __dirname 是参数传进来的,可以直接使用
  // 模块代码位置
});

ESM 中不存在,需要用 import.meta.url 获取当前模块文件的url

构建打包

为什么需要打包?

(1)浏览器只认识原生的HTML、CSS、Javascript

为了开发的方便,你使用了很多浏览器不认识的东西:

  • Scss\Less\TailWindCSS
  • JSX\VUE SFC
  • 模块化CJS
  • 新语法(部分老版本浏览器不支持)

打包工具的作用:将这些高级语言、框架语法翻译成浏览器认识的代码

(2)开发环境和生产环境的追求不同

开发环境 生产环境
不压缩代码,方便调试 极致的压缩,以提高加载速度
使用新语法、更好用 兼容老版本浏览器
源码映射 代码混淆

打包工具干了啥

Webpack 想象成一个流水线工厂,你扔进去一堆"原材料"(各种前端文件),它按规则加工,最后生成一份份"成品"(打包后的 JS/CSS/图片等)。

  • 1.Entry(入口)------从哪开始干活?
js 复制代码
entry: './src/main.js'

Webpack 会从入口文件 main.js 开始,顺着 import / require 一层层找依赖,把所有用到的文件都拉进来打包。

  • 2.Output(出口)------打包完放哪?叫啥名?
js 复制代码
output: {
  path: path.resolve(__dirname, 'dist'),
  filename: 'bundle.js'
}

打包完的文件放在dist文件夹下,名为bundle.js

  • 3.Loader ------遇到什么文件,怎么处理(通过配置rules)

Webpack 只认识 JavaScript 和 JSON。Loader 就是翻译官 + 加工厂

js 复制代码
module: {
  rules: [
    {
      test: /\.css$/,          // 遇到 .css 文件
      use: ['style-loader', 'css-loader'] // 用这两个 loader 处理
    }
  ]
}

Loader执行顺序:从右到左,从下到上

.css 文件-->css-loader处理-->style-loader处理-->最终输出

  • 4.Plugin(插件)------流水线上的"增强外挂"

凡是 Loader 做不了的"额外工作",基本都是 Plugin 的活。

js 复制代码
plugins: [
  new HtmlWebpackPlugin({
    // 自动生成 HTML 文件并自动注入打包后的 JS/CSS 资源
    template: './public/index.html'
    // 基于这个模板文件生成最终的 index.html
  })
]
  • 5.Mode(模式)------切换"开发 / 生产"环境
js 复制代码
mode: 'development' // 或 'production'

开发模式:不压缩代码,热更新速度快、有报错提示;

生产模式:自动压缩、优化、剔除无用代码(摇树优化等方式)。

  • 6.Bundle(打包结果)------得到最终产物

构建工具webpack和vite的区别

两者都是目前主流的构建工具,其中vite热更新、冷启动速度都更快。区别主要在开发环境中体现,启动开发服务器时:

(1)Webpack ------ 开张前先全做出来

  • 从 entry 开始递归解析所有 import/require ,构建完整依赖图
  • 用 Loader 转译所有文件(TS→JS、Vue→JS...)
  • 打包合并成 bundle.js,放内存
  • 才启动服务器让你访问

(2)Vite ------ 只开个厨房,现点现做

  • 不解析、不打包业务代码,直接启动轻量 HTTP Server
  • 浏览器通过原生 type="module" 直接请求需要的模块
  • Vite 拦截请求 → 按需用 esbuild(Go 编写,比 Babel 快 10~100 倍)实时转译 → 返回给浏览器
  • 第三方依赖做一次预构建(Pre-bundling) 转成 ESM 并缓存,解决请求瀑布流和CJS不能在浏览器环境中的问题

资源缓存

早期互联网时代,流量带宽是很大的成本,为了降低服务器负载和提升用户访问速度,引入了资源缓存。缓存策略是成本最低的性能优化策略,是指浏览器采用最优方式访问资源,分为强缓存和协商缓存,用于提高二次访问速度。

强缓存 :首次请求时,直接缓存资源(包括资源存多久);后续再请求相同资源时,只要没过期,直接使用缓存,不会向服务器发任何请求

协商缓存 :首次请求缓存资源,后续请求相同资源时,先向服务器发请求问资源更新了吗?没更新-->服务器响应304(Not Modified),使用本地缓存;资源更新了-->服务器响应200(ok)+新资源。

具体使用哪一种缓存策略由服务器返回的响应头控制:

  • 不适用任何缓存:Cache-Control: no-store
  • 强缓存:设置max-age=3600(缓存有效时间为3600s)或者Expires: Mon, 25 Jul 2025 12:00:00 GMT(具体的过期时间)
  • 协商缓存:设置ETag: "686897696a7c876b7e"(资源的唯一标识,可通过对比标识符判断资源是否更新)或者Last-Modified: Wed, 21 Oct 2025 07:28:00 GMT(资源上次更新时间);再次请求相同资源时,浏览器带着If-None-MatchIf-Modified-Since 去问服务器,服务器返回 304 就用缓存,返回 200 就用新资源。

资源缓存方案

  • 不经常变动、带哈希的静态资源:强缓存+较长的缓存时间(资源内容发生变动时,文件名也变,旧缓存自动失效)
  • 偶尔变动、不带哈希的资源:协商缓存(内容可能更新,定期询问)
  • 要求实时性、经常变动的资源(如入口文件):不缓存

对访问速度要求更高的网站未止步于仅仅利用缓存策略,因为缓存策略只能提高二次访问速度;对于资源首次访问,最常见的提速手段是CDN内容分发网络 ,核心是就近获取资源,即把资源存储在离用户很近的CDN服务器上,在避免服务器过载的同时提高访问速度。可以理解为京东在全国各地的主要城市建设本地仓库(CDN服务器),当用户购买商品时根据收货地址智能匹配离用户最近的仓库发货,极大地提高了物流配送时间。

打包体积优化

打包是将项目源码转换成最终上线的代码的过程,但打包之后的代码体积过大,导致首屏加载时间久、用户交互卡顿、响应慢等问题,亟需解决,目前比较常用的较小打包体积的办法如下:

  • 1.摇树优化:在打包阶段,通过静态分析 ES Module 的 import/export 关系,删掉没用到的代码

(1)把每个文件解析成抽象语法树,找出所有 import / export 声明;

(2)从入口 main.js 出发,构建依赖图;

(3)引用标记:顺着依赖图,用到的标记alive,没用到的标记dead;

(4)副作用检查:标记dead + 无副作用 才删除。

!!由于必须静态可分析,因此只对ES Module生效。

  • 2.压缩资源:压缩HTML/CSS/JS代码,压缩字体/图像/音频/视频

(1)HTML/CSS/JS代码:通过配置插件Plugin压缩

(2)字体/图像/音频/视频:部署到生产环境之前使用专业的工具压缩

  • 3.按需加载:将路由界面/触发性功能单独打包成一个chunk,使用时加载、 首屏不需要的资源懒加载等

规范化

  • 代码规范:Eslint

ESLint 负责发现代码中的逻辑问题和潜在错误,比如未使用的变量、可能的 bug、不安全的写法。

  • 代码格式化:Prettier

Prettier 负责代码风格的统一,比如缩进、引号、分号、换行等。它不关心代码逻辑,只关心外观。

  • 代码提交规范:Angular Commit Convention

Angular提交规范的格式包括 Header、Body 和 Footer 三个内容。Header 为必填项,Body 与Footer为可选项,在此不再赘述。header部分只写一行,包含以下三个字段:

(1)type:用于说明commit 的提交类型,也可加括号说明本次改动的影响范围,必选 (2)scope:用于说明commit 的影响范围,可选 (3)subject:用于说明 commit 的细节描述,可选

常见type如下

type 含义 示例
feat 新功能 feat(user): 新增用户登录功能
fix Bug 修复 fix(button): 修复按钮点击无响应
docs 文档变更 docs(readme): 更新安装说明
style 代码格式(不影响逻辑) style: 格式化代码
refactor 重构 refactor(api): 重构请求模块
perf 性能优化 perf(list): 优化虚拟滚动性能
test 测试相关 test(utils): 添加单元测试
chore 构建/工具变更 chore(deps): 升级依赖版本
ci CI/CD 配置变更 ci: 添加 GitHub Actions
相关推荐
做前端的娜娜子1 小时前
前端必看!我把一段"能跑不敢动"的报表代码用 AI 重构成了组件(附完整 Prompt)
前端·ai编程
hunterandroid1 小时前
[鸿蒙从零到一] ArkUI 动画与转场实战:状态驱动、组件过渡与页面衔接
前端
何时梦醒1 小时前
React + TypeScript + Vite 实战:从零构建 Color Picker 应用
前端·javascript·架构
谁在黄金彼岸1 小时前
Nuxt.js 详解(一):Vue 开发者为什么要关注 Nuxt
前端
英勇无比的消炎药1 小时前
TinyRobot v0.5.0 深度解读(三):Layout 组件——如何组织复杂工作区的页面布局
前端·vue.js
默_笙2 小时前
😋 我让 DeepSeek-R1 在浏览器本地跑了起来,后端同事说"你认真的?"
前端·javascript
英勇无比的消炎药2 小时前
TinyRobot v0.5.0 深度解读(四):CLI 脚手架——从零搭建 AI 应用的工程化实践
前端·vue.js·github
程序员黑豆2 小时前
鸿蒙应用开发之双向绑定实战:从 V1 到 V2 的完整迁移指南
前端·harmonyos