Fleet 官网开发指南:Sails.js + Parasails 网站工程全解析(fleet 仓库 website 目录)
Fleet 官网开发指南:Sails.js + Parasails 网站工程全解析(fleet 仓库 website 目录)
本文以 fleet 开源仓库中 website/.claude/CLAUDE.md 为骨架,完整梳理 website/ 目录(即 fleetdm.com 官方网站)的工程架构、后端/前端/样式编码规范、Markdown 内容管线和新建页面流程。读者读完将掌握:如何在这个 Sails.js 1.5.17 + Vue.js/Parasails 的网站中定位代码、遵循规范提交改动,以及如何安全地新增一个页面并跑通开发、构建与 lint 全流程。
一、技术栈总览:一个"营销站 + 文档站 + 客户系统"的 Sails 应用
website/ 是一个完整的 Sails.js 1.5.17 单页应用,官方托管着公开的 Fleet 官网。其技术选型与依赖版本可直接从 website/package.json 核实:
- 运行时:Node.js 20+(
engines: { "node": "^20.18" }) - Web 框架:Sails
^1.5.17(MVC 全栈框架),配合sails-hook-orm、sails-hook-sockets、sails-hook-apianalytics、sails-hook-organics - 数据库:PostgreSQL(
sails-postgresql ^5.0.1) - 视图层:EJS 模板 + Vue.js 2 / Parasails(
parasails.js为本地依赖,见 website/assets/dependencies/) - 样式层:LESS,由 Grunt(
grunt 1.0.4+sails-hook-grunt)编译 - 外部集成:Stripe(计费/订阅)、Google BigQuery、Salesforce(jsforce)、Google APIs
- 会话:Redis(
@sailshq/connect-redis、@sailshq/socket.io-redis),生产环境可水平扩展
从目录结构看,这是一个"混合型"应用:既有面向公众的营销落地页(view-linux-management、view-pricing 等),也有文档/文章渲染页(docs/*、articles/*、handbook/* 控制器),还包含客户自助系统(customers/ 下的 license 与 Stripe 订阅)以及若干第三方代理控制器(android-proxy/、microsoft-proxy/、vpp-proxy/、webhooks/)。理解这一点,才能判断改动的影响面。
二、目录架构:前后端与内容的物理边界
website/
├── api/
│ ├── controllers/ # Sails Actions2 控制器,按功能分目录(account/、articles/、docs/、landing-pages/ 等)
│ ├── models/ # Waterline ORM 模型(User、Subscription、Quote、NewsletterSubscription、AndroidEnterprise 等)
│ ├── helpers/ # 可复用逻辑,按领域分目录(stripe/、salesforce/、ai/、android-proxy/、microsoft-proxy/ 等)
│ ├── policies/ # 认证中间件(is-logged-in、is-super-admin、has-query-generator-access)
│ ├── responses/ # 自定义响应处理(unauthorized、expired、badConfig、notFound)
│ └── hooks/custom/ # 服务初始化、安全响应头、全局变量注入
├── assets/
│ ├── js/
│ │ ├── components/ # Vue/Parasails 组件(*.component.js)
│ │ ├── pages/ # 页面脚本(parasails.registerPage)
│ │ └── utilities/ # 共享工具(parasails.registerUtility)
│ ├── styles/ # LESS 样式表
│ ├── dependencies/ # 第三方前端依赖(vue.js、parasails.js、bootstrap-4、jquery、lodash、ace-editor、mermaid 等)
│ ├── images/ # 图片资产(含 500+ 应用图标、落地页配图)
│ └── pdfs/、videos/
├── views/
│ ├── layouts/ # EJS 布局模板(layout.ejs 为全站主布局)
│ ├── pages/ # 页面模板(含 landing-pages/ 子目录)
│ ├── partials/ # 可复用模板片段
│ └── emails/ # 邮件模板(newsletter、事务邮件、内部通知)
├── config/
│ ├── routes.js # 全部路由定义(约 1500+ 行,含大量 301 重定向)
│ ├── policies.js # 路由到策略(policy)的映射
│ ├── custom.js # 应用设置(API keys、TTL、feature flags)
│ ├── local.js # 本地开发覆盖(不入库、不部署)
│ └── datastores.js / http.js / security.js / session.js / sockets.js / views.js / i18n.js 等
├── scripts/ # 自定义脚本(如 markdown 内容构建)
├── tasks/、generators/ # Grunt 任务与 Sails 自定义生成器
├── Gruntfile.js、app.js、package.json
└── .claude/CLAUDE.md # 本文所依据的 AI 协作开发指南
三、后端规范:Actions2、Waterline、路由与认证
3.1 控制器与 Helper 统一采用 Actions2 机器格式
所有控制器和 helper 都使用 Sails Actions2 machine 格式,固定包含 friendlyName、inputs、exits、fn 四个核心部分。例如 website/api/controllers/view-pricing.js 即遵循该形态:通过 exits 声明 success 等出口,在 fn 中组织数据后返回页面。
调用 helper 的约定写法是:
await sails.helpers.domain.name.with({ ... });
抛出非成功出口的方式是直接 throw 'exitName',例如 throw 'notFound' 会触发控制器里定义的 notFound exit,从而走到 website/api/responses/notFound.js 之类的自定义响应处理器。
3.2 Waterline ORM 模型
模型(如 website/api/models/User.js)使用声明式属性 schema。敏感字段(密码、token)必须声明 protect: true,防止被序列化时意外泄露给客户端。完整的模型清单包括 User、Subscription、Quote、NewsletterSubscription、AndroidEnterprise、CertificateSigningRequest、MicrosoftComplianceTenant、HistoricalUsageSnapshot、VantaConnection、AdCampaign、Platform、RenderProofOfValue 等,见 website/api/models/。
3.3 路由:全部集中在 config/routes.js
路由不允许散落各处,统一维护在 website/config/routes.js。典型的路由定义包含 action 与 locals(页面 SEO 元信息、导航高亮等):
'GET /pricing': {
action: 'view-pricing',
locals: {
currentSection: 'pricing',
pageTitleForMeta: 'Pricing',
pageDescriptionForMeta: 'Use Fleet for free or get started with Fleet Premium (self-hosted or managed cloud).'
}
},
几点值得注意的约定:
- Webhook 路由必须关闭 CSRF:
webhooks/*一类的接收路由需要显式设置csrf: false(见 website/config/security.js 相关的安全配置上下文),因为 webhook 请求不带会话 cookie。 - 通配路由:文档、文章、手册使用
'GET /docs/?*'、'GET /articles/*'、'GET /handbook/?*'这类通配符,由对应的view-basic-*控制器根据 slug 渲染内容。 - 301 重定向:routes.js 末尾维护了上百条历史链接的重定向(例如
'GET /docs': '/docs/get-started/why-fleet'),页面改名时必须同步在此登记,避免旧链接 404。 - 需要动态逻辑的重定向可以直接给
fn函数,例如'GET /try-fleet/explore-data/:hostPlatform/:tableName'用fn拼参数后重定向。
3.4 认证:会话 + 策略(policies)
认证基于会话:登录后 req.session.userId 就位,已登录用户会被自动水合(auto-hydrate)为 req.me 传给模板与页面脚本。访问控制由 website/config/policies.js 统一映射,默认全局策略是 '*': 'is-logged-in',即除显式放行外所有 action 都需要登录:
module.exports.policies = {
'*': 'is-logged-in',
'admin/*': 'is-super-admin',
'query-generator/*': 'has-query-generator-access',
'microsoft-proxy/*': true,
'entrance/*': true,
'webhooks/*': true,
'docs/*': true,
'articles/*': true,
// ... 其余公开 action 逐个列出
};
新增一个公开页面时,必须在此处为对应 action 添加 true 放行条目,否则会被 is-logged-in 拦截。三个策略实现位于 website/api/policies/:is-logged-in.js、is-super-admin.js、has-query-generator-access.js。
3.5 配置分层
- website/config/custom.js —— 应用级设置:集成密钥、TTL、feature flags,通过
sails.config.custom.*读取; config/local.js—— 本地开发覆盖,不入库、不部署;config/env/production.js—— 生产环境覆盖;- 敏感凭据(数据库密码、Stripe key、Salesforce 凭据等)只能放在
config/local.js或环境变量中,严禁写入已提交的配置。
四、前端规范:数据流、组件与表单
4.1 控制器 → 页面的数据流
页面 view action(例如 api/controllers/view-pricing.js)返回的值会通过 data 对象传给页面脚本,在页面脚本中以 this 访问(如 this.pricingTable)。模板中有两条渲染路径:
- EJS 插值
<%- pricingTable %>:用于服务端静态数据,首屏直出、利于 SEO; - Vue 模板
{{pricingTable}}:用于会随用户交互变化的值(筛选、开关等)。
判断标准很简单:数据服务端定死用 EJS,需要响应式更新用 Vue。
4.2 可复用 Parasails 组件
全站复用的核心组件(位于 website/assets/js/components/):
| 组件 | 用途 | 使用要点 |
|---|---|---|
<scrollable-tweets> |
客户证言轮播 | 需要 view action 提供 testimonialsForScrollableTweets 数据(见下文 Testimonials) |
<parallax-city> |
落地页底部动画城市天际线 | 必须放在页面最顶层、page-container/page-content 之外,才能全宽无内边距渲染;通常紧跟 bottom-gradient 区块之后 |
<logo-carousel> |
客户 Logo 旋转条 | 通常放在 hero 区块 |
<modal> |
模态对话框 | 用 v-if="modal === 'modal-name'" 控制显示,@close="closeModal()" 关闭,常用于视频嵌入 |
组件实现可参考 website/assets/js/components/ajax-form.component.js(表单组件,含完整的校验规则实现)与 website/assets/js/components/scrollable-tweets.component.js。
视频弹窗模式(Video modal pattern)
落地页标配 "See Fleet in action" 视频按钮,需要三处配合:
- 页面脚本:
data中加modal: '',并实现clickOpenVideoModal与closeModal方法; - 模板:加入
<modal purpose="video-modal">并内嵌 YouTube iframe; - LESS:包含响应式视频弹窗样式(参考 website/views/pages/linux-management.ejs 对应的页面样式文件)。
Testimonials 数据管线
客户证言定义在 handbook/company/testimonials.yml,构建时被编译进 sails.config.builtStaticContent.testimonials。每条证言包含 quote、quoteAuthorName、quoteAuthorJobTitle、productCategories(如 Device management、Observability、Software management)以及可选的媒体字段。使用 <scrollable-tweets> 的 view action 必须负责过滤/排序证言并以 testimonialsForScrollableTweets 返回,参考 api/controllers/landing-pages/view-linux-management.js 的写法。
4.3 Cloud SDK:前后端 API 调用
前端到后端的 API 调用统一走 Cloud.* 方法,由 <ajax-form> 组件或页面脚本的 handleSubmitting 函数触发。每个 Cloud 方法映射到后端的一个 action。新增或重命名 action 后必须重新生成 SDK:
sails run rebuild-cloud-sdk
生成的 SDK 位于 website/assets/dependencies/cloud.js。
4.4 ajax-form 表单
表单提交使用 <ajax-form> 组件,两种用法:
- 简单场景:
action="cloudMethodName"直接绑定 Cloud 方法; - 复杂场景:
:handle-submitting="fn"传入自定义提交逻辑。
状态属性(syncing、cloudError、formErrors)使用 .sync 修饰符双向同步。完整示例见 website/views/pages/contact.ejs;支持的校验规则以 website/assets/js/components/ajax-form.component.js 的实现为准。
4.5 全局浏览器变量
页面脚本可用的全局变量:parasails、Cloud、io、_(Lodash)、$(jQuery)、moment、bowser、Vue、Stripe、gtag、ace。这些都在 website/views/layouts/layout.ejs 主布局中按序加载。
4.6 图片命名约定
assets/images/ 下的图片遵循模式:
{category}-{descriptor}-{css-dimensions}@2x.{extension}
文件名中的尺寸是 CSS 像素(即实际像素分辨率的一半)。例如一张 32x32 像素、以 16x16 CSS 像素显示的图片命名为:
icon-checkmark-green-16x16@2x.png
五、CSS/LESS 规范
5.1 预处理器与构建
LESS 由 Grunt 编译,单一入口是 website/assets/styles/importer.less,它 @import 所有样式。新增的 .less 文件必须被 importer.less import 才会生效。
5.2 选择器约定:优先使用 [purpose='name'] 属性选择器
这是本站最主要的样式方案,传统 CSS 类只是辅助(仅用于 Bootstrap 工具类和状态切换,如 .truncated、.expanded、.loading-spinner):
// EJS 模板中:
// <div purpose="hero-container">...</div>
// LESS 中:
[purpose='hero-container'] {
padding: 80px 0;
}
通过嵌套 [purpose] 选择器把样式限定在区块内。避免使用 Bootstrap 工具类(.d-flex、.justify-content-center、.flex-column 等)做布局和显示控制——这些应写成 [purpose] 选择器,让样式集中在一处、便于后续调整。Bootstrap 的栅格(.row、.col-*)在已建立的地方可以保留,但新工作优先用样式表定义布局。
5.3 页面级作用域
每个页面样式表以页面 ID 选择器为根,防止样式在页面间泄漏:
#pricing {
// 所有页面专属样式嵌套在内
[purpose='page-content'] { ... }
[purpose='hero-text'] { ... }
@media (max-width: 991px) { ... }
}
页面 ID 与 EJS 模板最外层 <div> 的 id 属性一致。部分页面使用 -page 后缀(如 #software-management-page 而非 #software-management):当基础名称会与自动生成的标题 ID 冲突时(例如 markdown 文章里出现 "Software management" 标题会自动得到 id="software-management"),就加上 -page 后缀避让。
5.4 变量与 mixin
- 所有颜色、字体、字重和 mixin 都集中在
mixins-and-variables/; - 必须用变量名而非裸 hex(如
@core-fleet-black,而不是#192147); - 常用 mixin:
.page-container()、.page-content()、.btn-reset()、.fade-in(); - 不要在新代码中使用
@core-vibrant-blue(已废弃); - 主 CTA 按钮使用 Bootstrap 的
btn btn-primary类——hover 时会有伪元素光泽效果(定义在bootstrap-overrides.less)。默认颜色是@core-vibrant-green,可逐页覆盖,关键是光泽效果而非颜色本身。
5.5 页面背景与响应式断点
页面不自行设置区块背景——页面背景是 website/views/layouts/layout.ejs 对应布局样式(layout.less)中定义的渐变,再逐页覆盖。带 <parallax-city> 页脚的页面通常在组件前以专门的 bottom-gradient 区块收尾。
响应式采用嵌套在页面根 ID 内的 max-width 媒体查询:
#my-page {
// 根层级:桌面样式
@media (max-width: 1199px) { /* 大屏桌面微调 */ }
@media (max-width: 991px) { /* 平板:卡片堆叠、内边距收窄 */ }
@media (max-width: 767px) { /* 手机:单列、小字号 */ }
@media (max-width: 575px) { /* 小屏手机:最小内边距 */ }
@media (max-width: 375px) { /* 极小屏:最终调整 */ }
}
5.6 Bootstrap 4 与浏览器兼容
Bootstrap 4 作为基础依赖加载(见 website/assets/dependencies/bootstrap-4/),全局覆盖在 bootstrap-overrides.less,页面级覆盖放在页面 ID 选择器内。
浏览器兼容性下限:主布局 website/views/layouts/layout.ejs 中通过 bowser 检查(LATEST_SUPPORTED_VERSION_BY_USER_AGENT 与 LATEST_SUPPORTED_VERSION_BY_OS 两个对象,位于该文件 1043 行附近),不支持的浏览器会看到整页升级提示;Internet Explorer 被完全封禁。
| 浏览器 | 最低版本 | 选型依据 |
|---|---|---|
| Chrome | 84 | gap 支持 |
| Edge | 84 | gap 支持 |
| Opera | 70 | gap 支持 |
| Safari | 14 | gap 支持 |
| Firefox | 103 | backdrop-filter 支持 |
| iOS | 14 | 支持内嵌播客播放器 |
| Android | 6 | Google 搜索爬虫的 UA |
可以放心使用:Flexbox 与 CSS Grid(含两者上的 gap)、backdrop-filter、CSS 自定义属性、现代 ES2017+ 语法(async/await、对象展开等)。
需要谨慎:Container queries(Safari 14 不支持,需退回媒体查询或等待提高下限)、:has() 选择器(Safari 14 不支持)、以及任何比 ~2021 更新的 CSS 特性——都要对照兼容表逐项核查。
调整下限:修改 layout.ejs 中 LATEST_SUPPORTED_VERSION_BY_USER_AGENT 和 LATEST_SUPPORTED_VERSION_BY_OS 两个对象,并为每个版本加上"为何选这个版本"的注释(说明启用的是哪个 CSS/JS 特性),沿用现有注释风格。跨浏览器检查按手册约定每月通过 BrowserStack 执行一次,以 macOS 最新版 Chrome 为基准。
5.7 LESS 格式规则(源自 .lesshintrc)
{前留一个空格:[purpose='hero'] {而非[purpose='hero']{- 属性
:后留一个空格:padding: 16px而非padding:16px - 避免
!important;确有必要时在同一行加//lesshint-disable-line importantRule - 不强制属性排序
- 零警告策略:
npm run lint必须零 lesshint 警告通过(该脚本定义于 website/package.json)
六、Markdown 内容管线
6.1 内容源文件
Markdown 内容不在 website/ 内,而是分布在仓库根的三个顶级目录:
6.2 构建流程
构建脚本把 Markdown 编译为 HTML(EJS partial):
sails run build-static-content # 编译 markdown → EJS partials
npm run build-for-prod # 完整生产构建(上述步骤 + 资源压缩)
npm run start-dev # 开发模式(先 build-static-content 再进入 console)
其中 build-for-prod 与 start-dev 都在 website/package.json 的 scripts 中定义。编译输出落在 views/partials/built-from-markdown/,元数据在运行时暴露为 sails.config.builtStaticContent。
6.3 元数据与自定义语法
- 元数据:以 HTML
<meta name="X" value="Y">标签内嵌在 markdown 文件中(不是 YAML frontmatter),每种内容类型有各自必需的 meta 标签,参照各目录现有文件; - 自定义语法:
((bubble-text))—— 转换为<bubble type="bubble-text">元素- 块引用 —— 自动应用
purpose="tip"样式 - 代码块 —— 按语言高亮(
js、bash、yaml、mermaid等) - 清单 ——
- [x]/- [ ]渲染为复选框
6.4 构建期强校验(会直接报错)
构建脚本对以下情况抛错:
- 代码块之外出现 Vue 模板语法
{{ }}(与客户端 Vue 冲突); - 相对 markdown 链接缺少
.md扩展名; - markdown 中出现
@fleetdm.com邮箱地址; - 各内容类型缺少必需的 meta 标签。
七、新建页面:必须走生成器,不要手搓
7.1 使用 sails generate page
新建页面一律用 sails generate page <name> 或 sails generate page <folder>/<name>,不要手工创建控制器/视图/脚本/LESS 文件。 生成器会产出正确的 Actions2 形态、视图路径和 locals 样板;手工脚手架容易出错(exits 写错、漏掉 exposeLocalsToBrowser() 页脚等)。
生成器产出四个文件(根级页面用 <name>,嵌套页面带上 <folder>/ 前缀):
api/controllers/view-<name>.js或api/controllers/<folder>/view-<name>.jsviews/pages/<name>.ejs或views/pages/<folder>/<name>.ejsassets/js/pages/<name>.page.js或assets/js/pages/<folder>/<name>.page.jsassets/styles/pages/<name>.less或assets/styles/pages/<folder>/<name>.less
7.2 生成后的四步收尾
- 在 website/config/routes.js 添加路由,并在
locals下配置pageTitleForMeta与pageDescriptionForMeta; - 在 website/assets/styles/importer.less 添加匹配的 import:根级
@import 'pages/<name>.less';,嵌套@import 'pages/<folder>/<name>.less';; - 若页面需要绕过
is-logged-in,更新 website/config/policies.js(已在放行目录如landing-pages/下的无需处理); - 重启开发服务器——后端改动不会热重载。
7.3 复用现有样式、布局与元素
新页面应镜像既有落地页的结构与样式,而不是发明新模式。动手写标记或 LESS 之前,先打开 1–2 个现有落地页(如 website/views/pages/linux-management.ejs、views/pages/landing-pages/replace-jamf.ejs)及其配套样式表,复制区块骨架——hero、feature rows、<scrollable-tweets>、bottom-gradient、<parallax-city> 页脚——以及 [purpose='...'] 命名约定。优先复用既有组件(<logo-carousel>、<animated-arrow-button>、<modal>、视频弹窗模式),不要另造一次性替代品。只有当现有 [purpose] 区块或 LESS 变量确实无法满足时,才引入新的。
7.4 已废弃
不要使用 sails generate landing-page(website/generators/landing-page/ 下的自定义生成器已废弃),落地页同样使用 sails generate page。
八、代码风格与开发命令
8.1 代码风格
| 项 | 规则 |
|---|---|
| 缩进 | 2 空格 |
| 引号 | 单引号(允许模板字符串) |
| 分号 | 始终需要 |
| 相等判断 | 仅严格相等 === / !== |
| 变量/函数 | camelCase |
| 文件/目录 | kebab-case |
8.2 开发命令
npm run start-dev # 启动开发服务器(带实时重载前的构建步骤)
npm run lint # 运行 ESLint + HTMLHint + lesshint
npm run build-for-prod # 编译 markdown、构建并压缩资源
以上脚本在 website/package.json 中均有定义。npm run lint 使用 --max-warnings=0 与 --report-unused-disable-directives 强制执行零警告;HTMLHint 会递归检查 views/ 下所有层级的 .ejs;lesshint 对 assets/styles/ 同样零警告。npm run test 即 lint + custom-tests。
8.3 部署与数据库注意事项
website/README.md 说明了发布模型:改动合并到 main 分支后,若涉及网站代码或网站构建所依赖的内容(查询库、osquery schema、docs、handbook、articles 等),网站会自动重新部署。涉及数据库 schema 变更或依赖外部系统(如 Stripe)的部署,需先在 Heroku 打开维护模式,执行数据迁移脚本后再合并。生产库的"清空"操作需要显式传入 sails_datastores__default__url 环境变量后运行 sails run wipe(务必确认自己在做什么)。
九、给 AI 协作工具的开发提示(CLAUDE.md 的定位)
website/.claude/CLAUDE.md 本身就是为 Claude Code 这类 AI 编程助手编写的仓库级指南,它的存在意味着:在这个仓库里做网站相关改动时,AI 助手与人类开发者共享同一套编码事实——Actions2 格式、[purpose] 选择器优先、生成器优先、零警告 lint、浏览器兼容下限表、markdown 构建校验规则。开发者可以让 AI 工具先读取该文件再开工,从而避免最常见的踩坑:手搓页面四件套、忘加 is-logged-in 放行、漏掉 importer.less import、在 markdown 里写 Vue 双花括号导致构建失败。
结语
从架构上看,website/ 是一个把"营销内容、文档系统、客户计费与第三方代理"统一收敛在 Sails MVC 框架内的复合应用。其工程纪律清晰可循:一切路由集中、一切策略显式放行、一切样式以 [purpose] 属性选择器为骨架、一切新页面走生成器、一切内容构建受校验保护。掌握本文梳理的规范(以 website/.claude/CLAUDE.md 为纲,辅以 website/package.json、website/config/routes.js、website/config/policies.js 等实现佐证),无论是人工开发还是借助 AI 工具,都能在这个网站上安全、一致、可预期地交付改动。