Old-School Front-End / 2026 ・ No.15

古法前端入门

第15讲(最终讲) 收尾 ── 用工具检查,加固防守,发布上线,站到框架的门口

发布

把完成的报销系统的 4 个页面并排。左上是新井 太郎的申请列表(本月申请 5 笔・待审批 0 日元・被退回 1 笔),右上是申请表单,纽约出差的 3 行明细,合计 46,851 日元,左下是古井 一郎的审批页面,正在用对话框查看山田 花子的 0010 研讨会参加费 5,500 日元,右下是仪表盘,汇总的费用 287,581 日元・19 笔和按月的柱状图
花了 15 讲亲手做出来的报销系统。申请列表・申请表单・审批・仪表盘(笔者的演示页面,经由练习用服务器,在云端的 Chromium 中截图,是用 Playwright 实际操作的画面。2026年10月19日拍摄,页面时钟固定在 2026年10月17日 15:00。账号是仅限本演示的虚构账号。浏览器边框是为了说明而补画的)

不靠 AI,一行一行亲手写的“古法”前端入门。最后一讲是收尾。
把花了 15 讲做出来的报销系统用工具检查一遍,发现的问题当场修好。
接着加上万一有漏洞时的防守,整理发布的方法。
最后,把同一个明细表用 Vue 写一遍,从框架的门口往里看一看。

一

最后一讲的目标

新井前辈,所有页面都做好了!能申请、能审批、能汇总,还能打印。这就算完成了吧?

古井是“能跑了”。完成,是指能交给别人用的状态。检查、加固防守、送出去。

新井检查,是用眼睛一个个看吗?

古井眼睛会累,也会看漏。能交给工具的就交给工具。古法的工匠也是用尺子的。

当前位置:第15讲(最终讲)

1 准备 2 HTML 骨架 3 表单 4 CSS 基础 5 布局 6 Bootstrap 7 JS 基础 8 DOM 与事件 9 输入校验 10 通信 11 登录 12 审批流程 13 统计与打印 14 jQuery 15 收尾 当前位置 准备 HTML CSS JavaScript 实战 收尾
  • 检查 ── 用工具数出 HTML 的问题、读屏・操作的易用性、页面的重量。发现了就修
  • 加固防守 ── 不运行没写过的脚本(CSP),不让别的网站套框,发现借来的文件被调包(SRI)
  • 了解发布方法 ── 页面文件放哪里,API 放哪里。发布前要做的事
  • 站到框架的门口 ── 用 Vue 写第8讲的明细表,确认什么变轻松了,又多需要了什么
二

用工具检查

这个连载每一讲都用 Nu Html Checker(vnu)检查了 HTML。最后,把登录・申请列表・申请表单・审批・仪表盘这 5 个页面一起检查,再用 axe-core(4.14.0)检查读屏・操作的易用性。检查结果汇总在一个表格页面里。

“收尾检查表”页面。质量:HTML 检查 5 个页面错误都是 0,读屏・操作的易用性从修改前的 14 处(color-contrast)变为 0 处,浏览器错误从 1 个 404 变为 0 个,申请表单 9 个文件 342KB,用到的 Bootstrap CSS 为 11.4%。安全:从没有响应头变为 CSP・nosniff・Referrer-Policy,插入的 img 的 onerror 从被执行变为被拦下,别的网站的 iframe 从能打开变为被拒绝,日常操作中 CSP 违规 0 个。Vue:vue.html 的 HTML 检查 16 处错误,严格的 CSP 下什么都不显示,允许 'unsafe-eval' 后合计 46,851 日元
把用工具自动检查的结果汇总成的“收尾检查表”。数字全部是实测(笔者的演示页面,经由练习用服务器,在云端的 Chromium 中截图,是用 Playwright 实际操作的画面。2026年10月19日拍摄,页面时钟固定在 2026年10月17日 15:00。账号是仅限本演示的虚构账号。浏览器边框是为了说明而补画的)
# HTML 检查(Nu Html Checker。三种语言都一样)
页面 错误
login.html 0
index.html 0
form.html 0
approve.html 0
dashboard.html 0
vue.html 16   ← Vue 模板的属性(v-for・:key・@click 等)

HTML 5 个页面都没有错误。vue.html 的 16 处,是第八节要写的 Vue 页面的事,稍后再说。axe-core 那边,发现了看漏的地方。

# axe-core 指出的地方(color-contrast。修改前的值来自 axe-core 的记录,修改后用同一公式计算)
部件 文字颜色 背景 修改前 修改后
页头的菜单和名字 #9aafbc → #d2dce1 #1f4e6b 3.91:1 6.38:1
灰色按钮(返回・保存草稿・打印) #6c757d → #5c636a #f8f9fa 4.44:1 5.78:1

# 指出的数量(5 个页面合计)
修改前 14 处 → 修改后 0 处(三种语言都是)

修改的两处。眼睛看得清,数字却不够

页头的菜单 申请列表 3.91:1 #9aafbc 申请列表 6.38:1 #d2dce1 → 灰色按钮 申请列表 4.44:1 #6c757d 申请列表 5.78:1 #5c636a → 普通大小的文字要 4.5:1 以上(WCAG 2.2 的 AA)。光是“看得清”还不够 4.44 很可惜,但不合格。用数字确认,就不会犹豫

第4讲写过“文字和背景的对比度要 4.5:1 以上”,可页头的文字只有 3.91:1。因为直接用了 Bootstrap 深色导航栏的默认颜色(白色的 55%)。灰色按钮也是 4.44:1,差一点点。两处都各改写了两行 CSS 变量来修好。

/* 第15讲:让文字和背景的对比度达到 4.5:1 以上(按 axe-core 的指出修改) */
.brand-bar {
  --bs-navbar-color: rgba(255, 255, 255, .8); /* 页头的文字(原来是白色的 55%) */
  --bs-navbar-hover-color: #fff;
}
.btn-outline-secondary {
  --bs-btn-color: #5c636a; /* 灰色按钮(原来是 #6c757d) */
  --bs-btn-border-color: #5c636a;
}

修改后,三种语言的 5 个页面,指出的问题全部变成 0。另外,在走完日常操作的浏览器记录里,有 1 个 404。浏览器为了找标签页的图标去取 /favicon.ico,而它不存在。在所有页面的 <head> 里加上 <link rel="icon" href="img/logo.svg">,就变成了 0 个。

新井看了 14 讲的页面,居然还有问题啊。

古井人眼越熟悉的东西越看不见。所以用工具。不过,工具能发现的只是一部分。只用键盘能不能用到最后、读屏时意思通不通,要人来试(就像第8讲・第12讲做的那样)。

三

称一称重量

数了打开申请表单时加载的文件,再用 Chromium 的覆盖率,测量实际操作(检查・添加和删除行・手机宽度的菜单)时用到了多少。

# 打开申请表单时加载的文件(字节),以及操作时实际用到的比例(Chromium 的覆盖率。在日文版上测量)
文件 字节 gzip 用到的
form.html   7,686  2,516 
css/custom.css   2,885  1,485   30%
vendor/bootstrap/css/bootstrap.min.css 232,111 30,869   11%
vendor/bootstrap/js/bootstrap.bundle.min.js  80,496 23,743   22%
js/app.js   2,100  1,183   61%
js/auth.js   1,646  1,031   54%
js/validate.js   3,186  1,592   53%
js/form.js   9,423  3,927   70%
img/logo.svg     356    235 
合计(9 个文件) 339,889 66,581 
  • 总共约 342KB。其中 232KB 是 Bootstrap 的 CSS,用到的只有一成左右
  • 自己写的 JavaScript(app.js・form.js 等)用到了一半以上。没用到的,是只在出错或选择美元时才运行的部分
  • 用 gzip 发送的话,总共约 67KB。压缩通常由发布目的地的分发机制代劳(第七节)

要削减没用到的 CSS,可以用 Sass 只组装需要的 Bootstrap 部件,或者用工具去掉没用到的规则。这两种都是转换文件的构建工具的工作。这里正是“只靠手写往前走”和“组合工具”两条路的分岔口(第八节)。像报销这样的内部页面,342KB 也足够轻,所以这次没有削减。

四

防守①:拦下没写过的脚本

第8讲定下了规矩:别人写的文字用 textContent 放进去,不放进 innerHTML。第14讲看到了放进 jQuery .html() 的脚本真的会运行。堵住漏洞,靠的是代码的写法。可是,万一有一处漏了呢?这时的第二道防线就是 CSP(Content Security Policy)。

把“这个页面上可以运行什么”的约定,和页面一起交出去

服务器 Content-Security-Policy 浏览器 遵守约定的一方 js/form.js 自己网站的文件 <img onerror=…> 插入的内联脚本 eval / new Function 用字符串生成程序 别的网站的框架 被 iframe 打开 把“在这个页面上能运行的, 只有自己网站的文件” 这个约定和页面一起交出去 堵住漏洞靠第8讲的 textContent。CSP 是万一漏了时的第二道防线 内联的 script・onclick・style 属性也会被拦,所以要写在外部文件里

CSP 由服务器加在回复的响应头里。练习用服务器加了下面这个函数。

// 第15讲:防守用的响应头。页面和 API 的回复都加上(S.secure 设为 0 就不加。为了比较)
function secureHeaders(res, p) {
  if (!S.secure) return;
  const evalOk = S.cspEval && p === '/vue.html'; // 只对不构建的 Vue 页面,允许用字符串生成程序(为了比较)
  res.setHeader('Content-Security-Policy', [
    "default-src 'self'",
    "script-src 'self'" + (evalOk ? " 'unsafe-eval'" : ''),
    "style-src 'self'",
    "img-src 'self' data:",
    "connect-src 'self'",
    "object-src 'none'",
    "base-uri 'self'",
    "form-action 'self'",
    "frame-ancestors 'none'",
  ].join('; '));
  res.setHeader('X-Content-Type-Options', 'nosniff');
  res.setHeader('Referrer-Policy', 'same-origin');
}
  • script-src 'self' ── 脚本只能是这个网站的文件。直接写在 HTML 里的脚本、onclick="…"、用字符串生成程序的 eval 都不能运行
  • style-src 'self' ── 样式也只能是这个网站的 CSS 文件。写在 HTML 里的 style="…" 不生效(JavaScript 的 el.style.width = … 生效)
  • img-src 'self' data: ── Bootstrap 的 CSS 把复选框等的标记做成 data: 图片,所以允许它
  • frame-ancestors 'none'、nosniff ── 见第五节

这个连载从一开始就把脚本和样式分开写在文件里,所以加上响应头后没有要改写的地方。实际用三种语言走了一遍提交申请・审批・打开仪表盘,一次违规都没有。我们分别在没有和有响应头的情况下,试了插入。

# 确认防守效果(Chromium 实测。左边没有响应头,右边有)
试了什么 没有响应头 有响应头
用 innerHTML 插入的 img 的 onerror 被执行了 被拦下(script-src-attr)
在别的网站(:3001)的 iframe 里打开 能打开 被拒绝(frame-ancestors)
SRI:正确的 integrity 加载了 加载了
SRI:差一个字符的 integrity 报错(不加载) 报错(不加载)
日常操作(申请・审批・仪表盘)中的违规 – 0 个(三种语言)

用 innerHTML 插入的 <img onerror>,没有响应头时执行了,有时被拦下(违规类型是 script-src-attr,也就是“写在属性里的脚本”)。顺带一提,检查用的 axe-core 也是插入页面的脚本,同样被 CSP 拦下。只有在用工具时,才在浏览器的设置里关掉 CSP 来检查。就是这么管用。

五

防守②:不让套框・不让调包

不让套框。有一种手法:坏网站用透明的框(iframe)盖住审批页面,让人以为按的是“领取礼物”,实际按下了“批准”(点击劫持)。加上 frame-ancestors 'none',页面就不会在别人的框里打开。如上表,从别的网站(端口 3001)用框打开,没有响应头时能打开,有时被拒绝。

不让乱猜内容。X-Content-Type-Options: nosniff 会阻止浏览器把声明为“不是 JavaScript”的文件,自己看了内容就当作 JavaScript 运行。

发现调包。这个连载把 Bootstrap 和 jQuery 都放在自己的网站上,但从 CDN 加载的情况也很多。万一 CDN 上的文件被改写了呢?SRI(Subresource Integrity)就是把文件的“指纹”(哈希)写在 HTML 里,不一致就不运行的机制。

<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/jquery.min.js"
        integrity="sha384-fgGyf7Mo7DURSOMnOy7ed+dkq5Job205Gnzu6QIg0BOHKaqt4D76Dt8VlDCzcMHV"
        crossorigin="anonymous"></script>

这个值是从 npm 上 jQuery 4.0.0 的发布文件计算出来的。值正确就加载,只改一个字符就报错,不运行(见上表)。文件升级了版本,指纹也要重写。

六

防守③:页面守不住的东西

到这里为止的防守,都是请浏览器“遵守约定”。可是页面的 JavaScript 在用户手边运行,谁都能看,谁都能改。把这个连载反复说的“守护靠服务器”整理成表。

页面能做的事,和只有服务器能做的事

要守护的东西 页面 服务器 输入引导(第9讲) 文字用 textContent 显示(第8讲) CSP・SRI(本讲) 正式的输入检查(第9讲) 登录和权限(第11讲) 同时按下时(第12讲) 密码的保存・密钥 页面的 JavaScript 谁都能看,谁都能改 所以密钥和密码不要写在 JavaScript 里
  • 输入检查,页面上是引导,服务器上才是正式的(第9讲)。直接发 fetch 就能绕过页面的检查
  • 登录和权限(第11讲)、同时按下时的 409(第12讲),只有服务器能判断
  • 密码在服务器上不原样保存(做成哈希)。外部服务的密钥不写在页面的 JavaScript 里。写进去的那一刻,就等于分发给了所有人

新井我让 AI“把 API 密钥放进去,从页面上调用”,它就直接写进 JavaScript 了。

古井它是照你说的写的。放在哪里,该由提要求的人决定。

七

发布

手写页面的好处是,文件原样放上去就能运行。没有转换的步骤。你正在读的这个博客,也是把 HTML、CSS 和图片放在分发静态文件的服务(Cloudflare Pages)上发布的。

页面文件放到静态托管,API 放到服务器

手边的文件 *.html css/ ・ js/ vendor/ ・ img/ 静态托管 CDN・Pages 等 API 服务器 登录・数据 git push deploy 用户 HTML /api 发布之前 ・改成 https(登录用的 Cookie 加上 Secure) ・在托管的设置里加上防守用的响应头(CSP 等) ・文件改了,名字也改(不让用旧缓存) ・删掉练习用账号和 __ctl 这类后门
  • https ── 给登录的 Cookie(第11讲)加上 Secure,只通过加密的通信发送。练习用服务器是 http,所以没加
  • 防守用的响应头 ── 在静态托管服务上,用配置文件代替服务器代码来加。比如 Cloudflare Pages 可以写在名为 _headers 的文件里
  • 缓存 ── 浏览器和 CDN 会记住同名的文件。改了内容,就像 form.js?v=2 或 form.2.js 这样把名字也改了,就不会用到旧的
  • 删掉后门 ── 练习用的 /__ctl 和演示用的账号・密码,不要带到正式环境
八

框架的门口

最后,用 Vue 3(3.5.44)写一遍第8讲的“明细行的添加・删除和合计”。React、Vue 这样的框架,是如今工作中常用的工具。有什么不同呢?

从“怎样改页面”,到“数据和页面的关系”

原生 JavaScript(第8讲) 接收 input 事件 逐行读取表格 计算合计 改写页面上的文字 Vue(本讲) 数据 rows 变了 total(computed)重新计算 页面自动改写 (不碰页面) 过程式=写“怎样改页面”。声明式=写“数据和页面的关系” 框架里做的事也和第8讲一样。亲手写过,才想象得出里面

第8讲的合计是这样写的。自己逐行读取表格,计算,再自己改写页面上的文字。

/** 从页面读取明细,显示合计和提示(第10讲:美元换算后相加) */
function updateTotal() {
  const items = [];
  let unreadable = 0;
  let waiting = 0;
  for (const row of tbody.rows) {
    const text = row.querySelector('[name="amount"]').value;
    const usd = row.querySelector('[name="currency"]').value === 'USD';
    const amount = usd ? toCents(text) : toAmount(text);
    if (amount === null) continue;                          // 空栏不计
    if (Number.isNaN(amount)) { unreadable++; continue; }  // 无法读成金额
    if (!usd) items.push({ amount });
    else if (rate === null) waiting++;                     // 还没有汇率
    else items.push({ amount: centsToYen(amount, rate) }); // 美元换算成日元
  }
  const total = calcTotal(items);
  totalOutput.textContent = formatYen(total);
  receiptNote.hidden = !needsReceipt(total);

  const notes = [];
  if (unreadable > 0) notes.push(`有 ${unreadable} 行的金额无法读取(未计入合计)`);
  if (waiting > 0) notes.push(`因为没有汇率,有 ${waiting} 行美元未计入合计`);
  skipNote.textContent = notes.join(' / ');
  skipNote.hidden = notes.length === 0;
}

在 Vue 里,在 HTML 这边写上“把数据 rows 的每一项显示成表格的一行”“合计显示在这里”(模板)。

<div id="app" v-cloak>
    <div>
      <table>
        <thead><tr><th scope="col">类别</th><th scope="col">内容</th><th scope="col">金额</th><th scope="col">币种</th><th scope="col"><span>删除</span></th></tr></thead>
        <tbody>
          <tr v-for="(row, i) in rows" :key="row.id">
            <td><select v-model="row.category" :aria-label="`第 ${i + 1} 行的类别`">…</select></td>
            <td><input v-model="row.item" :aria-label="`第 ${i + 1} 行的内容`"></td>
            <td><input v-model="row.amount" inputmode="numeric" autocomplete="off" :aria-label="`第 ${i + 1} 行的金额`"></td>
            <td><select v-model="row.currency" :aria-label="`第 ${i + 1} 行的币种`"><option value="JPY">日元</option><option value="USD">美元</option></select></td>
            <td><button type="button" @click="remove(i)" :aria-label="`删除第 ${i + 1} 行`">删除</button></td>
          </tr>
        </tbody>
      </table>
    </div>
    <div>
      <button type="button" id="add-row" @click="add" :disabled="rows.length >= MAX_ROWS">+ 添加一行</button>
      <p>合计:<strong id="total">{{ formatYen(total) }}</strong></p>
    </div>
    <p v-if="needsReceipt(total)">合计在 3 万日元以上,需要附上收据。</p>
    <p id="rate-note">{{ rate === null ? '正在取得汇率…' : `美元按 1 美元 = ${rate} 日元换算` }}</p>
</div>

JavaScript 这边只有数据和计算。找页面元素的 querySelector、改写的 textContent 都不出现。删除一行,也只是从数组里去掉一项。

// 报销系统:明细表 ── Vue 版(第15讲)
// 用 Vue 3 写第8讲的“行的添加・删除和合计”。不直接改写页面,而是改写数据,页面就跟着变

const { createApp, ref, computed, onMounted } = Vue;
const MAX_ROWS = 10;

createApp({
  setup() {
    let nextId = 1;
    const newRow = () => ({ id: nextId++, category: '', item: '', amount: '', currency: 'JPY' });
    const rows = ref([newRow()]); // 明细的数据。页面只是把它映出来
    const rate = ref(null); // 1 美元合多少日元(和第10讲一样,从服务器取)

    const total = computed(() => { // rows 或 rate 一变,就自动重新计算
      let sum = 0;
      for (const row of rows.value) {
        const usd = row.currency === 'USD';
        const amount = usd ? toCents(row.amount) : toAmount(row.amount); // 计算原样使用第7讲・第10讲的函数
        if (amount === null || Number.isNaN(amount)) continue;
        if (!usd) sum += amount;
        else if (rate.value !== null) sum += centsToYen(amount, rate.value);
      }
      return sum;
    });

    const add = () => {
      if (rows.value.length < MAX_ROWS) rows.value.push(newRow());
    };
    const remove = (i) => {
      rows.value.splice(i, 1);
      if (rows.value.length === 0) add(); // 至少保留一行
    };

    onMounted(async () => { // 页面做好后,去取汇率
      const res = await fetch('/api/rates');
      if (res.ok) rate.value = (await res.json()).rate;
    });

    return { rows, rate, total, add, remove, formatYen, needsReceipt, MAX_ROWS }; // 把模板(vue.html 的 #app 里)要用的东西交出去
  },
}).mount('#app');

requireLogin(); // 确认登录并显示页头的名字(第11讲的 auth.js)

运行起来是这样。

并排的两个 vue.html。左边保持严格的 CSP,只显示标题和说明,明细表格没有出现,页头的名字也停留在“确认中…”。右边允许了 'unsafe-eval',明细 3 行(成田特快 3070 日元、酒店 1 晚 189.50 美元、会议参加费 100 美元),合计 46,851 日元,需要收据的黄色横条,1 美元 = 151.23 日元的换算
用 Vue 3 写的明细表。左边保持严格的 CSP,表格不显示。右边是允许了 'unsafe-eval' 的(笔者的演示页面,经由练习用服务器,在云端的 Chromium 中截图,是用 Playwright 实际操作的画面。2026年10月19日拍摄,页面时钟固定在 2026年10月17日 15:00。账号是仅限本演示的虚构账号。浏览器边框是为了说明而补画的)
# 打开 vue.html 时(Chromium 实测)
CSP 页面 记录
保持严格(script-src 'self') 表格不显示 违规:script-src eval
加上 'unsafe-eval' 明细 3 行・合计 46,851 日元 删 1 行后 2 行・18,193 日元

保持严格的 CSP,表格整个不显示。不构建直接用的 Vue,会在浏览器里把 HTML 模板转换成程序。这时要用字符串生成程序(和 eval 一样),于是被第四节的 CSP 拦下了。被拦下之后的代码(显示页头名字的处理)也没有运行,停留在“确认中…”。允许 'unsafe-eval' 后,直到合计 46,851 日元,都和第8讲一样运行了。另外,v-for、@click 这样的写法不在 HTML 的规范里,所以 vnu 报出 16 处错误(第二节)。

实际工作中,Vue 的模板写在名为 .vue 的专用文件里,发布前用构建工具(Vite 等)预先转换成普通的 JavaScript。这样就不需要 eval,CSP 也能保持严格。React 也是把类似 HTML 的 JSX 写法,通过构建转换成 JavaScript 来用。也就是说,框架的门口有一道构建工具的门。这个连载停在“文件原样放上去就能运行”这一步,是因为想在那道门之前,亲手确认里面发生了什么。

新井Vue 好爽啊……只改数据,页面就跟着变。

古井是吧。那么,“跟着变”的里面是什么?

新井……读取行、计算、改写 textContent。第8讲做的事,Vue 替我们做了。

古井明白这一点,用什么工具都不会迷路。

九

回顾 15 讲

全 15 讲做了什么、学了什么

1 准备 网页如何显示 2 HTML 骨架 标题・表格・语义 3 表单 label 和输入类型 4 CSS 基础 选择器和盒子 5 布局 Flexbox・Grid・手机 6 Bootstrap 借用部件 7 JS 基础 函数和合计 8 DOM・事件 行的添加・删除 9 输入校验 汇总和红框 10 通信 fetch 和 FormData 11 登录 Cookie 和 401・403 12 审批流程 dialog 和 409 13 汇总与打印 Map・SVG・@media print 14 jQuery 重写并比较 15 收尾 检查・防守・发布

用 HTML 写意义,用 CSS 调外观,用 JavaScript 加动作,和服务器对话,做了登录、审批和汇总,用 jQuery 重写,最后检查并加固防守。每一讲都在重复写、在浏览器里运行、记录、确认。表格里列出的数字,全部是实际运行得到的值。

POINT

“古法”不是执着于旧工具,而是能亲手确认里面发生了什么。有了这种能力,无论是 AI 写的代码还是框架的代码,都能沉着地读、改、守。亲手写的能力,是用好 AI 的基础。

结 — 一页总结与今后

  1. 检查要用工具来数。vnu 查 HTML,axe-core 查读屏・操作的易用性。这次有两处对比度不够,修好后 14 → 0。
  2. 重量用覆盖率来称。Bootstrap 的 CSS 只用到一成左右。要削减,就轮到构建工具出场。
  3. 用 CSP 拦下没写过的脚本。只要把脚本和样式分到文件里,加上就行。
  4. 用 frame-ancestors 不让套框,用 nosniff 不让乱猜,用 SRI 发现调包。
  5. 密钥和密码不写在页面上。守护靠服务器。
  6. 发布时,页面放静态托管,API 放服务器。注意 https・响应头・缓存・后门。
  7. 框架是写“数据和页面的关系”的工具。门口有构建工具。

新井前辈,15 讲谢谢您了。接下来我要好好试试 Vite 和 Vue。

古井好。也用 AI。不过,出来的代码要像今天这样运行、记录、确认。

新井是。……对了,庆功宴的收据,可以申请吗?

古井3 万日元以上的话,别忘了附上收据。页面会提醒你的。

参考

页面截图是笔者制作的演示页面,经由练习用服务器(Node.js),于2026年10月19日在云端的 Chromium(Playwright)中拍摄(页面时钟固定在 2026年10月17日 15:00)。截图环境里没有 PingFang SC,中文以 Noto Sans CJK SC 显示。账号・申请都是练习用服务器的虚构数据。HTML 检查用 Nu Html Checker(vnu),读屏・操作的易用性用 axe-core 4.14.0,页面重量和用到的比例用 Chromium 的覆盖率,CSP・frame-ancestors・SRI 的效果和 Vue(3.5.44)页面的行为,都是用 Playwright 操作和运行同一个 Chromium 记录的实际值。只有在插入 axe-core 检查时,才在浏览器设置里关掉了 CSP。登场人物均为虚构。感谢陪伴这 15 讲。

古法前端入门 第15讲(最终讲) — 收尾 / 2026.10

← 返回文章列表