CyberTrain教程

入门:博客

对照 Rails 官方的入门指南:一个包含文章和评论的博客。你将为文章生成脚手架、添加验证、把评论嵌套在文章之下、编写一个在进程内驱动应用的测试,并把应用构建到可部署的 dist/ 目录中。完成后的应用就是 examples/blog,由 CI 在 Linux 和 macOS 上运行测试。

你需要
Ruby 3.2+、cc、make、git、curl、SQLite 3 头文件(Spinel 2026.09.12 会自动为你安装)第 00 步
从这里开始
gem install cybertrain
最终得到
dist/blog

当前位置:准备 Ruby、cc、SQLite

环境要求

为什么CyberTrain 通过 Spinel 编译为 C,所以需要 C 工具链和 SQLite 头文件;cybertrain 命令是一个 gem,所以需要 Ruby。

  • Ruby 3.2+ 和 RubyGems,仅供 cybertrain 命令使用;应用本身从不在 CRuby 上运行。
  • Spinel 2026.09.12,未在其他版本上测试。你无需手动安装:第一个需要它的命令会把该版本构建到 ~/.cybertrain 中(需要几分钟,仅一次),除非 PATH 上已有该版本的 spinel。
  • C 工具链(cc,或 CC 指定的编译器)和 make,以及 git 和 curl,用于构建 Spinel;Spinel 会把每个程序编译为 C。
  • SQLite 3 头文件和库:Debian/Ubuntu 上为 libsqlite3-dev;macOS 上随 Xcode 命令行工具提供。
  • OpenSSL 头文件(libssl-dev)是可选的:没有它们时,Spinel 构建时会不带其 openssl 包,而 CyberTrain 并不使用这个包。

cybertrain doctor 会检查这些工具和头文件以及 Spinel 的安装情况;缺少必需项时,它还会打印适用于你的平台的安装命令。

CyberTrain 如何安装 Spinel某个命令首次需要 spin 时会发生什么

cybertrain new、db、server、build 和 spin 会先查找 Spinel 2026.09.12:首先是 PATH 上已有的该版本的 spinel 和 spin,其次是 ~/.cybertrain/spinel/2026.09.12/ 下的副本;如果都没有,就从 Spinel 的发布标签构建这份副本(git clone、make deps、make -j、make install),只需一次,耗时几分钟。日志位于 ~/.cybertrain/log/spinel-2026.09.12-build.log;如果缺少构建所需的工具,cybertrain 会在开始前指出缺少哪个,并打印适用于你的平台的安装命令。

cybertrain setup 会按需执行同样的操作(--force 会重新构建 ~/.cybertrain 下的副本),并且在正在使用的是它自己的副本时,打印把它加入 PATH 的那一行,方便你自己运行 spin:

终端 — 任意目录shell
export PATH="$HOME/.cybertrain/bin:$PATH"

~/.cybertrain/bin 中存放指向当前所用版本的 spinel 和 spin 链接。即使它不在 PATH 上,cybertrain spin ARGS... 也能在任意目录下用该版本运行 spin。CYBERTRAIN_HOME 可替代 ~/.cybertrain,CYBERTRAIN_SPINEL_HOME=PREFIX 则改用安装在 PREFIX 中的 Spinel(必须是该版本)。PATH 上其他版本的 spinel 永远不会被使用或改动。

没有可以安装的机器?在线试用会在 GitHub Codespace 中打开这个博客,第 02、03、04 和 07 步都已完成。

当前位置:准备 cybertrain CLI

安装 CLI

为什么cybertrain 命令用来创建应用、生成资源脚手架、执行迁移、启动服务和构建。

第一行从 RubyGems 安装它。其余几行是整个教程的概览;下面的步骤会逐条命令讲解。

终端 — 任意目录,之后在 blog/shell
gem install cybertrain
cybertrain new blog          # 首次运行时安装 Spinel 2026.09.12(需要几分钟)
cd blog
cybertrain g scaffold article title:string body:text
cybertrain db migrate
cybertrain server            # http://127.0.0.1:3000
cybertrain build             # 生产环境:dist/blog、dist/public/、dist/storage/、dist/tmp/

cybertrain 命令是纯 Ruby 编写的,gem 在 CRuby(3.2 或更新版本)下运行它;框架和 Spinel 都不在这个 gem 里。它的命令如下(cybertrain help 会把它们打印出来):

  • cybertrain new NAME [--path DIR | --version V | --git URL [--ref R]] [--skip-spin] 在 NAME 中创建应用,然后在其中运行 spin lock 和 spin run gen,需要时会先安装 Spinel。
  • cybertrain generate scaffold NAME field:type ...(别名 g scaffold)为当前目录中的应用添加一个资源。
  • cybertrain db migrate 先生成代码,再执行待处理的迁移,然后再次生成(spin run gen; spin run db -- migrate; spin run gen);cybertrain migration 作为别名保留。
  • cybertrain db COMMAND... 在 spin run gen 之后运行其他任意数据库命令:status、rollback [N](之后会再次生成)、schema:dump、create。
  • cybertrain server [PORT] 先生成代码,然后启动开发服务器(spin run gen; spin run NAME,或 spin run NAME -- PORT;默认端口 3000)。
  • cybertrain build 构建嵌入了 app/views/ 的 NAME,并组装 dist/(二进制文件、public/、storage/、tmp/)。
  • cybertrain spin ARGS... 在任意目录下用锁定版本的 Spinel 运行 spin:cybertrain spin test、cybertrain spin run gen -- --check。
  • cybertrain setup [--force] 把 Spinel 2026.09.12 安装到 ~/.cybertrain 中,除非 PATH 上已有该版本的 spinel;--force 会重新构建 ~/.cybertrain 下的副本。
  • cybertrain doctor 检查 C 工具链、SQLite 头文件和 Spinel 的安装情况。
  • cybertrain version 打印版本(cybertrain 0.2.1);cybertrain help 打印用法。

db、server 和 build 需在应用目录内运行;NAME 是其 spin.toml 中的 [package] name。

当前位置:你编写 blog/

创建应用

为什么你会得到 Rails 的目录约定,外加三个 bin/ 程序:bin/blog.rb(应用)、bin/gen.rb(生成器)和 bin/db.rb(开发时的迁移)。

终端 — 任意目录shell
cybertrain new blog
cd blog

cybertrain new 会写出应用,其 spin.toml 指向与 CLI 相匹配的版本:

spin.tomltoml
[package]
name = "blog"
version = "0.1.0"

[dependencies]
cybertrain = { git = "https://github.com/saeki-mototsune/cybertrain", ref = "v0.2.1" }

然后它会确保 Spinel 已安装(第一次时,这意味着要把 Spinel 2026.09.12 构建到 ~/.cybertrain 中:需要几分钟,仅一次;参见第 00 步),并运行 spin lock(spin 会把框架拉取到其缓存 ~/.cache/spin/packages/ 中,并在 spin.lock 中锁定提交)和 spin run gen,就像 rails new 会运行 bundle install 一样,所以 cybertrain server 可以立即使用。最后它会打印接下来的步骤。框架的任何部分都不会被复制到应用中;请提交 spin.toml 和 spin.lock。

如果 Spinel 或两个 spin 步骤之一失败,已写出的文件会保留,new 以状态码 1 退出,并打印排除原因后需要运行的命令(对于 Spinel:先运行 cybertrain setup,再在新目录中运行 cybertrain spin lock && cybertrain spin run gen)。--skip-spin 只写出文件;--path DIR 改为依赖本地检出的代码,--git URL [--ref R] 依赖位于 URL 的框架(分支或标签 R),--version V 则在 cybertrain 进入 spin-index 后依赖索引中的版本。

应用名使用小写字母、数字和 _,并以字母开头。生成的目录树:

blog/text
blog/
  spin.toml  config/app.rb  config/routes.rb
  db/schema.rb  db/migrate/
  app/controllers/application_controller.rb  app/models/  app/views/  app/helpers/
  public/  bin/blog.rb  bin/gen.rb  bin/db.rb
  gen/  storage/  tmp/  test/

bin/blog.rb 是应用唯一的入口,也就是 spin build blog 编译的程序:./blog 提供服务,./blog migrate 和 ./blog db status 管理数据库。bin/gen.rb 运行生成器(spin run gen),bin/db.rb 在开发期间运行迁移器(cybertrain db migrate 会用到它,因为在第一次迁移生成 gen/models/ 之前,bin/blog.rb 无法编译)。gen/ 中已经有 views.rb,即已提交的、空的内嵌视图表。

ApplicationController 一开始是这样的,因此找不到记录时会通过 rescue_from 映射为 404:

app/controllers/application_controller.rbruby
class ApplicationController < Cybertrain::Controller
  rescue_from Cybertrain::RecordNotFound, with: :record_not_found

  private

  def record_not_found
    render plain: "Not Found", status: :not_found
  end
end

当前位置:你编写 app/, db/migrate/

生成文章脚手架

为什么一条命令写出整套 CRUD,Rails 风格。

终端 — blog/shell
cybertrain generate scaffold article title:string body:text

它会写出一个迁移、一个模型、一个控制器和五个视图(index、show、new、edit、_form),并把 resources :articles 插入 config/routes.rb。字段类型有 string、text、integer、float、boolean、date、datetime 和 references;默认为 string。快速上手中的 cybertrain g scaffold 是同一条命令。

已有文件永远不会被覆盖:每个文件都会报告为 create、identical 或 exist。

当前位置:你编写 config/routes.rb

设置根路由

为什么/ 应当显示文章列表。

config/routes.rbruby
# config/routes.rb
Cybertrain::Routes.draw do
  root "articles#index"
  resources :articles
end

路由在构建时读取:spin run gen 会把它们转换为 gen/routes.rb。cybertrain server、cybertrain db migrate 和 cybertrain build 会替你运行它。

当前位置:你编写 app/models/

添加模型验证

为什么与 Rails 指南一样,手动添加一个长度验证。

app/models/article.rbruby
# app/models/article.rb -- only the first string field gets a default
# presence validation; add anything past that yourself.
class Article
  validates :title, presence: true
  validates :body, presence: true, length: { minimum: 10 }
end

class Article 没有写超类:Cybertrain::Model 超类、列和查找方法都来自 gen/models/article.rb。保存失败时会报告诸如 “Title can't be blank” 和 “Body is too short (minimum is 10 characters)” 这样的消息。

当前位置:你编写 app/controllers/

ArticlesController

为什么完整的 CRUD,连强参数和“先 flash 再重定向”都是 Rails 风格的,只有两处是 Spinel 处处强制要求的。

app/controllers/articles_controller.rbruby
# app/controllers/articles_controller.rb (index/show/edit are as plain as
# Rails' own scaffold; update mirrors create)
class ArticlesController < ApplicationController
  before_action :set_article, only: [:show, :edit, :update, :destroy]

  def new_action   # a `new` method would shadow ArticlesController.new
    @article = Article.new
  end

  def create
    @article = Article.new(article_params)
    if @article.save
      flash[:notice] = "Article was successfully created."
      redirect_to article_path(@article), status: :see_other
    else
      render :new, status: :unprocessable_entity
    end
  end

  private

  def set_article
    @article = Article.find(params[:id])
  end

  def article_params
    params.require(:article).permit(:title, :body)
  end
end

脚手架写出的完整控制器还包含 index、show、edit、update 和 destroy;上面的节选省略了它们。在 index 中写的是 @articles = Article.all.to_a:其中的 .to_a 是必需的。

实例变量是通过对 @ivar = 的词法扫描找到的,所以新增实例变量后要重新运行 cybertrain spin run gen;开发服务器会替你完成这一步。

当前位置:先构建时,后运行时 gen/ → build/bin/blog

生成、迁移、运行

为什么把 schema 和路由转换成 gen/,创建 SQLite 数据库,启动服务器。

终端 — blog/shell
cybertrain db migrate    # spin run gen; spin run db -- migrate; spin run gen
cybertrain server        # http://127.0.0.1:3000;修改 Ruby 后会重新构建,视图无需重新构建即可重新加载

spin run gen 读取 db/schema.rb 和 config/routes.rb,扫描 app/ 并写入 gen/;spin run db -- migrate 执行 db/migrate/*.rb 并重写 db/schema.rb。顺序很重要:第一次 spin run gen 把新迁移放进 gen/migrations.rb(bin/db.rb 会 require 它),第二次则根据重写后的 db/schema.rb 构建模型。

cybertrain server 会打印它运行的每条 spin 命令,然后是应用的启动横幅。第一次启动需要编译应用,所以要花一些时间:

输出 — cybertrain servertext
=> Booting cybertrain 0.2.1
=> development environment (1 worker)
=> Watching app/, config/ and db/schema.rb for changes
* Listening on http://127.0.0.1:3000
Use Ctrl-C to stop
  • 修改 schema、路由,或者控制器、模型的回调和实例变量之后,重新运行 cybertrain spin run gen 并提交 gen/。开发服务器会替你完成这一步。
  • 用 cybertrain server 4000(或 PORT=4000)更改端口。
  • spin run db 要求在参数前加上 --,cybertrain db 会替你加上;cybertrain db status 和其他数据库命令也是如此。

当前位置:你编写 db/migrate/, app/

生成评论脚手架

为什么references 类型会为你生成外键,关联正是由它推导出来的。

终端 — blog/shell
cybertrain generate scaffold comment commenter:string body:text article:references
cybertrain db migrate    # 脚手架已为 comments 表写好了迁移

article:references 会添加 article_id 列、索引和外键——这足以让 Article/Comment 获得关联;不需要写 has_many/belongs_to,spin run gen 会从外键中读出关联。生成的 schema:

db/schema.rb(节选)ruby
  s.create_table "comments" do |t|
    t.string "commenter"
    t.text "body"
    t.integer "article_id", null: false
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
  end

  s.add_index "comments", ["article_id"], name: "index_comments_on_article_id"

  s.add_foreign_key "comments", "articles", column: "article_id"

脚手架会插入一行独立的 resources :comments,下一步会替换它。

当前位置:你编写 config/routes.rb

嵌套路由

为什么Rails 指南把评论嵌套在文章之下。

config/routes.rbruby
Cybertrain::Routes.draw do
  root "articles#index"

  # A nested block takes the mapper explicitly (Spinel's instance_eval
  # trampoline only rewires `self` for the outermost block): `articles.resources`.
  resources :articles do |articles|
    articles.resources :comments, only: [:create, :destroy]
  end
end

当前位置:你编写 app/controllers/

CommentsController

为什么只有两个动作可以访问,而父文章取自 URL(params[:article_id]),而不是 :id。

app/controllers/comments_controller.rbruby
class CommentsController < ApplicationController
  before_action :set_article

  def create
    @comment = Comment.new(comment_params)
    @comment.article_id = @article.id
    if @comment.save
      flash[:notice] = "Comment was successfully created."
    else
      flash[:alert] = "Comment could not be saved: #{@comment.errors.full_messages.join(", ")}"
    end
    redirect_to article_path(@article), status: :see_other
  end

  def destroy
    @comment = Comment.where(article_id: @article.id).find(params[:id])
    @comment.destroy
    flash[:notice] = "Comment was successfully destroyed."
    redirect_to article_path(@article), status: :see_other
  end

  private

  def set_article
    @article = Article.find(params[:article_id])
  end

  def comment_params
    params.require(:comment).permit(:commenter, :body)
  end
end

然后给评论模型添加验证:

app/models/comment.rbruby
class Comment
  validates :commenter, presence: true
  validates :body, presence: true
end

这里的 Article#comments 是一个 Array,所以 Rails 的 @article.comments.find(params[:id]) 要写成 Comment.where(article_id: @article.id).find(params[:id])。不属于该文章的评论会返回 404。

当前位置:运行时 app/views/,开发时从磁盘读取,由 cybertrain build 嵌入

视图与局部视图

为什么文章页面列出它的评论,并在末尾放一个绑定到 [@article, @comment] 的表单。

首先,删除现在已无法访问的评论视图 index、show、new 和 edit,并在 ArticlesController#show 中提供一个空白的 @comment:

app/controllers/articles_controller.rb(节选)ruby
  def show
    @comment = Comment.new
  end

然后在文章页面上渲染评论和表单:

app/views/articles/show.html.erb · app/views/comments/_form.html.erberb
<%# app/views/articles/show.html.erb %>
<% @article.comments.each do |comment| %>
  <%= render "comments/comment", comment: comment %>
<% end %>
<%= render "comments/form" %>

<%# app/views/comments/_form.html.erb %>
<%= form_with(model: [@article, @comment]) do |f| %>
  <%= f.label :commenter %> <%= f.text_field :commenter %>
  <%= f.label :body %> <%= f.text_area :body %>
  <%= f.submit %>
<% end %>

评论的局部视图,与示例中的一致:

app/views/comments/_comment.html.erberb
<%# locals: (comment:) %>
<div class="comment">
  <p>
    <strong>Commenter:</strong>
    <%= comment.commenter %>
  </p>

  <p>
    <strong>Comment:</strong>
    <%= comment.body %>
  </p>

  <%= button_to "Destroy comment", [@article, comment], method: :delete %>
</div>

button_to ... [@article, comment] 和 form_with(model: [@article, @comment]) 都通过同一条 [parent, child] 规则解析:新的子对象路由到嵌套集合(article_comments_path),已持久化的子对象路由到嵌套成员(article_comment_path)——这些路由都由上面的嵌套 resources 块生成到 gen/routes.rb 中。

当前位置:构建时 cybertrain spin test

测试

为什么minitest 和 RSpec 通过反射查找测试,而 AOT 编译器做不到这一点,因此 CyberTrain 提供了 Cybertrain::Test。

模型测试

test/articles.rb(片段)ruby
require_relative "support/blog_test"   # the app, BLOG and Cybertrain::Test

test "title must be present" do
  article = Article.new(body: "a body long enough to pass length")
  refute article.save
  assert_includes article.errors.full_messages, "Title can't be blank"
end

断言:assert、refute、assert_equal、assert_nil、assert_includes、assert_raises("Class") { }、flunk。示例中还用到了 assert_response 和 assert_redirected_to。

完整的 HTTP 流程

test/articles.rb(片段)ruby
require_relative "support/blog_test"   # the app, BLOG and Cybertrain::Test

test "POST /articles creates an article and redirects to it" do
  client = Cybertrain::Test::Client.new(BLOG)
  client.get("/articles/new")
  res = client.post("/articles", { "authenticity_token" => BlogTest.form_token(client),
                                   "article[title]" => "Hello Rails",
                                   "article[body]" => "I am on Rails! This is my first article." })
  assert_redirected_to res, BlogTest.article_path(Article.last)
  assert_response res, :see_other
end

Cybertrain::Test::Client 在进程内(不经过 socket)驱动应用,并带有 cookie jar,所以会话和 CSRF 的行为与在浏览器中一致。不带令牌的请求会得到 403。

运行测试

终端 — blog/shell
cybertrain spin test

cybertrain spin test 用锁定版本的 Spinel 运行 spin test;如果工具链已在 PATH 上(第 00 步),直接运行 spin test 效果相同。它会把每个 test/*.rb 编译成独立的程序,并将其输出(stdout 和 stderr 合在一起)与已提交的快照 test/<name>.rb.expected 进行比对。test/support/ 中的文件会被 require,而不会被运行。要写入快照:

终端 — blog/shell
cybertrain spin test --regen test/<name>.rb
rm -f build/test/<name>; cybertrain spin test test/<name>.rb; [ -x build/test/<name> ] && ./build/test/<name> > test/<name>.rb.expected 2>&1

--regen 根据 CRuby 的输出写入快照。通过 FFI 访问 SQLite 的测试无法在 CRuby 下运行,所以它们的快照来自编译后的二进制文件(上面的第二行)。只要快照过时,cybertrain spin test 就会以非零状态退出,所以它后面跟的是 ; 而不是 &&;[ -x ] 检查会在编译失败时跳过捕获,从而保留旧快照。无法在 CRuby 下运行的测试需要先有这个快照,cybertrain spin test 才能通过;其他没有快照的测试会与 CRuby 的输出进行比对。

当前位置:运行时 dist/blog

为部署而构建

为什么一条命令就能把应用构建到可部署的 dist/ 目录中,所有视图都已嵌入。

终端 — blog/shell
cybertrain build

这会写出 dist/:嵌入了所有视图的应用二进制文件,以及放在它旁边、供它使用的文件。它会运行 spin run gen -- --embed-views、spin build blog 和 spin run gen(最后这一步会恢复已提交的、空的 gen/views.rb,即使在失败或按下 Ctrl-C 之后也是如此),然后组装出:

  • dist/blog编译后的框架、控制器、模型和模板解释器,并嵌入了所有视图
  • dist/public/静态资源和错误页面,也可以交给反向代理来提供
  • dist/storage/创建时为空;默认存放 SQLite 文件(重新构建永远不会清空它)
  • dist/tmp/创建时为空(重新构建永远不会清空它)

把 dist/ 复制到操作系统和 CPU 都相同的机器上(该二进制文件链接系统的 SQLite),然后在那里运行。构建出的二进制文件默认使用生产环境(可用 CYBERTRAIN_ENV 覆盖)。设置 CYBERTRAIN_SECRET_KEY_BASE;示例如下:

终端 — 生产主机shell
cd dist
export CYBERTRAIN_SECRET_KEY_BASE="$(openssl rand -hex 32)"
./blog migrate && ./blog

./blog migrate 执行编译进二进制文件的迁移,./blog db status 显示它们的状态;PORT 设置端口。重启前后请保持密钥不变:更改它会使所有会话失效。

Cybertrain::Config 首先读取环境变量;config/app.rb 中的设置会覆盖它们:

变量属性默认值
CYBERTRAIN_ENVenv"development";由 cybertrain build 构建的二进制文件中为 "production"(也可以是 "test")
PORTport3000
CYBERTRAIN_DATABASEdatabase_pathstorage/#{env}.sqlite3
CYBERTRAIN_SECRET_KEY_BASEsecret_key_base生产环境中必需;开发/测试环境会自动生成一个并写入 tmp/secret_key
SPINEL_WORKERSworkers1
CYBERTRAIN_HOSThost"127.0.0.1";0.0.0.0 监听所有网络接口(容器)
CYBERTRAIN_SESSION_SAME_SITEsession_same_site"Lax";也可以是 Strict 或 None(None 总会加上 Secure);其他任何值都会让服务器在启动时停止
CYBERTRAIN_SESSION_PARTITIONEDsession_partitionedfalse;1 或 true 会加上 Partitioned(以及 Secure)
config/app.rbruby
# config/app.rb
Cybertrain.configure do |c|
  c.port = 3000
  c.workers = 1
end
  • 服务器使用纯 HTTP/1.1:如需 TLS 和 HTTP/2,请在前面放 nginx 或 Caddy。生产环境中会话 cookie 带有 Secure,所以要通过 HTTPS 提供服务(仅当生产环境确实使用纯 http 时,才在 config/app.rb 中设置 c.session_secure = false)。
  • 没有 CYBERTRAIN_SECRET_KEY_BASE 时,生产环境会拒绝启动。
  • 在生产环境中,异常会返回 500;而裸错误响应(路由器的纯文本 404、head :not_found、render plain:)在 public/<status>.html 存在时会被该文件替换;cybertrain new 会写出 404.html 和 500.html。
  • SIGTERM 会让服务器停止接受新连接,让正在处理的请求完成,并在它们完成后退出,或在服务器的 drain_timeout(10 秒)之后退出。
  • 监视器和重新构建循环永远不会在生产环境中运行。正在运行的 cybertrain server 会在 cybertrain build 持有 tmp/cybertrain-build.lock 期间跳过重新构建。

须知

为什么值得提前了解的限制和特殊之处,免得到时措手不及。

  • 会话只能保存 String,存放在 HMAC 签名的 cookie 中。
  • 仅支持 SQLite,通过 Spinel 的 FFI 访问。
  • 没有回溯(backtrace):错误只显示类、消息、请求行以及模板名/行号。
  • 视图中拼错的属性会渲染为空白,而不会抛出异常;这是一个已知的不足。
  • 关联只来自 schema 中的外键;不支持 has_many :through、includes、pluck、dependent:、枚举、STI 或多态关联。
  • 不支持 namespace、respond_to/格式、constraints 或 mount;render json: 只有最基本的功能。
  • 在脚手架生成的模型中,只有第一个字符串字段会获得默认的存在性验证。
  • 生成器对 app/controllers 和 app/models 使用词法扫描(正则表达式),而不是解析器。
  • 视图在构建时嵌入:即使只修改了视图,也需要运行 cybertrain build 并重启,改动才能到达生产环境。
  • 在与运行机器相同的操作系统和 CPU 上构建 dist/:该二进制文件链接系统的 SQLite。
  • SPINEL_WORKERS 默认为 1。
  • 只测试过 Spinel 2026.09.12。

与 Rails 的差异

为什么README 中的完整表格,集中在一处。

Rails 与 CyberTrain 的差异(README 中的完整表格)
RailsCyberTrain
rails console没有控制台——Spinel 没有 eval
修改代码,正在运行的应用会自动加载Ruby 代码需要重新构建;开发时 cybertrain server 会替你完成(重新构建、停止、execv)
修改视图,无需重新加载相同——开发时视图在每次请求时从磁盘解析
def newdef new_action(new 会遮蔽 Klass.new(ctx))
before_action { do_thing }(隐式 self)before_action { |c| c.do_thing }——无法对存储的块使用 instance_exec,因此回调需要显式接收控制器/记录
resources :posts do resources :comments endresources :posts do |posts| posts.resources :comments end——嵌套块需要显式接收 mapper
ERB 编译为方法;可以使用任何对象、任何方法按照固定的语法/分派表解释执行
会话可以保存任何经 marshal 序列化的对象会话值只能是 String,使用 HMAC 签名的 cookie
多种数据库适配器仅支持 SQLite,通过 FFI 访问
has_many :through、includes、pluck、dependent:、枚举、STI、多态关联未实现;关联只来自 schema 中的外键
namespace、格式/respond_to、constraints、mount未实现——只有扁平的名称,仅支持 render json:
异常时显示完整回溯类、消息、请求行、模板名/行号——Spinel 不提供回溯
Rack 及其中间件,以及 Gemfile 中的任何 gem不兼容 Rack;只有一小组固定的中间件;使用 Spinel 自己的 spin-index,仅限能在其 Ruby 子集下编译的内容
minitest / RSpecCybertrain::Test——基于反射的测试运行器无法在 AOT 编译下工作
认证、邮件、后台任务、ActionCable、资源管道、i18n未构建(不在 MVP 范围内)

此表并不完整。

下一步