跳转到主要内容

Redwood和Blitz是两个即将出现的全栈元框架,它们提供了创建SPAs、服务器端渲染页面和静态生成内容的工具,并提供了生成端到端支架的CLI。我一直在等待一个有价值的Rails JavaScript替代品,谁知道什么时候。这篇文章是对两者的概述,虽然我对Redwood给予了更多的广度(因为它与Rails有很大的不同),但我个人更喜欢Blitz。

由于这篇文章的篇幅很长,下面我们为草率的文章提供了一个对照表。

full-stack

先来点历史

如果你在2010年代开始从事网络开发工作,你可能甚至没有听说过Ruby on Rails,尽管它为我们提供了Twitter、GitHub、Urban Dictionary、Airbnb和Shopify等应用程序。与当时的web框架相比,使用它简直轻而易举。Rails打破了web技术的模式,成为一个高度固执己见的MVC工具,强调使用众所周知的模式,如约定而非配置和DRY,并添加了一个强大的CLI,创建了从模型到要渲染的模板的端到端支架。许多其他框架都建立在它的思想之上,比如用于Python的Django、用于PHP的Laravel或用于Node.js的Sails。因此,可以说,它是一种与LAMP堆栈一样有影响力的技术。

然而,自2004年创建以来,RubyonRails的名气已经消退了不少。当我在2012年开始使用Node.js时,Rails的辉煌时代已经结束了。Twitter——建立在Rails之上——因在2007年至2009年间频繁展示其失败鲸而臭名昭著。这在很大程度上归因于Rails缺乏可扩展性,至少根据我的过滤器泡沫中的口碑。当Twitter转向Scala时,这种对Rails的抨击得到了进一步加强,尽管当时他们并没有完全抛弃Ruby。

Rails(以及Django)的可伸缩性问题越来越受到媒体的广泛报道,这也与Web的转型相吻合。浏览器中运行的JavaScript越来越多。网页变成了高度互动的网络应用程序,然后是SPAs。Angular.js在2010年问世时也彻底改变了这一点。我们不希望服务器通过组合模板和数据来呈现整个网页,而是希望使用API并通过客户端DOM更新来处理状态变化。

因此,全栈框架失宠了。开发在编写后端API和前端应用程序之间分离。到那时,这些应用程序可能也意味着Android和iOS应用程序,所以放弃服务器端渲染的HTML字符串,以我们所有客户都可以使用的方式发送数据是有意义的。

用户体验模式也得到了发展。仅仅在后端验证数据是不够的,因为用户在填写越来越大的表格时需要快速反馈。因此,我们的生活变得越来越复杂:我们需要复制输入验证和类型定义,即使我们同时编写JavaScript。随着monoreos的广泛采用,后者变得更加简单,因为在整个系统中共享代码变得更加容易,即使它是作为微服务的集合构建的。但是monoretos带来了自己的复杂性,更不用说分布式系统了。

自2012年以来,我一直有一种感觉,无论我们解决什么问题,都会产生20个新问题。你可以说这被称为“进步”,但也许只是出于浪漫主义,或者渴望过去的事情变得更简单,我已经等了一段时间的“Node.jsonRails”了。Meteor看起来可能是它,但它很快就失宠了,因为社区大多认为它对MVP有好处,但不可扩展…Rails问题再次出现,但在产品生命周期的早期阶段就崩溃了。我必须承认,我甚至从来没有抽出时间去尝试。

然而,我们似乎正在缓慢而稳定地到达那里。Angular 2+采用了代码生成器ála Rails和Next.js,所以看起来可能是类似的东西。Next.js获得了API路由,从而可以使用SSR处理前端并编写后端API。但它仍然缺乏强大的CLI生成器,也与数据层无关。总的来说,要达到Rails的功率水平,等式中仍然缺少一个好的ORM。至少最后一点似乎是解决了棱镜现在的存在。

等一下。我们有代码生成器、成熟的后端和前端框架,最后还有一个好的ORM。也许我们已经把所有的谜题都准备好了?大概但首先,让我们从JavaScript出发,看看另一个生态系统是否成功地进一步发展了Rails的遗产,以及我们是否可以从中学习。

进入Elixir和Phoenix

Elixir是一种建立在Erlang的BEAM和OTP之上的语言,它提供了一个基于actor模型和进程的良好并发模型,与防御性编程相比,由于“让它崩溃”的哲学,这也使得错误处理变得容易。它也有一个很好的,Ruby启发的语法,但仍然是一种优雅的函数式语言。

Phoenix是在Elixir功能的基础上构建的,首先是对Rails的简单重新实现,具有强大的代码生成器、数据映射工具包(想想ORM)、良好的约定和总体良好的开发体验,并具有OTP的内置可扩展性。

是 啊到目前为止,我甚至都不会扬起眉毛。随着时间的推移,Rails的可扩展性越来越强,现在我可以从编写JavaScript的框架中获得我需要的大部分东西,即使所有这些都是自己动手完成的。无论如何,如果我需要一个交互式浏览器应用程序,我无论如何都需要使用React(或者至少Alpine.js)之类的东西来完成。

天哪,你甚至无法想象之前的说法有多错误。虽然Phoenix是Elixir中一个全面的Rails重新实现,但它有一个优点:使用其名为LiveView的超级功能,你的页面可以完全在服务器端渲染,同时进行交互。当您请求LiveView页面时,服务器端会预先呈现初始状态,然后构建WebSocket连接。状态存储在服务器的内存中,客户端发送事件。后端更新状态,计算diff,并通过高度压缩的变更集发送到UI,客户端JS库相应地更新DOM。

我过分简化了Phoenix的能力,但这一部分已经太长了,所以一定要自己看看!

我们绕了一段路来看看最好的,如果不是最好的全栈框架的话。因此,当涉及到全栈JavaScript框架时,至少实现Phoenix所取得的成就才是有意义的。因此,我想看到的是:

  1. 一个CLI,可以生成数据模型或模式,以及它们的控制器/服务和相应的页面
  2. 像Prisma这样强大的ORM
  3. 服务器端呈现但交互式的页面,使其变得简单
  4. Cross-platform可用性:让我很容易为浏览器创建页面,但我希望能够通过添加单行代码来创建一个API端点,以JSON响应。
  5. 把这整件事拼凑在一起

话虽如此,让我们看看红木还是闪电战是我们一直在等待的框架。

什么是RedwoodJS?

Redwood将自己定位为创业公司的全栈框架。这是每个人都在等待的框架,如果不是自切片面包发明以来最好的东西的话。故事结束了,这篇博文结束了。

至少根据他们的教程。

在阅读这些文件时,我有一种自吹自擂的过度自信,我个人觉得很难阅读。事实上,与通常枯燥的技术文本相比,它的语气更轻松,这是一个值得欢迎的变化。尽管如此,当文本远离对事物的安全、客观描述时,它也会陷入与读者品味相匹配或冲突的领域。

就我而言,我很欣赏这个选择,但不能享受结果。

尽管如此,本教程还是值得一读的。这是非常彻底和有益的。结果也是值得的……好吧,无论你在阅读时有什么感受,因为红木也很适合合作。它的代码生成器做了我期望它做的事情。事实上,它做的比我预期的还要多,因为它不仅用于设置应用程序框架、模型、页面和其他支架,而且非常方便。它甚至将您的应用程序设置为部署到不同的部署目标,如AWS Lambdas、Render、Netlify、Vercel。

说到列出的部署目标,我有一种感觉,Redwood有点强烈地推动我使用无服务器解决方案,Render是列表中唯一一个拥有持续运行服务的解决方案。我也喜欢这个想法:如果我有一个固执己见的框架,它肯定会对如何以及在哪里部署有自己的意见。当然,只要我可以自由表达不同意见。

但Redwood不仅对部署,而且对如何开发网络应用程序有着强烈的意见,如果你不同意这些意见,那么…

我希望您使用GraphQL

让我们来看看一个新生成的Redwood应用程序。红木有自己的入门套件,所以我们不需要安装任何东西,我们可以直接创建骨架。

$ yarn create redwood-app --ts ./my-redwood-app

如果您想使用纯JavaScript,可以省略--ts标志。

当然,您可以立即启动开发服务器,并看到您已经使用yarn redwood dev获得了一个不错的UI。需要注意的一点是,在我看来,这是非常值得称赞的,那就是您不需要全局安装redwood CLI。相反,它始终保持项目本地性,使协作更加容易。

现在,让我们看看目录结构。

my-redwood-app
├── api/
├── scripts/
├── web/
├── graphql.config.js
├── jest.config.js
├── node_modules
├── package.json
├── prettier.config.js
├── README.md
├── redwood.toml
├── test.js
└── yarn.lock

我们可以看到常规的beautier.config.js、jest.config.js,还有一个redwood.toml用于配置开发服务器的端口。我们有一个api和web目录,用于使用yarn工作区将前端和后端分离到各自的路径中。

但是等一下,我们还有一个graphql.config.js!没错,使用Redwood,您将编写GraphQL API。在引擎盖下,Redwood在前端使用Apollo,在后端使用Yoga,但大多数都可以使用CLI轻松完成。然而,GraphQL也有其缺点,如果你不同意这种权衡,那么,你对Redwood的运气就太差了。

让我们深入了解API。

my-redwood-app
├── api
│   ├── db
│   │   └── schema.prisma
│   ├── jest.config.js
│   ├── package.json
│   ├── server.config.js
│   ├── src
│   │   ├── directives
│   │   │   ├── requireAuth
│   │   │   │   ├── requireAuth.test.ts
│   │   │   │   └── requireAuth.ts
│   │   │   └── skipAuth
│   │   │       ├── skipAuth.test.ts
│   │   │       └── skipAuth.ts
│   │   ├── functions
│   │   │   └── graphql.ts
│   │   ├── graphql
│   │   ├── lib
│   │   │   ├── auth.ts
│   │   │   ├── db.ts
│   │   │   └── logger.ts
│   │   └── services
│   ├── tsconfig.json
│   └── types
│       └── graphql.d.ts
...

在这里,我们可以看到更多与后端相关的配置文件,以及tsconfig.json的首次亮相。

  • api/db/:这里是我们的schema.prisma,它告诉红木当然使用prisma。src/dir存储了我们的大部分逻辑。
  • directives/:存储我们的graphql模式指令。
  • functions/:以下是必要的lambda函数,这样我们就可以将应用程序部署到无服务器云解决方案中(还记得STRONG的意见吗?)。
  • graphql/:这里驻留了我们的gql模式,它可以从我们的db模式自动生成。
  • lib/:我们可以在这里保留更通用的助手模块。
  • services/:如果我们生成一个页面,我们将有一个services/目录,它将保存我们实际的业务逻辑。

这很好地映射到一个分层体系结构,其中GraphQL解析器充当我们的控制器层。我们有自己的服务,我们可以在Prisma上创建一个存储库或dal层,或者如果我们能保持简单,那么就直接将其用作我们的数据访问工具。

到目前为止还不错。让我们转到前端。

my-redwood-app
├── web
│   ├── jest.config.js
│   ├── package.json
│   ├── public
│   │   ├── favicon.png
│   │   ├── README.md
│   │   └── robots.txt
│   ├── src
│   │   ├── App.tsx
│   │   ├── components
│   │   ├── index.css
│   │   ├── index.html
│   │   ├── layouts
│   │   ├── pages
│   │   │   ├── FatalErrorPage
│   │   │   │   └── FatalErrorPage.tsx
│   │   │   └── NotFoundPage
│   │   │       └── NotFoundPage.tsx
│   │   └── Routes.tsx
│   └── tsconfig.json
...

从配置文件和package.json中,我们可以推断出我们处于不同的工作区中。目录布局和文件名也向我们表明,这不仅仅是一个重新打包的Next.js应用程序,而是一个完全针对Redwood的应用程序。

Redwood的路由器深受React router的启发。我觉得这有点烦人,因为在我看来,Next.js中基于dir结构的目录更方便。

然而,Redwood的缺点是它不支持服务器端渲染,只支持静态站点生成。没错,SSR是它自己的蠕虫,虽然目前你可能想避免它,即使在使用Next时也是如此,但随着服务器组件的引入,这种情况可能很快就会改变,看看Redwood会如何反应会很有趣(双关语并非有意)。

另一方面,Next.js因其使用布局的巧妙方式而臭名昭著(不过很快就会改变),而Redwood则按照您的预期进行处理。在Routes.tsx中,您只需将Routes包装在Set块中,就可以告诉Redwood您想在给定的路由中使用什么布局,并且永远不要再考虑它。

import { Router, Route, Set } from "@redwoodjs/router";
import BlogLayout from "src/layouts/BlogLayout/";

const Routes = () => {
  return (
    <Router>
      <Route path="/login" page={LoginPage} name="login" />
      <Set wrap={BlogLayout}>
        <Route path="/article/{id:Int}" page={ArticlePage} name="article" />
        <Route path="/" page={HomePage} name="home" />
      </Set>
      <Route notfound page={NotFoundPage} />
    </Router>
  );
};

export default Routes;

请注意,您不需要导入页面组件,因为它是自动处理的。为什么我们不能像Nuxt 3那样自动导入布局呢?打败了我。

另一件需要注意的事情是/article/{id:Int}部分。如果你从路径变量中获得整数id,那么你总是需要确保转换它们的日子已经一去不复返了,因为Redwood可以为你自动转换它们,只要你提供必要的类型提示。

现在是了解SSG的好时机。NotFoundPage可能没有任何动态内容,所以我们可以静态生成它。只要加上预浇剂,你就很好了。

const Routes = () => {
  return (
    <Router>
      ...
      <Route notfound page={NotFoundPage} prerender />
    </Router>
  );
};

export default Routes;

您还可以告诉Redwood,您的某些页面需要身份验证。如果未经身份验证的用户尝试请求,则应重定向。

import { Private, Router, Route, Set } from "@redwoodjs/router";
import BlogLayout from "src/layouts/BlogLayout/";

const Routes = () => {
  return (
    <Router>
      <Route path="/login" page={LoginPage} name="login" />
      <Private unauthenticated="login">
        <Set wrap={PostsLayout}>
          <Route
            path="/admin/posts/new"
            page={PostNewPostPage}
            name="newPost"
          />
          <Route
            path="/admin/posts/{id:Int}/edit"
            page={PostEditPostPage}
            name="editPost"
          />
        </Set>
      </Private>
      <Set wrap={BlogLayout}>
        <Route path="/article/{id:Int}" page={ArticlePage} name="article" />
        <Route path="/" page={HomePage} name="home" />
      </Set>
      <Route notfound page={NotFoundPage} />
    </Router>
  );
};

export default Routes;

当然,你也需要保护你的突变和查询。因此,请确保使用预先生成的@requireAuth来附加它们。

Redwood的另一个好处是,您可能不想使用本地身份验证策略,而是将用户管理问题外包给身份验证提供商,如Auth0或Netlify Identity。Redwood的CLI可以自动安装必要的软件包并生成所需的样板文件。

然而,看起来奇怪的是,至少对于本地身份验证来说,客户端对服务器进行了多次往返以获取令牌。更具体地说,每个currentUser或isAuthenticated调用都会命中服务器。

红木的前端美食

我非常喜欢与红木合作的两件事:细胞和形式。

单元是一个获取和管理自己的数据和状态的组件。您可以定义它将使用的查询和突变,然后导出一个用于呈现组件的Loading、Empty、Failure和Success状态的函数。当然,您可以使用生成器为您创建必要的样板。

生成的单元格如下所示:

import type { ArticlesQuery } from "types/graphql";
import type { CellSuccessProps, CellFailureProps } from "@redwoodjs/web";

export const QUERY = gql`
  query ArticlesQuery {
    articles {
      id
    }
  }
`;

export const Loading = () => <div>Loading...</div>;

export const Empty = () => <div>Empty</div>;

export const Failure = ({ error }: CellFailureProps) => (
  <div style={{ color: "red" }}>Error: {error.message}</div>
);

export const Success = ({ articles }: CellSuccessProps<ArticlesQuery>) => {
  return (
    <ul>
      {articles.map((item) => {
        return <li key={item.id}>{JSON.stringify(item)}</li>;
      })}
    </ul>
  );
};

然后,您只需像导入任何其他组件一样导入并使用它,例如,在页面上。

import ArticlesCell from "src/components/ArticlesCell";

const HomePage = () => {
  return (
    <>
      <MetaTags title="Home" description="Home page" />
      <ArticlesCell />
    </>
  );
};

export default HomePage;

然而如果您在带有单元格的页面上使用SSG,或者实际使用任何动态内容,那么只有它们的加载状态会得到预呈现,这并没有多大帮助。没错,如果你选择红木,就没有getStaticProps。

红木的另一个好处是它简化了表单处理,尽管它们的框架方式给我留下了一点不好的味道。但首先,漂亮的部分。

import { Form, FieldError, Label, TextField } from "@redwoodjs/forms";

const ContactPage = () => {
  return (
    <>
      <Form config={{ mode: "onBlur" }}>
        <Label name="email" errorClassName="error">
          Email
        </Label>
        <TextField
          name="email"
          validation={{
            required: true,
            pattern: {
              value: /^[^@]+@[^.]+\..+$/,
              message: "Please enter a valid email address",
            },
          }}
          errorClassName="error"
        />
        <FieldError name="email" className="error" />
      </Form>
    </>
  );
};

TextField组件验证属性期望传递一个对象,该对象带有一个模式,可以根据该模式验证所提供的输入值。

errorClassName可以在验证失败时轻松设置文本字段及其标签的样式,例如将其变为红色。验证消息将打印在FieldError组件中。最后,config={{mode:'onBlur'}}}neneneba告诉表单在用户离开每个字段时验证它们。

唯一破坏快乐的是,这种模式与Phoenix提供的模式惊人地相似。别误会我的意思。复制其他框架中的好东西是非常好的,甚至是有益的。但我已经习惯了在到期时表达敬意。当然,教程的作者完全有可能不知道这种模式的灵感来源。如果是这样的话,请告诉我,我很乐意向医生打开一个拉取请求,并添加简短的礼貌句子。

但让我们继续看一下整个工作形式。

import { MetaTags, useMutation } from "@redwoodjs/web";
import { toast, Toaster } from "@redwoodjs/web/toast";
import {
  FieldError,
  Form,
  FormError,
  Label,
  Submit,
  SubmitHandler,
  TextAreaField,
  TextField,
  useForm,
} from "@redwoodjs/forms";

import {
  CreateContactMutation,
  CreateContactMutationVariables,
} from "types/graphql";

const CREATE_CONTACT = gql`
  mutation CreateContactMutation($input: CreateContactInput!) {
    createContact(input: $input) {
      id
    }
  }
`;

interface FormValues {
  name: string;
  email: string;
  message: string;
}

const ContactPage = () => {
  const formMethods = useForm();

  const [create, { loading, error }] = useMutation<
    CreateContactMutation,
    CreateContactMutationVariables
  >(CREATE_CONTACT, {
    onCompleted: () => {
      toast.success("Thank you for your submission!");
      formMethods.reset();
    },
  });

  const onSubmit: SubmitHandler<FormValues> = (data) => {
    create({ variables: { input: data } });
  };

  return (
    <>
      <MetaTags title="Contact" description="Contact page" />

      <Toaster />
      <Form
        onSubmit={onSubmit}
        config={{ mode: "onBlur" }}
        error={error}
        formMethods={formMethods}
      >
        <FormError error={error} wrapperClassName="form-error" />

        <Label name="email" errorClassName="error">
          Email
        </Label>
        <TextField
          name="email"
          validation={{
            required: true,
            pattern: {
              value: /^[^@]+@[^.]+\..+$/,
              message: "Please enter a valid email address",
            },
          }}
          errorClassName="error"
        />
        <FieldError name="email" className="error" />

        <Submit disabled={loading}>Save</Submit>
      </Form>
    </>
  );
};

export default ContactPage;

是的,真是太难吃了。但是,如果我们想正确处理从服务器返回的提交和错误,那么这整件事是必要的。我们现在不会深入研究,但如果你感兴趣,一定要看看Redwood写得很好、很全面的教程。

现在将其与Phoenix LiveView中的外观进行比较。

<div>
  <.form
    let={f}
    for={@changeset}
    id="contact-form"
    phx-target={@myself}
    phx-change="validate"
    phx-submit="save">

    <%= label f, :title %>
    <%= text_input f, :title %>
    <%= error_tag f, :title %>

    <div>
      <button type="submit" phx-disable-with="Saving...">Save</button>
    </div>
  </.form>
</div>

在提供几乎相同的功能的同时,更容易看透。是的,你打电话给我把苹果比作桔子是对的。一种是模板语言,另一种是JSX。LiveView中的大部分逻辑都发生在elixir文件中,而不是模板中,而JSX则是将逻辑与视图相结合。然而,我认为理想的全栈框架应该允许我为输入编写一次验证代码,然后让我简单地在视图中提供插入错误消息的插槽,并允许我为无效输入设置条件样式并使用它。这将提供一种在前端编写更干净代码的方法,即使在使用JSX时也是如此。你可以说这违背了React的原始哲学,我的论点只是表明我对此有意见。你这样做可能是对的。但这毕竟是一篇关于固执己见的框架的观点文章,仅此而已。

RedwoodJS背后的人

信用,在哪里信用到期。

Redwood由GitHub联合创始人兼前首席执行官Tom Preston Werner、Peter Pistorius、David Price和Rob Cameron创建。此外,其核心团队目前由23人组成。因此,如果你害怕尝试新的工具,因为你可能永远不知道他们的唯一维护者什么时候会厌倦在空闲时间开发FOSS工具的困难,你可以放心:Redwood会一直存在。

红木:荣誉提名

红木

  • 还与Storybook捆绑在一起,
  • 提供了必备的GraphQL类GraphQL Playground,
  • 提供开箱即用的辅助功能,如RouteAnnouncemnet SkipNavLink、SkipNavContent和RouteFocus组件,
  • 当然,它会自动按页面分割代码。

最后一个预计将在2022年推出,而无障碍功能总体上应该有自己的职位。尽管如此,这场比赛已经太长了,我们甚至还没有提到另一位竞争者。

让我们看看BlitzJS

Blitz构建在Next.js之上,其灵感来自RubyonRails,并提供了“Zero-API”数据层抽象。没有GraphQL,向前辈致敬…似乎我们有了一个良好的开端。但它没有辜负我的厚望吗?有点。

麻烦的过去

与Redwood相比,Blitz的教程和文档要差得多。它还缺少几个便利功能:

  • 它并没有真正自动生成特定于主机的配置文件。
  • Blitz无法运行简单的CLI命令来设置身份验证提供程序。
  • 它不提供辅助功能。
  • 它的代码生成器在生成页面时不考虑模型。

Blitz的最初承诺是在2020年2月做出的,比Redwood在2019年6月做出的承诺晚了半年多一点,虽然Redwood有相当多的贡献者,但Blitz的核心团队只有2-4人。有鉴于此,我认为他们的工作值得赞扬。

但这还不是全部。如果你打开他们的文档,你会看到顶部的横幅宣布了一个转折点。

虽然Blitz最初包含Next.js并围绕它构建,但Brandon Bayer和其他开发人员认为它的局限性太大。因此,他们放弃了,结果证明这是一个非常错误的决定。很快就很明显,维护叉子需要付出比团队投入更多的努力。

然而,并没有失去一切。pivot旨在将最初的价值主张“JavaScriptonRails with Next”转变为“JavaScripton Rails,带来您自己的前端框架”。

我无法告诉你,Rails的重新开发不会强迫我使用React,这让我松了一口气。

别误会我的意思。我喜欢React带来的创造性。得益于React,前端开发在过去九年中取得了长足的进步。Vue和Svelte等其他框架可能在遵循新概念方面落后,但这也意味着他们有更多的时间进一步完善这些想法,并提供更好的DevX。或者至少我发现它们更容易使用,而不必担心我的客户端代码的性能会停滞不前。

总的来说,我觉得事态的转变是一个幸运的失误。

如何创建闪电战应用程序

在创建Blitz应用程序之前,您需要全局安装Blitz(运行yarn global add Blitz或npm install-g Blitz–legacy peer deps)。当涉及到Blitz的设计时,这可能是我的主要苦恼,因为这样,你就不能在所有贡献者之间锁定你的项目来使用给定的Blitz CLI版本,并在你认为合适的时候增加它,因为Blitz会不时地自动更新自己。

安装blitz后,运行

$ blitz new my-blitz-app

它会问你

  • 无论您想使用TS还是JS,
  • 如果它应该包括DB和Auth模板(稍后将详细介绍),
  • 如果要使用npm、yarn或pnpm安装依赖项,
  • 如果你想使用React Final Form或React Hook Form。

一旦您回答了它的所有问题,CLI就会按照惯例开始下载一半的互联网。找点喝的,吃午饭,完成锻炼,或者做任何事情来打发时间,当你完成后,你可以通过跑步来启动服务器

$ blitz dev

当然,你会看到应用程序在运行,UI告诉你要运行

$ blitz generate all project name:string

但在此之前,让我们先查看一下项目目录。

my-blitz-app/
├── app/
├── db/
├── mailers/
├── node_modules/
├── public/
├── test/
├── integrations/
├── babel.config.js
├── blitz.config.ts
├── blitz-env.d.ts
├── jest.config.ts
├── package.json
├── README.md
├── tsconfig.json
├── types.ts
└── yarn.lock

同样,我们可以看到常见的嫌疑:配置文件、节点模块、测试等等。毫无疑问,公共目录是存储静态资产的地方。Test保存您的测试设置和utils。集成用于配置您的外部服务,如支付提供商或邮件服务。说到mailer,这就是您可以处理邮件发送逻辑的地方。Blitz生成了一个很好的模板,其中包含信息丰富的评论,供您开始使用,其中包括一个忘记密码的电子邮件模板。

正如你可能猜到的,应用程序和数据库目录是你拥有大量应用程序相关代码的目录。现在是时候按照生成的登录页所说的去做了,并运行blitz生成所有项目名称:string。

当它问您是否要迁移数据库并给它一个描述性的名称(如add project)时,请说是。

现在让我们看看db目录。

my-blitz-app/
└── db/
    ├── db.sqlite
    ├── db.sqlite-journal
    ├── index.ts
    ├── migrations/
    │   ├── 20220610075814_initial_migration/
    │   │   └── migration.sql
    │   ├── 20220610092949_add_project/
    │   │   └── migration.sql
    │   └── migration_lock.toml
    ├── schema.prisma
    └── seeds.ts

迁移目录由Prisma处理,所以如果你已经熟悉它,它不会让你感到惊讶。如果不熟悉,我强烈建议你在使用Blitz或Redwood之前先自己尝试一下,因为它们严重而透明地依赖它。

就像在Redwood的db-dir中一样,我们有schema.prisma和sqlite-db,所以我们有一些东西可以开始。但我们也有seeds.ts和index.ts。如果你看一下index.ts文件,它只是重新导出了一些增强功能的Prisma,而seeds_ts文件本身就说明了这一点。

现在是时候仔细看看我们的计划了。

// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema

datasource db {
  provider = "sqlite"
  url      = env("DATABASE_URL")
}

generator client {
  provider = "prisma-client-js"
}

// --------------------------------------

model User {
  id             Int      @id @default(autoincrement())
  createdAt      DateTime @default(now())
  updatedAt      DateTime @updatedAt
  name           String?
  email          String   @unique
  hashedPassword String?
  role           String   @default("USER")

  tokens   Token[]
  sessions Session[]
}

model Session {
  id                 Int       @id @default(autoincrement())
  createdAt          DateTime  @default(now())
  updatedAt          DateTime  @updatedAt
  expiresAt          DateTime?
  handle             String    @unique
  hashedSessionToken String?
  antiCSRFToken      String?
  publicData         String?
  privateData        String?

  user   User? @relation(fields: [userId], references: [id])
  userId Int?
}

model Token {
  id          Int      @id @default(autoincrement())
  createdAt   DateTime @default(now())
  updatedAt   DateTime @updatedAt
  hashedToken String
  type        String
  // See note below about TokenType enum
  // type        TokenType
  expiresAt   DateTime
  sentTo      String

  user   User @relation(fields: [userId], references: [id])
  userId Int

  @@unique([hashedToken, type])
}

// NOTE: It's highly recommended to use an enum for the token type
//       but enums only work in Postgres.
//       See: https://blitzjs.com/docs/database-overview#switch-to-postgre-sql
// enum TokenType {
//   RESET_PASSWORD
// }

model Project {
  id        Int      @id @default(autoincrement())
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
  name      String
}

正如您所看到的,Blitz从要与全功能用户管理一起使用的模型开始。当然,它还提供了应用程序脚手架中所有必要的代码,这意味着抽象掉了最少的逻辑,您可以根据自己的意愿自由修改它。

在所有与用户相关的模型下面,我们可以看到我们使用CLI创建的项目模型,其中包含自动添加的id、createdAt和updatedAt文件。与Redwood相比,我更喜欢Blitz的一点是,它的CLI模仿了Phoenix,并且您可以从命令行端到端创建所有内容。

这确实使快速移动变得容易,因为在代码和命令行之间发生的上下文切换更少。好吧,如果它真的起作用的话,就像你可以正确地生成模式一样,生成的页面、突变和查询总是使用name:string,而忽略模式定义的实体类型,这与Redwood不同。已经有一个开放的pull请求来解决这个问题,但可以理解的是,Blitz团队一直专注于完成v2.0,而不是修补当前的稳定分支。

数据库到此为止,让我们转到应用程序目录。

my-blitz-app
└── app
    ├── api/
    ├── auth/
    ├── core/
    ├── pages/
    ├── projects/
    └── users/

核心目录包含Bliz的好东西,比如预定义和参数化的Form(但没有Redwood或Phoenix的好东西)、useCurrentUser钩子和Layouts目录,因为Bliz可以很容易地在页面之间保持布局,而在即将推出的Next.js Layouts中,这将是完全不必要的。这进一步强化了放弃分叉并转向工具包的决定可能是一个困难但必要的决定。

auth目录包含我们前面谈到的功能齐全的身份验证逻辑,包括所有必要的数据库突变,如注册、登录、注销和忘记密码,以及相应的页面和注册和登录表单组件。getCurrentUser查询在users目录中有自己的位置,这是完全合理的。

我们到达了页面和项目目录,所有的操作都发生在那里。

Blitz创建了一个目录,用于在一个地方存储数据库查询、突变、输入验证(使用zod)和特定于模型的组件,如创建和更新表单。你需要在这些方面花很多时间,因为你需要根据你的实际模型更新它们。这在教程中安排得很好……一定要读,不像我第一次尝试闪电战时那样。

my-blitz-app/
└── app/
    └── projects/
        ├── components/
        │   └── ProjectForm.tsx
        ├── mutations/
        │   ├── createProject.ts
        │   ├── deleteProject.ts
        │   └── updateProject.ts
        └── queries/
            ├── getProjects.ts
            └── getProject.ts

而如果你已经熟悉Next,页面目录就不会有任何惊喜了。

my-blitz-app/
└── app/
    └── pages/
        ├── projects/
        │   ├── index.tsx
        │   ├── new.tsx
        │   ├── [projectId]/
        │   │   └── edit.tsx
        │   └── [projectId].tsx
        ├── 404.tsx
        ├── _app.tsx
        ├── _document.tsx
        ├── index.test.tsx
        └── index.tsx

如果你还没有尝试过Next,请解释一下:Blitz和Next一样使用基于文件系统的路由。pages目录是您的根目录,当访问与给定目录对应的路径时,将呈现索引文件。因此,当请求根路径时,pages/index.tsx将被呈现,访问/projects将呈现pages/projects/index.tsx,/projects/new将呈现pages/projects/new.tsx等等。

如果文件名包含在[]-s中,则表示它对应于一个路由参数。因此/projects/15将呈现pages/projects/[projectId].tsx。与Next不同,您可以使用<code>useParam(名称:字符串,类型:字符串)</code>钩子访问页面中的param值。要访问查询对象,请使用<code>useRouterQuery(名称:字符串)</code>。老实说,我从来没有真正理解为什么Next需要将两者结合在一起。

使用CLI生成页面时,默认情况下会保护所有页面。要公开它们,只需删除[PageComponent].authenticate=true行。如果用户未登录,这将引发AuthenticationError,因此,如果您希望将未经身份验证的用户重定向到登录页面,则可能需要使用[PageComponent].authenticate={redirectTo:'/login'}。

在查询和突变中,您可以使用ctx上下文参数值在管道中调用ctx.session.$authorize或resolver.authorize来保护您的数据。

最后,如果您仍然需要一个合适的httpneneneba API,您可以创建Express-style处理程序函数,使用与页面相同的文件系统路由。

一个可能的光明未来

虽然闪电战有一个麻烦的过去,但它可能有一个光明的未来。它肯定还在制作中,还没有准备好被广泛采用。创建一个与框架无关的全栈JavaScript工具包是一个通用的概念。良好的起点进一步强化了这一强大的概念,这就是目前稳定的闪电战版本。我正在进一步观察工具包将如何随着时间的推移而发展。

红木与闪电战:比较与结论

我想看看我们是否有Rails,甚至更好的Phoenix等效JavaScript。让我们看看他们的表现如何。

1.CLI代码生成器

Redwood的CLI在这一点上得到了复选标记,因为它是通用的,并且可以做它需要做的事情。唯一的小缺点是必须首先在文件中写入模型,并且无法生成。

Blitz的CLI仍在开发中,但总体而言,Blitz也是如此,因此,根据准备好的内容来判断它是不公平的,而只能根据它将要做什么来判断。从这个意义上说,如果Blitz功能齐全(或者在它将要运行的时候会运行),它就会获胜,因为它真的可以端到端地生成页面。

判决:平局

2.强大的ORM

这是一个短的。两者都使用Prisma,这是一个足够强大的ORM。

判决:平局

3.服务器端渲染但交互式的页面

好吧,在今天的生态系统中,这可能是一厢情愿的想法。即使在Next中,SSR也是应该避免的,至少在React中有服务器组件之前是这样。

但是哪一个最能模仿这种行为呢?

Redwood并没有试图让自己看起来像Rails的替代品。它有清晰的边界,由前端和后端之间的纱线工作区划分。它确实提供了很好的惯例,为了保持慈善性,它很好地重新设计了Phoenix表单处理的正确部分。然而,严格依赖GraphQL感觉有点过头了。对于我们一开始选择使用全栈框架的小型应用程序来说,这肯定会让人感到尴尬。

红木也是React独有的,所以如果你更喜欢使用Vue、Svelte或Solid,那么你必须等到有人为你最喜欢的框架重新实现红木。

Blitz遵循Rails的方式,但控制器层有点抽象。不过,这是可以理解的,因为使用Next的基于文件系统的路由,很多对Rails有意义的事情对Blitz来说都没有意义。总的来说,它感觉比对任何事情都使用GraphQL更自然。同时,成为框架不可知论者使其比Redwood更加通用。

此外,Blitz正在成为框架不可知论者,所以即使你从未接触过React,你也可能在不久的将来看到它的好处。

但为了遵守最初的标准:Redwood提供客户端渲染和SSG(有点),而Blitz在前两者之上提供SSR。

结论:铁杆GraphQL粉丝可能会选择Redwood。但根据我的标准,闪电战轻而易举地赢得了这场比赛。

4.API

Blitz自动为数据访问生成一个API,如果您愿意,可以使用它,但您也可以显式地编写处理程序函数。有点尴尬,但可能性是存在的。

Redwood在前端和后端之间保持着严格的分离,因此,从一开始就拥有API是微不足道的。即使它是GraphQL API,也可能太难满足您的需求。

结论:平局(TBH,我觉得他们两个在这个程度上都很糟糕。)

再见!

总之,Redwood是一个面向生产的、基于React+GraphQL的全栈JavaScript框架。它完全没有遵循Rails制定的模式,只是非常固执己见。如果你认同它的观点,它是一个很好的工具,但我的观点与Redwood的观点大不相同,因为是什么让开发变得有效和愉快。

另一方面,Blitz紧随Rails和Next的脚步,正在成为一个与框架无关的全栈工具包,它消除了对API层的需求。

我希望你觉得这个比较有帮助。如果你同意我的结论并分享我对闪电战的热爱,请留下评论。如果你不这样做,就和开明的人争论……他们说争议会增加游客数量。