经常有人问我:想做一个网站,是不是得先埋头学三年前端,再学一门后端语言,然后才能动手?我的回答通常是“不一定”。制作网站的一般方法,说到底不是某个框架的搬运工,而是先搞清楚网站是怎么运转的、你要解决什么问题,再用合适的技术把页面搭起来。这篇文章不讲虚的,从网站分类、技术选型,到真正能跑起来的 HTML/CSS/JavaScript 代码,再到后端最小实现和上线部署,我会按自己做项目时实际走的路径来讲。文章里每段代码都能直接复制到本地跑通,适合零基础入门,也适合会一点前端但没完整做过一个站点的人参考。
1. 网站的本质:先搞清楚你在做的到底是什么
1.1 静态页面、动态网站和“看起来动态”的区别
很多新手会纠结“我做的到底是静态网站还是动态网站”。其实判断标准很简单:有没有程序在服务器上帮你处理数据。
纯静态网站,就是服务器把写好的 HTML/CSS/JS 文件原样发给浏览器,浏览器负责渲染展示。它的优点是快、便宜、不容易出问题,缺点是内容一多,改起来很麻烦——比如你有 100 篇文章,每篇都要手写一个 HTML 页面,维护成本就会爆炸。动态网站则多了一个服务端程序,浏览器发起请求后,程序去读数据库、做计算、拼页面,再把结果返回。像登录、评论、下单这些功能,都需要动态程序在背后支撑。
还有一种很常见的“伪动态”,比如某些页面加载了一堆 JavaScript,你刷信息流时感觉它在实时更新,但实际上数据是从服务器某个接口拉来的,页面框架是静态的。今天大量所谓的“现代网站”都是这种模式:静态壳子加动态接口。我刚做项目时常在这些概念里绕晕,后来想通了一点——不要纠结名词,先问自己:你的内容要不要随时变?要不要收用户数据?要不要登录?如果三个都是“否”,静态网站完全够用。
1.2 浏览器是如何把代码变成页面的
我见过不少同学对着教程敲了很久代码,却不知道 render 的过程到底是什么。这里用一个简单的类比:HTML 是房子的骨架和结构,CSS 是装修方案,JavaScript 是水电和智能家居系统。
浏览器拿到 HTML 后,会构建一棵 DOM 树;遇到引用的 CSS 文件,会计算每个节点该用什么样式;再执行 JavaScript,动态修改 DOM 结构或者样式。整个过程主要发生在浏览器的渲染引擎里。你按 F12 打开开发者工具的“Performance”面板,能看到解析、样式计算、布局、绘制这些阶段。
这对你理解“重要代码”很有帮助。因为你写 HTML 时其实在做语义化结构,写 CSS 时在描述规则而非精确像素,写 JS 时在操作浏览器的运行时。理解了这一层,后面写代码就不会把三样东西混在一起。记住:HTML 负责语义,CSS 负责表现,JS 负责行为,这个边界越清晰,代码越容易维护。
1.3 从想法到上线,一般要踩过哪几个节点
我一般把做网站的流程分成六步:需求定义、信息架构与原型、界面设计、开发、测试、部署维护。
需求定义就是回答“这个网站给谁用、解决什么问题”。这一步很多人会跳过,结果做到一半才发现页面结构不合理。信息架构则是梳理导航、栏目、内容层级,一个简单的个人站,通常只需要首页、关于、作品列表、联系页。原型阶段可以用手绘或者任意在线白板工具画线框,不用追求好看,重点是确认布局逻辑。界面设计在个人项目里可以省略,直接进入开发。
开发阶段不要贪多,先做能用的最小版本,再做华丽的功能。测试需要覆盖不同浏览器和手机尺寸。最后部署就是买域名、找服务器或托管平台、传文件、配 HTTPS。对新手来说,最痛苦的不是写代码,而是网络概念:域名解析、DNS、端口、反向代理。这些我会在第 5 章专门讲。
2. 技术栈选型:先别急着学框架,想清楚你要做哪种网站
2.1 三类典型场景,分别适合什么方案
我做过的项目里,大部分需求可以归为三类。
第一类是个人主页、作品集、活动宣传页,内容量小、更新频率低。这种最适合纯静态页面:写几个 HTML 文件,或者用静态站点生成器,把 Markdown 转成 HTML。部署最省事,甚至可以丢到免费对象存储上。第二类是博客、新闻站、资料库这类“内容多但不复杂”的站点。这时候不建议自己从零造轮子,可以直接用一套开源内容管理系统,装好后后台里发文章,前端自动展示。第三类是工具类网站、后台管理、电商网站,要求有用户、数据、支付等业务逻辑,这才需要认真选服务端技术栈。
很多教程会让你一上来就学巨型的全家桶框架,这在我看来是过度设计。你做个人主页,真的用不上它。
2.2 为什么我不建议新手直接上全家桶框架
我见过一个同学,想做个简历页,结果先花了两周学习前端框架和工程化配置,最后写出来的页面比直接用 HTML 写得还慢。问题不是框架不好,而是你在用不对等的工具解决小问题。
框架解决的是“复杂应用的状态管理、组件复用、路由组织”这些痛点,而静态页面根本没有那么多状态。如果一开始就依赖框架,你会发现自己在学习各种概念:组件生命周期、状态管理、依赖注入……这些概念本身没问题,但它们没法帮你理解“浏览器如何渲染网页”这个最核心的本质。我的建议是:先用原生 HTML/CSS/JS 做两三个小页面,等真的遇到重复代码太多、页面交互复杂到难以维护时,再引入框架,你会发现那时学起来快得多,因为你已经知道它要解决什么问题了。
2.3 本地开发环境:编辑器、Node 环境和版本控制
开发网站不是装个记事本就行。我的最低配置是:一个现代编辑器、一个能跑本地服务的运行环境、一个版本控制工具。
编辑器我推荐看看带中文界面和插件市场的现代编辑器,内置终端、代码高亮、格式化功能,新手不用折腾。Node 环境是很多前端工具链的基础,这里不一定一开始就要装,但如果你想尝试本地启动服务,或者用 Node.js 写后端,它就很重要。安装完成后,在命令行输入 node -v 能看到版本号就说明可用。版本控制建议学会 Git,建仓后每次改动提交一个版本。哪怕就你自己一个人开发,它也能救你命——改坏代码还能退回上一版。
这套环境准备的时间大约一小时内能完成,不需要刻意背命令,遇到不会的能搜索到用法就够了。真正的重点是把代码写出来。
3. 从零写一个能跑的页面:HTML/CSS/JavaScript 核心代码拆解
3.1 页面骨架:语义化 HTML 与几个容易漏掉的细节
下面我给出一个完整的个人作品集页面骨架。别小看这段代码,它里面藏了三个新人经常漏掉的细节:字符集、视口设置、语义化标签。
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>A君的个人作品集</title> <link rel="stylesheet" href="style.css"> </head> <body> <header class="site-header"> <nav class="navbar"> <a href="#about" class="brand">A君</a> <ul class="menu"> <li><a href="#about">关于我</a></li> <li><a href="#works">作品</a></li> <li><a href="#contact">联系</a></li> </ul> </nav> </header> <main> <section id="about" class="hero"> <h1>你好,我是 A君</h1> <p>一名网页开发者,专注前端交互与性能优化。</p> </section> <section id="works" class="works"> <h2>代表作品</h2> <div class="card-list"> <article class="card"> <h3>模拟项目X</h3> <p>一个用来学习前后端联调的小系统。</p> </article> <article class="card"> <h3>某跨平台工具</h3> <p>解决日常重复操作的效率小工具。</p> </article> </div> </section> <section id="contact" class="contact"> <h2>联系我</h2> <form id="contact-form"> <p> <label for="name">称呼</label> <input type="text" id="name" name="name" required> </p> <p> <label for="email">邮箱</label> <input type="email" id="email" name="email" required> </p> <p> <label for="message">留言</label> <textarea id="message" name="message" rows="4" required></textarea> </p> <button type="submit">发送</button> <p id="form-status" class="status" hidden></p> </form> </section> </main> <footer class="site-footer"> <p>© 2024 A君的个人空间</p> </footer> <script src="main.js"></script> </body> </html>meta name="viewport" 是移动端适配的关键,不写的话手机浏览器会按桌面宽度渲染,页面会变得又挤又小。lang="zh-CN" 则是给搜索引擎和辅助技术读的,表示页面主语言是中文。语义化标签 header、nav、main、section、article、footer 不仅让代码结构一目了然,对 SEO 和屏幕阅读器也更友好。
表单里的 label 一定要通过 for 属性关联输入框。这样做以后,你点击“称呼”两个字,输入框会自动获得焦点,这个细节体验很细微,但能提现页面是否做得到位。另外所有表单字段都加了 required,浏览器会自动做空值检查——这是不需要写一行 JS 就能获得的基础校验。
3.2 样式系统:Flex 布局、盒模型和响应式断点
接着写 style.css。这段代码会用到盒模型重置、Flex 布局、卡片样式和媒体查询。
* { box-sizing: border-box; margin: 0; padding: 0; } body { font-family: system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; line-height: 1.7; color: #222; background: #fafafa; } .site-header { background: #fff; box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08); position: sticky; top: 0; z-index: 10; } .navbar { display: flex; justify-content: space-between; align-items: center; max-width: 960px; margin: 0 auto; padding: 1rem; } .menu { display: flex; gap: 1.2rem; list-style: none; } .menu a { text-decoration: none; color: inherit; } .hero { text-align: center; padding: 4rem 1rem; background: linear-gradient(135deg, #e0e7ff, #f3f4f6); } .works { max-width: 960px; margin: 0 auto; padding: 2rem 1rem; } .card-list { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 1rem; } .card { background: #fff; border-radius: 8px; padding: 1.2rem; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06); } .contact { max-width: 720px; margin: 0 auto; padding: 2rem 1rem; } .contact input, .contact textarea { width: 100%; padding: 0.6rem; border: 1px solid #ccc; border-radius: 6px; font-size: 1rem; } .contact button { margin-top: 0.8rem; padding: 0.6rem 1.4rem; border: none; border-radius: 6px; background: #2563eb; color: #fff; font-size: 1rem; cursor: pointer; } .status { margin-top: 0.8rem; font-size: 0.9rem; } @media (max-width: 600px) { .navbar { flex-direction: column; gap: 0.5rem; } .menu { flex-wrap: wrap; justify-content: center; } }box-sizing: border-box 是第一个要养成习惯的代码。它的意思是:给元素设置的宽度包含内边距和边框。不写这一行,你给一个元素设 width: 100% 再设 padding,实际宽度会超出父容器,布局就会乱。
navbar 用 display: flex 配合 justify-content: space-between 实现左右两端对齐,这是最常用的导航栏布局方式。卡片列表用 CSS Grid 的 repeat(auto-fit, minmax(240px, 1fr)),能根据容器宽度自动决定一行放几张卡片,当屏幕变窄时自动换行。这个写法比手动写媒体查询处理列数要省心得多。
媒体查询放在最后,当屏幕宽度小于 600px 时,把导航从横排变成竖排,这是最基础的移动端适配。做响应式不需要一开始就设计很复杂的断点,先保证 600px 这个门槛,再逐步优化中间尺寸。
3.3 交互脚本:事件监听、Fetch 与表单校验
下面写 main.js。这里的核心不是炫技,而是演示“事件处理 + 网络请求 + 状态反馈”这个最常见的交互闭环。
const form = document.getElementById('contact-form'); const statusEl = document.getElementById('form-status'); form.addEventListener('submit', async (event) => { event.preventDefault(); const name = document.getElementById('name').value.trim(); const email = document.getElementById('email').value.trim(); const message = document.getElementById('message').value.trim(); if (!name || !email || !message) { statusEl.hidden = false; statusEl.textContent = '请完整填写所有字段。'; return; } statusEl.hidden = false; statusEl.textContent = '发送中...'; try { const res = await fetch('/api/contact', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name, email, message }) }); const json = await res.json(); if (res.ok) { statusEl.textContent = '消息已收到,我会尽快回复。'; form.reset(); } else { statusEl.textContent = json.message || '发送失败,请稍后重试。'; } } catch (err) { statusEl.textContent = '网络异常,请检查连接后再试。'; } });event.preventDefault() 是最重要的一行。它阻止浏览器默认的表单提交行为,让我们可以自己控制请求流程。如果不调用它,页面会跳转刷新,你辛苦写的交互逻辑就白费了。
里面的 trim() 会去掉首尾空白字符,避免用户只输入空格也通过校验。而 fetch 发出的网络请求,是浏览器内置的 API,不需要额外引入库。这里整个交互逻辑是:收集表单字段、发请求、根据服务器返回结果更新页面状态。这个模式几乎适用于所有“提交表单”的场景,比如登录、留言、问卷。
至此,你已经有了一个完整的静态页面骨架,和一个能向后端发消息的前端逻辑。但这个页面目前还不能真正处理留言数据,因为还没有服务端程序。我们下一步就把它补上。
4. 让页面真正“动”起来:表单提交和后端最小实现
4.1 为什么单纯的静态页面不够用
如果你只是展示信息,静态页面当然没问题。但只要你想让访客留言、注册、下单,就绕不开“把数据存下来”这个诉求。数据不能存在浏览器里,因为浏览器属于访客,关掉页面就没了,而且别人也看不到。正确的做法是:浏览器把数据通过 HTTP 请求发给服务器,服务器把数据存进数据库或者文件。
这里我再强调一个概念——前后端分离。前端负责展示和收数据,后端负责逻辑和数据存储,两边通过固定格式的接口通信。你对 /api/contact 这个地址发一个 POST 请求,服务器收到后解析出 name、email、message,做后续处理。只要接口约定不变,前后端各自用什么技术改都不影响对方。
新手最容易犯的错误是:前端代码里写了一段假的模拟接口逻辑,跑通了就觉得项目做完了。其实那只是静态演示,数据根本没有被任何地方接收和保存。为了让项目可交付,必须有真实的后端响应。
4.2 Node.js 最小服务端:Express 接收留言数据
我这里用 Node.js 和 Express 做示例,因为这个环境在选择前端开发时就已经装好了,上手成本最低。如果你更熟悉 Python,用对应框架写也是一样的思路。先创建一个新目录 server 文件夹,在里面初始化项目并安装依赖,然后写 server.js:
mkdir my-website cd my-website npm init -y npm install express服务端代码:
const express = require('express'); const path = require('path'); const fs = require('fs'); const app = express(); app.use(express.json()); app.use(express.urlencoded({ extended: true })); app.use(express.static(path.join(__dirname, 'public'))); app.post('/api/contact', (req, res) => { const { name, email, message } = req.body; if (!name || !email || !message) { return res.status(400).json({ message: '请完整填写所有字段。' }); } const line = `[${new Date().toISOString()}] ${name} <${email}>: ${message}\n`; fs.appendFile('messages.log', line, (err) => { if (err) { return res.status(500).json({ message: '服务器保存失败。' }); } res.json({ ok: true }); }); }); const PORT = process.env.PORT || 3000; app.listen(PORT, () => { console.log(`Server is running at http://localhost:${PORT}`); });这段代码做了四件事:第一,express.json 和 express.urlencoded 两个中间件负责解析请求体,让 req.body 能拿到前端发来的 JSON 数据;第二,express.static 把 public 目录下的 HTML、CSS、JS 文件作为静态资源直接服务;第三,POST 接口里先做服务器端校验,再通过 fs.appendFile 把留言追加到 messages.log 文件;第四,启动监听。
实际生产环境肯定要接数据库,把数据写入文件也只是教学演示。但它已经构成一个完整的前后端闭环:浏览器打开页面 → 用户填写表单 → fetch 发请求 → 服务端校验并存储 → 返回结果 → 前端显示状态。这个链路你能自己跑通,做网站的大半块拼图就补齐了。
4.3 本地联调:页面文件该放哪里
这里有一个很容易搞混的点:你的 HTML 文件要放在 public 目录下,而不是项目根目录。因为 express.static 只开放了 public 这层目录给浏览器访问。
你启动服务器后,浏览器访问 http://localhost:3000,Express 会自动去找 public/index.html 并返回它。如果你把 HTML 放在根目录,访问时会看到 404。这个细节卡住过很多人。
本地联调时,遇到接口 404,第一反应是看浏览器开发者工具 Network 面板里请求的完整地址和响应状态码。看到什么状态码就对应什么问题:404 多半是路径不对,500 多半是服务器代码报错,400 多半是数据校验失败。用这个思路排查,比在代码里盲目加日志要快得多。
5. 从 localhost 到公网:部署上线要做的关键动作
5.1 域名、DNS 解析和 HTTPS 证书,各是干什么的
开发完的网站只能在自己电脑上打开,别人访问不到。上线这个词的本质,就是把你电脑上的程序放到一台常开的服务器上,并给它一个好记的地址。
你需要三样东西:一台有公网 IP 的服务器或托管平台、一个域名、一张 HTTPS 证书。域名是网站的门牌号,DNS 解析是把“某个域名”指向“某台服务器 IP”的翻译系统。你买的域名需要去域名服务商的控制台加一条解析记录,类型选 A 记录,值填服务器 IP。解析生效可能需要几分钟到几小时,期间访问不到是正常的。
HTTPS 证书负责加密浏览器和服务器之间的通信。现在申请免费证书非常方便,找到自动签发证书的工具,申请后配置到服务器上即可。没有 HTTPS 的网站在现代浏览器里会被明确提示“不安全”,这对访客信任度影响很大。部署完成后,在浏览器地址栏看到小锁图标才算真正合格。
5.2 静态网站与 Node 服务的两种部署路径
部署方式取决于你的技术栈。
纯静态网站最简单:把包含 index.html、CSS、JS 的文件夹上传到对象存储或静态托管平台,关掉目录列表功能,绑定域名,完成。这种方式几百行代码的页面都能免费跑,性价比极高。它背后有对象存储和 CDN 加速,访问速度通常比自己买服务器还快。
带 Node 后端的项目就要稍复杂一点。常见做法是买一台云服务器,装上 Node 环境,把你的项目通过 Git 拉到服务器上,执行 npm install 和启动命令,再用一个进程管理工具保证程序持续运行,防止窗口一关服务就断掉。还要在前面挂一个 Nginx 做反向代理,把 80 和 443 端口的请求转发到你程序实际监听的端口。这里有个容易理解错的概念:Node 程序默认监听 3000 端口,但用户访问网站用的是 80 或 443 端口,浏览器不会自动把两者对应起来。所以要靠 Nginx 把外网请求“反向代理”给 3000 端口。
一个简化的 Nginx 配置片段可以长这样:
server { listen 80; server_name example.com; location / { proxy_pass http://127.0.0.1:3000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }配置里的 server_name 替换成你自己的域名,proxy_pass 指向 Node 服务。改完配置后用命令重载 Nginx,再向证书机构申请并配置 HTTPS 证书,网站就正式可访问了。
5.3 上线后的基础监控与数据备份意识
网站上线不是终点,只是开始。我建议至少做两件最基本的维护动作:看日志和备份数据。
Node 进程的日志里会记录请求报错,进程管理工具会把日志输出到固定文件里。遇到用户反馈页面打不开,先去看这些日志。另外,像本站 messages.log 这样的用户数据,要定期下载或同步到本地,避免服务器故障时数据全丢。我见过有人做了半年博客没备份,服务器商一次磁盘故障,所有文章没了,那真是叫天天不应。
没有预算的新手项目,备份可以简单粗暴:把数据目录打包,定时传到另一个存储位置。哪怕每周一次也好过从不备份。这个习惯越早养成越好。
6. 新手最容易踩的坑和我的解决习惯
6.1 五个我在实战中反复见过的翻车现场
第一个坑是编码问题。HTML 文件没有保存为 UTF-8 编码,或者缺少字符集声明,页面上出现乱码。解决习惯是:编辑器右下角留意编码格式,所有新文件默认 UTF-8。
第二个坑是路径问题。用了相对路径 /style.css 还是 style.css,在不同层级的目录下效果不一样。我建议在本地项目里统一用相对路径,部署后配合域名结构再检查一遍资源引用。检查方法很简单:F12 打开 Network 面板,看到哪个资源 404 就改哪个路径。
第三个坑是端口冲突。本地启动服务时提示端口被占用,换一个端口号即可。但要注意,前端 fetch 里请求的地址也要跟着改。如果你把端口硬编码写在代码里,以后部署会非常痛苦。尽量用相对路径,比如直接写 /api/contact,而不是 http://localhost:3000/api/contact。
第四个坑是服务器时区问题。你用本地时间测试好好的,部署到服务器后发现日志时间差了 8 个小时。这不是代码逻辑错了,而是服务器的系统时区没有设置为本地时区。在服务器控制面板里把时区改一下即可。
第五个坑是图片未压缩。新手喜欢直接丢原图,一个页面加载几张几兆字节的图片,首屏速度惨不忍睹。上线前一定要压缩图片,用图片压缩工具把每张图的体积降到 500KB 以下,能切到现代图片格式更好。加载速度是用户体验的及格线,不能等到用户抱怨才处理。
6.2 我每次发布前都会过的检查清单
发布前过一遍这几个问题,能避免大多数低级错误:第一,所有链接是否可用,导航里的锚点是否能跳到对应区块;第二,表单提交后是否有成功和失败两种反馈;第三,手机尺寸下布局有没有横向滚动;第四,资源文件是否都走了 HTTPS;第五,有没有留下调试用的 console.log 和测试数据。
这套清单看起来琐碎,但每一次都能拦住实际事故。我自己的习惯是发布前把网站当新用户完整走一遍:从搜索结果进入首页,点开每个菜单,填一次表单,检查手机上显示效果。当你做过几次完整的发布流程后,这些检查动作会变成肌肉记忆,做网站的速度会明显提升。
我到现在依然记得第一次部署时,对着 nginx 配置文件反复改却总是 502 的那种焦虑。后来明白了,所谓“一般方法”其实没有标准答案,它是在一次次失败中沉淀出来的判断力:知道先解决问题的主干,再处理项目的枝节。这大概就是做网站最值得积累的东西。