Skip to main content

浏览器原理

缓存机制​

为什么需要缓存?

  • 更快获取资源,本地
  • 减少服务器压力,重复资源
  • 节省宽带,少请求

缓存分类​

强缓存​

直接使用本地资源,不会发起浏览器请求

通过两个http响应头实现:

  • Expires,指定过期时间,已弃用。(可以通过故意改变系统时钟来引发问题)
  • Cache-Control,指定经过时间,且优先级更高

案例:

HTTP/1.1 200 OK
Cache-Control: max-age=3600
  • max-age=3600 表示资源在 3600 秒(1 小时)内有效。
  • 在这 1 小时内,浏览器会直接从缓存加载资源,不会向服务器发送请求。

协商缓存​

当强缓存失效时,浏览器会向服务器发起请求,服务器会根据请求头判断资源是否过期;

若资源未修改,服务器返回304,Not Modified,浏览器继续使用本地缓存;

若资源已修改,服务器返回新资源。

通过两组http头实现:

  • Last-Modified和If-Modified-Since

    • 服务器返回Last-Modified,表示资源最后修改时间
    • 浏览器下次请求时带上If-Modified-Since,浏览器根据时间判断资源是否更改
  • Etag和If-None-Match

    • 服务器返回ETag,表示资源唯一标识
    • 浏览器下次请求带上If-None-Match,浏览器根据标识判断

案例:

HTTP/1.1 200 OK
ETag: "123456"
  • 服务器返回资源的唯一标识 ETag。
  • 浏览器下次请求时带上 If-None-Match: "123456"。
  • 如果资源未修改,服务器返回 304 Not Modified,浏览器继续使用缓存。

工作流程​

第一次请求

  • 浏览器向服务器请求资源
  • 服务器返回资源,并设置相关http头(如Cache-control、Etag)
  • 浏览器将资源保存到缓存中

后续请求

  • 浏览器检查强缓存是否有效

    • 如果有效,直接从缓存加载资源
    • 如果无效,向服务器发起请求,并携带上协商缓存相关的http头(如If-Modified-Since、If-None-Match)
  • 服务器检查资源是否修改

    • 如果未修改,返回304 Not-Modified,浏览器继续使用缓存
    • 如果修改,返回新的资源,并更新缓存

最佳实践​

静态资源使用强缓存

对于不经常变化的资源,如图片,css,js,可以设置较长缓存时间

Cache-Control: max-age=31536000
  • 表示缓存,max-age=31536000 表示缓存有效期为 1 年。
  • 如果资源需要更新,可以通过修改文件名或添加版本号的方式强制浏览器重新加载。

如

<script src="app.v1.js"></script>
<!-- 更新后 -->
<script src="app.v2.js"></script>

动态资源使用协商缓存

对于经常变化的资源,如html、api数据,可以使用协商缓存

如

Cache-Control: no-cache
ETag: "123456"
  • no-cache 表示每次请求都需要验证缓存是否有效。
  • 服务器通过 ETag 或 Last-Modified 判断资源是否修改。

禁用缓存

对于一些用户隐私相关的资源,可以完全禁用缓存,

如

Cache-Control: no-store
  • no-store 表示禁止浏览器缓存资源。

工程化

工程化基础​

构建工具​

用过哪些构建工具?对比下各自优缺点以及适用场景

webpack

  • 优点:生态成熟,插件系统丰富
  • 缺点:配置复杂,构建速度相对较慢
  • 场景:需要代码分割,懒加载,复杂资源处理的场景

vite

  • 优点:基于ESM的按需编译,开发环境启动快速,开箱即用
  • 缺点:生态较新,部分插件不完善
  • 场景:现代浏览器项目,快速原型开发

rollup

  • 优点:输出代码更干净,适合库打包
  • 缺点:插件生态少,不适合复杂应用
  • 场景:开发第三方库

vite为什么快

如果构建速度慢,你会如何分析和优化(考察tree shaking、缓存、并行)

模块化与包管理​

代码分割配置和原理​

代码分割配置和原理

旨在将代码拆分成更小的文件块(chunks),以便实现按需加载或并行加载,从而提高页面加载性能

主要分3种方式:

  • 入口起点:手动配置多入口
  • 动态导入:使用import
  • SplitChunksPlugin:提取公共依赖和第三方库

技术原理

  • 编译阶段:webpack分析模块依赖关系,识别出可分割的代码块(Chunks)
  • 运行时:通过jsonp或script标签动态加载分割后的代码
  • 缓存优化:利用contenthash实现长期缓存(Long-term Caching)
入口起点分割​

适用场景:明确需要分离的第三方库或基础包

// webpack.config.js
module.exports = {
entry: {
app: './src/index.js',
vendor: ['react', 'react-dom', 'lodash'] // 显式分离第三方库
},
output: {
filename: '[name].[contenthash].js',
path: path.resolve(__dirname, 'dist')
}
};

效果:

  • 生成 app.xxxx.js 和 vendor.xxxx.js 两个文件
  • 第三方库变更时只需更新 vendor 文件的 hash
动态导入​

适用场景:路由懒加载/组件按需加载

// React 组件懒加载
const ProductList = React.lazy(() => import(
/* webpackChunkName: "product" */
'./components/ProductList'
));

// 普通模块动态加载
document.getElementById('btn').addEventListener('click', () => {
import(
/* webpackPrefetch: true */
/* webpackChunkName: "analytics" */
'./analytics'
).then(module => {
module.trackEvent();
});
});

效果:

  • 生成 product.[hash].js 和 analytics.[hash].js 独立 chunk
  • 点击按钮时才会加载 analytics 模块
  • prefetch 会在浏览器空闲时预加载
SplitChunksPlugin 智能分割​

适用场景:自动提取公共依赖

// webpack.config.js
module.exports = {
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
react: {
test: /[\\/]node_modules[\\/](react|react-dom)[\\/]/,
name: 'react-vendor',
priority: 20
},
lodash: {
test: /[\\/]node_modules[\\/]lodash[\\/]/,
name: 'lodash-vendor',
priority: 10
},
commons: {
name: 'commons',
minChunks: 2,
minSize: 0
}
}
}
}
};

效果:

  • 生成 react-vendor.[hash].js(包含 react 相关)
  • 生成 lodash-vendor.[hash].js(仅 lodash)
  • 生成 commons.[hash].js(被引用2次以上的公共模块)

ES Module和Common JS的区别是什么?webpack如何解析他们?

如何设计一个可复用的前端组件库?需要考虑哪些工程问题(按需加载、工程化等问题)?

遇到过npm 或 yarn的依赖冲突吗?如何解决?

代码质量规范​

如何保证团队代码风格统一?如何配置ESLint/Prettier

如何在项目中集成单元测试(Jest)和E2E测试(如Cypress)

如何通过git hooks在提交代码时自动检查规范

性能优化和部署​

优化策略​

首屏加载时间过长的原因有哪些?如何针对性优化?

如何利用 Webpack 的 SplitChunks 优化产物体积?

是否实现过 SSR(服务端渲染)或静态站点生成(SSG)?解决了什么问题?

自动化部署ci/cd​

如何配置项目的自动化部署流程(如 Jenkins/GitHub Actions)?

如何实现灰度发布或 AB 测试?

前端资源如何做 CDN 加速?如何解决缓存更新问题(如文件名 Hash)?

原理​

工具链底层​

webpack中plugin和loader的区别是什么?

loader​

  • 背景:webpack将项目所有资源(js,css,图片,字体等资源)视为模块,但webpack核心功能是处理js模块。所以需要一种转换机制,将非js模块转化成js模块

  • 作用:非js模块转化成js模块(转换文件)

  • 应用场景:

    • css/scss【css-loader、style-loader、sass-loader】转化为js模块后,注入DOM
    • 图片/字体【file-loader、url-loader】转化为js模块后,输出到指定目录
    • es6的js【babel-loader】转为es5的代码,兼容旧浏览器
    • vue/jsx/tsx【vue-loader、babel-loader】转化为js模块
  • 工作流程:

    • 匹配文件:通过正则匹配需要处理的文件(.css、.png)
    • 转换文件:使用loader将文件内容转化成为js模块
    • 添加到依赖图:将转换后的模块添加到webpack的依赖图中,参与打包

plugin​

  • 前置知识:webpack构建过程

    • 初始化:读取配置文件和命令行参数
    • 编译:从入口文件开始,递归接解析模块依赖,调用loader处理模块
    • 输出:将处理后的模块,打包成最终的bundle文件

    在每个阶段,webpack提供了hooks,运行开发者干预构建过程

  • 背景:loader的作用是处理单个文件的转换,有些场景无法处理

    • 全局操作:打包优化、资源管理、环境变量注入
    • 构建过程干预:如打包完成生成一个报告、或打包完成清理输出目录

    这些是loader的局限性,所以需要一种机制,干预构建过程,这便是plugin的作用

  • 应用场景:

    • 干预构建过程:通过webpack提供的钩子,在特定阶段实现自定义逻辑
    • 拓展功能:生成HTML文件【HtmlWebpackPlugin】、压缩代码【TerserWebpackPlugin】、提取CSS【MiniCssExtractPlugin】等
    • 优化构建:代码分割,tree-shaking、缓存优化等
  • 工作原理

    • 钩子机制:webpack构建过程每个阶段,都会触发相应的钩子,plugin可以通过监听这些钩子来执行自定义逻辑

      常见钩子:

      • compilation:编译阶段触发,可以访问模块和chunk
      • emit:生成资源输出到目录之前触发,可以修改输出内容
      • done:在构建完成后触发,可以生成构建报告
    • 实现方式

      plugin是一个类,他需要实现一个apply方法,在apply方法中,可以监听到webpack提供的钩子,从而实现一下自定义逻辑

      class MyPlugin {
      apply(compiler) {
      compiler.hooks.done.tap('MyPlugin', (stats) => {
      console.log('构建完成!');
      });
      }
      }

      module.exports = MyPlugin;
apply方法的设计模式​

apply方法是基于依赖注入(Dependency Injection)和观察者模式(Observer Pattern)的

依赖注入

  • 定义:将依赖对象通过参数注入到目标对象中,而不是在目标对象内部创建
  • 应用:webpack将compiler实例注入到插件的apply方法中,插件不需要关系compiler何时创建
// Webpack 源码简化逻辑
class Webpack {
constructor(options) {
this.options = options;
this.compiler = new Compiler();
this.setupPlugins();
}

setupPlugins() {
const plugins = this.options.plugins || [];
plugins.forEach((plugin) => {
plugin.apply(this.compiler); // 调用插件的 apply 方法
});
}
}

观察者模式

  • 定义:定义对象间一对多依赖关系,当一个对象的状态发生变化时,所有依赖它的对象都会收到通知并自动更新
  • 应用:webpack的钩子机制基于观察者模式,插件通过监听钩子来响应构建过程中的不同阶段
// Webpack 源码简化逻辑
class Compiler {
constructor() {
this.hooks = {
done: new SyncHook(['stats']), // 定义 done 钩子
};
}

run() {
// 构建过程
this.hooks.done.call(stats); // 触发 done 钩子
}
}

手写一个简易的plugin和loader

loader

// 文件内容替换loader
module.exports = function (source) {
// source 是文件内容
return source.replace('World', 'Loader');
};

plugin

上文有

Babel 是如何实现代码转换的?AST(抽象语法树)在其中扮演什么角色?

如何理解 Vite 的“基于 ESM 的按需编译”原理?对比 Webpack 的打包机制。

工程化架构设计​

如何设计一个支持微前端(Micro Frontends)的项目架构?

如何实现项目的多环境配置(开发、测试、生产)?

如果让你从零搭建一个前端工程化体系,你会考虑哪些模块?

场景应用​

项目中需要兼容低版本浏览器,你会如何制定技术方案?

如何实现一个“编译时自动替换业务配置”的功能?(如根据环境变量切换 API 地址)

如果发现线上代码的 Source Map 泄漏,如何快速定位并修复?

如何设计一个支持多主题切换的前端项目

如何实现前端资源的离线化(PWA)

如何通过webpack实现国际化(i18)支持

如何设计一个高性能的表格组件

开放性问题​

你如何看待前端工程化的未来趋势?

是否关注过 Turbopack、Rust 工具链等新技术?

在过去的项目中,你做过哪些提升工程化效率的改进?具体量化结果是什么?

如何设计一个高可用、高性能的前端架构?

前端工程化中有哪些常见的坑?你是如何解决的?

评估候选人能力的核心点​

  1. 工具使用熟练度:是否能灵活配置 Webpack/Vite,解决实际构建问题。
  2. 原理理解深度:是否了解构建工具、Babel、性能优化等底层逻辑。
  3. 实战经验:是否有复杂项目的工程化落地经验(如微前端、SSR、自动化流水线)。
  4. 解决问题能力:面对性能瓶颈、部署异常等场景是否能给出合理方案。
  5. 技术视野:是否关注工程化前沿技术(如 Bundleless、Rust 工具链)。