阅读视图

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

《前端基础实战:从零搭建用户列表,掌握前后端分离核心思想》

从零搭建用户列表:一个完整的前后端分离项目实战

本文将带你从零开始,使用原生HTML/CSS/JS配合json-server,完成一个用户列表展示功能。涵盖语义化HTML、DOM编程、模块化思想、后端Mock数据等核心知识点。

写在前面

最近在整理前端基础知识时,发现很多同学虽然能用框架快速搭建页面,但对底层的HTML语义、DOM操作、模块化设计等概念却掌握不牢。这篇文章从一个简单的用户列表需求出发,完整记录了我的开发思路和技术选型,希望能帮助大家夯实基础。

项目结构

project/
├── fe/                    # 前端目录
│   ├── index.html        # 页面结构
│   ├── common.js         # JS逻辑
│   └── style.css         # 样式(可选)
├── backend/              # 后端目录
│   ├── package.json      # 项目配置
│   └── db.json          # 模拟数据库
└── README.md

📚 知识点:为什么这样组织目录?

  • 前后端分离febackend可以分别部署在不同的服务器
  • 职责单一:每个文件只做一件事,符合软件工程原则
  • 便于协作:前端和后端可以并行开发,互不干扰

一、HTML 结构设计

语义化标签优先

很多同学写页面喜欢 div 一把梭,但语义化标签对SEO和代码可读性都更有好:

<header>页面头部</header>
<main class="container">
    <aside>侧边栏</aside>
    <div class="row">
        <div class="col-md-6 col-md-offset-3">
            <!-- 主要内容 -->
        </div>
    </div>
    <aside>右侧边栏</aside>
</main>
<footer>页面底部</footer>

📚 知识点详解:语义化标签

标签 语义 对SEO的影响 替代的div方案
<header> 页眉/区块头部 告诉搜索引擎这是导航区域 <div class="header">
<main> 页面主体内容 标识核心内容,一个页面只有一个 <div class="main">
<aside> 侧边栏/辅助信息 表示与主内容相关的辅助信息 <div class="sidebar">
<footer> 页脚/版权信息 标识页面底部信息区域 <div class="footer">
<nav> 导航链接 标识网站导航区域 <div class="nav">

为什么语义化很重要?

  1. 搜索引擎优化(SEO):谷歌爬虫给语义化标签更高的权重
  2. 无障碍访问(a11y):屏幕阅读器可以快速定位页面结构
  3. 代码可维护性:其他开发者接手时能快速理解页面结构
  4. 浏览器默认样式:部分标签自带合理的默认样式

Table 的正确写法

表格一定要区分 theadtbody,这不仅让结构清晰,也方便后续JS操作:

<table class="table" id="user-table">
    <thead>
        <tr>
            <th>ID</th>
            <th>姓名</th>
            <th>昵称</th>
            <th>家乡</th>
        </tr>
    </thead>
    <tbody></tbody>  <!-- JS动态填充内容 -->
</table>

📚 知识点详解:表格标签

table(表格容器)
├── thead(表头区域,01个)
│   └── tr(表格行)
│       └── th(表头单元格,自动加粗居中)
├── tbody(表格主体,0或多个)
│   └── tr
│       └── td(普通单元格)
└── tfoot(表尾区域,01个,可选)
    └── tr
        └── td

th 与 td 的区别:

  • <th>(Table Header):表头单元格,默认字体加粗、居中,表示该列的含义
  • <td>(Table Data):普通数据单元格,默认左对齐、普通字重

为什么JS要操作tbody而不是table?

  • 只刷新数据区域,表头保持不变
  • 避免意外破坏表格结构
  • 性能更好(只重新渲染tbody部分)

盒模型思维

写HTML时先搭盒子框架,再填充内容。container 固定宽度左右留白,row 表示一行,这是经典的PC端布局思路。

📚 知识点详解:CSS盒模型

┌─────────────────────────────────────┐
│            margin(外边距)           │
│  ┌───────────────────────────────┐  │
│  │         border(边框)          │  │
│  │  ┌─────────────────────────┐  │  │
│  │  │     padding(内边距)     │  │  │
│  │  │  ┌───────────────────┐  │  │  │
│  │  │  │    content(内容)   │  │  │  │
│  │  │  └───────────────────┘  │  │  │
│  │  └─────────────────────────┘  │  │
│  └───────────────────────────────┘  │
└─────────────────────────────────────┘

块级元素 vs 行内元素:

类型 特点 常见标签 宽度
块级元素 独占一行,可设宽高 <div><p><header><main> 默认100%
行内元素 与其他元素同行,不可设宽高 <span><a><strong> 内容撑开

二、CSS 与 Bootstrap

直接引入 Bootstrap 3 快速获得样式支持,省去手写大量CSS的时间:

<link href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet">

Bootstrap的栅格系统非常实用:col-md-6 col-md-offset-3 表示在中等屏幕上占据一半宽度并居中。

📚 知识点详解:Bootstrap栅格系统

Bootstrap把一行(row)分成12等份:

类名 含义 宽度计算
col-md-4 中等屏幕占4格 4/12 = 33.33%
col-md-6 中等屏幕占6格 6/12 = 50%
col-md-12 中等屏幕占12格 100%
col-md-offset-3 向右偏移3格 产生左边距

响应式断点:

  • col-xs-*:超小屏幕(手机,<768px)
  • col-sm-*:小屏幕(平板,≥768px)
  • col-md-*:中等屏幕(桌面,≥992px)
  • col-lg-*:大屏幕(宽屏,≥1200px)

CDN工作原理:

  1. 浏览器解析到<link>标签
  2. 向CDN服务器发起HTTP请求
  3. 下载bootstrap.min.css文件
  4. 解析CSS并应用到页面

为什么用CDN?

  • 速度快:CDN就近分配节点
  • 缓存共享:访问其他使用同一CDN的网站无需重新下载
  • 节省带宽:不占用自己服务器流量
  • 并发加载:不同域名可并行下载

三、JavaScript DOM 编程

模块化思维

不要把代码都塞在一个文件里。common.js 专门处理前端逻辑,与HTML结构分离,便于维护和扩展。

📚 知识点详解:模块化设计

设计原则 说明 本例体现
单一职责 一个模块只做一件事 HTML管结构,CSS管样式,JS管逻辑
高内聚 模块内部紧密相关 common.js只处理用户列表相关逻辑
低耦合 模块之间依赖少 通过ID/class连接,不直接依赖

动态渲染表格

核心步骤:找到挂载点 → 遍历数据 → 动态生成HTML

// 获取tbody元素作为挂载点
const oBody = document.querySelector('.table tbody');

let i = 1;
for (let user of users) {
    oBody.innerHTML += `
        <tr>
            <td>${i}</td>
            <td>${user.name}</td>
            <td>${user.nickname}</td>
            <td>${user.hometown}</td>
        </tr>
    `;
}

📚 知识点详解:DOM操作

DOM树结构:

document(根节点)
  └─ html(<html>)
      └─ body(<body>)
          └─ main.container
              └─ div.row
                  └─ div.col-md-6
                      └─ table
                          ├─ thead
                          └─ tbody ← querySelector找到这里

querySelector vs 其他选择器:

// 返回第一个匹配的元素
document.querySelector('.table tbody')

// 返回所有匹配的元素(NodeList)
document.querySelectorAll('td')

// 通过ID获取(最快)
document.getElementById('user-table')

// 通过类名获取
document.getElementsByClassName('table')

innerHTML 的工作原理:

  1. 浏览器解析传入的HTML字符串
  2. 构建新的DOM节点
  3. 替换原有的子节点

⚠️ 性能注意事项:

// ❌ 性能差:每次循环都重新解析
for (let user of users) {
    oBody.innerHTML += '<tr>...</tr>';
}

// ✅ 性能好:一次构建,一次插入
let html = '';
for (let user of users) {
    html += '<tr>...</tr>';
}
oBody.innerHTML = html;

ES6 语法亮点

  • for...of 循环:比传统 for (let i=0; i<arr.length; i++) 更简洁
  • 模板字符串:用反引号 + ${变量} 替代字符串拼接,可读性暴增

📚 知识点详解:ES6语法

数组遍历方法对比:

// 传统for循环(需要索引时使用)
for (let i = 0; i < users.length; i++) {
    console.log(users[i]);
}

// for...of(只需要值,不需要索引)
for (let user of users) {
    console.log(user.name);
}

// forEach(函数式编程)
users.forEach(user => {
    console.log(user.name);
});

// map(返回新数组)
const names = users.map(user => user.name);

模板字符串特性:

// 传统拼接(难写、难读)
const html = '<tr><td>' + user.id + '</td><td>' + user.name + '</td></tr>';

// 模板字符串(优雅、支持换行)
const html = `
    <tr>
        <td>${user.id}</td>
        <td>${user.name}</td>
    </tr>
`;

四、后端模拟:json-server

前端开发经常遇到后端接口还没好的情况,json-server 可以快速Mock一个REST API。

初始化项目

cd backend
npm init -y
npm install json-server

📚 知识点详解:npm包管理

package.json的作用:

{
  "name": "backend",           // 项目名称
  "version": "1.0.0",          // 版本号(语义化版本)
  "description": "",           // 项目描述
  "main": "index.js",          // 入口文件
  "scripts": {                 // 自定义命令
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],              // 关键词
  "author": "",                // 作者
  "license": "ISC",            // 开源协议
  "dependencies": {            // 生产环境依赖
    "json-server": "^0.17.0"   // ^表示兼容版本
  }
}

npm install 做了什么?

  1. 读取package.json中的dependencies
  2. 下载依赖包到node_modules目录
  3. 生成package-lock.json锁定版本

创建 db.json

{
  "users": [
    {
      "id": 1,
      "name": "昌哥",
      "hometown": "南昌",
      "nickname": "昌哥nb666"
    },
    {
      "id": 2,
      "name": "胡航",
      "hometown": "南昌",
      "nickname": "航哥"
    }
  ]
}

📚 知识点详解:JSON格式

JSON语法规则:

  • 键名必须用双引号包裹
  • 字符串值必须用双引号
  • 数字、布尔值、null不需要引号
  • 不能有注释
  • 最后一个属性后面不能有逗号

数据类型:

类型 示例 说明
字符串 "昌哥" 双引号包裹
数字 25 整数或浮点数
布尔值 true true或false
数组 [1, 2, 3] 方括号
对象 {"name": "昌哥"} 花括号
null null 空值

启动服务

npx json-server db.json --port 3000

访问 http://localhost:3000/users 就能拿到JSON数据。

📚 知识点详解:REST API

HTTP方法 端点 功能 请求体
GET /users 获取所有用户
GET /users/1 获取id=1的用户
POST /users 新增用户 {"name": "新用户"}
PUT /users/1 完整更新id=1 全量数据
PATCH /users/1 部分更新id=1 {"nickname": "新昵称"}
DELETE /users/1 删除id=1

npx是什么?

  • npm 5.2+ 自带的工具
  • 直接运行node_modules/.bin下的命令
  • 不需要全局安装json-server

五、前后端联调

fetch 发起网络请求,拿到数据后渲染到页面:

fetch('http://localhost:3000/users')
    .then(response => response.json())
    .then(data => {
        users = data;
        renderTable();  // 重新执行渲染逻辑
    });

📚 知识点详解:fetch API和异步编程

fetch执行流程:

fetch() 发起请求
    ↓(异步,不阻塞后续代码)
浏览器发送HTTP请求
    ↓
服务器返回响应
    ↓
第一个.then() 拿到Response对象
    ↓
调用.json() 解析JSON
    ↓
第二个.then() 拿到实际数据
    ↓
执行渲染逻辑

Response对象常用属性和方法:

fetch(url).then(response => {
    response.status      // HTTP状态码,200表示成功
    response.ok          // 布尔值,status在200-299之间为true
    response.headers     // 响应头信息
    response.json()      // 解析为JSON对象
    response.text()      // 解析为字符串
    response.blob()      // 解析为二进制数据
})

Promise的三种状态:

状态 含义 触发时机
pending 进行中 初始状态
fulfilled 成功 调用resolve()
rejected 失败 调用reject()

async/await语法糖:

// 等价于上面的fetch写法,但更直观
async function loadUsers() {
    const response = await fetch('http://localhost:3000/users');
    const data = await response.json();
    users = data;
    renderTable();
}

六、完整数据流图解

1. 用户打开页面
   ↓
2. 浏览器加载index.html
   ↓
3. 解析HTML,构建DOM树
   ↓
4. 遇到<link>加载Bootstrap CSS
   ↓
5. 遇到<script>加载并执行common.js
   ↓
6. common.js执行fetch请求
   ↓
7. 请求到达json-server
   ↓
8. json-server读取db.json
   ↓
9. 返回JSON数据给前端
   ↓
10. JS遍历数据,生成HTML字符串
    ↓
11. 设置tbody.innerHTML
    ↓
12. 浏览器重新渲染表格
    ↓
13. 用户看到用户列表

知识小结

知识点 要点 代码体现
HTML语义化 用语义标签替代div <header><main><aside><footer>
表格语义化 区分表头和主体 <thead> + <tbody>
DOM查询 用选择器查找元素 document.querySelector('.table tbody')
动态渲染 用innerHTML插入内容 oBody.innerHTML += ...
模块化 代码按职责拆分 HTML结构 + JS逻辑 + CSS样式
后端模拟 用json-server npx json-server db.json --port 3000
网络请求 用fetch获取数据 fetch(url).then(res => res.json())
异步处理 Promise处理异步 .then() 链式调用

常见问题排查指南

问题 可能原因 解决方法
表格不显示 找不到tbody 检查选择器是否正确
数据不更新 fetch是异步的 确认渲染在.then回调中
中文乱码 缺少meta标签 添加<meta charset="UTF-8">
跨域报错 不同端口访问 json-server默认支持CORS
页面空白 JS语法错误 打开F12控制台查看
连接失败 json-server未启动 运行npx json-server db.json --port 3000

写在最后

这个项目虽然简单,但涵盖了Web开发的核心流程。打好HTML语义、DOM操作、模块化设计这些基础,后续学习React/Vue等框架会事半功倍。

学习路径建议:

  1. 手敲一遍代码,不要复制粘贴
  2. 修改参数观察变化,理解每一行
  3. 尝试添加新功能(搜索、分页、添加用户)
  4. 理解原理后再学框架

如果觉得有收获,欢迎点赞收藏!


项目源码已整理,需要的小伙伴可以私信我~

《前端三权分立:HTML、CSS、JS为什么不能“乱搞”》

0.1秒价值1000万美元:一个时钟教会我的前端性能课

为什么有些网站打开就像“丝滑咖啡”,有些像“水泥搅拌机”?答案藏在HTML、CSS、JS的“三权分立”里

185cd7436fe0d81862578179c69fe314.jpg

⏰ 一个让CEO暴怒的下午

去年在某电商公司,我亲历了一个魔幻场景:

大促前夜,CTO盯着监控大屏,脸色铁青——页面加载时间从0.8秒飙升到了1.9秒

“每慢0.1秒,转化率掉7%!你们知道这意味什么吗?”CEO在会议上拍着桌子。

我后来算了一笔账:按该平台日活5000万、客单价200元计算,0.1秒的延迟=每天损失700万元

亚马逊更早算过:每100毫秒的延迟,年损失16亿美元

这让我重新思考一个看似简单的问题:一个网页,到底应该怎么“组装”才对?

🎯 从一个时钟开始

假设我们要做一个CSS时钟,就像掘金上那些炫酷的教程:

一个圆形的表盘
三根指针(时、分、秒)
指针会真实地转动

普通开发者可能随手就写了,但专业和业余的区别,藏在代码的组织方式里

🏛️ 前端三权分立:不是政治,是性能

前端的“三权分立”不是孟德斯鸠说的,但道理一样:

权力 掌管 负责 隐喻
📄 HTML 结构 页面骨架、盒子层级 宪法(规定有什么)
🎨 CSS 样式 颜色、大小、位置、动画 法律实施细则(规定长什么样)
⚡ JS 行为 交互、数据处理、动态效果 执法机构(规定怎么动)

一个优秀的前端,会让这三者各司其职,互不越界。

❌ 反模式:混乱的“屎山”

<!-- 样式内联,CSS和HTML混在一起 -->
<div style="color: red; font-size: 20px;" onclick="alert('hi')">
  点我
</div>

✅ 正确姿势:职责分离

<!-- clock.html:只有结构 -->
<div class="clock">
  <div class="clock-face">
    <div class="hand hour-hand"></div>
    <div class="hand min-hand"></div>
    <div class="hand second-hand"></div>
  </div>
</div>
/* clock.css:只有样式 */
.clock {
  width: 300px;
  height: 300px;
  background: #f0f0f0;
  border-radius: 50%;
}

.hand {
  position: absolute;
  background: #333;
  transform-origin: 100%;
}
// clock.js:只有行为
function setDate() {
  const now = new Date();
  const seconds = now.getSeconds();
  // 更新指针角度...
}

为什么要这么麻烦?

当你一个月后回来改代码,或者团队里其他人接手时,清晰的结构意味着:

  • 找样式去CSS文件
  • 改结构去HTML
  • 修交互去JS

就像图书馆的书按编号排列,闭着眼睛都能找到。

📥 文件加载顺序:被忽视的性能刺客

浏览器解析HTML是从上到下、一行一行的。

这就引出一个关键问题:CSS和JS,应该放在哪里?

🎨 CSS:放头部,越快越好

<!DOCTYPE html>
<html>
<head>
  <!-- ✅ CSS放这里! -->
  <link rel="stylesheet" href="clock.css">
</head>
<body>
  <!-- 页面内容 -->
</body>
</html>

为什么?

浏览器读到<link>就会去下载CSS,同时继续解析后面的HTML。CSS下载完成后,会把样式应用到已经解析的元素上。

这样用户看到的是:结构出现 → 立刻穿上衣服(样式)

如果CSS放底部呢?

<!-- ❌ CSS放底部 -->
<body>
  <div class="clock">...</div>
  <link rel="stylesheet" href="clock.css">
</body>

浏览器先画出没有样式的“裸奔”HTML,然后突然“穿上衣服”——用户会看到内容闪烁、布局跳动,体验极差。

这叫 FOUC(Flash of Unstyled Content),前端界的常见病。

⚡ JS:放底部,别挡路

<body>
  <!-- 页面内容 -->
  <div class="clock">...</div>
  
  <!-- ✅ JS放body结束前 -->
  <script src="clock.js"></script>
</body>

为什么?

JS有一个“恶习”:它会阻塞HTML解析

当浏览器遇到<script>标签时,它会:

  1. 停止解析HTML
  2. 下载并执行JS
  3. 执行完毕后,继续解析HTML

如果JS放在<head>里:

<head>
  <script src="huge-library.js"></script> <!-- 这个文件要下载300ms -->
</head>

在这300ms里,用户看到的是一片空白——浏览器被JS“卡住”了,连HTML结构都没来得及画

如果JS放底部:

  • HTML和CSS先加载完毕
  • 用户看到完整的静态页面
  • JS最后加载,给页面添加交互能力

这叫“渐进增强”策略:先让用户看到东西,再让它动起来。

![对比图:JS在头部 vs JS在尾部的加载时序](上面画着时间轴,JS在头部时有一段空白期;JS在尾部时,结构和样式先出现,JS最后悄无声息地加上)

📊 性能的残酷经济学

你可能会想:“差个0.1秒,至于吗?”

让我们算一笔真实的账:

场景 延迟 转化率影响 日损失(5000万日活,客单价200元)
慢0.1秒 100ms -7% 700万人民币
慢0.3秒 300ms -15% 1500万人民币
慢1秒 1000ms -28% 2800万人民币

这不是理论数据。Google的报告显示:移动端加载时间从1秒增加到3秒,跳出率增加32%。

亚马逊的工程师曾分享:每100ms延迟,年损失16亿美元

沃尔玛更夸张:页面加载时间每减少1秒,转化率提升2%

所以,当你纠结“要不要优化这0.1秒”时,想想背后可能是几百万的生意。

🛠️ Emmet:写结构的“快枪手”

说到HTML结构,不得不提一个效率神器——Emmet

刚才那个时钟的结构,手写需要十几行。但用Emmet,一行搞定:

.clock>.clock-face>(.hand*3)

按一下Tab键,展开为:

<div class="clock">
  <div class="clock-face">
    <div class="hand"></div>
    <div class="hand"></div>
    <div class="hand"></div>
  </div>
</div>

Emmet语法速查:

符号 含义 示例 输出
. class类名 .box <div class="box">
# id #header <div id="header">
> 子元素 ul>li <ul><li>
* 重复 li*3 三个<li>
+ 兄弟元素 div+p <div><p>
() 分组 (header>h1)+main 复杂嵌套结构

学会Emmet,你的HTML手速提升3倍。

🎯 从时钟到大型项目:不变的原则

一个时钟遵循的原则,和淘宝首页、微信网页版是一样的:

1️⃣ HTML:语义化结构

<!-- 好:语义清晰 -->
<header>导航</header>
<main>内容</main>
<footer>版权</footer>

<!-- 差:div地狱 -->
<div class="top">导航</div>
<div class="center">内容</div>
<div class="bottom">版权</div>

2️⃣ CSS:避免“样式污染”

/* 好:类名有作用域 */
.clock .hand { }

/* 差:全局裸奔 */
.hand { }  /* 可能影响页面其他.hand */

3️⃣ JS:DOM操作最小化

// 好:批量修改,只触发一次重绘
document.querySelector('.clock').classList.add('tick');

// 差:循环里逐个修改(触发N次重绘)
for(let i=0; i<100; i++) {
  document.querySelector('.hand').style.transform = `rotate(${i}deg)`;
}

📈 一个曾经巨慢的网站,后来怎样了?

还是开头那家电商公司。

我们做了三件事:

  1. CSS全部移到<head>,消除FOUC
  2. JS拆分,核心逻辑放底部,非关键JS用async/defer延迟加载
  3. 优化关键渲染路径,首屏只加载首屏需要的代码

结果:

  • 加载时间:1.9秒 → 0.7秒(优化63%
  • 转化率:回升12%
  • 大促当天GMV:比预期多卖了4700万

CTO后来在复盘会上说:“我们没加一台服务器,没改一行业务代码,只是把东西放在了该放的位置。”

💡 一句话总结

前端性能优化的第一步,不是压缩代码、不是CDN加速,而是把HTML、CSS、JS放在它们该在的位置。

CSS去头部,别让页面“裸奔”
JS去底部,别阻塞渲染
三者各司其职,互不越界

这看起来简单,但价值千万。

image.png

互动时间:你有没有遇到过“打开一个网页,等了3秒还是白屏”的经历?在评论区吐槽一下那些“水泥搅拌机”式的网站吧!


💡 彩蛋:如果你想知道自己的网站性能如何,打开Chrome DevTools → Lighthouse,跑一次评分,它会告诉你优化建议。试试看,可能吓你一跳。

❌