Skip to content

EJS 模板引擎与Express 项目生成器 express(Day11 3/3) ​

本页对应 Day11_Node,共 6 段视频,约 1 小时 6 分。

学习目标 ​

  1. 能使用 express-generator 创建 Express 项目目录结构,并通过安装依赖和启动项目验证初始化成功。
  2. 能设计并实现记账本所需路由,包括首页重定向、账单列表、添加表单、执行添加和按 ID 删除,并正确配置路由挂载。
  3. 能配置 EJS 模板与 public 静态资源目录,将账单模板放入 views/account,将 CSS/JS 放入静态资源目录并修正页面引用路径。
  4. 能完成添加账单功能:为表单控件设置 name、配置 method 和 action,获取请求体,使用 lowdb 持久化数据,并使用 shortid 生成唯一 ID。
  5. 能完成账单列表展示:从 lowdb 读取数据并传入模板,使用 EJS 遍历账单,通过条件判断渲染收入/支出样式及金额信息。
  6. 能完成删除指定账单功能:在列表模板中拼接删除链接,使用 req.params 获取 ID,从 lowdb 删除对应记录并渲染操作成功页面。

知识点 ​

1. 执行语句:<% code %> ​

为什么需要:模板里不只要输出变量,还要写循环、条件等逻辑。账单列表要把数组逐条生成表格行,就必须用 EJS 的执行语句。

怎么想:EJS 中 <% %> 只执行 JS,不直接输出内容;真正输出用 <%= %>。循环时把“开始标签”和“结束标签”拆开,中间放 HTML。

怎么做:笔记中的写法是:

ejs
<% top.forEach(item => { %>
    <tr>
        <td><%= item.id %></td>
        <td><%= item.name %></td>
        <td><%= item.money %> 亿美元</td>
    </tr>
<% }) %>

易错点:forEach 的开始和结束必须成对,<% }) %> 不能漏;循环变量 item 只在循环体内有效;不要把 forEach 写成 <%= top.forEach... %>,它不负责输出。

2. 转义输出:<%= code %> ​

为什么需要:大多数数据来自用户或数据库,直接当 HTML 插入可能破坏页面结构,甚至带来 XSS 风险。默认转义更安全。

怎么想:普通数据显示用 <%= %>。它会把值转成字符串输出,如果值里有 HTML 标签,会变成字符实体,原样显示。

怎么做:

ejs
<%= code %>
js
<p class="alert alert-warning">
    <%= Date.now() %> <br>
    <%= Math.random() %> <br>
    <%= 10 * 7 + 8 %> <br>
</p>

易错点:<%= %> 里放的是表达式,不是语句,不要写 const、if 等;表达式结果里有标签时不会生成真实标签,而是显示文本。

3. 非转义输出:<%- code %> ​

为什么需要:有时确实要插入一段可信 HTML 片段,例如后端拼好的结构。此时需要关闭转义。

怎么想:只有确认内容安全才用 <%- %>;用户输入不要轻易非转义输出。

怎么做:

ejs
<%- code %>

笔记结论要记住:

text
<%= code %> 如果 code 的值中有html标签,会被转义成字符实体,原样显示
<%- code %> 如果 code 的值中有html标签,浏览器会解析处理

易错点:把 <%= %> 和 <%- %> 混用;该转义的数据用了非转义,导致标签被浏览器解析。

4. 在 Express 项目中落地 EJS ​

为什么需要:记账本项目要展示账单列表、添加表单、成功页,这些都需要模板渲染;CSS、JS 又不能走模板渲染,要作为静态资源托管。

怎么想:模板放 views,静态资源放 public。路由里用 res.render 渲染模板并传数据;模板路径省略 views 和 .ejs 扩展名。

怎么做:转写中生成器已配置好视图目录和 EJS,路由中可写 res.render('account/index', { data })。列表模板里对 data 循环,条件判断收入/支出;删除链接把 item.id 拼到 /account/delete/:id。静态资源从 ./ 改成 /,因为 public 已是静态资源目录。转写里 lowdb 删除要 remove({ id }).write(),shortid 用来生成唯一 ID。

易错点:转写里的“EGS”实际是 EJS,“will死”是 views,“plic”是 public,“lowDB”是 lowdb,“色塔ID”是 shortid。变量名拼错会报 is not defined;表单控件漏写 name,req.body 里就没有该字段,转写中 title 漏 name 就是调试出来的问题;静态资源用相对路径会找不到,要放到 public 并改成 / 开头。

1. 为什么用 express-generator 生成项目骨架 ​

为什么需要: 如果从零手写 Express 项目,就要自己建 app.js、路由目录、模板目录、静态资源目录,还要配 package.json 和启动脚本,重复且容易漏。生成器能一次性铺好标准骨架。

怎么想: 先用工具生成结构,再在生成好的结构里写业务。记账本项目后续要加 routes/account.js、views/account 模板、public 静态资源,这些位置生成器已经预留好,所以先搭骨架最稳。

怎么做: 进入准备放项目的父目录,在集成终端里执行生成命令。转写稿里强调:命令名是 express,不是 express-generator;参数必须指定模板引擎为 ejs。生成后能看到目录结构,并且 package.json 已经由生成器创建。

易错点: 转写稿里的“VVREW 等于 EGS”应纠正为 --view=ejs,不要把 ejs 写成 egs;生成器只生成骨架,不等于依赖已经装好,后面还要安装依赖。

2. 全局安装 express-generator ​

shell
npm install -g express-generator

为什么需要: 只有全局安装后,终端里才能直接使用 express 这个生成命令。

怎么想: 生成器是开发工具,不是项目运行依赖,所以用 -g 全局安装;装一次即可。

怎么做: 在终端执行上面的命令。转写稿中“expressGGENERY”就是 express-generator 的口语化说法,实际包名要写完整。

易错点: 安装的是 express-generator,但运行命令是 express;如果权限报错,先检查 Node/npm 环境,不要盲目改项目代码。

3. 生成目录结构并指定模板引擎为 ejs ​

shell
express --view=ejs

为什么需要: 本项目要用 EJS 渲染账单列表和添加表单,所以生成时就要把模板引擎定为 ejs,否则后续视图文件扩展名和渲染配置会对不上。

怎么想: 先生成项目,再进入项目目录安装依赖。转写稿里生成后“全在这儿了”,说明目录结构已经出来;此时 package.json 也已存在,因此不需要再执行 npm init。

怎么做: 在目标父目录执行 express --view=ejs。生成后检查是否出现 app.js、routes、views、public 等结构;后续记账本的路由会放进 routes,模板会放进 views/account,CSS/JS 会放进 public。

易错点: 参数写成 --view ejs 或 --view=egs 都不符合本节笔记;生成后不要以为项目已经能跑,依赖还没安装。

4. 安装依赖 ​

shell
npm install

为什么需要: 生成器只创建 package.json 和目录骨架,项目依赖还没有下载到 node_modules,不安装就无法启动。

怎么想: 转写稿里一开始说“并初始化”,随后纠正为“安装依赖”,因为 package.json 已由生成器创建。这里要做的是 npm install,不是 npm init。

怎么做: 进入生成出来的项目根目录,执行 npm install。安装完成后,package.json 中声明的依赖才会真正可用。

易错点: 不要在父目录安装;不要在没进入项目根目录时执行 npm start;转写稿里的“NPMinstore”“ISTL”应纠正为 npm install。

5. 启动项目 ​

shell
npm start

注意不要直接运行入口文件!

为什么需要: 生成器已经把启动脚本配置在 package.json 里,用 npm start 才会按项目约定启动服务。

怎么想: 转写稿中启动后端口是 3000,说明启动脚本负责监听端口。若直接运行入口文件,可能绕过启动脚本,导致端口、启动参数不对。

怎么做: 在项目根目录执行 npm start,然后访问 http://localhost:3000。如果页面暂时没反应,先检查路由是否写了响应,而不是先怀疑生成器;转写稿里就出现过“路由设计了但没做处理,所以页面没反应”的调试过程。

易错点: 不要直接 node app.js 之类运行入口文件;启动前必须完成 npm install;端口被占用时先换端口或结束占用进程。

6. 生成器产物与后续记账本结构的关系 ​

为什么需要: 生成器不是只为了“生成一次”,而是给后续业务提供固定位置,避免路由、模板、静态资源乱放。

怎么想: 转写稿后续把账单路由挂到 /account,在 routes 里新增 account.js;模板放到 views/account,如 index.ejs、create.ejs、success.ejs;页面用到的 CSS/JS 放到 public,因为 public 是静态资源目录,URL 能直接匹配到文件。

怎么做: 生成项目后,先按本节完成安装和启动;再在 routes 中写路由,在 views 中写 EJS,在 public 中放静态资源。转写稿里“把点杠改成斜杠”的调试,就是为了让模板里的 CSS/JS 走 public 静态资源路径。

易错点: 模板要经过后端渲染,静态资源不需要后端处理,两者不要混放;表单控件忘记写 name,请求体里就没有对应数据,这是后续添加账单时踩过的坑。

本节操作清单 ​

text
第一步: 使用 express-generator 创建目录结构并安装依赖
	   express --view=ejs
	   npm install

第二步: 设计路由
      GET /  				    重定向到 /account
      GET /account			    展示账单列表
      GET /account/create       添加表单页面
      POST /account/create  	执行添加
      GET  /account/delete/:id  执行删除	
   
第三步: 模板和静态资源设置
	  1. 将账单相关模板文件放入 views 下的 account 目录中
	  2. 将模板所需要的css、js 放入静态资源目录 public
	  3. 路由回调函数渲染对应的模板
	  
第四步: 添加账单记录
      1. get  /account/create 
         给表单控件设置 name, 给 form 设置 method 和 action
      2. post /account/create  
         取出请求体
         使用 shortid 创建唯一 id
         将id和请求体里的数据添加到 lowdb 中(提取进行手动初始化)
         渲染 success 模板
         
第五步: 账单列表
       get  /account
       1. 从lowdb中取出所有账单
       2. 渲染模板,向模板发送数据
       3. 在模板中展示数据
          遍历
          三元运算符
          双向分支
          
 第六步: 删除指定的账单
       1. 在列表页模板中设置超级链接,将id拼接到路径中
       2. get /account/delete/:id
          获取id
          根据id从lowdb中删除对应的记录
          渲染 success 模板

一、创建目录结构并安装依赖 ​

为什么需要: 手写 Express 目录容易漏文件、漏依赖;用生成器先得到可运行骨架,后面只改业务。

怎么想: 先保证项目能启动,再设计路由和页面。模板引擎选 ejs,模板文件用 .ejs。

怎么做:

bash
express --view=ejs
npm install

易错点: 命令名是 express,不是 express-generator;--view=ejs 不要漏。生成器已生成 package.json,转写稿里说的“初始化”实际重点是安装依赖。

二、设计路由 ​

为什么需要: 先把 URL 和请求方式定下来,页面、表单、删除链接才有稳定接口。

怎么想: / 重定向到 /account;列表用 GET /account;表单页用 GET /account/create;提交用 POST /account/create;删除用 GET /account/delete/:id。

怎么做: 在 app.js 挂载 /account,在 routes/account.js 里只写后半段:

js
// app.js
const accountRouter = require('./routes/account');
app.use('/account', accountRouter);

// routes/account.js
router.get('/', ...);              // 列表
router.get('/create', ...);        // 表单
router.post('/create', ...);       // 执行添加
router.get('/delete/:id', ...);    // 执行删除

易错点: 挂载后路由里不要再写 /account;变量名拼错会报 accountRouter is not defined,转写稿中就出现过变量名多写字母的调试过程。

三、模板和静态资源设置 ​

为什么需要: 模板要经过后端渲染数据;CSS、JS 是静态资源,不需要后端处理。

怎么想: 账单模板放 views/account,如 index.ejs、create.ejs、success.ejs;CSS/JS 放 public;模板中引用静态资源用 /css/...、/js/...。

怎么做:

js
res.render('account/index');
res.render('account/create');

易错点: 模板里写 ./css/... 在 /account/create 下会按当前路径找,容易 404;应改成 /css/...。转写稿中就是把 ./ 统一替换成 / 后样式才恢复。

四、添加账单记录 ​

为什么需要: 表单数据要进入 lowdb,并且每条记录要有唯一 id,后面删除才能定位。

怎么想: GET /account/create 只展示表单;POST /account/create 取 req.body,用 shortid 生成 id,再写入 lowdb。

怎么做: 表单设置 method="post"、action="/account/create",控件设置 name,如 time、type、account、title。支出可提交 -1,收入提交 1。lowdb 先手动初始化 dbs/db.json:

json
{ "accounts": [] }
js
const shortid = require('shortid');
const lowdb = require('lowdb');
const FileSync = require('lowdb/adapters/FileSync');
const path = require('path');

const adapter = new FileSync(path.resolve(__dirname, '../dbs/db.json'));
const db = lowdb(adapter);

const id = shortid.generate();
db.get('accounts').push({ id, ...req.body }).write();
res.render('account/success', { title: '账单添加成功', url: '/account' });

易错点: 表单控件漏写 name,req.body 里就没有该字段;转写稿中漏了“事项”的 name,导致列表缺 title,只能删掉重加。lowdb 添加、删除、修改后都要 .write()。

五、账单列表 ​

为什么需要: 把 lowdb 里的数组渲染成页面,并区分收入、支出样式。

怎么想: 先 db.get('accounts').value() 取数组,再 res.render 传对象 { data };模板里用 forEach 遍历,用三元运算符切类名,用 if/else 显示收入或支出。

怎么做:

js
const data = db.get('accounts').value();
res.render('account/index', { data });
ejs
<% data.forEach(function(item){ %>
  <div class="panel <%= item.type > 0 ? 'success' : 'danger' %>">
    <h3><%= item.title %></h3>
    <% if (item.type > 0) { %>
      <span>收入</span>
    <% } else { %>
      <span>支出 <%= item.account %></span>
    <% } %>
  </div>
<% }) %>

易错点: 不能把纯数组直接当 data 传,要传对象;模板里属于 JS 环境,类名字符串要加引号;历史脏数据要删掉重加或手动补字段。

六、删除指定账单 ​

为什么需要: 列表要能删除单条记录。

怎么想: 列表页超链接把 id 拼到路径;路由用 req.params.id 取 id;lowdb 用 remove({ id }).write() 删除;最后渲染成功页。

怎么做:

ejs
<a href="/account/delete/<%= item.id %>"><span>×</span></a>
js
const id = req.params.id;
db.get('accounts').remove({ id }).write();
res.render('account/success', { title: '删除成功', url: '/account' });

易错点: :id 和 req.params.id 名称要一致;删除也是写操作,必须 .write();成功页标题和跳转地址从路由传,不要写死。

课堂画面 ​

课堂画面 1

课堂画面 2

课堂画面 3

课堂画面 4

课堂画面 5

课堂画面 6

代码示例 ​

1. EJS 三种标签:views/demo.ejs(配合下面的路由渲染) ​

ejs
<!-- ① 执行语句:<% code %> -->
<table class="table">
    <thead>
        <tr>
            <th>ID</th>
            <th>姓名</th>
            <th>资产</th>
        </tr>
    </thead>
    <tbody>
        <% top.forEach(item => { %>
            <tr>
                <td><%= item.id %></td>
                <td><%= item.name %></td>
                <td><%= item.money %> 亿美元</td>
            </tr>
        <% }) %>
    </tbody>
</table>

<!-- ② 输出转义的数据:<%= code %> -->
<p class="alert alert-warning">
    <%= Date.now() %> <br>
    <%= Math.random() %> <br>
    <%= 10 * 7 + 8 %> <br>
</p>

<!-- ③ 输出非转义的数据:<%- code %> -->
<div><%= code %></div>
<div><%- code %></div>
js
// 对应的路由:把 top 和 code 交给模板
router.get('/demo', function (req, res, next) {
    res.render('demo', {
        top: [
            { id: 1, name: '雪莉', money: 100 },
            { id: 2, name: '小乐', money: 200 }
        ],
        code: '<b>我是 HTML 标签</b>'
    });
});

关键行与易错点

  • <% code %> 只执行 JS,不输出任何内容,所以 forEach 必须拆成「开始标签 / 循环体 / 结束标签」三段写。
  • <%= code %> 是转义输出:值里有 HTML 标签会被转成字符实体、原样显示;<%- code %> 是非转义输出,浏览器会解析处理。本例中同一份 code,<%= %> 显示成字面量 <b>我是 HTML 标签</b>,<%- %> 才显示粗体。
  • <%= 10 * 7 + 8 %> 说明尖括号里可以直接写表达式(Date.now()、Math.random() 同理),不用提前在路由里算好。

2. 记账本路由:dbs/db.json + routes/account.js + 挂载 ​

json
// dbs/db.json —— 手动初始化,只做一次
{
  "accounts": []
}
js
// routes/account.js
var express = require('express');
var router = express.Router();

// ---------- lowDB 相关 ----------
const low = require('lowdb');
const FileSync = require('lowdb/adapters/FileSync');
const path = require('path');

const adapter = new FileSync(path.resolve(__dirname, '../dbs/db.json'));
const db = low(adapter);

// ---------- 能够生成唯一 ID 的模块 ----------
const shortid = require('shortid');

/* GET /account —— 展示账单列表 */
router.get('/', function (req, res, next) {
    // 1. 从 lowdb 中取出所有账单
    const data = db.get('accounts').value();
    // 2. 渲染模板,向模板发送数据(传对象,属性名 data)
    res.render('account/index', { data: data });
});

/* GET /account/create —— 添加表单页面 */
router.get('/create', function (req, res, next) {
    res.render('account/create');
});

/* POST /account/create —— 执行添加 */
router.post('/create', function (req, res, next) {
    // 1. 创建唯一 ID
    const id = shortid.generate();
    // 2. 将 id 和请求体里的数据添加到 lowdb 中
    db.get('accounts').unshift({ id, ...req.body }).write();
    // 3. 渲染 success 模板
    res.render('account/success', { title: '账单添加成功', url: '/account' });
});

/* GET /account/delete/:id —— 执行删除 */
router.get('/delete/:id', function (req, res, next) {
    // 1. 获取 id
    const id = req.params.id;
    // 2. 根据 id 从 lowdb 中删除对应的记录
    db.get('accounts').remove({ id: id }).write();
    // 3. 渲染 success 模板
    res.render('account/success', { title: '删除成功', url: '/account' });
});

module.exports = router;
js
// app.js 中:导入 + 挂载(注意变量名别多写/少写字母)
var indexRouter = require('./routes/index');
var accountRouter = require('./routes/account');   // ← 变量名写错会报 is not defined

app.use('/', indexRouter);
app.use('/account', accountRouter);                // 挂载后,路由里只写后半段

// routes/index.js 中:首页重定向
router.get('/', function (req, res, next) {
    res.redirect('/account');
});

关键行与易错点

  • path.resolve(__dirname, '../dbs/db.json') 用来拼绝对路径,__dirname 是当前文件所在目录,路径写错就找不到 json 文件。
  • db.get('accounts') 里的 accounts 必须和 db.json 里手动初始化的键名完全一致,否则拿到的是 undefined,forEach 会直接报错。
  • unshift / remove 之后必须 .write() 才会真正写入文件;只读的 .value() 不需要 write。
  • remove({ id: id }) 可简写为 remove({ id }),条件对象的键名要和数据里的键名一致。
  • { id, ...req.body } 是简写:id 是上面 shortid.generate() 生成的变量;req.body 能直接拿到值,是因为 express 已经内置处理好了请求体,不用再额外装 body-parser。
  • 路由设计共 5 个:GET /(重定向)、GET /account、GET /account/create、POST /account/create、GET /account/delete/:id——同路径不同 method 不算重复。

3. 模板:views/account/index.ejs、create.ejs、success.ejs ​

ejs
<!-- views/account/index.ejs —— 列表页:遍历 + 三元运算符 + 双向分支 -->
<div class="container">
    <h2>记账本 <a class="btn btn-primary" href="/account/create">添加记录</a></h2>

    <% data.forEach(item => { %>
        <!-- 三元运算符:由 type 决定面板颜色 -->
        <div class="panel <%= item.type > 0 ? 'panel-success' : 'panel-danger' %>">
            <div class="panel-heading">
                <%= item.title %>

                <!-- 双向分支:收入 / 支出 -->
                <% if (item.type > 0) { %>
                    <span class="label label-success">收入</span>
                <% } else { %>
                    <span class="label label-danger">支出 <%= item.account %></span>
                <% } %>

                <!-- 删除超链接:把 id 拼到路径中 -->
                <a href="/account/delete/<%= item.id %>"><span>&times;</span></a>
            </div>
            <div class="panel-body">
                <%= item.remarks %>
            </div>
        </div>
    <% }) %>
</div>
ejs
<!-- views/account/create.ejs —— 添加表单 -->
<form method="post" action="/account/create">
    <div class="form-group">
        <label>事项</label>
        <input type="text" name="title" class="form-control">
    </div>
    <div class="form-group">
        <label>发生时间</label>
        <input type="date" name="time" class="form-control">
    </div>
    <div class="form-group">
        <label>类型</label>
        <select name="type" class="form-control">
            <option value="-1">支出</option>
            <option value="1">收入</option>
        </select>
    </div>
    <div class="form-group">
        <label>金额</label>
        <input type="number" name="account" class="form-control">
    </div>
    <div class="form-group">
        <label>备注</label>
        <textarea name="remarks" class="form-control"></textarea>
    </div>
    <button type="submit" class="btn btn-primary">添加</button>
</form>
ejs
<!-- views/account/success.ejs —— 添加/删除共用 -->
<h2><%= title %></h2>
<p><a href="<%= url %>">点击跳转</a></p>

关键行与易错点

  • 模板里的 data 就是路由 res.render('account/index', { data: data }) 中对象的属性名;res.render 第二个参数必须是对象,直接把数组传进去,模板里拿不到 data。
  • 三元运算符里 'panel-success' 必须加引号——尖括号内是 JS 作用域,不加引号会被当成变量名而报错。
  • item.type > 0 判断收入:select 里 value="1" 是收入、value="-1" 是支出,提交上来的是数字,所以用 > 0 判断。
  • 每个表单控件都必须写 name,不写 name 的控件数据不会进 req.body(视频里漏写「事项」的 name,结果那条记录就少了 title 字段)。
  • form 的 method="post"、action="/account/create" 要和路由的 router.post('/create') 对应。
  • 静态资源路径要用 /css/...、/js/...(以 / 开头),不能用 ./;public 目录已被 express-generator 托管为静态资源目录,模板文件放 views/account/,CSS/JS 放 public/,两者不能混放。

常见错误 ​

1. 启动即报 xxx is not defined,路由整段失效

  • 现象:npm start 后终端抛 ReferenceError: accountRou is not defined,页面全 404。
  • 原因:require('./routes/account') 接的变量名与 app.use('/account', accountRouter) 里用的名字不一致(少写一个字母),或改完 require 忘了改挂载处。
  • 修法:require 与 app.use 两处变量名逐字对照;命名统一成 accountRouter 这类不易手滑的写法,改完重启进程再验证。

2. 页面能打开但样式、图标、JS 全部丢失

  • 现象:列表页文案正常,panel、按钮、图标样式全无,Network 里 GET /account/css/style.css 404。
  • 原因:模板里沿用了 ./css/...、./js/... 这类相对路径,而模板是在 /account、/account/create 下渲染的,浏览器把相对路径解析成 /account/css/...;同时静态文件根本没放进 public。
  • 修法:CSS/JS 复制进 public 对应子目录,模板中统一改成以 / 开头的站内绝对路径(/css/...),用编辑器的全局替换一次改完。

3. 提交成功,但记录里少了某个字段

  • 现象:列表页某一行「事项」列空白,db.json 里对应对象没有 title 键,其余 time、type、account、remarks 都在。
  • 原因:那个 <input> 没写 name,浏览器不会提交无名字段,req.body 里自然没有它;后端解构时也不会报错,属于静默丢数据。
  • 修法:给每个表单控件补 name,并与后端解构出的字段名一一对应;提交后先在路由里 console.log(req.body) 核对四个字段齐全再入库。

4. 点「添加」后 404,或页面只是原地刷新

  • 现象:点提交按钮跳到 /account/create?time=...,或者提示 Cannot POST /create。
  • 原因:<form> 没设 method="post"(默认 GET),或 action 写成相对路径/漏了 /account 前缀;也有把 /account 在挂载处和路由文件里各写一遍,变成 /account/account/create 的。
  • 修法:method="post" action="/account/create";记住挂载点已在 app.use('/account', ...) 里给过,路由文件内部只写 /、/create、/delete/:id。

5. 接口返回正常,刷新后数据却没了 / 删除不生效

  • 现象:添加后页面跳到成功页,但列表页依旧空白;删除后刷新,那条记录还在。
  • 原因:lowdb 的 push、remove 只改了内存里的对象,链尾漏了 .write();另一类是先 db.get('accounts') 拿到的是 undefined,因为 db.json 没手动初始化成 {"accounts": []},或 path.resolve 拼的路径指错了目录。
  • 修法:手动建好 db/db.json 并写入空数组,路径用 path.resolve(__dirname, '../db/db.json');增删改一律以 .write() 收尾,只有纯读取才可省略。

6. EJS 里写三元/分支时字符串没加引号,渲染直接 500

  • 现象:列表页报 success is not defined 或 Unexpected token,模板渲染中断。
  • 原因:<%= %> 内部是纯 JS 环境,panel-<%= item.type > 0 ? success : danger %> 里 success、danger 被当成变量;同理 forEach 的 <% }) %> 收尾括号对不上也会报语法错。
  • 修法:类名字符串加引号 'success' / 'danger';写循环时先把 <% xxx.forEach(item => { %> 和 <% }) %> 成对落好,再往中间填 HTML。

练习 ​

练习 1(排错) 启动项目后访问 /account,页面骨架正常但没有任何样式。请依次说明:你会打开哪几个地方核对、如何判断是路径问题还是文件没放对、最终改哪一处、为什么改完不需要动后端代码。

练习 2(契约) 现有添加表单漏了「事项」的 name。请写出:补哪个属性、值叫什么、后端解构时对应哪个键;并解释为什么这个错误在接口层不会报错、只能靠数据核对发现。

练习 3(模板渲染) 用一条 forEach 渲染账单列表,要求:收入行 panel-success、支出行 panel-danger;收入在金额后显示「收入」,支出显示「支出」。写出这段 EJS,并标出哪几处必须加引号、为什么。

练习 4(路由与参数) 为列表页每一行加上删除链接,并写出对应的删除路由:从路径取 id、按 id 删除、.write() 落盘、渲染成功页并传入 title 与 url。说明 req.params.id 和 req.query.id 在此场景下的区别。

练习 5(扩展设计) 参照现有 5 条路由,为「编辑账单」设计路由表(方法 + 路径 + 职责),并说明模板侧需要新增哪个文件、表单需要哪些隐藏字段、为什么编辑不能复用 POST /account/create。

原视频对照 ​

#视频时长
1记账本项目01-创建目录结构01:51
2记账本项目02-路由设计08:04
3记账本项目03-模板和静态资源设置11:41
4记账本项目04-添加账单记录25:09
5记账本项目05-展示账单列表14:58
6记账本项目06-删除指定账单05:09

小结 ​

  1. 先定路由表再写代码:方法、路径、职责三列写清楚,5 条路由确定后,实现只是填空,能避免边写边改导致的挂载点混乱。
  2. 挂载点决定模板内所有路径的写法:app.use('/account', ...) 一旦确定,路由文件内部只写后缀,静态资源一律用 / 开头的站内绝对路径。
  3. 模板和静态资源是两类东西:模板要「过一手」填数据,静态资源原样返回;把 CSS/JS 塞进 views 是最常见的错位。
  4. 表单是前后端唯一契约:name 决定字段名、method/action 决定去向,任何一个缺失都不会报错,只会静默丢数据,所以提交后先打印 req.body 核对。
  5. 数据层三件套缺一不可:手动初始化结构、增删改后 .write()、唯一 id 交给 shortid;前两步漏了表现为「不生效」,第三步漏了表现为「id 冲突」。
  6. EJS 的 <% %> 内部就是 JS 环境,写字符串必须加引号、写循环必须成对闭合;报错信息里的变量名往往就是漏引号的那个词。
  7. 排错顺序固定为:看终端报错定位文件和行 → 查变量名 → 查路径 → 查挂载点。这四类覆盖了本项目绝大多数故障。
  8. 每完成一步就跑一次:路由骨架跑通、模板渲染跑通、写库跑通、展示跑通、删除跑通,问题永远只出在最后改动的那一小块。