从模板渲染到响应式驱动:前端崛起的技术演进之路
引言:界面是如何“动”起来的?
不论是用户看到的哪个页面,都不应该是一成不变的静态 HTML。
待办事项的增删、商品库存的实时变化,还是聊天消息的即时推送,都让页面指向了一个必需功能————界面必须要随着数据的变化而自动更新。
而围绕着这一核心诉求,出现了两条主流路径:
- 后端动态生成HTML(传统的 MVC 模式): 数据在服务端组装成完整页面,再一次性返还给浏览器。
- 前端接管界面更新(现代响应式范式): 后端只提供原始数据(如JSON API),而前端通过响应式系统来驱动视图自动同步。
而在这两条路背后,反映着前后端职责划分,同时也催生了以Vue和React为代表的前端技术框架革命。
时代一:纯后端渲染 —— MVC 模式主导
假如有一个需求如:写一个简单的 HTTP 服务器,当用户访问 / 或 /users 路径时,返回一个包含用户列表的 HTML 页面,其他路径则返回 404 错误。
在Node.js早期,如果我想实现这个需求,那么后端渲染将是不二之选。
代码示例:早期 Node.js 实现简单用户列表页
首先就是引入 Node.js 内置模块http和url,而使用的方法则是Node.js最早的CommonJS 模块系统中的 require()来“导入”
const http = require("http"); // commonjs
const url = require("url"); // url
-
http模块:用于创建 HTTP 服务器(处理请求和响应)。 -
url模块:用于解析浏览器发来的 URL 字符串(如/users?id=1)。
然后再准备一些模拟数据
const users = [
{ id: 1, name: '张三', email: '123@qq.com' },
{ id: 2, name: '李四', email: '123456@qq.com' },
{ id: 3, name: '王五', email: '121@qq.com' }
]
接下来就要创建生成 HTML 页面的函数了
先使用.map()方法来动态生成表格行,对每个用户生成一行 HTML 表格,用反引号来插入变量,使用 .join('')来拼接所有行,最后返还一个完整的 HTML 文档。
function generateUsersHtml(users) {
const userRows = users.map(user => `
<tr>
<td>${user.id}</td>
<td>${user.name}</td>
<td>${user.email}</td>
</tr>
`).join('');
return `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>User List</title>
<style>
table { width: 100%; border-collapse: collapse; margin-top: 20px; }
th, td { border: 1px solid #ccc; padding: 8px; text-align: left; }
th { background-color: #f4f4f4; }
</style>
</head>
<body>
<h1>Users</h1>
<table>
<thead>
<tr>
<th>ID</th>
<th>Name</th>
<th>Email</th>
</tr>
</thead>
<tbody>
${userRows}
</tbody>
</table>
</body>
</html>
`;
}
最后也是最重要的就是创建 HTTP 服务器了。
const server = http.createServer((req, res) => {
const parsedUrl = url.parse(req.url, true);
if (parsedUrl.pathname === '/' || parsedUrl.pathname === '/users') {
res.statusCode = 200;
res.setHeader('Content-Type', 'text/html;charset=utf-8');
const html = generateUsersHtml(users);
res.end(html);
} else {
res.statusCode = 404;
res.setHeader('Content-Type', "text/plain");
res.end('Not Found');
}
});
关键概念解释:
-
req(Request):用户的请求对象,包含 URL、方法、头信息等。 -
res(Response):你要返回给用户的内容,通过它设置状态码、头、正文。
解析 URL:
// url.parse(pathname, query)
const parsedUrl = url.parse(req.url, true);
-
req.url即用户请求的 路径+参数部分 -
url.parse()将 URL 字符串“拆解”成结构化的对象,从而方便读取,其中:-
pathname: 路径部分(如/users) -
query: 查询参数(如?id=1就变成了{ id: '1' }),这里的true是用于判断你是否需要自动解析URL参数部分并转换为对象(通常为 true)
-
路由判断(简单路由) :
if (parsedUrl.pathname === '/' || parsedUrl.pathname === '/users')
如果路径是根路径 / 或 /users,就显示用户列表,否则返回 404。
成功响应(200) :
res.statusCode = 200; // 设置状态码为 200
res.setHeader('Content-Type', 'text/html;charset=utf-8');
const html = generateUsersHtml(users);
res.end(html);
通过.setHeader告诉浏览器:“我返回的是 HTML,用 UTF-8 编码”。
然后利用函数 generateUsersHtml(users),传入用户数据,最后调用res.end()生成 HTML 并发送。
错误响应(404 Not Found) :
res.statusCode = 404;
res.setHeader('Content-Type', "text/plain");
res.end('Not Found');
状态码 404 表示“页面不存在”,如果产生错误则返还Not Found。
注意:
url.parse()是旧 API,现代开发基本弃用
Node.js HTTP服务器启动的最后一步
server.listen(1234, () => {
console.log('Server is running on port 1234')
})
让服务器监听 1234 端口(任意修改)。此时就可以在浏览器访问:http://localhost:1234
或http://localhost:1234/users,而访问其他路径(如 /about)会显示 “Not Found”。
效果图:
核心缺点 + 时代局限性:
- 前后端高度耦合,协作效率低下:
HTML 结构、样式、JavaScript 逻辑全部硬编码在一个函数里,如果要修改表格样式等操作,就得修改这个函数,并且无法复用。
而这也几乎将前后端工程师捆绑起来了:
- 前端工程师无法独立开发或调试 UI,必须依赖后端接口和模板
- 后端工程师被迫处理本应属于前端范畴的展示逻辑
阻碍了团队协作,让前后端工程师的开发体验都极差。
- 用户体验受限,交互能力弱:
页面完全由服务端生成,每次跳转或操作都需整页刷新,无法实现局部更新、动态加载、表单实时校验等现代 Web 交互,即使只是点击一个按钮,也要重新请求整个 HTML 文档。
时代二:转折点 AJAX 与前后端分离的诞生
在 2005 年之前,Web 应用基本是:用户点击 → 浏览器发请求 → 后端生成完整 HTML → 返回 → 整页刷新 ,导致用户每次交互都像“重新打开一个页面”,体验感大打折扣。
转折事件:Google Maps(2005)首次大规模使用 XMLHttpRequest(XHR)
- 地图拖拽时不刷新页面
- 动态加载新区域数据
- 用户体验飞跃 → 行业震动
这就是 AJAX(Asynchronous JavaScript and XML) 范式的诞生—— “让网页像桌面应用一样流畅”
范式对比再深化
| 维度 | 后端渲染(传统) | 前后端分离(AJAX 时代) |
|---|---|---|
| 职责划分 | 后端一家独大 | 前端负责 UI/交互,后端负责数据/API |
| 开发模式 | 全栈一人干 | 前后端并行开发 |
| 部署方式 | 服务端部署 HTML | 前端静态资源(CDN),后端 API(独立服务) |
| 用户体验 | 卡顿、白屏、跳转 | 流畅、局部更新、SPA雏形 |
| 技术栈 | PHP/Java/Node + 模板引擎 | HTML/CSS/JS + REST API |
代码示例:
已经配置好的环境
在后端 backend 文件夹中包含一个存储用户数据的db.json文件:
{
"users": [
{
"id": 1,
"name": "张三",
"email": "123@qq.com"
},
{
"id": 2,
"name": "李四",
"email": "1232@qq.com"
},
{
"id": 3,
"name": "王五",
"email": "121@qq.com"
}
]
}
注:
json-server会把 JSON 的顶层 key(如"users")自动映射为 RESTful 路由。
package.json 中的脚本
{
"scripts": {
"dev": "json-server --watch db.json"
}
}
就使得运行 npm run dev 时,json-server 会监听 backend/db.json 文件变化(--watch),并且启动一个 HTTP 服务器,默认端口 3000。(别忘了启动后端服务哦~~)
前端代码(重头戏):
基础页面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>User List</title>
<style>
table { width: 100%; border-collapse: collapse; margin-top: 20px; }
th, td { border: 1px solid #ccc; padding: 8px; text-align: left; }
th { background-color: #f4f4f4; }
</style>
</head>
<body>
<h1>Users</h1>
<table>
<thead>
<tr>
<th>ID</th>
<th>Name</th>
<th>Email</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</body>
</html>
<script>中的内部逻辑
<script>
// DOM 编程
fetch('http://localhost:3000/users') // 发出请求
.then(res => res.json()) // 将 JSON 字符串转为 JS 对象数组
.then(data => {
const tbody = document.querySelector('tbody');
tbody.innerHTML = data.map(user => `
<tr>
<td>${user.id}</td>
<td>${user.name}</td>
<td>${user.email}</td>
</tr>
`).join('');
})
</script>
在.then(data => ...)中的 data 就是上一步 res.json() 解析的结果,并且通过 .map()生成字符串数组,再用.join('') 拼接字符串,而通过 tbody.innerHTML 让浏览器重新解析并渲染表格。
这代表了“纯手工”前后端分离的起点:
- 前端不再依赖后端吐 HTML
- 数据通过 JSON API 获取
- 视图由 JavaScript 动态生成
但这种方法并非完美,仍然存在痛点:手动操作 DOM 繁琐且易错
举个“胶水代码灾难”的例子:
// 用户点击“删除”
button.onclick = () => {
fetch(`/api/users/${id}`, { method: 'DELETE' })
.then(() => {
// 从列表中移除元素
li.remove();
// 更新
countSpan.textContent = --totalCount;
// 如果列表空了,显示“暂无数据”
if (totalCount === 0) emptyMsg.style.display = 'block';
// 可能还要发埋点、更新缓存、通知其他组件...
});
};
视图更新逻辑散落在各处,难以维护,极易出错,删除数据要:
- 找到
<tr>并删除 - 更新
- 找到空状态提示并显示
- 可能还要:更新侧边栏统计、刷新分页、清除搜索高亮……
每次 UI 变化都要手动找一堆 DOM 节点去修改,并且难以复用。
这时期的前端程序员内心都憋着一句话:我不想再写 document.getElementById 了!
AJAX 让网页活了过来,但也让前端开发者陷入了新的地狱(DOM)——直到框架降临
时代三:革命!响应式数据驱动界面的崛起
核心思想:
“你只管改数据,界面自动更新。”
关键技术:ref 与响应式系统
-
ref()将普通值包装成响应式对象 - 模板中通过
{{ }}或v-for声明式绑定数据 - 数据变化会自动触发视图更新(无需手动 DOM 操作)
响应式(以 Vue 为例)
<template>
<table>
<thead>
<tr>
<th>ID</th>
<th>Name</th>
<th>Email</th>
</tr>
</thead>
<tbody>
<!-- 遍历数据渲染到界面 -->
<tr v-for="user in users" :key="user.id">
<td>{{ user.id }}</td>
<td>{{ user.name }}</td>
<td>{{ user.email }}</td>
</tr>
</tbody>
</table>
</template>
-
v-for:声明遍历users数组 -
{{ }}:显示user的某个属性 -
:key:帮助 Vue 高效追踪列表变化(性能优化)
但是关键在于:我没有写任何 DOM 操作代码!
只是在“描述 UI 应该是什么样子”,而不是“怎么去修改 DOM”。
<script setup>
import {
ref,
onMounted // 挂载之后的生命周期
} from 'vue'; // 响应式api(将数据包装成响应式对象)
// 用 ref() 将普通数组包装成一个 响应式引用对象
const users = ref([]);
// onMounted:确保 DOM 已创建后再发起请求(避免操作不存在的元素)
onMounted(() => {
console.log('页面挂载完成');
fetch('http://localhost:3000/users')
.then(res => res.json())
.then(data => {
users.value = data; // 只修改数据
})
})
// 定时器添加数据
setTimeout(() => {
users.value.push({
id: '4',
name: '钱六',
email: '12313@qq.com'
})
}, 3000)
</script>
没有 innerHTML!
没有 createElement!
没有 getElementById!
并且所有 UI 更新都是数据变化的自然结果,无需人工干预!
整个历史进程:
| 阶段 | 开发模式 | 核心关注点 | 开发体验 |
|---|---|---|---|
| 后端渲染 | MVC | 数据 → 模板 → HTML | 前端边缘化 |
| 前后端分离 | AJAX + DOM | 手动同步数据与视图 | 繁琐、易错 |
| 响应式框架 | 数据驱动 | 聚焦业务逻辑 | 高效、声明式、愉悦 |
这段短短的 Vue 代码,浓缩了前端开发十年的演进:
- 从“操作 DOM”到“描述 UI”
- 从“分散状态”到“单一数据源”
- 从“易错胶水”到“自动同步”
它让前端开发者终于认识到一个新的自己:前端不再只是“切图仔”,而是复杂应用的架构者与体验设计师。 这,就是 响应式数据驱动界面 的革命性所在。