X

曜彤.手记

随记,关于互联网技术、产品与创业

  1. CSS
    1. 响应式设计(Responsive Web Design,RWD)
      1. 媒体查询(Media Query)
      2. 弹性盒子(FlexBox)
      3. CSS Grid
      4. 响应式图形、媒体
      5. 响应式排版
      6. 视口元标记
    2. Tailwind CSS
    3. CSS Module
    4. CSS 预处理器(SCSS、Less)
    5. 其他
      1. BEM(Block, Element, Modifier)
  2. JavaScript
    1. 原型链
      1. 显式指定原型链
      2. 函数对象的原型链
      3. 属性的“遮盖”
      4. setter 的“遮盖”
      5. prototype 与 constructor 属性
      6. new 运算符
      7. 函数作为成员方法调用
    2. 深拷贝
    3. 值深比较(基础版)
    4. instanceof 增强型实现
    5. 简单 Promise 实现
    6. Reactive 响应式基本实现
    7. 斐波那契生成器
    8. 可取消函数(2650. 设计可取消函数)
  3. React
  4. Next.js(App Router)
  5. HTTP
  6. Node.js
  7. 其他前端相关
    1. Module Federation
    2. Redux 核心原理与实现
    3. Throttle & Debounce
    4. Virtual List 虚拟列表
    5. Webpack 打包基本原理
    6. 前端模块化
    7. Service Worker + PWA
    8. 性能优化
      1. 代码分割(Bundle Splitting)
      2. 其他常用优化手段
    9. 前端测试
      1. 测试驱动开发(TDD)
    10. 事件循环(Event Loop)
    11. 前端动画
    12. 单页应用
    13. GraphQL
    14. 正则表达式
    15. Git 代码版本控制
    16. TypeScript
  8. 解决方案
    1. DOM-to-Image 方案

前端面试模版

CSS

响应式设计(Responsive Web Design,RWD)

媒体查询(Media Query)

  • 先为窄屏设备创建简单的单列布局,然后再检查是否适用于更宽的屏幕,并在确定屏幕宽度足够处理时实现多列布局。优先考虑移动设备的设计被称为移动优先设计
@media screen and (min-width: 80rem) {  /* 断点,使用相对单位 */
  .container {
    margin: 1em 2em;
  }
}

弹性盒子(FlexBox)

  • 通过更改 flex-grow 和 flex-shrink 控制元素在空间过大、过小时的表现形式;
@media screen and (min-width: 600px) {  /* 配合媒体查询 */
  .wrapper {
    display: flex;
  }

  .col1 {
    flex: 1;
    margin-right: 5%;
  }

  .col2 {
    flex: 2;
  }
}

CSS Grid

@media screen and (min-width: 600px) {
  .wrapper {
    display: grid;
    grid-template-columns: 1fr 2fr;   /* 动态列宽度,比例 1:2 */
    column-gap: 5%;
  }
}

响应式图形、媒体

  • 使用支持响应式图像的 <img> 标签(利用 srcset 和 sizes 属性),可以让浏览器根据设备的屏幕分辨率或视口宽度,自动选择最合适的图片资源,实现更快加载、清晰显示和节省带宽。
img,
picture,
video {
  max-width: 100%;  /* 确保媒体永远不会大于其响应容器 */
}

响应式排版

  • 基于媒体查询:
html {
  font-size: 1em;
}

h1 {
  font-size: 2rem;
}

@media (min-width: 1200px) {
  h1 {
    font-size: 4rem;
  }
}
  • 基于视口单位:
h1 {
  font-size: calc(1.5rem + 4vw);  /* 1vw 等于视口宽度的百分之一 */
}

视口元标记

<!-- 将视口的宽度设置为设备宽度,并将文档缩放到其预期大小的 100% -->
<meta name="viewport" content="width=device-width,initial-scale=1" />

Tailwind CSS

  • Tailwind 提供了原子化的「实用类」,一个类对应一个样式,通过组合可以灵活实现各种样式。
  • 代码即样式,可读性高;
  • Tailwind 配置中定义了统一的颜色、间距、字体等设计系统;
  • 内置响应式断点类,写法简洁:
<div class="text-sm md:text-lg lg:text-xl">Responsive Text</div>

CSS Module

  • 目的:为每个 CSS 类名生成一个作用于当前模块的唯一标识符,从而实现“样式隔离”;
  • 工作流程:
    • 编写 *.module.css 文件;
    • 构建工具读取该文件,为每个类名生成一个全局唯一类名。类名中的 hash 部分通常基于 CSS 模块文件路径、类名,甚至文件内容生成,可以保证最终类名的唯一性;
    • 生成「原始类名 -> 唯一类名」的映射对象(JS);
    • 在组件中通过原始类名嵌入唯一类名;
    • React 根据唯一类名找到 CSS 样式。

CSS 预处理器(SCSS、Less)

  • 目的:让 CSS 更像“程序语言”一样强大、可复用、结构清晰;

其他

BEM(Block, Element, Modifier)

  • 一种 CSS 命名规范,为了写出可读性强、结构清晰、易维护的 CSS 类名;
<div class="card card--featured">
  <h2 class="card__title">Article Title</h2>
  <p class="card__content">Some content here...</p>
</div>
/**
  .block {}                  // 块;
  .block__element {}         // 元素(block 的子部分);
  .block--modifier {}        // 修饰符(block 的一种状态);
  .block__element--modifier  // 元素的状态。
*/
.card {
  border: 1px solid #ddd;
}

.card--featured {
  border-color: gold;
}

.card__title {
  font-size: 20px;
}

.card__content {
  color: #666;
}

JavaScript

原型链

原型链原型链

let foo = { x: 42 }
Object.getPrototypeOf(foo) === Object.prototype  // true
Object.getPrototypeOf(Object.prototype) === null  // true
foo.hasOwnProperty('x')  // true
foo.hasOwnProperty('toString')  // false
  • 对象通过内部槽 [[Prototype]] 指向其构造函数的 prototype 对象;

显式指定原型链

显式指定原型链显式指定原型链

let bar = Object.create(foo)  // 将 foo 作为 bar 的隐式原型;
foo.isPrototypeOf(bar)  // true
Object.prototype.isPrototypeOf(bar)  // true
// let bar = {}
// Object.setPrototypeOf(bar, foo)
  • 通过 Object.create 可以显式修改对象的隐式原型链;

函数对象的原型链

函数对象的原型链函数对象的原型链

function foo() {}
  • JS 函数是“头等对象(First-class Objects)”,意味着它们具有类似普通对象一样的特征:
    • 可作为参数传递;
    • 原型链继承自 Object.prototype。

属性的“遮盖”

let foo = {}
Object.defineProperty(foo, 'x', { writable: false, value: 40 })  // 只读属性;
let bar = Object.create(foo)
bar.x = 100  // 不会遮盖位于 foo 上的同名属性;
console.log(bar.x)  // 40
  • 位于最近原型链上的属性会“遮盖(Shadowing)”位于上层原型链的同名属性。
  • 位于原型链上的只读属性无法被“遮盖”;

setter 的“遮盖”

const foo = {
  set myProp(v) {
    this.x = v + 1
  }
}
const bar = Object.create(foo)
bar.myProp = 10  // setter 被调用;
bar.x === 11  // true
  • 位于原型链上的 setter 不会被遮盖,反而会被调用;

prototype 与 constructor 属性

函数对象的原型链函数对象的原型链

function foo() {}
foo.prototype.constructor === foo  // true
foo.prototype.x = 42
foo.y = 100
Object.getPrototypeOf(foo) === Function.prototype  // true

let obj = new foo()
obj.x  // 42
obj.constructor === foo  // true
foo.prototype.isPrototypeOf(obj)  // true
  • constructor 属性不参与新对象的创建过程,只用来反向查找某个对象的构造函数是什么;
  • 函数对象的 prototype 属性用于放置,需要以该函数对象作为构造函数创建出的对象继承的属性;
    • [[prototype]]:真实的原型链,查找属性时的遍历路径;
    • prototype 属性:可能的原型链,当生成新对象时可能会继承。
  • 在构造函数的函数体执行前,JS 引擎就会将 this 指向新创建的对象。而当构造函数执行后,this 被隐式返回;

new 运算符

let obj = new foo()  // 使用 new 创建新对象;
let obj = Object.create(foo.prototype)  // 设置原型链继承,返回空对象;
foo.call(obj)  // 执行 foo 构造函数,this 指向 obj;
return obj

函数作为成员方法调用

function foo() { this.x *= 2 }
let obj = { x: 10, bar: foo }
obj.bar()  // foo 中的 this 指向 obj;
obj.x === 20  // true

深拷贝

function deepClone(obj) {
  if (obj === null || typeof obj !== 'object') return obj
  const clone = Array.isArray(obj) ? [] : {}
  for (const key in obj) {
    if (obj.hasOwnProperty(key))  // 注意 for..in 会迭代自身和原型链上的可枚举属性;
      clone[key] = deepClone(obj[key])
  }
  return clone
}

值深比较(基础版)

function areDeeplyEqual(o1, o2) {
  if (o1 === o2) return true
  if (o1 === null || o2 === null) return false
  if (typeof(o1) !== 'object' || typeof(o2) !== 'object') return false
  if (Array.isArray(o1) !== Array.isArray(o2)) return false

  const key1 = Object.keys(o1)
  const key2 = Object.keys(o2)
  if (key1.length !== key2.length) return false
  for (const key of key1) {
    if (!o2.hasOwnProperty(key)) return false
    if (!areDeeplyEqual(o1[key], o2[key])) return false
  }
  return true 
}

instanceof 增强型实现

function checkIfInstanceOf(obj, classFunction) {
  if (obj === undefined || obj === null) return false
  if (classFunction === undefined || classFunction === null) return false
  while (true) {
    const proto = obj.__proto__
    if (proto === null) return false
    if (proto === classFunction.prototype) return true
    obj = obj.__proto__
  }
}
  • *.prototype.__proto__ 通常等于 Object.prototype,因为 prototype 本身就是一个普通对象,类似于通过 new Object() 产生的对象;
  • Object 是一个函数,所以 Object.__proto__ 等于 Function.prototype。Function 也类似。

简单 Promise 实现

// 使用方;
new MyPromise((resolve) => {
  setTimeout(() => { 
    resolve('First resolved!') 
  }, 1000)
}).then((value) => {
  console.log(value)
  return 'Second resolved!'
}).then((value) => {
  console.log(value)
  return new MyPromise((resolve) => {
    setTimeout(() => {
      resolve('Third resolved!')
    }, 1000)
  })
}).then((value) => {
  console.log(value)
})
// 实现方 - 仅处理了 fulfilled 状态;
// 1. rejected 状态的处理;
// 2. then 回调函数应该按照微任务执行(queueMicrotask);
// 3. 错误处理逻辑。
class MyPromise {
  #value = null
  #state = 'pending'
  #callbacks = []
  constructor(executor) {
    const resolve = (value) => {
      if (this.#state != 'pending') return
      this.#value = value
      this.#state = 'fulfilled'
      this.#callbacks.forEach(cb => cb(value))  // 异步 resolve 时被调用;
    }
    executor(resolve)
  }
  then(cb) {
    return new MyPromise((resolve) => {
      const handleFulfilled = () => {
        const result = cb(this.#value)
        if (result instanceof MyPromise) {
          result.then(resolve)
        } else {
          resolve(result)
        }
      }

      if (this.#state === 'fulfilled') {
        handleFulfilled()  // 同步 resolve 时被调用;
      } else {
        this.#callbacks.push(handleFulfilled)
      }
    })
  }
}

Reactive 响应式基本实现

当数据变化时,系统能自动追踪依赖并更新相关视图或逻辑,无需手动管理更新。基本原理:通过 Proxy 自动追踪依赖、触发更新

let activeEffect = null

function effect(fn) {  // 添加依赖入口;
  activeEffect = fn
  fn()
  activeEffect = null
}

// target (响应式对象) → Map (属性名 → Set(effect 函数));
// 保持对响应式对象的弱引用,不影响 GC;
const targetMap = new WeakMap()

function track(target, key) {  // 收集依赖;
  if (!activeEffect) return
  let depsMap = targetMap.get(target)
  if (!depsMap) {
    depsMap = new Map()
    targetMap.set(target, depsMap)
  }
  let deps = depsMap.get(key)
  if (!deps) {
    deps = new Set()
    depsMap.set(key, deps)
  }
  deps.add(activeEffect)
}

function trigger(target, key) {  // 找到对应的 effects,全部重新调用;
  const depsMap = targetMap.get(target)
  if (depsMap) {
    const deps = depsMap.get(key)
    if (deps) {
      for (const effect of deps) {
        effect()
      }
    }
  }
}

function reactive(target) {
  return new Proxy(target, {
    get(obj, key) {
      track(obj, key)  // 当触发某个 effect 时,开始收集依赖;
      return obj[key]
    },
    set(obj, key, value) {
      obj[key] = value
      trigger(obj, key)
      return true
    }
  })
}
const state = reactive({ count: 0 })
effect(() => {
  console.log(state.count)
})
++state.count
++state.count

斐波那契生成器

// 调用后返回一个迭代器,可以用 for...of 遍历;
function *fibGenerator() {
  const dp = Array.from({ length: 50 }, (_, idx) => idx)
  dp[0] = 0
  yield dp[0]
  dp[1] = 1
  yield dp[1]
  for (let i = 2; i < dp.length; ++i) {
    dp[i] = dp[i - 1] + dp[i - 2]
    yield dp[i]
  }
}

可取消函数(2650. 设计可取消函数

function cancellable(generator) {
  let shouldCancel = false
  let cancel = () => {
    shouldCancel = true
  }
  // AbortController 取消 Promise 的本质是 Promise 在内部监听了 signal 的 abort 事件,-
  // 然后借此通过 reject 取消 Promise。无法从外部直接取消任何 Promise;
  const promise = new Promise((resolve, reject) => {
    const driver = (input = undefined, isThrow = false) => {
      let p
      try {
        // generator.next 传入一个值作为上一个 yield 的结果,并返回下一个 yield / return 的值(通过 done 字段区分);
        // generator.throw 传入一个值作为 catch 原因,并返回下一个 yield 的值;
        p = isThrow ? generator.throw(input) : generator.next(input)
      } catch(e) {
        reject(e)
      }

      if (p.done) {
        resolve(p.value)
        return
      }
      
      p.value.then((v) => {
        if (shouldCancel) {
          driver('Cancelled', true)
        } else {
          driver(v, false)  // 继续处理下一个 Promise / return;
        }
      }).catch(e => {
        if (shouldCancel) {
          driver('Cancelled', true)
        } else {
          driver(e, true)  // 返回错误,并继续处理下一个 Promise / return;
        }
      })
    }

    driver(undefined, false)
  })

  return [
    cancel, 
    promise,
  ]
};

React

Next.js(App Router)

  • Next.js 使用基于文件系统的路由,意味着文件的组织结构直接映射了可用路由(路由位置上的 page.tsx 必须存在);
  • 默认情况下,Layout 和 Page 是 React 服务器组件,对应两种 SSR 方式:
    • 静态渲染(Static Rendering):在编译或缓存重验证时进行。结果会被缓存;
    • 动态渲染(Dynamic Rendering):在收到请求时进行,结果不会缓存。
  • Next.js 的 <Link> 组件会在用户进入路由所在视口时,自动预取路由对应页面的内容;
  • 当提供 loading.tsx 时,Next.js 会启动流式渲染(Streaming SSR),即先渲染占位 Loading,再异步渲染主体部分;
  • Next.js SSR 基本流程:
    • 服务器组件在后端被渲染为静态 HTML + RSC Payload(描述组件信息);
    • 浏览器收到 HTML 优先显示骨架;
    • 浏览器下载 JS(包含客户端组件),并使用 RSC Payload 将服务器组件渲染整合入客户端组件;
    • 最后,JS 水合客户端组件,将事件绑定和状态注入,让它们变得可交互。
  • Server Action 将服务端逻辑直接嵌入组件,可用于表单提交、数据操作等,不再需要手动创建 API 路由和使用 fetch。它是比 HTTP 请求更上层的一种抽象,隐藏了底层网络细节,让你像调用本地函数一样调用服务端逻辑。所以不适合基于 HTTP 上层其他应用协议的数据交互形式;

HTTP

Node.js

其他前端相关

Module Federation

微前端架构微前端架构

Module Federation 是 Webpack5(其他构建工具也提供类似支持)引入的一种模块共享机制,允许多个独立构建的应用在运行时动态加载和共享代码。适用于微前端架构,多个不同团队采用不同技术栈,最后在应用层通过 MF 进行功能整合。

Redux 核心原理与实现

统一状态对象树(state),关注者设置订阅,发布者通过分发(dispatch)action 进行状态变更,dispatch 通过 action.type 找到对应 reducer,结合 action.data 完成状态变更并返回新的全局状态树,最后依次通知所有订阅者状态发生变化。

  • 单一状态树:应用的所有状态保存在一个对象树中,方便管理和调试;
  • 状态只读:状态只能通过 dispatch(action) 更新,不能直接修改;
  • 纯函数更新:reducer 是一个纯函数,确保状态更新逻辑可预测。
function createStore(reducer) {
  let state // 保存单一状态树;
  let listeners = [] // 保存订阅者;

  function getState() { return state }  // 获取当前状态;

  // 派发 Action;
  function dispatch(action) {  // { type, data, }
    state = reducer(state, action)  // 根据 reducer 更新状态;
    listeners.forEach((listener) => listener(state))  // 通知所有订阅者;
  }

  function subscribe(listener) {
    listeners.push(listener) // 添加订阅者;
    return () => {
      // 返回取消订阅函数;
      listeners = listeners.filter((l) => l !== listener)
    };
  }

  return { getState, dispatch, subscribe }
}

// Reducer 示例;
function counterReducer(state = { count: 0 }, action) {
  switch (action.type) {
    case 'INCREMENT':
      return { count: state.count + (action.data || 1) }
    case 'DECREMENT':
      return { count: state.count - (action.data || 1) }
    default:
      return state
  }
}

const store = createStore(counterReducer)  // 使用 Store;

const unsubscribe = store.subscribe((state) => {  // 订阅状态变化;
  console.log('State changed:', state)
});

// 派发 Action;
store.dispatch({ type: 'INCREMENT' })   // 输出: State changed: { count: 1 }
store.dispatch({ type: 'INCREMENT' })   // 输出: State changed: { count: 2 }
store.dispatch({ type: 'DECREMENT' })   // 输出: State changed: { count: 1 }

unsubscribe()  // 取消订阅;
store.dispatch({ type: 'INCREMENT' }) // 不会输出订阅信息;

Throttle & Debounce

  • Throttle(节流):限制函数在指定时间间隔内只能执行一次。适合高频触发场景,控制触发次数,比如:限制重试按钮的实际触发频率;
function throttle(fn, delay = 1000) {  // 毫秒;
  let lastTime = 0
  return function(...args) {
    const now = Date.now()
    if (now - lastTime >= delay) {
      if (typeof fn === 'function') fn.apply(this, args)
      lastTime = now
    }
  }
}
  • Debounce(去抖动):延迟函数执行,只有在指定时间内没有再次触发时才会执行。适合用户输入或操作停止后处理,比如:搜索框输入、自动保存、滚动事件。
function debounce(fn, delay = 1000) {
  let timer
  return function(...args) {
    clearTimeout(timer)
    timer = setTimeout(() => {  // 事件触发后至少等待特定时间再执行;
      fn.apply(this, args)
    }, delay)
  }
}

Virtual List 虚拟列表

虚拟列表基本原理虚拟列表基本原理

import React, { useRef, useState, useEffect, useCallback } from 'react';

const itemHeight = 30;         // 每个列表项高度;
const visibleCount = 10;       // 可视区域最多显示多少个项;
const buffer = 2;              // 上下额外渲染几项做缓冲,避免滚动闪烁;

export const VirtualList = (props) => {
  const { items = Array.from({ length: 100 }, (_, i) => `Item ${i}`) } = props
  const containerRef = useRef(null)
  const [scrollTop, setScrollTop] = useState(0) 

  // 核心逻辑;
  const startIdx = Math.max(0, Math.floor(scrollTop / itemHeight) - buffer)
  const endIdx = Math.min(items.length, startIdx + visibleCount + 2 * buffer)
  const visibleItems = items.slice(startIdx, endIdx)

  useEffect(() => {
    const ref = containerRef.current
    let ticking = false
    const onScroll = () => {
      if (!ticking) {
        window.requestAnimationFrame(() => {
          setScrollTop(ref.scrollTop)  // 获得相对于父容器的滚动距离,决定现在屏幕顶端是第几个 item;
          ticking = false
        })
        ticking = true
      }
    }
    if (ref) ref.addEventListener('scroll', onScroll)
    return () => {
      ref.removeEventListener('scroll', onScroll)
    }
  }, [])

  return (
    <div
      ref={containerRef}
      style={{
        height: visibleCount * itemHeight,
        width: '100px',
        overflowY: 'auto',
        border: '1px solid #ccc',
        position: 'relative',
      }}
    >
      {/* 渲染滚动条 */}
      <div style={{
        height: items?.length * itemHeight,
        position: 'relative',
      }}>
        {/* 渲染列表项 */}
        {
          visibleItems.map((item, index) => {
            const realIdx = startIdx + index
            return (
              <div 
                key={realIdx}
                style={{
                  position: 'absolute',
                  height: itemHeight,
                  willChange: 'transform',
                  transform: `translateY(${realIdx * itemHeight}px)`,  
                }}
              >
                {item}
              </div>
            )
          })
        }
      </div>
    </div>
  )
}

Webpack 打包基本原理

  1. 读取配置
  2. 构建依赖图:从 entry 入口文件出发,递归地分析模块依赖关系,构建一个模块依赖图。
  • 解析每个模块(包括 import、require、动态导入);
  • 通过 AST(抽象语法树)分析依赖;
  • 递归分析子依赖模块;
  • 形成一张模块图(有向图),节点是模块,边是依赖关系。
  1. 模块转换:通过相应的 Loader 转换 JS 能识别的模块;
  2. 模块打包
  • 动态导入(import())会被打包为分离的 chunk,形成代码分割(code splitting);
  • 最终输出静态资源文件(Bundle)。
  1. 插件机制:插件钩子机制,支持在生命周期的不同阶段插入自定义逻辑。
  2. 输出:将构建好的文件输出到 output.path 指定目录,完成打包过程。

前端模块化

CommonJS、AMD 和 ES Modules 是 JavaScript 中的三种模块化规范。

特性 CommonJS AMD ES Modules
加载方式 同步 异步 异步
使用环境 Node.js 浏览器 浏览器 + Node
支持静态分析 ✅(基于严格的语法和语义限制)
是否原生 Node.js 原生 需库支持 现代浏览器原生
语法 require() define() 定义模块、require() 加载模块 import/export
  • UMD 是一种“模块适配层”,不是语法规范。它是为“发布通用 JS 库”而生的模块包装格式,核心目标是让代码“在任何环境中都能跑”。
(function (root, factory) {
  if (typeof define === 'function' && define.amd) {
    // AMD;
    define([], factory);
  } else if (typeof module === 'object' && module.exports) {
    // CommonJS;
    module.exports = factory();
  } else {
    // 浏览器全局变量;
    root.MyLibrary = factory();
  }
}(typeof self !== 'undefined' ? self : this, function () {
  // 模块实现代码;
  return {
    hello: function () {
      console.log('Hello from UMD!');
    }
  };
}));

Service Worker + PWA

Service Worker 是一种在后台运行(独立于网页)的特殊 JavaScript 工作线程,它的基本能力:

  • 缓存响应:拦截请求并返回缓存的资源(例如 HTML、CSS、JavaScript 文件);
  • 修改请求或响应:动态生成或替换请求的响应内容;
  • 控制网络流量:决定某些请求是否发送到服务器,或者完全阻止某些请求;
  • 离线支持:如果网络不可用,则直接从本地缓存中获取资源,无需发起网络请求。

PWA(Progressive Web App)基于 SW 构建,它的核心目标是让网页拥有类似原生应用的流畅体验,同时无需安装。

性能优化

前端常用性能指标前端常用性能指标

代码分割(Bundle Splitting)

Bundle Splitting 关键在于代码切分点的合理选取,以及对应的懒加载机制。

  • 提升首屏加载速度:只加载首屏需要的代码,其他代码按需加载(lazy-loading);
  • 更高的缓存命中率:未变化的子包请求可以利用缓存(更细的代码粒度,更好地缓存);

其他常用优化手段

  • 资源优化:
    • 代码、图片等资源压缩
    • Tree Shaking 分析 ES Module 的静态依赖关系,移除无用代码。要注意的是,默认情况下,Webpack 认为所有模块都具有副作用,因此哪怕在当前文件中没有使用 import 文件导出的任何内容,该 import 也不会被移除。
  • 加载优化:
    • 提前加载关键资源或预测用户行为:
      • preload:当前页面即将使用的资源(优先级高);
      • prefetch:未来可能使用的资源(低优先级);
    • 异步加载 JS 脚本、不阻塞 DOM 渲染:
      • defer:页面解析完再按顺序执行;
      • async:下载完成立即执行,可能打断渲染;
    • 代码分割,按需懒加载
    • 首屏关键 CSS 内联,提升 FCP。
  • 网络优化:
    • HTTP/2HTTP/3:支持多路复用、减少 HTTP 和 TCP 层的队头阻塞;
    • CDN 加速:就近缓存静态资源,加速访问;
    • 服务端开启 Gzip 等压缩策略
  • 渲染优化:
    • SSRSSG(Static Site Generation,构建时渲染成 HTML)提高首屏渲染速度;
    • 图片懒加载(loading=”lazy”),进入视口时再加载、尺寸固定避免 reflow;
  • 运行时优化:
    • 使用 React 中的 Memoization,减少组件重新渲染;
    • 使用虚拟列表优化长列表;
    • 使用 requestAnimationFrame 替代 setTimeout 控制动画;
    • 合理使用防抖节流
    • 使用 GPU 加速的 CSS 属性(transform \ opacity);
    • 使用 Web Worker 优化任务执行(比如配合 OffscreenCanvas 将图形计算任务从主线程剥离);
    • 使用 requestIdleCallback 在浏览器空闲时间处理非紧急任务;
    • 使用 CSS 中的 will-change 属性提前告知元素的哪些属性将要发生变化。

前端测试

前端基本测试流程前端基本测试流程

测试驱动开发(TDD)

  • 三步走流程:
    • 编写失败的测试用例(Red);
    • 实现功能使测试通过(Green);
    • 重构代码(Refactor)。
  • 优势:保证代码可靠,能写出可测试代码;
  • 劣势:时间成本增加(初期和后期维护,尤其当功能和 UI 频繁变化时)。

事件循环(Event Loop)

JS 事件循环JS 事件循环

步骤 描述 回调类型
1 JS 微任务队列执行(优先级高) Promise.then、queueMicrotask、MutationObserver 等
2 JS 宏任务执行(优先级低) setTimeout、setInterval 等
3 requestAnimationFrame 执行 在浏览器准备开始绘制前执行
4 浏览器执行布局、绘制(Layout + Paint)
5 如果还有时间,执行 requestIdleCallback 空闲时间任务

前端动画

requestAnimationFrame 能够确保动画在浏览器下一帧绘制之前被调用,与显示器刷新率同步,同时避免掉帧和资源浪费。而 setInteval 则需要人工指定固定的动画执行频率,帧率不精准(受宏任务调度影响),不受页面可见性影响(不节能)。

操作系统或 GPU 每隔一帧(16.6ms)发出一次 VSync 信号,浏览器内部的 Frame Scheduler(帧调度器)监听该信号。触发后,浏览器开始准备调度 rAF 回调函数,并准备进行 DOM 更新、样式计算、布局、绘制。

单页应用

SPA(Single Page Application,单页应用)的整个应用在加载初始 HTML 页面之后,后续的所有页面更新都通过 JavaScript 在客户端完成,而不需要重新加载整个页面。

  • 路由:
    • Hash 路由:通过 window.onhashchange 监听 hash 变化,切换页面;
    • History 路由:基于 HTML5 的 History API,页面变化基于真实的 URL 路径变化。但需要服务器支持(URL Rewrite),当访问不同路径时均返回 index.html。
  • SEO 优化:
    • SSR、SSG;
    • 动态渲染:判断请求方如果是爬虫,则返回完整快照;
    • 配置 sitemap.xml、robots.txt。

GraphQL

GraphQLGraphQL

REST 是基于 HTTP 协议的一种 API 架构模式,它使用 HTTP 类型及请求状态来表示资源的处理方式(动作)及处理结果,通过仅带有名词的 URL 来表示请求的资源类型,因此我们可以说 REST 基于「资源」来设计 API。而 GraphQL 则是围绕「数据需求」和「查询」设计 API 的查询语言。

  • 基本特点:
    • 客户端驱动:客户端发送的 Query 决定返回哪些字段,从而避免数据过多或不足;
    • 结构即查询:响应结构和查询结构一一对应;
    • 单一的入口:所有数据通过一个统一的入口获取,而不是多个 REST 路由;
    • 强类型系统:Schema 定义了客户端可以查询或操作的数据结构,提升了类型安全;
    • 组合与解耦:更像是一个数据抽象层,能将多个数据源聚合为一个图(通过 Resolver 控制数据来源)。
  • 技术选型:新项目、公司有配套的 GraphQL 团队、生态和经验;

正则表达式

Git 代码版本控制

TypeScript

解决方案

DOM-to-Image 方案




评论 | Comments


Loading ...