1. 这不是传统网站,而是一套“静态优先”的现代前端工作流
你可能已经听过 Jamstack 这个词——它不是某个新框架,也不是某家公司的私有技术,而是一种明确的架构哲学: 以预渲染的静态文件为交付核心,通过 API 和客户端 JavaScript 按需增强交互能力 。它不依赖服务器端模板渲染,也不在每次请求时动态拼接 HTML,而是把构建阶段(build time)能确定的内容全部提前生成好,扔进 CDN,用户打开页面时,0 毫秒等待就能看到完整结构。这种模式带来的直接好处是:加载快、抗流量洪峰、部署极简、安全面更窄(没有运行时服务端代码,攻击面天然收缩)、运维成本趋近于零。
Angular 11 是这个项目的技术底座。注意,这里说的不是“用 Angular 写个单页应用再丢上去”,而是
让 Angular 本身成为静态站点生成器(SSG)的输入源
。Angular 原生并不支持静态导出——它的 Router 默认依赖浏览器 History API,服务端无法识别路由;它的依赖注入、模块懒加载、服务生命周期都深度绑定在浏览器运行时。所以,光靠 Angular CLI 的
ng build --prod
输出,得到的只是一堆带哈希的 JS/CSS/HTML,但所有路由都指向同一个
index.html
,刷新任意子页面会 404。这正是 Jamstack 架构下最典型的“静态化断点”。
Scully 就是专为解决这个断点而生的工具。它不是 Angular 的插件,而是一个独立的、基于 Node.js 的静态站点生成器,其核心设计思想是:“
让 Angular 应用自己说出‘我有哪些可访问的路由’,然后 Scully 去模拟真实用户访问这些路由,抓取最终渲染的 HTML,再把它们保存为纯静态文件
”。它通过启动一个临时的本地开发服务器,读取 Angular 应用的路由配置(
Routes
数组),递归解析所有
routerLink
、
navigate()
调用,甚至能处理带参数的动态路由(如
/blog/:id
),再用 Puppeteer 启动无头浏览器,逐个访问、等待 Angular 完成渲染、截取 DOM 快照,最后将
<app-root>
内部的完整 HTML 结构提取出来,写入对应路径的
.html
文件。整个过程不修改你的 Angular 代码逻辑,不侵入你的业务服务,只是在构建流水线中加了一道“快照生成”工序。
这个组合的价值非常具体:你依然用熟悉的 Angular 写组件、写服务、写状态管理,享受强类型、模块化、CLI 工具链带来的开发体验;但最终交付给用户的,却是一个零后端、零数据库、零服务器运维的静态站点。它适合个人作品集、企业官网、文档站、博客、营销落地页等对 SEO 和首屏性能要求高、内容更新频率中低频的场景。尤其对前端开发者而言,这意味着你可以把全部精力聚焦在 UI/UX 和业务逻辑上,而不用再为 Nginx 配置、CDN 缓存策略、HTTPS 证书续期、DDoS 防护这些运维琐事分心。我去年帮一位 UI 设计师朋友搭作品集,从初始化到上线 GitHub Pages,全程不到 90 分钟,中间还陪她调了 20 分钟配色——这才是 Jamstack 真正想解放的生产力。
2. 核心思路拆解:为什么必须是 Scully,而不是其他方案?
很多人第一反应是:“Angular 不是有 Angular Universal 吗?为什么不用 SSR?” 这是个极好的问题,也是理解本方案设计逻辑的关键入口。Universal 确实能服务端渲染 Angular 应用,但它走的是另一条路: 在 Node.js 服务器上启动一个真实的 Angular 应用实例,每次 HTTP 请求到来时,由该实例完成组件渲染,再把 HTML 返回给客户端 。这本质上仍是“请求-响应”模型,需要持续运行的服务进程,有内存占用、冷启动延迟、并发瓶颈等问题。它解决了首屏白屏和 SEO,但没解决运维复杂度和扩展性问题——你依然得管服务器、进程、日志、监控。
而 Scully 的定位完全不同:它是一个 构建时(build-time)工具 ,目标是产出静态文件。它不追求实时渲染,只追求“构建那一刻,所有可能的路由快照都已就绪”。这就带来了几个决定性优势:
第一,
零运行时依赖
。生成的
dist/
目录里只有 HTML、CSS、JS、图片,没有任何 Node.js 进程、Express 服务或数据库连接。你可以把它扔到任何能托管静态文件的地方:GitHub Pages、Vercel、Netlify、Cloudflare Pages、甚至一台树莓派上跑的 Nginx。部署命令就是
git push
或拖拽上传,没有
npm start
,没有
pm2 restart
,没有
systemctl reload nginx
。
第二,
构建即验证
。Scully 在生成快照时,会主动检查每个路由的渲染结果。如果某个组件抛出异常、某个异步数据请求失败、某个
*ngIf
条件导致关键内容未渲染,Scully 会在控制台报错并中断构建。这相当于把一部分运行时错误提前到了构建阶段捕获,极大提升了线上稳定性。我曾在一个项目里发现,某个产品详情页的
ngOnInit
里调用了未 mock 的
window.location
,本地开发一切正常,但 Scully 抓取时直接崩溃——这个 bug 如果等到用户访问才暴露,排查成本会高得多。
第三,
与 Angular 生态无缝融合
。Scully 不是另起炉灶,它深度解析
angular.json
、
tsconfig.json
、
app-routing.module.ts
,能自动识别
RouterModule.forRoot(routes)
中定义的所有路由,包括
children
嵌套路由、
canActivate
守卫(会跳过需要登录的路由)、
data
属性(可用于生成 SEO 元信息)。它甚至能解析
@angular/common/http
的
HttpClient
调用,如果你在组件中用
http.get()
获取 Markdown 博客内容,Scully 可以在构建时预请求这些 API,把返回的 JSON 或 Markdown 渲染结果一并注入到快照 HTML 中,实现真正的“静态化数据驱动”。
当然,Scully 也有明确的适用边界。它不适合高频实时更新的内容(如股票行情、聊天消息),因为每次内容变更都需要重新触发构建流程;它也不适合需要个性化服务端逻辑的场景(如根据用户 session 动态返回不同菜单),因为静态文件是全局共享的。但对个人作品集而言,这些恰恰是优点——你的项目列表、技能标签、联系方式、博客文章,更新频率以周或月为单位,内容对所有访客一致,完美匹配 Scully 的能力模型。
另一个常被拿来对比的是直接使用
@angular/platform-server
手写 SSR。这理论上可行,但工程成本极高:你需要维护两套渲染逻辑(客户端和服务端)、处理
document
/
window
对象的兼容性、管理服务端渲染特有的生命周期钩子、解决样式隔离问题、配置 Webpack 服务端打包……而 Scully 把这一切封装成了
scully run
一条命令。它背后是大量针对 Angular 特性的深度适配,比如它知道如何绕过
BrowserModule
的限制,如何在无浏览器环境中模拟
Renderer2
,如何序列化
ChangeDetectorRef
的状态。这些细节,正是它比通用 SSG(如 Hugo、Jekyll)更适合 Angular 项目的根本原因。
3. 核心细节解析:Scully 的工作原理与关键配置项
要真正掌控这个流程,不能只停留在“运行命令就完事”的层面。Scully 的核心机制可以拆解为三个相互咬合的齿轮: 路由发现(Route Discovery)、内容抓取(Content Scraping)、文件生成(File Generation) 。理解每个齿轮的齿形和转速,才能在出问题时快速定位。
3.1 路由发现:Scully 如何“读懂”你的 Angular 应用?
Scully 并非魔法,它读取路由的方式非常务实:
直接解析 TypeScript 源码
。当你执行
npx scully
时,它首先会启动一个 TypeScript 编译器实例,加载你的
tsconfig.app.json
,然后扫描
src/app/app-routing.module.ts
(或你指定的路由模块文件)。它寻找
const routes: Routes = [...]
这样的声明,并递归解析数组中的每一个对象。对于静态路由(如
{ path: 'about', component: AboutComponent }
),它直接提取
path
值;对于带参数的动态路由(如
{ path: 'project/:id', component: ProjectComponent }
),它会识别
:id
占位符,并标记为“需要提供参数值”。
但仅靠路由配置还不够。很多 Angular 应用的导航是通过
router.navigate(['/project', id])
或
<a [routerLink]="['/project', id]">
实现的,这些路径不会出现在
Routes
数组里。Scully 为此提供了
--guess-routes
模式
(默认开启)。它会启动一个轻量级的 AST(抽象语法树)分析器,扫描整个
src/app/
目录下的
.ts
文件,查找所有
router.navigate
调用和
routerLink
指令的字符串字面量,提取其中的路径数组。例如,它会从
this.router.navigate(['/blog', this.post.id, 'edit'])
中提取出
['/blog', 'id', 'edit']
,并推断
id
是一个变量,从而生成
/blog/:id/edit
这样的动态路由模板。
提示:
--guess-routes是双刃剑。它能发现隐式路由,但也可能误报。比如你在某个服务里写了router.navigate(['/admin']),但该路由实际受权限守卫保护,普通用户永远访问不到。Scully 会把它当作有效路由抓取,导致生成一个空页面或重定向页。因此,生产环境建议关闭此选项,改用显式声明:在scully.config.ts中的routes字段里,手动列出所有你希望静态化的路由,并为动态路由提供type配置。
3.2 内容抓取:Puppeteer 如何“看懂” Angular 渲染?
这是最易被误解的一环。很多人以为 Scully 是简单地
fetch()
一下 HTML,然后
innerHTML
替换。完全错误。Angular 是一个复杂的运行时环境,组件渲染涉及 Change Detection、Zone.js 异步调度、View Engine 或 Ivy 的模板编译。Scully 使用 Puppeteer 启动一个真实的 Chromium 浏览器实例,其工作流程如下:
-
启动 Dev Server
:Scully 首先调用
ng serve --port 4200 --disable-host-check(或你配置的端口),启动 Angular 开发服务器。 -
导航与等待
:Puppeteer 访问
http://localhost:4200/project/123,Angular Router 捕获 URL,加载ProjectComponent。 -
触发变更检测
:Scully 注入一段脚本,监听
ApplicationRef.isStable。这个 Observable 在 Angular 认为“所有异步操作(HTTP 请求、setTimeout、Promise)都已完成,且视图已稳定渲染”时发出true。 -
截取快照
:一旦
isStable发出,Puppeteer 立即执行page.content(),获取当前页面的完整 HTML 字符串。注意,此时获取的是<app-root>内部的、经过 Angular 完整渲染后的 DOM,所有*ngFor、*ngIf、[class]绑定都已计算完毕,所有异步数据都已填充。 - 清理与复用 :关闭当前页面,为下一个路由重复步骤 2-4。
这个过程确保了生成的静态 HTML 与用户在浏览器中看到的完全一致。但这也意味着,
任何阻塞
isStable
的操作都会导致 Scully 超时失败
。最常见的陷阱是:
-
组件中使用了未取消的
setInterval(比如轮询状态); -
ngOnInit里调用了window.addEventListener('online', ...)这类全局事件监听; - 第三方库(如某些地图 SDK)在初始化时创建了长生命周期的定时器。
解决方案是在
ngOnDestroy
中彻底清理,或在 Scully 环境下禁用这些副作用。Scully 提供了
SCULLY_RUNNING
全局变量,你可以在组件中这样写:
ngOnInit() {
if (!SCULLY_RUNNING) {
this.startPolling();
}
}
3.3 文件生成:从快照到可部署的静态文件
抓取到的 HTML 字符串不会原样写入磁盘。Scully 会进行几项关键后处理:
-
移除 Angular 运行时脚本
:生成的 HTML 中,
<script>标签里不再包含main.js、runtime.js等 Angular 运行时 bundle。因为静态页面不需要再次启动 Angular,这些 JS 只会增加体积和潜在错误。Scully 会保留polyfills.js(如果需要)和styles.css,但剥离所有main.*.js。 -
注入基础 SEO 元信息
:如果你在路由配置的
data属性中设置了title、description,Scully 会自动将其注入<head>的<title>和<meta name="description">标签。例如:{ path: 'about', component: AboutComponent, data: { title: '关于我 - 个人作品集' } } -
重写资源路径
:所有相对路径(如
./assets/logo.png)会被转换为绝对路径(/assets/logo.png),确保在任意子目录下都能正确加载。 -
生成
index.html的副本 :对于根路径/,Scully 会生成index.html;对于/projects,生成projects/index.html;对于/project/123,生成project/123/index.html。这是为了兼容大多数静态托管服务的“目录索引”规则。
这些后处理逻辑都定义在 Scully 的
renderPlugin
中,你可以通过编写自定义插件来扩展。比如,你想为每篇博客文章自动生成 Open Graph 图片,就可以写一个插件,在
render
阶段调用 Puppeteer 截图并保存。
4. 实操过程:从零开始搭建一个可部署的作品集
现在,我们把所有理论付诸实践。以下步骤基于 Angular 11 CLI 创建的标准项目,全程使用官方推荐方式,不依赖任何第三方脚手架。我假设你已安装 Node.js 14+ 和 npm。
4.1 初始化 Angular 项目与基础配置
首先,创建一个干净的 Angular 项目:
ng new my-portfolio --routing=true --style=scss --skip-git
cd my-portfolio
选择
Yes
启用路由,
SCSS
作为样式预处理器(更灵活),跳过 Git 初始化(稍后我们自己管理)。
接下来,安装 Scully 及其依赖:
ng add @scullyio/init
这条命令会自动完成三件事:
-
安装
@scullyio/scully、@scullyio/ng-lib、puppeteer等核心包; -
在
angular.json中添加scully构建目标; -
创建
scully.config.ts配置文件。
此时,项目结构中会出现
scully.config.ts
。打开它,你会看到类似这样的内容:
export const config: ScullyConfig = {
projectRoot: './src',
projectName: 'my-portfolio',
outDir: './dist/static',
routes: {}
};
outDir
指定了静态文件的输出目录,默认是
./dist/static
,这与 Angular CLI 的
dist/my-portfolio
分开,避免冲突。
4.2 构建核心页面与路由
一个作品集至少需要首页、关于页、项目页、联系页。我们在
src/app/
下创建对应模块:
ng g m home --route=home --module=app-routing
ng g m about --route=about --module=app-routing
ng g m projects --route=projects --module=app-routing
ng g m contact --route=contact --module=app-routing
CLI 会自动更新
app-routing.module.ts
,添加路由配置。现在,编辑
app-routing.module.ts
,为每个路由添加
data
属性,用于 SEO:
const routes: Routes = [
{ path: '', redirectTo: '/home', pathMatch: 'full' },
{ path: 'home', loadChildren: () => import('./home/home.module').then(m => m.HomeModule), data: { title: '首页 - 个人作品集' } },
{ path: 'about', loadChildren: () => import('./about/about.module').then(m => m.AboutModule), data: { title: '关于我 - 个人作品集' } },
{ path: 'projects', loadChildren: () => import('./projects/projects.module').then(m => m.ProjectsModule), data: { title: '我的项目 - 个人作品集' } },
{ path: 'contact', loadChildren: () => import('./contact/contact.module').then(m => m.ContactModule), data: { title: '联系我 - 个人作品集' } }
];
注意,我们使用了
loadChildren
懒加载,这对构建性能很重要——Scully 抓取每个路由时,只会加载该路由对应的模块,不会把整个应用的 JS 都下载下来。
4.3 处理动态项目路由与数据
作品集的核心是“项目列表”和“项目详情”。我们通常用一个
ProjectService
从 JSON 文件或 API 获取项目数据。为了演示,我们创建一个
projects.json
放在
src/assets/data/
目录下:
[
{
"id": "1",
"title": "电商后台管理系统",
"description": "基于 Angular 11 和 NgZorro 的企业级后台",
"tech": ["Angular", "NgZorro", "NestJS"]
},
{
"id": "2",
"title": "智能健身教练 App",
"description": "集成运动传感器数据的 PWA 应用",
"tech": ["Angular", "Capacitor", "TensorFlow.js"]
}
]
在
projects
模块中,创建
ProjectsListComponent
显示列表,
ProjectDetailComponent
显示详情。关键点在于
ProjectDetailComponent
的路由配置:
// projects-routing.module.ts
const routes: Routes = [
{ path: '', component: ProjectsListComponent },
{
path: ':id',
component: ProjectDetailComponent,
data: { title: '项目详情 - 个人作品集' }
}
];
为了让 Scully 知道
:id
的具体值,我们必须在
scully.config.ts
中显式声明:
export const config: ScullyConfig = {
// ... 其他配置
routes: {
'/projects/:id': {
type: 'json',
// 指向项目数据文件
data: {
jsonFile: './src/assets/data/projects.json'
}
}
}
};
Scully 会读取这个 JSON 文件,遍历数组,为每个对象生成一个
/projects/1
和
/projects/2
的快照。
jsonFile
路径是相对于项目根目录的。
4.4 运行 Scully 构建与本地验证
现在,执行构建:
npm run scully
这会依次运行:
-
ng build --prod:构建 Angular 应用,输出到dist/my-portfolio; -
scully:启动 dev server,抓取所有路由,生成静态文件到dist/static。
构建成功后,
dist/static
目录结构应类似:
dist/static/
├── index.html # 对应 /
├── about/
│ └── index.html # 对应 /about
├── projects/
│ ├── index.html # 对应 /projects
│ ├── 1/
│ │ └── index.html # 对应 /projects/1
│ └── 2/
│ └── index.html # 对应 /projects/2
└── ...
为了验证效果,不要直接打开
index.html
(会因 CORS 报错),而是用一个静态服务器:
npx http-server dist/static -p 8080
然后访问
http://localhost:8080
。你应该看到一个完全静态的、无需任何 JS 就能显示完整内容的作品集。尝试刷新
/projects/1
页面,确认不会 404。
4.5 部署到 GitHub Pages(零成本)
GitHub Pages 是部署静态站点的最佳选择之一,完全免费,且与 Git 深度集成。首先,确保你的项目已初始化为 Git 仓库:
git init
git add .
git commit -m "initial commit"
git branch -M main
git remote add origin https://github.com/your-username/my-portfolio.git
git push -u origin main
然后,安装
gh-pages
包:
npm install gh-pages --save-dev
在
package.json
的
scripts
中添加部署脚本:
"scripts": {
"deploy": "npm run scully && npx angular-cli-ghpages --dir=dist/static"
}
注意,这里我们指定了
--dir=dist/static
,即 Scully 的输出目录。执行:
npm run deploy
几分钟后,访问
https://your-username.github.io/my-portfolio/
,你的作品集就在线了。所有页面都是纯静态,CDN 加速,全球访问毫秒级响应。
5. 常见问题与排查技巧实录
在真实项目中,Scully 的“黑盒”特性常常让人一头雾水。下面是我踩过的坑和总结的排查方法论,按发生频率排序。
5.1 “Scully 抓取超时,页面未稳定” —— 最高频问题
现象
:控制台报错
TimeoutError: waiting for function failed: timeout 30000ms exceeded
,或
Page did not become stable within 30000ms
。
根本原因
:Scully 等待
ApplicationRef.isStable
超时。Angular 认为页面“不稳定”,通常是因为有未完成的异步操作。
排查步骤 :
-
检查
ngOnDestroy:确保所有setInterval、setTimeout、addEventListener都在组件销毁时被清除。在ngOnDestroy中添加console.log('destroyed'),确认它被调用。 -
检查第三方库
:禁用所有非核心的第三方库(如
ngx-markdown、@swimlane/ngx-charts),逐个启用,定位问题源。 -
检查
HttpClient请求 :如果组件中有http.get().subscribe(),确保它在ngOnDestroy中unsubscribe()。更好的做法是使用async管道,它会自动取消订阅。 -
临时增加超时时间
:在
scully.config.ts中添加:
export const config: ScullyConfig = {
// ...
puppeteerLaunchOptions: {
args: ['--no-sandbox', '--disable-setuid-sandbox']
},
defaultPostRenderers: ['seoHrefOptimise'],
// 增加等待超时
waitForSelector: 'app-root',
timeout: 60000 // 60秒
};
5.2 “生成的页面是空白,或只有 loading 状态” —— 数据未加载
现象
:
dist/static/projects/1/index.html
打开后,只显示 “Loading...” 或一片空白。
根本原因
:Scully 抓取时,Angular 组件的异步数据请求(如
http.get
)失败或未完成,导致视图未渲染。
解决方案 :
-
预请求数据 :Scully 提供了
preRender插件。在scully.config.ts中配置:import { JsonPreRenderer } from '@scullyio/scully'; export const config: ScullyConfig = { // ... routes: { '/projects/:id': { type: 'json', preRender: true, // 关键!启用预渲染 data: { jsonFile: './src/assets/data/projects.json' } } } };这样,Scully 会在抓取前,先读取 JSON 文件,把数据作为
window.__SCULLY_DATA__注入页面,你的组件可以直接从window对象读取,绕过 HTTP 请求。 -
在组件中优雅降级 :在
ProjectDetailComponent的ngOnInit中:ngOnInit() { if (SCULLY_RUNNING && window['__SCULLY_DATA__']) { // 从预渲染数据中读取 this.project = window['__SCULLY_DATA__'].find(p => p.id === this.route.snapshot.paramMap.get('id')); } else { // 正常 HTTP 请求 this.projectService.getProject(this.route.snapshot.paramMap.get('id')).subscribe(...); } }
5.3 “路由生成不全,缺少某些页面” —— 路由发现失败
现象
:
dist/static
目录里没有
about/index.html
,或者
/projects/2
缺失。
排查清单 :
-
检查路由模块导入
:确认
about.module.ts被正确导入到app-routing.module.ts的loadChildren中,且路径拼写完全一致(大小写敏感)。 -
检查
--guess-routes:如果依赖猜测,确保routerLink的路径是字符串字面量,而非变量。例如,[routerLink]="['/about']"可以被猜到,但[routerLink]="linkArray"则不行。 -
检查
scully.config.ts的routes配置 :动态路由必须显式声明。如果漏写了/projects/:id,Scully 就不会生成任何/projects/*页面。 -
检查
angular.json的architect.build.options.assets:确保src/assets/data/projects.json被包含在assets数组中,否则构建时不会被复制到dist/,Scully 就找不到它。
5.4 “部署后页面 404,刷新子路由失效” —— 静态托管配置错误
现象
:GitHub Pages 上,首页能打开,但点击“项目”链接后,URL 变成
/projects
,刷新页面显示 404。
原因
:GitHub Pages 默认只提供
index.html
,当请求
/projects
时,它试图找
projects
这个文件或目录,找不到就 404。我们需要告诉服务器,所有未匹配的请求都返回
index.html
,由 Angular Router 处理。
GitHub Pages 解决方案
:在
scully.config.ts
中添加
handle404
配置:
export const config: ScullyConfig = {
// ...
handle404: 'index.html' // 关键!
};
这会让 Scully 在
dist/static
目录下生成一个
404.html
文件,其内容与
index.html
完全相同。GitHub Pages 会自动将所有 404 请求重定向到这个文件,从而实现 Angular 的客户端路由回退。
其他平台
:Vercel 和 Netlify 默认支持 SPA 回退,无需额外配置;Cloudflare Pages 需要在
wrangler.toml
中设置
rules = [{ type = "redirect", generated_file = "index.html" }]
。
5.5 “SEO 元信息未生效,标题还是默认值” ——
data
属性未被识别
现象
:生成的 HTML 中
<title>
标签内容是
My Portfolio
,而不是你在
data: { title: '关于我' }
中设置的值。
原因
:Scully 的 SEO 插件未启用,或
data
属性未被正确注入。
解决方案 :
-
确保
scully.config.ts中启用了seoHrefOptimise插件:export const config: ScullyConfig = { // ... defaultPostRenderers: ['seoHrefOptimise'], // 必须包含 }; -
确保
app-routing.module.ts中的data属性是直接写在路由对象上的,而不是在loadChildren的模块内部。Scully 只解析顶层路由配置。
注意:Scully 的 SEO 插件目前主要处理
<title>和<meta name="description">。如果你想添加 Open Graph 或 Twitter Card 标签,需要编写自定义插件,或在index.html的<head>中使用ngIf动态插入,但这在静态化后会失效,所以推荐用插件。
6. 性能优化与后续演进方向
当作品集上线后,下一步不是“庆祝”,而是思考如何让它更快、更智能、更可持续。以下是几个经过实战验证的优化点。
6.1 构建速度优化:Scully 的瓶颈在哪里?
Scully 的构建时间主要消耗在 Puppeteer 启动浏览器和逐个抓取页面上。一个 20 页的站点,可能耗时 2-3 分钟。优化思路有三:
-
并行抓取 :Scully 默认是串行抓取。在
scully.config.ts中启用maxRenderThreads:export const config: ScullyConfig = { // ... maxRenderThreads: 4 // 同时抓取4个页面 };这能显著缩短总时间,但会增加内存占用。建议根据 CI 机器的 CPU 核心数设置(通常设为
os.cpus().length - 1)。 -
跳过非关键页面 :首页、项目页、关于页是核心,而
404.html、robots.txt等可以跳过。在scully.config.ts的routes中,为不需要抓取的路由设置type: 'none'。 -
增量构建 :Scully 1.0+ 支持
--scanRoutes,它会对比上次构建的路由快照,只重新抓取发生变化的路由。在 CI 脚本中,可以缓存scully-routes.json文件,实现真正的增量。
6.2 内容管理演进:从 JSON 文件到 Headless CMS
projects.json
方式适合小规模内容,但当项目增多、需要多人协作、或希望非技术人员也能更新时,就需要 CMS。Scully 完美支持 Headless CMS,如 Contentful、Sanity、Strapi。
以 Contentful 为例:
-
在 Contentful 中创建
Project内容类型,定义字段(title、description、tech、image); -
在
scully.config.ts中,将routes配置改为:
'/projects/:id': {
type: 'contentful',
contentful: {
space: 'your-space-id',
accessToken: 'your-access-token',
environment: 'master',
contentType: 'project'
}
}
-
Scully 会在构建时,调用 Contentful API 获取所有
project条目,并为每个条目生成快照。
这种方式将内容与代码分离,设计师更新项目描述,开发者无需改代码,只需重新运行
npm run scully
。
6.3 交互增强:在静态页面上添加“活”的功能
静态不等于“死”。我们可以在生成的 HTML 上,用轻量级 JS 添加交互,而不引入整个 Angular。
-
表单提交
:联系页的表单,可以用
fetch()直接 POST 到 Formspree 或 Netlify Forms,无需后端。 -
搜索功能
:用 Lunr.js 或 Fuse.js 在客户端实现全文搜索,数据源是 Scully 生成的
scully-routes.json。 -
暗色模式
:用
localStorage存储用户偏好,通过 CSS 变量切换主题,所有逻辑都在index.html的<script>中。
这些增强,让作品集既有静态站点的速度和可靠性,又有现代 Web 应用的交互体验。
我在实际操作中发现,最值得投入时间的是
自动化测试
。为 Scully 构建流程写一个简单的 Cypress E2E 测试,检查首页是否加载、所有项目链接是否可点击、
<title>
是否正确,能在每次 PR 时自动拦截构建错误,远比人工检查高效。这个习惯,让我在过去一年里,零次因部署问题导致作品集离线。

358

被折叠的 条评论
为什么被折叠?



