专栏文章

从零开始编写你的网站

个人记录参与者 3已保存评论 2

文章操作

快速查看文章及其快照的属性,并进行相关操作。

当前评论
2 条
当前快照
1 份
快照标识符
@mio0x1i6
此快照首次捕获于
2025/12/02 11:33
3 个月前
此快照最后确认于
2025/12/02 11:33
3 个月前
查看原文

0. 前言

也许你使用过各种各样的网站,有的时候你可能想要建立一个属于自己的网站。实现你的想法的方式有很多,也许是一些开源项目,也许是一些现成的网站。这篇文章将带来一种更有意思的解决方案,就是自己编写。
根据数据显示,在 Server-Side 编程语言的使用情况统计中,提到了 JavaScript 作为第四常用的后端语言,占比约 4.9%4.9\%(第一的 PHP 占据了 73.6%73.6\%),在 StackOverflow 的最受欢迎的语言的统计中,JavaScript 以 63.61%63.61\% 的得票率占据第一,再加上其语法与 C++ 有较多的相似之处,本篇文章笔者将以 JavaScript 作为后端语言进行编写。
笔者所给出的代码或命令,如无特殊说明,均基于 Ubuntu 22.04 LTS 和 NodeJS v22.18.0。
本文部分文字取自 MDN 文档(developer.mozilla.org)和中文维基百科(zh.wikipedia.org),部分示例代码由 LLM 生成。
目录
1. 基础准备和 JavaScript 初探
  • 1-1. 开发环境准备
  • 1-2. 一切的背后:JavaScript,HTML 和 CSS
    • 1-2.1. JavaScript
    • 1-2.2. JSON(JavaScript 对象表示法)
    • 1-2.3. HTML(超文本标记语言)
    • 1-2.4. CSS(层叠样式表)
    • 1-2.5. 综合运用
2. HTTP,路由与模板引擎
  • 2-1. HTTP:与服务器交流的语言
  • 2-2. 路由:构建网站导航的基石
  • 2-3. 模板引擎:网页的蓝图
  • 2-4. [Task #1] 实战:构建一个简单的个人博客
3. 前端设计与用户交互
  • 3-1. 组织你的静态资源
  • 3-2. 编写基础 CSS
    • 3-2.1. 原生 CSS 布局
    • 3-2.2. 元素美化
    • 3-2.3. 动画
  • 3-3. 设计前端交互
  • 3-4. Semantic UI
    • 3-4.1. 网格布局
    • 3-4.2. 常用组件
    • 3-4.3. [Task #2] 实战:设计导航栏
  • 3-5. 规划内容布局
  • 3-6. 让操作更用户友好
  • 3-7. [Task #3] 实战:设计博客文章显示页
  • 3-8. 常用的外部库
    • 3-8.1. jQuery
    • 3-8.2. Chart.js
    • 3-8.3. SweetAlert
    • 3-8.4. Toastr
4. 数据的力量:数据库
  • 4-1. 数据库的工作原理
  • 4-2. MariaDB 的安装
  • 4-3. 数据库的基本概念
  • 4-4. 基础的 SQL 语法
  • 4-5. 用 JavaScript 连接数据库
  • 4-6. 设计数据模型
  • 4-7. [Task #4] 实战:读取文章并显示在网页上
5. 走向完整应用:用户系统
  • 5-1. 用户注册与信息安全
  • 5-2. 用户登录与注销:Session 和 Cookie
  • 5-3. 鉴权:Express 中间件的使用
  • 5-4. [Task #5] 实战:为博客添加用户系统
  • 5-5. [Task #6] 实战:设计博客后台
6. 处理用户输入
  • 6-1. 使用 express-validator 验证表单数据
  • 6-2. 使用 multer 处理用户提交的文件
  • 6-3. 安全性
  • 6-4. [Task #7] 实战:为博客新增上传头像功能
7. 让网站更健壮:ORM,错误处理和日志系统
  • 7-1. 使用 TypeORM 简化数据库操作步骤
  • 7-2. 日志外部库:Morgan 和 Winston
  • 7-3. 编写全局错误处理中间件
  • 7-4. [Task #8] 实战:为博客添加日志和错误页面
8. 从本地到网络:部署你的网站
  • 8-1. 服务器与域名
    • 8-1.1. 推荐的服务器厂商
    • 8-1.2. 中国大陆备案政策
    • 8-1.3. 如何连接你的服务器
  • 8-2. 进程管理:PM2 和 systemd
  • 8-3. 反向代理
9. 后端性能优化
  • 9-1. 数据库缓存:Redis
    • 9-1.1. Redis 的工作原理
    • 9-1.2. Redis 的安装
    • 9-1.3. 用 JavaScript 连接 Redis
    • 9-1.4. 常用 Redis 命令
    • 9-1.5. [Task #9] 实战:Redis 实现用户登录状态存储
  • 9-2. 数据库索引
10. 网站加载速度优化
  • 10-1. 使用内容分发网络
    • 10-1.1. CDN 的工作原理
    • 10-1.2. 如何配置 CDN
  • 10-2. 开启内容压缩
11. 结束了?
  • 11-1. 我们做了什么
  • 11-2. 接下来怎么做

1. 基础准备和 JavaScript 初探

1-1. 开发环境准备:Hello, world!

笔者默认你已经有了一个 Ubuntu 22.04 LTS 的可用系统(其实不是 22.04 LTS 也可能没有什么关系喵...),并且可以流畅连接 GitHub。
首先我们要来更新你的软件包列表,这很简单,打开你的终端(也许是 SSH),输入下面的命令:
BASH
sudo apt update
执行后可能要求你输入密码,输入就好啦。
这是什么意思?sudo 表示以管理员的身份执行,而 apt 则是 Ubuntu 系统默认的软件包管理器,update 是更新软件包列表的谓词。
既然我们将使用 JavaScript,那么我们就应当准备一个正确的 NodeJS 环境。
Ubuntu 的软件源中的 NodeJS 版本可能过旧,我们使用 nvm 进行 NodeJS 版本管理。
输入下面的命令安装 nvm:
BASH
sudo apt install wget -y
wget -qO- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash
export NVM_DIR="$([ -z "${XDG_CONFIG_HOME-}" ] && printf %s "${HOME}/.nvm" || printf %s "${XDG_CONFIG_HOME}/nvm")"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # This loads nvm
我们需要的是 NodeJS 22 喵!执行:
BASH
nvm install 22
等读条读完过后,执行:
BASH
node -v
如果有输出,就说明安装好了,输出可能长这样:
CPP
v22.18.0
接下来你需要新建一个文件夹,作为你的工作目录。
选择一个适合你的 IDE,比如 Visual Studio Code,或是 WebStorm,好的 IDE 可以改善你的开发体验和加快开发速度。
打开你的工作目录并在目录下执行:
BASH
npm init
接下来 npm 会引导你填写一些信息,示例输出如下(笔者拿 Windows 跑的,不过在 Ubuntu 上是一样的):
BASH
This utility will walk you through creating a package.json file.
It only covers the most common items, and tries to guess sensible defaults.

See `npm help init` for definitive documentation on these fields
and exactly what they do.

Use `npm install <pkg>` afterwards to install a package and
save it as a dependency in the package.json file.

Press ^C at any time to quit.
package name: (tutorial)
version: (1.0.0)
description:
entry point: (index.js) app.js
test command:
git repository:
keywords:
author: Federico2903
license: (ISC) GPL-3.0-or-later
About to write to D:\Users\asus\Desktop\tutorial\package.json:

{
  "name": "tutorial",
  "version": "1.0.0",
  "description": "",
  "main": "app.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "Federico2903",
  "license": "GPL-3.0-or-later"
}


Is this OK? (yes) yes
然后 npm 会生成一个 package.json 到这个目录下,这是你这个项目的基本信息。
注意一点,你要记住你上面的 entry point,如果你直接回车(默认值),就是 index.js,否则就是你填的值。
我喜欢用 app.js 作为入口点,后续代码也会这样写,如果你喜欢 index.js,直接替换就行了。
接下来的操作你暂时不需要理解,直接照做即可,在工作目录下执行:
BASH
npm install express --save
创建文件 app.js(或是你的入口点),写入如下内容:
JAVASCRIPT
const express = require('express');
const app = express();

app.get('/', (req, res) => {
    res.send('Hello World!');
});

app.listen(3000);
然后回到工作目录下执行:
BASH
node app.js
这段命令的意思是让 NodeJS 执行
此时你会发现他卡住了,什么也不会输出。不要关闭终端窗口,打开浏览器,网址栏输入 http://localhost:3000
不出意外的话,你会看见一条 Hello World!。这就是你的第一个网站了。
这是什么
这是一个真正的后端(服务端),它已经能够处理来自用户的请求。
Express 是一个快速、灵活、极简的 Web 框架,可以用来方便的解析和处理请求。
这段代码的第一行是一个 require,像 C++ 一样,需要导入你需要的包。
第二行创建了一个 Express 实例,用于设置路由等等。
高亮部分的 app.get('/', () => {...}) 点明了当以 GET 方法请求 / 时,执行后面的代码。
这个 / 是一个路径,例如 https://www.luogu.com.cn/article 的路径是 /article
https://www.luogu.com.cn 的路径就是 /,可以理解为什么都没有。
而这个的第二个参数,会在后面的 JavaScript 语法部分提到,你可以把它看作 C++ 的 lambda 表达式。
其中的代码 res.send() 则是将括号内的数据发回到前端(客户端)。
而最开始执行的 npm install express --save 则是在安装 Express。
npm 是 NodeJS 自带的包管理器,类似的还有 pnpmyarn 等。
安装的文件会存放于 node_modules 文件夹下,依赖信息会写入 package.json
在终端中按 Ctrl + C 可以停止运行后端。
尝试修改 res.send() 里的文字,重新运行 node app.js,看看变化。
请不要删除这个工作目录
我们的后续教学会继续使用这个工作目录内的文件!

1-2. 一切的背后:JavaScript,HTML 和 CSS

想象一下你要制作一个机器人:
HTML 是机器人的骨架和零部件。它定义了结构——哪里是头,哪里是手臂,哪里是按钮。但它看起来只是个灰秃秃的金属架子。
例子:<button>点击我!</button>——这里有一个按钮零件。
CSS 是机器人的皮肤、油漆和外观设计。它负责让机器人变得好看——给金属外壳涂上颜色,让眼睛发光,调整手臂的粗细。
例子:button { color: white; background-color: blue; }——把按钮涂成蓝底白字。
JavaScript 是机器人的程序和电路。它负责让机器人动起来——告诉它“当用户按下这个按钮时,手臂要抬起来”。
例子:button.addEventListener('click', () => { alert('你好!'); });——让按钮被点击时弹出问候。
它们三者协同工作,缺一不可。HTML 搭建结构,CSS 进行美化,JavaScript 实现交互。

1-2.1. JavaScript

JavaScript 和 C++ 的最大的区别在于:解释型,弱类型和垃圾回收
下面展示的两段代码,一段由 C++ 编写,一段由 JavaScript 编写,但实现的功能是一样的:
JAVASCRIPT
let message = "Hello"; // 用 let 声明变量
const pi = 3.14;       // 用 const 声明常量
if (true) {
    console.log(message);
}
for (let i = 0; i < 5; i++) {
    console.log(i);
}
function greet(name) { // 函数定义
    return "Hello, " + name;
}
CPP
#include <iostream>
using namespace std;

string greet(string name) {
    return "Hello, " + name;
}

int main() {
    string message = "Hello";
    const double pi = 3.14;
    if (true) {
        cout << message << endl;
    }
    for (int i = 0; i < 5; i++) {
        cout << i << endl;
    }
    return 0;
}
不难发现,无论是变量类型,形参类型还是函数返回值,在 JavaScript 中都不需要指明类型,这就是弱类型
我们再来看一段代码:
JAVASCRIPT
console.log("This is the first message.");
throw new Error('Example error'); // 抛出一个错误
console.log("This is the second message.");
这段代码在笔者 Windows 中的输出为:
JAVASCRIPT
This is the first message.
D:\Users\asus\Desktop\tutorial\test.js:2
throw new Error('Example error');
^

Error: Example error
    at Object.<anonymous> (D:\Users\asus\Desktop\tutorial\test.js:2:7)
    at Module._compile (node:internal/modules/cjs/loader:1688:14)
    at Object..js (node:internal/modules/cjs/loader:1820:10)
    at Module.load (node:internal/modules/cjs/loader:1423:32)
    at Function._load (node:internal/modules/cjs/loader:1246:12)
    at TracingChannel.traceSync (node:diagnostics_channel:322:14)
    at wrapModuleLoad (node:internal/modules/cjs/loader:235:24)
    at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:171:5)
    at node:internal/main/run_main_module:36:49

Node.js v22.18.0
你会发现他执行了第一条语句过后,由于出现错误,停止了后续代码执行并打印了堆栈。
这因为 JavaScript 是一门解释型语言
至于垃圾回收,你可以认为 JavaScript 会检测你哪些变量再也用不到,并把它们从内存中永远删除掉。
箭头函数
在 JavaScript 中,你会经常看到一种看起来有点奇怪的函数写法:() => {}。这叫做箭头函数,它是传统函数表达式的一种更简洁的写法。
JAVASCRIPT
const greet1 = function(name) {
    return "Hello, " + name;
};

const greet2 = (name) => {
    return "Hello, " + name;
};
执行 greet1greet2 的效果是完全一样的。
如果函数只有一个参数,可以省略括号 ()。如果函数体只有一行返回语句,可以省略大括号 {}return 关键字:
JAVASCRIPT
const greet = name => "Hello, " + name;

// 等价于
const greet = function(name) {
    return "Hello, " + name;
};
箭头函数还有一个非常重要的特性:它没有自己的 this。它的 this 值继承自定义它时所处的上下文(父级作用域)。这解决了传统函数中 this 指向混乱的著名难题(P.S. 你不需要理解下面这段代码,因为他所涉及的 document 对象只存在于浏览器中)。
JAVASCRIPT
document.addEventListener('click', function() {
    console.log(this); // this 指向 document
});

// this 继承自外部(定义它的地方)
document.addEventListener('click', () => {
    console.log(this); // this 指向 window(或外部作用域的this)
});
在需要传递一个回调函数时,使用箭头函数通常更安全、更不容易出错。
回调函数
JavaScript 的参数可以是一个函数,常见的使用方式是当函数逻辑执行完之后,执行位于参数列表最末端的函数,上报结果。我们称其为回调函数(callback)。
示例代码:
JAVASCRIPT
function add(x, y, callback) {
    const result = x + y;
    callback(result);
}

add(1, 2, result => console.log(result));
以上这段代码输出 3
JavaScript 也可以运行在浏览器中(当网页上包含 JavaScript 时会自动运行)。
此时的 JavaScript 使用的是 ESM 语法(如果你好奇的话可以去搜一下,最显著的区别是使用 import 进行导入)。
并且这个时候的 JavaScript 脚本会多出几个对象,比较常用的有 documentwindow,后面设计前端时我们再来说这个。
将 JavaScript 嵌入 HTML 的方式是使用 <script> 标签。可以使用 src 属性加载外部 JavaScript,也可以直接包裹 JavaScript 代码:
HTML
<script src="script.js"></script>
<!-- 这个会加载同目录下的 script.js -->
<script>
    alert('Alert');
</script>
<!-- 这个会在你打开的时候弹出一个提示 -->
可能对你有用的 MDN 文档链接:
  • https://developer.mozilla.org/zh-CN/docs/Learn_web_development/Getting_started/Your_first_website/Adding_interactivity
  • https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Functions/Arrow_functions
  • https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects

1-2.2. JSON(JavaScript 对象表示法)

JSON 是一种轻量级资料交换格式,其内容由属性和值所组成,因此也有易于阅读和处理的优势。
JSON的基本数据类型:
  • 数值:一般用双精度浮点数表示所有数值。
  • 字符串:以半角引号括起来的字符,支持转义。
  • 布尔值:表示为 true 或者 false
  • 数组:有序的零个或者多个值。每个值可以为任意类型。数组使用方括号包裹。多个数组元素之间用半角逗号分隔,形如:[value, value]
  • 对象:若干无序的键值对,其中键只能是字符串。建议但不强制要求对象中的键是独一无二的。对象以大括号包裹。多个键值对之间使用半角逗号分隔。键与值之间用半角冒号分隔。
  • 空值:值写为 null
以下是一段有效的 JSON:
JSON
{
     "firstName": "John",
     "lastName": "Smith",
     "sex": "male",
     "age": 25,
     "address": {
         "streetAddress": "21 2nd Street",
         "city": "New York",
         "state": "NY",
         "postalCode": "10021"
     },
     "phoneNumber": [
         {
           "type": "home",
           "number": "212 555-1234"
         },
         {
           "type": "fax",
           "number": "646 555-4567"
         }
     ]
 }
JavaScript 处理 JSON 的方法也很简单:
JAVASCRIPT
const json = '{"name":"Federico2903","uid":381949}';
// 这是一个 JSON 字符串
const jsonObject = JSON.parse(json);
// 解析成一个 JSON 对象
console.log(jsonObject.name);
// 输出 Federico2903
console.log(JSON.stringify(jsonObject));
// 输出 {"name":"Federico2903","uid":381949}
// 这就变回了字符串

const coyoteStrengthInfo = {
    type: "msg",
    strength: {
        strengthA: 0,
        strengthALimit: 200,
        strengthB: 0,
        strengthBLimit: 200
    }
};
// 直接定义 JSON 对象也是可以的

console.log(coyoteStrengthInfo.strength.strengthA);
// 输出 0

const coyotePulseInfo = {
    type: "msg",
    pulse: [
        "0A0A0A0A000A141E",
        "0F0F0F0F28323C46"
    ]
}

console.log(coyotePulseInfo.pulse[0]);
// 输出 0A0A0A0A000A141E
所以通过 JSON,可以很方便地序列化信息。
可能对你有用的链接:
  • https://zh.wikipedia.org/wiki/JSON

1-2.3. HTML(超文本标记语言)

HTML(超文本标记语言——HyperText Markup Language)是构成 Web 世界的一砖一瓦。它定义了网页内容的含义和结构。
——MDN 文档
正如最开始提到的那样,HTML 规划了整个网页的基本结构。
HTML 使用标签来描述网页。HTML 标签是由尖括号包围的关键词,比如 <html>。HTML 标签通常是成对出现的,比如 <b></b>。标签对中的第一个标签是开始标签,第二个标签是结束标签,结束标签的尖括号内有一个正斜杠。标签大小写不敏感
HTML 元素指的是标签对中的内容以及标签对本身
HTML 元素都含有属性。这些额外的属性值可以通过各种途径对元素进行配置或调整其行为。
HTML 文件的后缀名是 .html,可以直接在浏览器中打开,所以下面的 HTML 你都可以直接保存进一个文件,放进浏览器看效果!
HTML
<p style="font-size: 2em; text-align: center;" class="title">An example title</p>
上面的这段 HTML 会展示一个居中的标题,字体大小是 2em(这个环境下是 32px)。
这个段落元素(就是 <p>)具有两个属性,一个是 style,其中包含两个属性,一个是 font-size,表示字体大小,一个是 text-align,表示对齐情况。同时这个元素具有一个 title 的类。
HTML 文本HTML
<!-- 标题,h1最大,h6最小 -->
<h1>这是一个一级标题</h1>
<h2>二级标题</h2>
<h3>三级标题</h3>

<!-- 段落 -->
<p>这是一个段落。HTML让文本有了结构和意义。</p>

<!-- 加粗和斜体 -->
<p>这是<strong>重要</strong>的文字,这是<em>强调</em>的文字。</p>

<!-- 换行和水平线 -->
第一行<br>第二行
<hr>
<p>上面有一条水平分割线</p>

<!-- 超链接 -->
<a href="https://www.example.com">访问示例网站</a>
<a href="/about">绝对路径</a>
<a href="about">相对路径</a>

<!-- 图片 -->
<img src="cat.jpg" alt="一只可爱的猫" width="300">
<img src="https://placekitten.com/200/300" alt="随机猫咪图片">

<!-- 链接包裹图片,点击图片就会跳转 -->
<a href="large-image.jpg">
    <img src="thumbnail.jpg" alt="查看大图">
</a>

<!-- 无序列表(带项目符号) -->
<ul>
    <li>苹果</li>
    <li>香蕉</li>
    <li>橙子</li>
</ul>

<!-- 有序列表(带数字) -->
<ol>
    <li>起床</li>
    <li>刷牙</li>
    <li>吃早餐</li>
</ol>

<!-- 定义列表 -->
<dl>
    <dt>HTML</dt>
    <dd>超文本标记语言,用于创建网页结构</dd>
  
    <dt>CSS</dt>
    <dd>层叠样式表,用于美化网页</dd>
</dl>
上面的 HTML 展示了一些常用标签(LLM 写的(逃))。
注意到这里会有特例,<br><hr><img> 等没有结束标签。
两个比较特殊的标签:<div><span>,他们用来分组元素,区别是一个是块级(会占据整行,挤掉别的内容),一个是行间(嵌在一行里面的)。
HTML 标签可以无限嵌套,形成一个树形结构,这个也叫 DOM 树
你可以用 style 属性给元素加一段 CSS,可以用 class 属性给元素加一些类(比如你想做相同样式的卡片,不需要重复写 style,只需要在 CSS 里面选中所有含有某个类的进行设置,然后给对应元素相应的 class 就好了。还可以使用 id 属性给元素加一个唯一 ID,可以用于选择器的选择。
HTML 布局
每个元素从内到外是内容区域(content),内边距(padding),边框(border),外边距(margin)。这个又称为盒模型。在后面的前端设计会很有用。
可能对你有用的 MDN 文档链接:
  • https://developer.mozilla.org/zh-CN/docs/Learn_web_development/Getting_started/Your_first_website/Creating_the_content
  • https://developer.mozilla.org/zh-CN/docs/Web/HTML/Reference/Elements
  • https://developer.mozilla.org/zh-CN/docs/Web/HTML/Reference/Attributes

1-2.4. CSS(层叠样式表)

层叠样式表(Cascading Stylesheet,简称 CSS),其基本目标是让浏览器以指定的特性去绘制页面元素,比如颜色、定位、装饰。CSS 的语法反映了这个目标,由下面两个部分构建:
属性(property)是一个标识符,用可读的名称来表示其特性。
值(value)则描述了浏览器引擎如何处理该特性。每个属性都包含一个有效值的集合,它有正式的语法和语义定义,被浏览器引擎实现。
CSS 的核心功能是将 CSS 属性设定为特定的值。一个属性与值的键值对被称为“声明”(declaration)。
CSS 引擎会计算页面上每个元素都有哪些声明,并且会根据结果绘制元素,排布样式。
在 CSS 中,无论是属性名还是属性值都是对大小写不敏感的。属性与值之间以英文冒号 ':' (U+003A COLON)隔开。属性与值前面、后面与两者之间的空白不是必需的,会被自动忽略。
——MDN 文档
CSS 文件的后缀名是 .css,可以通过 <link> 标签引入到 HTML 中。
例如:<link rel="stylesheet" href="style.css">,就会把同目录下的 style.css 引入到这个 HTML。
也可以用 <style> 标签包裹,直接写进 HTML,比如:
HTML
<style>
p {
    font-size: 2em;
    color: red;
}
</style>
<p>An example paragraph.</p>
<!-- 这段文字会显示为大一些的红色 -->
CSS 一个很重要的部分,叫做选择器。这个东西比较复杂,笔者只讲解常用的部分。
一个 CSS 声明块是由一对大括号包裹起来的,就像这样:
CSS
{
    Here is you css content.
}
CSS 可以在声明块前面放置选择器(selector),选择器是用来选择页面多个元素的条件。
一对选择器与声明块称为规则集(ruleset),常简称为规则(rule)。
比如刚刚给出的第一段代码的高亮部分,就是一个规则,他选中了所有的 <p> 标签。
常用的选择方式有:
CSS
span {
    /* 选中所有 <span> */
}

.card {
    /* 选中所有类中含有 card 的元素 */
}

#element {
    /* 选中 ID 是 element 的元素 */
}
选择器可以复合,例如 .card.shadow 就会选中同时具有 cardshadow 两个类的元素。
当多个选择器共享相同的声明时,它们可以被编组进一个以逗号分隔的列表。
CSS
.card-1, .card-2 {
    /* 选中所有类中含有 card-1 或 card-2 的元素 */
}
常用的 CSS 属性:
  • 文字颜色(color)
  • 文字对齐(text-align)
  • 文字大小(font-size)
  • 显示模式(display)
  • 外边距(margin)
  • 内边距(padding)
  • 字体(font-family)
  • 内容区域宽度(width)
  • 内容区域高度(height)
可能对你有用的 MDN 文档链接(加粗的进阶必读):
  • https://developer.mozilla.org/en-US/docs/Web/CSS/Properties
  • https://developer.mozilla.org/zh-CN/docs/Web/CSS/Class_selectors
  • https://developer.mozilla.org/zh-CN/docs/Web/CSS/ID_selectors
  • https://developer.mozilla.org/zh-CN/docs/Web/CSS/Type_selectors
  • https://developer.mozilla.org/zh-CN/docs/Web/CSS/Attribute_selectors
  • https://developer.mozilla.org/zh-CN/docs/Web/CSS/Child_combinator
  • https://developer.mozilla.org/zh-CN/docs/Web/CSS/Selector_list
  • https://developer.mozilla.org/zh-CN/docs/Web/CSS/Pseudo-classes
  • https://developer.mozilla.org/zh-CN/docs/Web/CSS/Pseudo-elements
  • https://developer.mozilla.org/zh-CN/docs/Web/CSS/CSS_Values_and_Units/CSS_Value_Functions

1-2.5. 综合运用

我给出一段实现了简单任务列表的 HTML,包含 CSS 和 JavaScript,我们基于这个讲解这三者的综合运用。
HTML 文本HTML
<!DOCTYPE html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>简单的 DOM 操作</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            max-width: 600px;
            margin: 50px auto;
            padding: 20px;
			/*
			    设置字体为 Arial,如果出现不支持的字符使用 sans-serif(fallback)
			    容器最大宽度为 600 像素
			    外边距上下各为 50 像素,左右自动设置
			    内边距上下左右均为 20 像素
			*/
        }
        .container {
            background: #f9f9f9;
            padding: 20px;
            border-radius: 10px;
            box-shadow: 0 2px 5px rgba(0,0,0,0.1);
			/*
			    设置背景颜色为 #f9f9f9
			    内边距上下左右均为 20 像素
			    边框圆角半径 10 像素
			    设置阴影为 x 不偏移,y 偏移 2 像素
			    模糊半径 5 像素,颜色黑色,不透明度 10%
			    https://developer.mozilla.org/zh-CN/docs/Web/CSS/box-shadow
			*/
        }
        input, button {
            padding: 10px;
            margin: 5px;
            border: 1px solid #ddd;
            border-radius: 5px;
			/*
			    内边距上下左右均为 10 像素
			    外边距上下左右均为 5 像素
			    边框圆角半径 5 像素
			    边框粗细 1 像素,实线,颜色 #ddd
			    https://developer.mozilla.org/zh-CN/docs/Web/CSS/border
			*/
        }
        button {
            background: #4CAF50;
            color: white;
            cursor: pointer;
			/*
			    设置背景颜色为 #4CAF50
			    设置字体颜色为白色
			    设置鼠标悬浮时变为小手
			*/
        }
        button:hover {
            background: #45a049;
        }
        .item {
            background: white;
            padding: 10px;
            margin: 10px 0;
            border-radius: 5px;
            display: flex;
            justify-content: space-between;
            align-items: center;
			/*
			    这里涉及到了 Flex 布局
			    如果你有兴趣的话请阅读:
			    https://developer.mozilla.org/zh-CN/docs/Web/CSS/CSS_flexible_box_layout/Basic_concepts_of_flexbox
			    简单来说,这个地方的 display: flex 指定了使用 flex 布局
			    元素之间的分割方式是用间距分割并占完剩余空间(justify-content)
			    每个元素交叉轴方向上居中对齐
			*/
        }
        .completed {
            text-decoration: line-through;
            opacity: 0.6;
			/*
			    设置文本装饰为删除线
			    设置不透明度为 60%
			*/
        }
        .delete-btn {
            background: #ff4444;
            padding: 5px 10px;
        }
    </style>
</head>
<body>
    <div class="container">
        <h2>简单的任务列表</h2>

        <div>
            <input type="text" id="taskInput" placeholder="输入新任务">
            <button onclick="addTask()">添加任务</button>
        </div>

        <div id="taskList">
            <!-- 任务会动态添加在这里 -->
        </div>

        <div>
            <p>总任务数: <span id="totalCount">0</span></p>
        </div>
    </div>

    <script>
        function addTask() {
            const taskInput = document.getElementById('taskInput');
            const taskText = taskInput.value.trim();
            if (taskText === '') {
                alert('请输入任务内容!');
                return;
            }

            // 创建新的任务元素
            const taskItem = document.createElement('div');
            taskItem.className = 'item';
            taskItem.innerHTML = `
                <span>${taskText}</span>
                <div>
                    <button onclick="toggleTask(this)">完成</button>
                    <button class="delete-btn" onclick="deleteTask(this)">删除</button>
                </div>
            `;

            // 添加到任务列表
            document.getElementById('taskList').appendChild(taskItem);

            // 清空输入框
            taskInput.value = '';

            // 更新计数
            updateCount();
        }

        // 切换任务完成状态
        function toggleTask(button) {
            const taskItem = button.parentElement.parentElement;
            const taskText = taskItem.querySelector('span');

            taskText.classList.toggle('completed');

            if (taskText.classList.contains('completed')) {
                button.textContent = '未完成';
            } else {
                button.textContent = '完成';
            }
        }

        // 删除任务
        function deleteTask(button) {
            const taskItem = button.parentElement.parentElement;
            taskItem.remove();
            updateCount();
        }

        // 更新任务计数
        function updateCount() {
            const taskList = document.getElementById('taskList');
            const totalCount = document.getElementById('totalCount');

            totalCount.textContent = taskList.children.length;
        }

        // 初始化
        document.addEventListener('DOMContentLoaded', function() {
            // 给输入框添加回车键支持
            document.getElementById('taskInput').addEventListener('keypress', function(e) {
                if (e.key === 'Enter') {
                    addTask();
                }
            });
        });
    </script>
</body>
</html>
以上 HTML 涉及的知识点:
  • CSS 选择器
  • CSS 伪类
  • HTML 元素属性
  • JavaScript DOM 操作
页面效果预览
gif
你会发现这份 HTML 好像多了很多不熟悉的标签,比如 <head><html>,这些标签你可以理解为一个 HTML 所需要的模板,并且有一些规则:例如我们常常在 <head> 里面引入外部 JavaScript 和 CSS 文件。
无需深究
不完全理解上述内容对你设计网页影响不会很大。粗略了解一般写法即可。
如果想详细了解,访问以下 MDN 文档地址:
https://developer.mozilla.org/zh-CN/docs/Web/HTML/Reference/Elements#文档元数据
CSS 的设计细节我已经写进注释了,我们重点来讲一下 JavaScript 这些你没见过的函数。
document.getElementById():这个方法输入一个 ID,返回 ID 为指定值的 HTML 元素
这个 HTML 元素会拥有一些属性(可以直接访问或是修改),例如 innerHTML,就可以修改他的 HTML(但是请注意,这个地方你修改的内容是写在标签里面的,就是你修改不到标签,如果你要修改标签,应该去获取它的父级元素),对于可能存在值的元素(例如 input),会有 value 属性,表示当前他内部的值。
document.createElement():这个方法输入一个标签(不带尖括号),会新建并返回一个元素,表示标签对应的元素,此时的元素还没有进入 DOM 树(没有插入到网页上)。
element.parentElement:这个成员是它的父级元素。
element.classList:这个成员是它的类列表。
element.addEventListener(event, callback):表示为这个元素的某个事件绑定一个函数。
例如按钮点击事件是 click,网页加载完成事件是 DOMContentLoaded
小任务
这个删除按钮的大小不太对,你能把他修正吗?

2. HTTP,路由与模板引擎

2-1. HTTP:与服务器交流的语言

HTTP(HyperText Transfer Protocol,超文本传输协议)是互联网上应用最为广泛的一种网络协议,用于客户端和服务器之间的通信。它是Web数据通信的基础,定义了客户端如何向服务器请求资源,以及服务器如何响应这些请求。
HTTP 采用请求-响应模型:
  1. 客户端(如浏览器)向服务器发送 HTTP 请求
  2. 服务器处理请求并返回 HTTP 响应
  3. 客户端接收并处理响应
HTTP 常见请求方法
方法描述特点
GET请求指定的资源参数在 URL 中
POST向指定资源提交数据参数在请求体中
PUT替换指定资源用于更新操作
DELETE删除指定资源用于删除操作
HTTP 状态码
状态码范围类别常见示例
100-199信息响应100 Continue
200-299成功响应200 OK, 201 Created
300-399重定向301 Moved Permanently
400-499客户端错误404 Not Found, 400 Bad Request
500-599服务器错误500 Internal Server Error
想象一下,你走进一家餐厅。你的浏览器,就像是顾客(你)。你的代码,就像是餐厅后厨。
HTTP,就是你们之间使用的点餐语言。一次完整的“点餐”过程是这样的:
你(浏览器)发起请求:你在地址栏输入 http://localhost:3000/about 然后按下回车。这相当于你对服务员猫猫说:“我想看看‘关于’页面(GET /about)”。这就是一个 HTTP 请求。
GET 是一种最常见的请求方法,意思是“我想获取点东西喵”。就像你说“我想看看菜单喵”。
另一种常见方法是 POST,意思是“我想提交点东西喵”。就像你吃完后说“我要给钱了喵”。
后厨(服务器)处理请求:服务员把你的请求(“关于页面”)告诉后厨。后厨的厨师(你的代码)一听:“哦,是要 /about 啊,收到喵!”
后厨做出响应:厨师迅速准备好了一份“关于页面”的套餐,让服务员猫猫端给你。这就是 HTTP 响应。这个响应里包含了:
状态码:比如 200 OK(一切顺利)或 404 Not Found(找不到你点的菜)。
响应内容:最终的 HTML、文本或图片,就是你会在浏览器里看到的东西。
所以,HTTP 就是一套规则,规定了浏览器和服务器之间如何“说话”才能互相理解。

2-2. 路由:构建网站导航的基石

我们的后端框架采用的是 Express,在 Express 中,路由用于确定应用程序如何响应对特定端点的客户机请求,包含一个 URI(或路径)和一个特定的 HTTP 请求方法(GET、POST 等)。
每个路由可以具有一个或多个处理程序函数,这些函数在路由匹配时执行。
路由可以由如下方法定义:
JAVASCRIPT
app.method(path, handler);
method 表示请求的方法,path 表示路径(回忆章节 1-1 中提到的路径的定义),handler 是回调函数。
如果你希望处理 /user 路径上的 PUT 方法请求,你可以像下面的代码这样写:
JAVASCRIPT
app.put('/user', (req, res) => {
    res.send('Got a PUT request at /user')
})
观察这个 Handler,他是一个有两个参数的箭头函数。
我们一般命名为 reqres,分别是请求对象和响应对象。
req 包含了这个请求携带的信息,res 则是你要做出的响应的信息。
常用的 res 的方法如下表:
方法描述是否结束响应过程
res.send()发送各种类型的响应
res.json()发送 JSON 响应
res.status()设置 HTTP 状态码
res.sendStatus()设置状态码并发送对应的状态消息
res.sendFile()发送文件作为响应
res.download()提示下载文件
res.redirect()重定向请求
res.render()渲染视图模板
res.set()设置响应头
res.get()获取响应头值
res.cookie()设置 cookie
res.clearCookie()清除 cookie
res.type()设置 Content-Type
res.end()结束响应过程
res.format()根据 Accept 头进行内容协商
res.location()设置 Location 响应头
res.append()追加响应头
  • 结束响应过程的方法:这些方法会自动结束响应,调用后不能再发送其他内容。
  • 不结束响应过程的方法:这些方法只是设置响应属性,需要后续调用其他方法来结束响应。
  • 组合使用:通常先调用设置方法,最后调用发送方法来结束响应。
以上函数的详细用法可查看 Express 文档:
  • https://expressjs.com/zh-cn/5x/api.html#res.methods
如果你一直不结束响应过程,HTTP 连接就会一直挂起,直到超时。而响应过程结束后也不能再发送数据,例如:
JAVASCRIPT
// 正确用法:先设置后发送
app.get('/api/user', (req, res) => {
    res.set('X-Custom-Header', 'value');  // 不结束响应
    res.status(200);                      // 不结束响应
    res.json({ user: 'John' });           // 结束响应
});

// 错误用法:发送后不能再设置
app.get('/error-1', (req, res) => {
    res.send('Hello');                    // 结束响应
    res.set('X-Header', 'value');         // 错误:响应已结束
});

// 错误用法:未结束响应过程
app.get('/error-2', (req, res) => {
    res.set('X-Custom-Header', 'value');  // 不结束响应
    res.status(200);                      // 不结束响应
    // HTTP 连接将一直等待响应结束,但是永远也不会结束,直到超时
});
到现在我们还没有讲到怎么跟用户输入做交互,这就需要使用我们的 req 对象。
常用的 req 属性和方法如下表:
方法/属性描述
req.params路由参数对象
req.queryURL 查询参数对象
req.body请求体数据(需要中间件解析)
req.headers请求头对象
req.cookiescookies 对象(需要中间件解析)
req.methodHTTP 请求方法
req.url请求的 URL
req.originalUrl原始请求 URL
req.baseUrl路由的基本 URL
req.path请求路径
req.hostname主机名
req.ip客户端 IP 地址
req.get()获取请求头
现在进入我们的工作目录,执行下面的命令:
BASH
npm install cookie-parser --save
这指示 npm 为我们的项目安装 cookie-parser 库,我们会用他来解析 Cookie。
在你的 app.jsapp.get 前面插入这样一段代码:
JAVASCRIPT
app.use(cookieParser());
// 解析 Cookie
app.use(express.json());
// 解析 application/json
app.use(express.urlencoded({ extended: true }));
// 解析 application/x-www-form-urlencoded
无需深究
在这里你暂时不需要理解它,这是 Express 的中间件,在这里用来解析 Cookie 和 req.body
中间件将在“5-3. 鉴权:Express 中间件的使用”中提及。
把路由改成这样:
JAVASCRIPT
app.post('/', (req, res) => {
    res.send(req.body);
});
这段所构建的一个路由,接收到 POST 请求后会输出你的请求体。
打开你的终端,我们安装一个工具:curl
BASH
sudo apt install curl -y
随后我们可以用这样的方法来构建请求:
BASH
curl -X POST -H "Content-Type: application/json" -d "{\"x\":1}" http://localhost:3000
        ^            ^                                    ^             ^
  指定请求方法    指定是 JSON 格式                      请求体           网址
如果你已经修改了路由并在终端里执行了这个的话,你会看到他原样返回了你输入的请求体:
BASH
$ curl -X POST -H "Content-Type: application/json" -d "{\"x\":1}" http://localhost:3000
{"x":1}
尝试一下,在 res.send 之前输出 req.body 到终端,并重新发送请求,你应该会看见这样的内容:
BASH
$ node app.js
{ x: 1 }
注意到什么不同了吗?这里的 req.body 已经是一个 JSON 对象了,可以用“1-2.2. JSON(JavaScript 对象表示法)”中提到的方式读取出里面的内容。
我们尝试做一点点用户交互:
JAVASCRIPT
app.post('/', (req, res) => {
    res.send(`Hello, ${req.body.username}`);
});
模板字符串
像上面代码一样的,用反引号包起来的字符串叫模板字符串。
可以用 ${expression} 的方式计算一个表达式,计算结果会替换到相应的位置。
此时我们模拟一个用户请求,这个用户正在发送自己的用户名到后端:
BASH
$ curl -X POST -H "Content-Type: application/json" -d "{\"username\":\"Federico2903\"}" http://localhost:3000
Hello, Federico2903
你可以看到它和你打了一个招呼!这就是最基础的用户交互,根据用户的输入返回不同的内容。
这个东西,已经是一个 API 了。但是常常这样并不够,因为和这个 API 的交互太麻烦,我们需要让用户能更简单地操作它。这一部分会在“3. 前端设计与用户交互”中提到。
路由还支持占位符形式的定义,例如这个路由:
JAVASCRIPT
app.get('/user/:id', (req, res) => res.send(req.params.id));
你可以尝试着把他插入到我们的 app.jsapp.listen 前并运行,打开你的浏览器,输入 http://localhost:3000/user/Federico2903,看看网页上是什么。
不出意外的话,应该显示 Federico2903
Federico2903 取代了路由定义中的 :id 的部分,并且被记录到了 req.params.id 中。
如果你定义一个 /user/:abcd,自然获取它的方式就变成了 req.params.abcd
除了路由参数,我们还可以通过 URL 查询参数来获取用户输入的数据。这些参数通常写在 URL 的 ? 后面,用 & 分隔,例如 http://localhost:3000/search?keyword=node&limit=10,这里 keyword=nodelimit=10 就是查询参数。
我们可以通过 req.query 来获取它们,在你的 app.jsapp.listen 前插入如下内容:
JAVASCRIPT
app.get('/search', (req, res) => {
    console.log(req.query);
    res.send(`搜索关键词:${req.query.keyword},限制条数:${req.query.limit}`);
});
打开你的浏览器,输入 http://localhost:3000/search?keyword=node&limit=10
你会在终端看到:
BASH
{ keyword: 'node', limit: '10' }
而网页上会显示:
PLAIN
搜索关键词:node,限制条数:10
注意
req.query 中的值都是字符串类型,即使你在 URL 里写的是数字,它也会被当作字符串处理。如果需要数值,可以用 Number() 或者 parseInt() 转换。
也就是说,req.params 是用来匹配路径占位符的,而 req.query 是用来获取 URL 中 ? 后的查询参数的,而 req.body 则是 POST 得到的内容。
利用好这三个属性,就可以实现大部分后端逻辑。
可能对你有用的文档链接:
  • https://expressjs.com/zh-cn/5x/api.html#res
  • https://expressjs.com/zh-cn/5x/api.html#req

2-3. 模板引擎:网页的蓝图

我们刚刚已经得到了一个 API,

评论

2 条评论,欢迎与作者交流。

正在加载评论...