A collection of hands-on web exercises and experiments. Like a latte—one part espresso, two parts milk, and one part foam—a familiar, approachable blend for everyday learning.一组动手实践的 Web 练习与实验。就像一杯拿铁——一份浓缩、两份牛奶、一份奶泡——熟悉、易入口,适合日常学习。
Learn how the web works by building small versions of it: Promise/A+, mini React, a router, a bundler and more. Run a page, follow the guide beside it, then read the code.从零实现一些小版本,看懂 Web 是怎样工作的:Promise/A+、mini React、前端路由、打包器,等等。打开一个页面,跟着旁边的说明一步步看,再去读代码。
Start building开始动手View on GitHub在 GitHub 上查看
74 examples · 872/872 Promises/A+ tests · every page tested in Chromium, Firefox and WebKit74 个示例 · Promises/A+ 测试 872/872 · 每个页面都在 Chromium、Firefox 和 WebKit 中测试
How JavaScript and the browser behave, one small page at a time.一次一个小页面,看 JavaScript 和浏览器的真实行为。
Short scripts that show how closures, this and object construction really work.用几段短脚本,看懂闭包、this 和对象构造的真实行为。
Rebuild call, apply and bind to see how a function decides what this is.自己实现 call、apply 和 bind,看懂函数调用时 this 是怎么确定的。
Rebuild new and instanceof: create the object, run the constructor, walk the prototype chain.自己实现 new 和 instanceof:创建对象、执行构造函数、沿原型链查找。
Compare class extends with old-style function inheritance, including why static members are inherited too.对比 class extends 和老式函数继承,包括静态成员为什么也会被继承。
The same two-step calculation written with .then() and with await.同一个两步计算,分别用 .then() 和 await 来写。
Watch events bubble, then predict and check the order of tasks, microtasks and timers.先看事件如何冒泡,再预测并验证任务、微任务和定时器的执行顺序。
Move the same box with top and with transform, and see which one makes the browser redo layout.分别用 top 和 transform 移动同一个方块,看哪种会让浏览器重新计算布局。
Block formatting contexts, grid placement and several ways to center an element.块格式化上下文、Grid 布局,以及几种居中方法。
Build a page from meaningful sections instead of anonymous boxes.用有含义的标签搭页面,而不是一堆没有含义的盒子。
A discriminated union tells the compiler which action each branch of a reducer handles.用可辨识联合类型,让编译器知道 reducer 的每个分支处理的是哪种 action。
Small, tested versions of tools you use every day.日常工具的小型实现,每一个都有测试。
Grow a Promise in three steps, from a tiny state machine to a version that passes all 872 official Promises/A+ tests.分三步写出 Promise:从最小的状态机,到通过全部 872 项 Promises/A+ 官方测试的版本。
See the state machine that Babel turns yield into, and how values flow in and out.看 Babel 把 yield 编译成的状态机,以及值是怎样传进传出的。
Singleton, factory, adapter, proxy and publish/subscribe, each in a few lines.单例、工厂、适配器、代理、发布订阅,每个只用几行代码。
See what goes wrong with a shared prototype or a borrowed constructor, and how Object.create fixes it.看共享原型和借用构造函数会出什么问题,以及 Object.create 怎样解决。
Intercept property access with Proxy, and handle many clicks from one parent element.用 Proxy 拦截属性读写,用一个父元素处理所有子元素的点击。
A minimal on / emit / off event hub; senders never need to know who is listening.一个最小的 on / emit / off 事件中心,发送方不需要知道谁在监听。
Debounce, throttle, deep clone and more, written as short algorithms with clear limits.防抖、节流、深拷贝等,写成边界清楚的短算法。
Copy an object graph while keeping shared references and cycles, then compare with structuredClone.复制对象图时保留共享引用和循环引用,再和 structuredClone 对比。
Tell a real cycle apart from an object that is referenced twice.区分真正的循环引用,和只是被引用了两次的对象。
Wait until a burst of calls stops, then run once.等一连串调用停下来之后,只执行一次。
Run at most once per interval; the first call goes through right away.每个时间间隔最多执行一次,第一次调用立即执行。
Measure how late timer callbacks arrive and adjust the next deadline.测量定时器回调晚了多少,并据此调整下一次的触发时间。
Group the digits of a number string without converting it to Number; compare with Intl.NumberFormat.不把数字字符串转成 Number 也能加千分位;再和 Intl.NumberFormat 对比。
Keep a running total in a closure, and see how a function turns into a number.用闭包累加,再看一个函数是怎么变成数字的。
Parse imports, build a dependency graph and emit one file: first as plain functions, then as a small compiler.解析 import、构建依赖图、输出一个文件;先用几个函数写,再改成一个小型编译器。
createElement, mount and setState in a few files; two counters keep their own state.用几个文件实现 createElement、挂载和 setState;两个计数器各自保存状态。
A small router on the History API: links, back and forward, all without a page reload.基于 History API 的小路由:点击链接、前进后退,都不刷新页面。
The compose helper behind Redux middleware, written by hand.Redux 中间件背后的 compose,自己写一遍。
Save the cursor before focus leaves, insert text at it, and restore it after re-rendering.在失去焦点前保存光标,在光标处插入文字,重新渲染后再恢复光标。
How source files become something a browser can load.源代码是怎样变成浏览器能加载的文件的。
A set of small builds, each changing one thing: entries, loaders, source maps, hot reload, splitting, caching and more.一组小构建,每个只改一处:入口、loader、source map、热更新、拆包、缓存等。
One entry file becomes one bundle; follow the dependency graph in between.一个入口文件打包成一个文件,看中间的依赖图是怎么走的。
Import CSS, an SVG and an XML file, and see which loader or asset type handles each one.导入 CSS、SVG 和 XML,看各自由哪种 loader 或资源类型处理。
Two entries produce two named files, and the HTML plugin adds their script tags for you.两个入口生成两个文件,HTML 插件自动插入对应的 script 标签。
Build in development mode and read your original source in DevTools.用开发模式构建,在 DevTools 里看到原始源码。
Edit a module while the dev server runs, and the page updates without a reload.开发服务器运行时修改模块,页面不刷新就能更新。
A module is downloaded only when you click the button.点击按钮时才下载对应的模块。
Two entries share one copy of Lodash instead of bundling it twice.两个入口共享一份 Lodash,不再各打包一份。
File names change only when their content changes, so browsers can cache them safely.只有内容变了文件名才变,浏览器就能放心缓存。
One project, two configs: readable output for debugging, optimized output for users.同一个项目两套配置:调试用可读的输出,发布用优化后的输出。
Import one function and watch the unused one disappear from the production bundle.只导入一个函数,看没用到的那个从生产包里消失。
Give legacy code the global it expects, even though it never imports it.给从不 import 的旧代码提供它依赖的全局变量。
A plugin that hooks into the build and writes a list of every output file.一个接入构建流程、输出全部产物清单的插件。
Package a tiny number-to-word converter as a library and load it from a separate Node program.把一个很小的数字与英文单词互转工具打包成库,再在另一个 Node 程序里加载。
A clean-then-copy build, the way many projects worked before bundlers.先清理再复制的构建流程,打包器流行之前很多项目就是这样构建的。
Variables, mixins and guards in Less, and why native CSS variables can change at runtime while Less variables cannot.Less 的变量、混入和条件守卫;以及为什么原生 CSS 变量能在运行时改变,而 Less 变量不能。
Compile a Handlebars template and see why {{value}} is escaped while {{{value}}} is not.编译 Handlebars 模板,看为什么 {{value}} 会转义而 {{{value}}} 不会。
Clean, compile TypeScript and copy HTML as a small Gulp pipeline.用一条小的 Gulp 流水线完成清理、编译 TypeScript 和复制 HTML。
See why Babel strips types without checking them, and where tsc still fits in.看懂 Babel 为什么只删类型不检查,以及 tsc 还负责什么。
Complete applications with old and new versions side by side, plus visual experiments.完整的应用示例(新旧写法并排对照),以及视觉实验。
Ship the same Card and Button with Vite, webpack and Babel, and load each output.同一组 Card 和 Button,分别用 Vite、webpack 和 Babel 构建,并实际加载每种产物。
The original architecture, repaired and running on React 19.原来的架构,修好后运行在 React 19 上。
The same features as the 2018 version, so the two can be compared side by side.功能和 2018 版完全相同,方便两边对照阅读。
Type, bold and save as JSON with the editor Meta archived in 2023.输入、加粗、保存为 JSON;Draft.js 已于 2023 年被 Meta 归档。
The same type, bold and save steps with Draft.js's successor.同样的输入、加粗、保存流程,换成 Draft.js 的继任者。
Read the same cross-origin data two ways, and cancel a request with AbortController.用两种方式读取同一份跨域数据,并用 AbortController 取消请求。
Register, cache, serve offline and clean up, without a framework.不依赖框架,完成注册、缓存、离线访问和清理。
Schema, resolvers and a JSON response: the smallest complete GraphQL request.Schema、resolver 和 JSON 响应:最小的一次完整 GraphQL 请求。
A small link feed with sign-up, voting, pagination and live updates.一个小型链接分享站:注册登录、投票、分页和实时更新。
Apollo Server, Prisma and SQLite, rebuilt from migrations and a fictional seed.Apollo Server + Prisma + SQLite,用迁移脚本和虚构数据重建。
Queries, mutations and live subscriptions from a Vite app.在 Vite 应用里发起查询、变更和实时订阅。
Compile a Rust function to .wasm and call it from a button.把一个 Rust 函数编译成 .wasm,再通过按钮调用它。
Digits drawn from a grid of dots; when a digit changes, its dots fall away as particles.用点阵画出数字;数字变化时,点会化作粒子落下。
Scale, watermark, magnify and filter an image by reading and writing its pixels.通过读写像素,实现缩放、水印、放大镜和滤镜。
The same drag written twice; Pointer Events also cover touch and pen.同一个拖拽写两遍;Pointer Events 同时支持触屏和手写笔。
Turn a swipe into a page change using distance and speed thresholds, with touch events and with Pointer Events.用距离和速度阈值把滑动变成翻页,分别用 touch 事件和 Pointer Events 实现。
Place layered slides with geometry, then let CSS Scroll Snap do similar work natively.用几何计算排布层叠的轮播图,再让 CSS Scroll Snap 原生完成类似的事。
Scattered, tilted cards that straighten and zoom in on hover.散落倾斜的卡片,鼠标悬停时摆正并放大。
Filter suggestions as you type and choose one with the arrow keys, using accessible combobox markup.边输入边筛选建议,用方向键选择;使用无障碍的 combobox 标记。
An expanding menu (JavaScript tween vs CSS transition), a step bar and a conic-gradient progress ring.展开的导航(JS 补间与 CSS 过渡对比)、步骤条,以及用 conic-gradient 画的环形进度。
Spin a wheel that always stops on the sector it reports, using the Web Animations API.转盘停下的扇区和显示的结果始终一致,用 Web Animations API 实现。
Canvas, CSS and pointer experiments that run straight in the browser.直接在浏览器里运行的 Canvas、CSS 和指针交互实验。
Navigation Timing, PerformanceObserver and Core Web Vitals, measured on your own visit.用 Navigation Timing、PerformanceObserver 和 Core Web Vitals 测量你自己的这次访问。
Notes kept from earlier years. Read them for context; they are not current advice.早年留下的笔记,用来了解背景,不代表今天的推荐做法。
The original notes (in Chinese) on domain-driven structure, sagas and routing.原始的中文笔记:领域驱动的目录结构、saga 与路由。
Shallow rendering and DOM tests from an older tutorial, kept for comparison with Testing Library.来自早年教程的浅渲染和 DOM 测试,留作与 Testing Library 对比。
How page speed was measured with performance.timing, and why those numbers are read differently now.当年怎样用 performance.timing 测速,以及为什么今天要换一种方式解读。