从零搭建 Node.js 学习官网:Express + SQLite + ESM 全流程实战
发布时间:2026/10/1 13:40:48来源:尧图网络
一句话就能生成一个 Node.js 学习官网并且直接发布上线——这个标题第一次看到的时候我第一反应是又是标题党。但仔细拆解下来这件事在当下确实是可以做到的而且整个链路并不复杂用 Express 搭一个内容站点用 SQLite 存学习资料和课程数据用 ESM 组织模块前端用原生 JavaScript 做交互最后部署到一台普通服务器上。真正花时间的不是写代码而是把学习官网这个模糊需求拆成可执行的结构。这篇内容适合两类人看一类是刚学完 Node.js 基础、想找个完整项目练手的开发者另一类是手里有一堆学习笔记、想快速做成一个可访问网站的技术博主。我会把从零到上线的完整过程拆开讲包括技术选型的理由、目录结构怎么设计、数据库表怎么建、页面怎么渲染、部署时容易踩的坑以及上线之后怎么继续维护。代码部分我会给出可直接运行的版本环境配置和命令也会写清楚尽量让你照着做就能跑起来。1. 为什么这个项目选 Express SQLite ESM 这套组合1.1 学习官网的本质是一个内容管理系统很多人一听到官网就想到企业站、CMS、后台管理觉得要上 React、要上数据库集群、要搞微服务。但 Node.js 学习官网的本质其实很简单它就是一个内容管理系统只不过内容全部围绕 Node.js 学习资料展开。核心功能无非是几个展示课程或文章列表、展示单篇详情、支持分类筛选、可能还有一个简单的搜索。这些功能用 Express 做路由、用 SQLite 存数据、用模板引擎或原生 JS 渲染页面完全够用。我见过太多人一上来就选重型框架结果光环境配置就耗掉两天最后项目还没跑起来就放弃了。选型的核心原则是让技术栈的复杂度匹配需求的复杂度。学习官网这种项目需求复杂度低、并发量低、数据量小选轻量方案反而更容易做完整、做上线。1.2 Express 在这个场景下的真实优势Express 是 Node.js 生态里最成熟的 Web 框架之一它的优势不在于功能多而在于中间件模型足够简单。一个请求进来经过一系列中间件处理最后返回响应这个心智模型非常直观。对于学习官网这种项目你需要的功能无非是静态文件服务、路由分发、请求体解析、模板渲染Express 全部内置或一行代码就能接入。对比一下其他选择Koa 更现代但生态相对小Fastify 性能更好但对新手不够友好NestJS 功能全但学习曲线陡。Express 的文档和社区案例是最多的遇到问题搜索一下基本都能找到答案。对于一个练手项目来说能快速解决问题比技术先进更重要。1.3 SQLite 为什么比 MySQL 更适合这个项目SQLite 是一个嵌入式数据库整个数据库就是一个文件不需要单独安装数据库服务不需要配置用户名密码不需要管理连接池。对于学习官网这种单机部署、读多写少的场景SQLite 的性能完全够用甚至在某些读场景下比 MySQL 还快因为它省去了网络通信开销。更重要的是部署体验。用 MySQL 的话你需要在服务器上装 MySQL、建库、建用户、配权限、改配置文件每一步都可能出问题。用 SQLite 的话你只需要确保数据库文件存在代码里指定路径就行。我实测下来一个几千条数据的学习官网SQLite 的查询响应基本在毫秒级完全感受不到差异。注意SQLite 不适合高并发写入场景。如果你的官网需要支持大量用户同时提交评论或点赞写入会锁库。但学习官网主要是读操作这个问题基本不存在。1.4 ESM 带来的模块组织方式变化ESM 是 ECMAScript 模块系统也就是import和export那套语法。Node.js 早期用的是 CommonJS也就是require和module.exports。现在 Node.js 对 ESM 的支持已经非常成熟新项目直接用 ESM 是更好的选择。ESM 的好处有几个语法更清晰静态分析更友好和前端 JavaScript 的模块语法统一。你在写前端代码的时候用import写后端也用import心智负担更小。不过要注意用 ESM 需要在package.json里加type: module或者把文件后缀改成.mjs。我建议直接加type: module这样所有.js文件都按 ESM 解析。2. 项目骨架搭建从空目录到可运行的最小系统2.1 环境准备与 Node.js 版本选择第一步是确认 Node.js 版本。ESM 的稳定支持从 Node.js 12 开始但建议用 18 LTS 或更高版本因为 18 之后 ESM 的各种边界情况处理得更完善。截至我写这篇内容的时候Node.js 22 已经是 LTS 版本可以直接用。检查版本node -v npm -v如果版本太低去 Node.js 官网下载对应系统的安装包。Windows 用户直接下载.msi安装macOS 用户可以用.pkg或者 HomebrewLinux 用户建议用 nvm 管理版本。安装完之后再跑一次node -v确认。提示如果你在服务器上部署CentOS 7.9 这类老系统自带的 Node.js 版本可能很低建议用 nvm 安装新版本不要用系统包管理器直接装。2.2 初始化项目与依赖安装新建一个目录初始化项目mkdir nodejs-learn-site cd nodejs-learn-site npm init -y然后修改package.json加上type: module{ name: nodejs-learn-site, version: 1.0.0, type: module, scripts: { start: node src/app.js, dev: node --watch src/app.js } }--watch是 Node.js 18.11 之后内置的热重载功能改代码后自动重启不需要装 nodemon。这个细节很多人不知道还在用 nodemon其实内置的已经够用了。安装依赖npm install express better-sqlite3 ejs这里解释一下三个依赖的选择expressWeb 框架负责路由和中间件。better-sqlite3SQLite 的 Node.js 驱动。相比sqlite3包它是同步 API代码写起来更直观性能也更好。对于学习官网这种低并发场景同步 API 完全不会成为瓶颈。ejs模板引擎用来渲染 HTML 页面。选 EJS 是因为它的语法接近原生 HTML学习成本低。2.3 目录结构设计与理由我建议的目录结构是这样的nodejs-learn-site/ ├── src/ │ ├── app.js # 应用入口 │ ├── db.js # 数据库连接与初始化 │ ├── routes/ │ │ ├── index.js # 首页路由 │ │ ├── courses.js # 课程路由 │ │ └── api.js # API 路由 │ └── views/ │ ├── layout.ejs # 布局模板 │ ├── index.ejs # 首页 │ └── course.ejs # 课程详情 ├── public/ │ ├── css/ │ │ └── style.css │ └── js/ │ └── main.js ├── data/ │ └── site.db # SQLite 数据库文件 └── package.json这个结构的设计逻辑是按职责分层而不是按文件类型分层。routes目录放路由views放模板public放静态资源data放数据文件。这样当项目变大时你知道该去哪里找对应的代码。2.4 数据库初始化与表结构设计在src/db.js里初始化数据库import Database from better-sqlite3; import { fileURLToPath } from url; import { dirname, join } from path; const __filename fileURLToPath(import.meta.url); const __dirname dirname(__filename); const db new Database(join(__dirname, ../data/site.db)); db.pragma(journal_mode WAL); db.exec( CREATE TABLE IF NOT EXISTS courses ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, slug TEXT UNIQUE NOT NULL, summary TEXT, content TEXT, category TEXT, level TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS categories ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT UNIQUE NOT NULL, slug TEXT UNIQUE NOT NULL ); ); export default db;这里有几个关键点journal_mode WAL是 SQLite 的写前日志模式能提升并发读性能建议开启。slug字段用来做 URL 友好路径比如/course/nodejs-basics比/course/1更利于分享和 SEO。created_at用CURRENT_TIMESTAMP自动填充省去手动维护时间字段。3. 路由与页面渲染让内容真正显示出来3.1 Express 应用入口的完整配置src/app.js是整个应用的入口import express from express; import { fileURLToPath } from url; import { dirname, join } from path; import indexRouter from ./routes/index.js; import coursesRouter from ./routes/courses.js; import apiRouter from ./routes/api.js; const __filename fileURLToPath(import.meta.url); const __dirname dirname(__filename); const app express(); const PORT process.env.PORT || 3000; app.set(view engine, ejs); app.set(views, join(__dirname, views)); app.use(express.static(join(__dirname, ../public))); app.use(express.json()); app.use(express.urlencoded({ extended: true })); app.use(/, indexRouter); app.use(/courses, coursesRouter); app.use(/api, apiRouter); app.use((req, res) { res.status(404).render(404, { title: 页面未找到 }); }); app.listen(PORT, () { console.log(服务已启动http://localhost:${PORT}); });这段代码里express.static负责静态文件服务express.json和express.urlencoded负责解析请求体三个路由分别处理首页、课程页和 API。最后加了一个 404 兜底中间件。3.2 首页路由与数据查询src/routes/index.jsimport { Router } from express; import db from ../db.js; const router Router(); router.get(/, (req, res) { const courses db.prepare( SELECT id, title, slug, summary, category, level FROM courses ORDER BY created_at DESC LIMIT 12 ).all(); const categories db.prepare(SELECT * FROM categories).all(); res.render(index, { title: Node.js 学习官网, courses, categories }); }); export default router;这里用better-sqlite3的prepare和all方法查询数据。prepare会预编译 SQL 语句重复执行时性能更好同时也能防止 SQL 注入。3.3 课程详情页与动态路由src/routes/courses.jsimport { Router } from express; import db from ../db.js; const router Router(); router.get(/:slug, (req, res) { const course db.prepare( SELECT * FROM courses WHERE slug ? ).get(req.params.slug); if (!course) { return res.status(404).render(404, { title: 课程未找到 }); } const related db.prepare( SELECT id, title, slug FROM courses WHERE category ? AND id ! ? LIMIT 5 ).all(course.category, course.id); res.render(course, { title: course.title, course, related }); }); export default router;动态路由/:slug会匹配/courses/nodejs-basics这样的路径req.params.slug拿到nodejs-basics然后去数据库查询对应课程。查不到就返回 404查到了就渲染详情页同时查出同分类的相关课程。3.4 EJS 模板的布局复用技巧EJS 本身没有布局继承但可以通过include实现类似效果。views/layout.ejs放公共部分!DOCTYPE html html langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 title% title %/title link relstylesheet href/css/style.css /head body header classsite-header a href/ classlogoNode.js 学习站/a nav a href/首页/a a href/courses课程/a /nav /header main classcontainer %- body % /main footer classsite-footer p持续更新 Node.js 学习资料/p /footer script src/js/main.js/script /body /html然后在index.ejs里这样用%- include(layout, { body: section classhero h1系统学习 Node.js/h1 p从基础到实战覆盖 Express、SQLite、ESM 等核心内容/p /section section classcourse-grid ${courses.map(c article classcourse-card h3a href/courses/${c.slug}${c.title}/a/h3 p${c.summary}/p span classtag${c.category}/span /article ).join()} /section }) %这种写法把页面内容作为字符串传给布局虽然不如专门的布局引擎优雅但胜在简单直接不需要额外依赖。4. 前端交互与 API 设计让官网不只是静态页面4.1 用原生 JavaScript 实现搜索与筛选学习官网如果只能翻列表体验会很差。加一个搜索框和分类筛选实用性会大幅提升。public/js/main.jsconst searchInput document.querySelector(#search-input); const categorySelect document.querySelector(#category-select); const courseGrid document.querySelector(#course-grid); let debounceTimer null; async function fetchCourses() { const keyword searchInput?.value.trim() || ; const category categorySelect?.value || ; const params new URLSearchParams(); if (keyword) params.set(q, keyword); if (category) params.set(category, category); const res await fetch(/api/courses?${params.toString()}); const data await res.json(); if (!courseGrid) return; if (data.courses.length 0) { courseGrid.innerHTML p classempty没有找到匹配的课程/p; return; } courseGrid.innerHTML data.courses.map(c article classcourse-card h3a href/courses/${c.slug}${c.title}/a/h3 p${c.summary || }/p span classtag${c.category || 未分类}/span /article ).join(); } function debounce(fn, delay 300) { return (...args) { clearTimeout(debounceTimer); debounceTimer setTimeout(() fn(...args), delay); }; } searchInput?.addEventListener(input, debounce(fetchCourses)); categorySelect?.addEventListener(change, fetchCourses);这里用了debounce防抖避免用户每输入一个字符就发一次请求。300 毫秒的延迟在输入体验和请求频率之间是个不错的平衡点。4.2 API 路由的实现与参数校验src/routes/api.jsimport { Router } from express; import db from ../db.js; const router Router(); router.get(/courses, (req, res) { const { q, category } req.query; let sql SELECT id, title, slug, summary, category, level FROM courses WHERE 11; const params []; if (q) { sql AND (title LIKE ? OR summary LIKE ?); params.push(%${q}%, %${q}%); } if (category) { sql AND category ?; params.push(category); } sql ORDER BY created_at DESC LIMIT 50; const courses db.prepare(sql).all(...params); res.json({ courses }); }); export default router;这个 API 支持关键词搜索和分类筛选返回 JSON 数据。前端拿到数据后动态渲染页面不需要刷新。4.3 移动端适配的几个关键细节学习官网的用户很可能在手机上看移动端适配不能忽略。CSS 里几个关键点* { box-sizing: border-box; } body { margin: 0; font-family: -apple-system, BlinkMacSystemFont, Segoe UI, sans-serif; line-height: 1.6; color: #333; } .container { max-width: 1100px; margin: 0 auto; padding: 0 16px; } .course-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 20px; } media (max-width: 600px) { .course-grid { grid-template-columns: 1fr; } }grid-template-columns: repeat(auto-fill, minmax(280px, 1fr))这行是关键它会根据容器宽度自动决定列数宽屏多列、窄屏单列不需要写一堆媒体查询。提示移动端记得加meta nameviewport contentwidthdevice-width, initial-scale1.0否则页面会按桌面宽度渲染手机上看起来会很小。5. 部署上线从本地到公网可访问5.1 服务器环境准备与 Node.js 安装部署到服务器第一步是装 Node.js。以常见的 Linux 服务器为例推荐用 nvmcurl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash source ~/.bashrc nvm install 22 nvm use 22装完之后node -v确认版本。然后安装 pm2 做进程管理npm install -g pm2pm2 的好处是进程挂了会自动重启服务器重启后也能自动拉起比直接node app.js靠谱得多。5.2 用 pm2 守护进程与开机自启把代码传到服务器后在项目目录执行npm install --production pm2 start src/app.js --name nodejs-learn-site pm2 save pm2 startuppm2 save保存当前进程列表pm2 startup生成开机自启脚本。执行pm2 startup后会输出一行命令复制执行即可。查看运行状态pm2 status pm2 logs nodejs-learn-site5.3 反向代理配置与静态资源缓存Node.js 应用直接监听 3000 端口对外访问不太合适一般用 Nginx 做反向代理。Nginx 配置server { listen 80; server_name your-domain.com; location / { proxy_pass http://127.0.0.1:3000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } location /css/ { alias /path/to/nodejs-learn-site/public/css/; expires 7d; } location /js/ { alias /path/to/nodejs-learn-site/public/js/; expires 7d; } }静态资源单独配置并设置缓存时间能减少 Node.js 进程的压力同时提升用户加载速度。5.4 数据库文件备份与迁移注意事项SQLite 的数据库就是一个文件备份非常简单直接复制data/site.db就行。但要注意复制之前先停掉应用或者执行 WAL 检查点否则可能复制到不完整的数据。pm2 stop nodejs-learn-site cp data/site.db data/site.db.backup pm2 start nodejs-learn-site迁移到新服务器时把整个项目目录打包传过去数据库文件一起带上npm install之后直接启动就行。这也是 SQLite 相比 MySQL 的一大优势——迁移成本极低。6. 上线之后内容维护与性能优化的实际经验6.1 内容录入的几种方式官网跑起来之后内容从哪来最直接的方式是写一个简单的管理接口或者直接用 SQLite 客户端工具手动插入。我推荐用 DB Browser for SQLite 这个开源工具图形化界面打开数据库文件就能编辑表数据比写 SQL 方便。如果要批量导入可以写一个脚本import db from ./src/db.js; const courses [ { title: Node.js 基础入门, slug: nodejs-basics, summary: 从零开始了解 Node.js 的运行机制与核心模块, content: ..., category: 基础, level: 入门 }, // 更多课程... ]; const insert db.prepare( INSERT OR IGNORE INTO courses (title, slug, summary, content, category, level) VALUES (title, slug, summary, content, category, level) ); const insertMany db.transaction((items) { for (const item of items) insert.run(item); }); insertMany(courses); console.log(已导入 ${courses.length} 条课程数据);用事务批量插入几千条数据几秒钟就能完成比逐条插入快几十倍。6.2 查询性能优化的几个实用手段SQLite 在数据量不大时性能很好但数据量上去之后还是要注意优化。几个实用手段加索引经常用来查询的字段加索引比如category、slug。CREATE INDEX IF NOT EXISTS idx_courses_category ON courses(category); CREATE INDEX IF NOT EXISTS idx_courses_slug ON courses(slug);避免SELECT *只查需要的字段减少数据传输量。分页查询列表页不要一次查全部用LIMIT和OFFSET分页。SELECT id, title, slug, summary FROM courses ORDER BY created_at DESC LIMIT 12 OFFSET 0;开启 WAL 模式前面提到的journal_mode WAL对读多写少的场景提升明显。6.3 常见部署问题排查清单部署过程中最容易遇到的问题我整理了一个排查清单问题现象可能原因排查方法启动报错找不到模块依赖没装或路径错误检查node_modules是否存在npm install重装页面 502Node 进程挂了pm2 status查看状态pm2 logs看错误日志数据库写入失败文件权限不足ls -l data/site.db检查权限chmod 664修正静态资源 404Nginx 路径配置错误检查alias路径是否指向实际目录端口被占用其他进程占用 3000lsof -i:3000查看换端口或杀掉进程提示ESM 模式下__dirname不存在必须用fileURLToPath(import.meta.url)转换。这是从 CommonJS 迁移到 ESM 时最容易踩的坑报错信息通常是__dirname is not defined。6.4 后续可扩展的方向这个项目跑起来之后可以继续扩展的方向不少。比如加一个 Markdown 渲染让课程内容支持 Markdown 格式加一个评论功能用 SQLite 存评论数据加一个 RSS 订阅方便读者订阅更新加一个简单的后台管理页面用表单录入课程。但我的建议是先把核心功能做扎实再考虑扩展。很多项目死在功能太多、每个都做不完整。学习官网的核心就是内容展示和检索把这两件事做好比加十个花哨功能更有价值。我在实际维护这个站的过程中发现真正影响体验的往往不是功能多少而是内容质量和加载速度。内容持续更新、页面打开够快用户就愿意留下来。技术选型上Express SQLite ESM 这套组合我已经用了好几个项目稳定性和开发效率都很满意推荐你也试试。
网站建设高端定制企业官网