专栏文章
从零开始编写你的网站
个人记录参与者 3已保存评论 2
文章操作
快速查看文章及其快照的属性,并进行相关操作。
- 当前评论
- 2 条
- 当前快照
- 1 份
- 快照标识符
- @mio0x1i6
- 此快照首次捕获于
- 2025/12/02 11:33 3 个月前
- 此快照最后确认于
- 2025/12/02 11:33 3 个月前
0. 前言
也许你使用过各种各样的网站,有的时候你可能想要建立一个属于自己的网站。实现你的想法的方式有很多,也许是一些开源项目,也许是一些现成的网站。这篇文章将带来一种更有意思的解决方案,就是自己编写。
根据数据显示,在 Server-Side 编程语言的使用情况统计中,提到了 JavaScript 作为第四常用的后端语言,占比约 (第一的 PHP 占据了 ),在 StackOverflow 的最受欢迎的语言的统计中,JavaScript 以 的得票率占据第一,再加上其语法与 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),输入下面的命令:
BASHsudo apt update
执行后可能要求你输入密码,输入就好啦。
这是什么意思?
sudo 表示以管理员的身份执行,而 apt 则是 Ubuntu 系统默认的软件包管理器,update 是更新软件包列表的谓词。既然我们将使用 JavaScript,那么我们就应当准备一个正确的 NodeJS 环境。
Ubuntu 的软件源中的 NodeJS 版本可能过旧,我们使用 nvm 进行 NodeJS 版本管理。
输入下面的命令安装 nvm:
BASHsudo 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 喵!执行:
BASHnvm install 22
等读条读完过后,执行:
BASHnode -v
如果有输出,就说明安装好了,输出可能长这样:
CPPv22.18.0
接下来你需要新建一个文件夹,作为你的工作目录。
选择一个适合你的 IDE,比如 Visual Studio Code,或是 WebStorm,好的 IDE 可以改善你的开发体验和加快开发速度。
打开你的工作目录并在目录下执行:
BASHnpm init
接下来 npm 会引导你填写一些信息,示例输出如下(笔者拿 Windows 跑的,不过在 Ubuntu 上是一样的):
BASHThis 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,直接替换就行了。接下来的操作你暂时不需要理解,直接照做即可,在工作目录下执行:
BASHnpm install express --save
创建文件
JAVASCRIPTapp.js(或是你的入口点),写入如下内容:const express = require('express');
const app = express();
app.get('/', (req, res) => {
res.send('Hello World!');
});
app.listen(3000);
然后回到工作目录下执行:
BASHnode 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 自带的包管理器,类似的还有 pnpm 和 yarn 等。安装的文件会存放于
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 编写,但实现的功能是一样的:
JAVASCRIPTlet 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 中都不需要指明类型,这就是弱类型。
我们再来看一段代码:
JAVASCRIPTconsole.log("This is the first message.");
throw new Error('Example error'); // 抛出一个错误
console.log("This is the second message.");
这段代码在笔者 Windows 中的输出为:
JAVASCRIPTThis 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;
};
执行
greet1 和 greet2 的效果是完全一样的。如果函数只有一个参数,可以省略括号
JAVASCRIPT()。如果函数体只有一行返回语句,可以省略大括号 {} 和 return 关键字:const greet = name => "Hello, " + name;
// 等价于
const greet = function(name) {
return "Hello, " + name;
};
箭头函数还有一个非常重要的特性:它没有自己的
JAVASCRIPTthis。它的 this 值继承自定义它时所处的上下文(父级作用域)。这解决了传统函数中 this 指向混乱的著名难题(P.S. 你不需要理解下面这段代码,因为他所涉及的 document 对象只存在于浏览器中)。document.addEventListener('click', function() {
console.log(this); // this 指向 document
});
// this 继承自外部(定义它的地方)
document.addEventListener('click', () => {
console.log(this); // this 指向 window(或外部作用域的this)
});
在需要传递一个回调函数时,使用箭头函数通常更安全、更不容易出错。
回调函数
JavaScript 的参数可以是一个函数,常见的使用方式是当函数逻辑执行完之后,执行位于参数列表最末端的函数,上报结果。我们称其为回调函数(callback)。
示例代码:
JAVASCRIPTfunction add(x, y, callback) {
const result = x + y;
callback(result);
}
add(1, 2, result => console.log(result));
以上这段代码输出
3。JavaScript 也可以运行在浏览器中(当网页上包含 JavaScript 时会自动运行)。
此时的 JavaScript 使用的是 ESM 语法(如果你好奇的话可以去搜一下,最显著的区别是使用
import 进行导入)。并且这个时候的 JavaScript 脚本会多出几个对象,比较常用的有
document 和 window,后面设计前端时我们再来说这个。将 JavaScript 嵌入 HTML 的方式是使用
HTML<script> 标签。可以使用 src 属性加载外部 JavaScript,也可以直接包裹 JavaScript 代码:<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 的方法也很简单:
JAVASCRIPTconst 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。也可以用
HTML<style> 标签包裹,直接写进 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> 标签。常用的选择方式有:
CSSspan {
/* 选中所有 <span> */
}
.card {
/* 选中所有类中含有 card 的元素 */
}
#element {
/* 选中 ID 是 element 的元素 */
}
选择器可以复合,例如
.card.shadow 就会选中同时具有 card 和 shadow 两个类的元素。当多个选择器共享相同的声明时,它们可以被编组进一个以逗号分隔的列表。
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 操作
页面效果预览

你会发现这份 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 采用请求-响应模型:
- 客户端(如浏览器)向服务器发送 HTTP 请求
- 服务器处理请求并返回 HTTP 响应
- 客户端接收并处理响应
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,就是你们之间使用的点餐语言。一次完整的“点餐”过程是这样的:
你(浏览器)发起请求:你在地址栏输入 猫猫说:“我想看看‘关于’页面(GET /about)”。这就是一个 HTTP 请求。
http://localhost:3000/about 然后按下回车。这相当于你对服务员GET 是一种最常见的请求方法,意思是“我想获取点东西喵”。就像你说“我想看看菜单喵”。
另一种常见方法是 POST,意思是“我想提交点东西喵”。就像你吃完后说“我要给钱了喵”。
后厨(服务器)处理请求:服务员把你的请求(“关于页面”)告诉后厨。后厨的厨师(你的代码)一听:“哦,是要
/about 啊,收到喵!”后厨做出响应:厨师迅速准备好了一份“关于页面”的套餐,让服务员猫猫端给你。这就是 HTTP 响应。这个响应里包含了:
状态码:比如
200 OK(一切顺利)或 404 Not Found(找不到你点的菜)。响应内容:最终的 HTML、文本或图片,就是你会在浏览器里看到的东西。
所以,HTTP 就是一套规则,规定了浏览器和服务器之间如何“说话”才能互相理解。
2-2. 路由:构建网站导航的基石
我们的后端框架采用的是 Express,在 Express 中,路由用于确定应用程序如何响应对特定端点的客户机请求,包含一个 URI(或路径)和一个特定的 HTTP 请求方法(GET、POST 等)。
每个路由可以具有一个或多个处理程序函数,这些函数在路由匹配时执行。
路由可以由如下方法定义:
JAVASCRIPTapp.method(path, handler);
method 表示请求的方法,path 表示路径(回忆章节 1-1 中提到的路径的定义),handler 是回调函数。如果你希望处理
JAVASCRIPT/user 路径上的 PUT 方法请求,你可以像下面的代码这样写:app.put('/user', (req, res) => {
res.send('Got a PUT request at /user')
})
观察这个 Handler,他是一个有两个参数的箭头函数。
我们一般命名为
req 和 res,分别是请求对象和响应对象。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.query | URL 查询参数对象 |
req.body | 请求体数据(需要中间件解析) |
req.headers | 请求头对象 |
req.cookies | cookies 对象(需要中间件解析) |
req.method | HTTP 请求方法 |
req.url | 请求的 URL |
req.originalUrl | 原始请求 URL |
req.baseUrl | 路由的基本 URL |
req.path | 请求路径 |
req.hostname | 主机名 |
req.ip | 客户端 IP 地址 |
req.get() | 获取请求头 |
现在进入我们的工作目录,执行下面的命令:
BASHnpm install cookie-parser --save
这指示 npm 为我们的项目安装
cookie-parser 库,我们会用他来解析 Cookie。在你的
JAVASCRIPTapp.js 的 app.get 前面插入这样一段代码: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 中间件的使用”中提及。
把路由改成这样:
JAVASCRIPTapp.post('/', (req, res) => {
res.send(req.body);
});
这段所构建的一个路由,接收到 POST 请求后会输出你的请求体。
打开你的终端,我们安装一个工具:
BASHcurl。sudo apt install curl -y
随后我们可以用这样的方法来构建请求:
BASHcurl -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}
尝试一下,在
BASHres.send 之前输出 req.body 到终端,并重新发送请求,你应该会看见这样的内容:$ node app.js
{ x: 1 }
注意到什么不同了吗?这里的
req.body 已经是一个 JSON 对象了,可以用“1-2.2. JSON(JavaScript 对象表示法)”中提到的方式读取出里面的内容。我们尝试做一点点用户交互:
JAVASCRIPTapp.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. 前端设计与用户交互”中提到。
路由还支持占位符形式的定义,例如这个路由:
JAVASCRIPTapp.get('/user/:id', (req, res) => res.send(req.params.id));
你可以尝试着把他插入到我们的
app.js 中 app.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=node 和 limit=10 就是查询参数。我们可以通过
JAVASCRIPTreq.query 来获取它们,在你的 app.js 中 app.listen 前插入如下内容: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 条评论,欢迎与作者交流。
正在加载评论...