Fleet 官网开发指南:Sails.js + Parasails 网站工程全解析(fleet 仓库 website 目录)

原创2026-09-20 15:35:421,950 阅读
文章标签:后端前端企业应用应用安全运维GitOps

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" 视频按钮,需要三处配合:

  1. 页面脚本:data 中加 modal: '',并实现 clickOpenVideoModal 与 closeModal 方法;
  2. 模板:加入 <modal purpose="video-modal"> 并内嵌 YouTube iframe;
  3. 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/ 内,而是分布在仓库根的三个顶级目录:

  • docs/ —— 技术文档
  • articles/ —— 博客、案例研究、白皮书、对比文章
  • handbook/ —— 公司内部手册

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>.js
  • views/pages/<name>.ejs 或 views/pages/<folder>/<name>.ejs
  • assets/js/pages/<name>.page.js 或 assets/js/pages/<folder>/<name>.page.js
  • assets/styles/pages/<name>.less 或 assets/styles/pages/<folder>/<name>.less

7.2 生成后的四步收尾

  1. 在 website/config/routes.js 添加路由,并在 locals 下配置 pageTitleForMeta 与 pageDescriptionForMeta;
  2. 在 website/assets/styles/importer.less 添加匹配的 import:根级 @import 'pages/<name>.less';,嵌套 @import 'pages/<folder>/<name>.less';;
  3. 若页面需要绕过 is-logged-in,更新 website/config/policies.js(已在放行目录如 landing-pages/ 下的无需处理);
  4. 重启开发服务器——后端改动不会热重载。

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 工具,都能在这个网站上安全、一致、可预期地交付改动。

登录后查看全文
fleet