RedwoodJS vs. BlitzJS:全栈式JavaScript元框架的未来

884 阅读14分钟

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

由于这篇文章最后相当长,下面,我们为匆忙的人提供一个比较表。

先说一下历史

如果你是在2010年代开始从事网络开发工作的,你可能根本没有听说过Ruby on Rails,尽管它为我们提供了Twitter、GitHub、Urban Dictionary、Airbnb和Shopify等应用。与当时的网络框架相比,它在工作上是轻而易举的。Rails打破了Web技术的模式,它是一个非常有主见的MVC工具,强调使用众所周知的模式,如惯例高于配置和DRY,并增加了一个强大的CLI,创建了从模型到模板的端到端脚手架。许多其他框架都建立在它的理念上,如Python的Django、PHP的Laravel或Node.js的Sails。因此,可以说,它是一项和LAMP栈一样具有影响力的技术,在它的时代之前。

然而,Ruby on Rails的名气自2004年创建以来已经淡了不少。当我在2012年开始使用Node.js时,Rails的辉煌时代已经过去。构建于Rails之上的Twitter在2007年至2009年间因频繁展示其失败的鲸鱼而臭名昭著。其中大部分原因是Rails缺乏可扩展性,至少根据我的过滤泡中的口碑。当Twitter改用Scala时,这种对Rails的抨击进一步加强了,尽管他们当时并没有完全抛弃Ruby。

Rails(以及Django)的可扩展性问题得到了更多的媒体报道,这与网络的转型也是一致的。越来越多的JavaScript在浏览器中运行。网页变成了高度互动的WebApps,然后是SPA。Angular.js在2010年问世时也彻底改变了这一点。我们不希望服务器通过结合模板和数据来渲染整个网页,而是希望通过客户端的DOM更新来消耗API并处理状态变化。

因此,全栈框架不再受到青睐。开发在编写后端API和前端应用程序之间被分开。当时这些应用也可能是指安卓和iOS的应用,所以抛弃服务器端渲染的HTML字符串,并以我们所有客户都能使用的方式发送数据,这一切都很有意义。

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

自从2012年以来,我有一种感觉,无论我们解决什么问题,都会产生20个新的问题。你可以说这就是所谓的 "进步",但也许仅仅是出于浪漫主义,或者是对过去事情简单的憧憬,我已经等待 "Rails上的Node.js "有一段时间了。Meteor似乎就是那个东西,但它很快就失宠了,因为社区大多认为它适合MVP,但不能扩展......Rails的问题又来了,但在产品生命周期的早期阶段就坏了。我必须承认,我甚至从来没有去尝试过它。

然而,我们似乎正在缓慢而稳定地实现这一目标。Angular 2+与Next.js一起接受了Rails的代码生成器,所以它似乎可以成为类似的东西。Next.js有了API Routes,使得用SSR处理前端并编写后端API成为可能。但它仍然缺乏一个强大的CLI生成器,而且与数据层也没有关系。而且总的来说,要达到Rails的强大水平,仍然缺少一个好的ORM。至少,随着Prisma的出现,这最后一点似乎得到了解决。

等一下。我们有代码生成器、成熟的后端和前端框架,最后,还有一个好的ORM。也许我们已经有了所有的拼图碎片?也许吧。但首先,让我们从JavaScript再往前走一点,看看另一个生态系统是否已经成功地推进了Rails的遗产,以及我们是否可以从中学习。

进入Elixir和Phoenix

Elixir是一种建立在Erlang的BEAM和OTP上的语言,提供了一个基于行为体模型和进程的漂亮的并发模型,由于 "让它崩溃 "的哲学,与防御性编程相比,这也使得错误处理变得容易。它也有一个很好的、受Ruby启发的语法,但仍然是一个优雅的、功能性语言。

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

是的,到目前为止,我甚至都不会挑起眉毛。随着时间的推移,Rails的可扩展性越来越强,而且现在我可以通过编写JavaScript框架来获得我所需要的大部分东西,即使将其全部连接起来仍然是相当多的DIY。总之,如果我需要一个交互式的浏览器应用程序,我需要使用像React(或者至少是Alpine.js)这样的东西来完成它。

孩子,你甚至无法想象前面的说法是多么错误。虽然Phoenix是一个成熟的Rails在Elixir中的重新实现,但它有一个最重要的特点:你的页面可以完全在服务器端渲染并同时进行交互,使用它的超级能力,即LiveView。当你请求一个LiveView页面时,初始状态会在服务器端预渲染,然后建立一个WebSocket连接。状态被存储在服务器的内存中,客户端发送事件。后台更新状态,计算差异,并发送一个高度压缩的变化集到用户界面,客户端的JS库相应地更新DOM。

我严重地过度简化了Phoenix的功能,但这一节已经太长了,所以一定要自己去看一看

我们已经绕道看了一下最好的,甚至是最好的UI框架之一。因此,当涉及到全栈JavaScript框架时,至少要达到Phoenix所取得的成就才有意义。因此,我希望看到的是。

  1. 一个可以生成数据模型或模式的CLI,以及它们的控制器/服务和它们相应的页面
  2. 一个强大的ORM,如Prisma
  3. 服务器端渲染但交互式的页面,使之简单化
  4. 跨平台的可用性:让我能够轻松地为浏览器创建页面,但我希望能够通过添加一行代码来创建一个响应JSON的API端点。
  5. 把这整个事情捆绑在一起

说到这里,让我们看看Redwood或Blitz是否是我们一直在等待的框架。

BlitzJS与RedwoodJS的比较

什么是RedwoodJS?

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

至少根据他们的教程。

在阅读文档时,我感到一种夸耀性的过度自信,我个人觉得很难阅读。与通常的、干巴巴的技术文本相比,它采取了一种轻松的语气,这是一个值得欢迎的变化。尽管如此,当一个文本脱离了对事物的安全、客观的描述时,它也徘徊在与读者的品味相匹配或相冲突的领域中。

就我而言,我很欣赏这种选择,但无法享受这种结果。

不过,该教程还是值得通读的。它是非常彻底和有帮助的。结果也值得......嗯,不管你在阅读时有什么感觉,因为Redwood也很好用。它的代码生成器做了我期望它做的事情。事实上,它做的甚至比我预期的更多,因为它不仅在设置应用程序的骨架、模型、页面和其他支架方面非常方便。它甚至可以将你的应用程序设置为部署到不同的部署目标,如AWS Lambdas、Render、Netlify、Vercel。

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

但Redwood不仅对部署有强烈的意见,而且对Web应用的开发方式也有强烈的意见,如果你不同意这些意见,那么......

我希望你能使用GraphQL

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

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

如果你想使用普通的JavaScript,你可以省略--ts 标志。

当然,你可以立即启动开发服务器,看到你已经用yarn redwood dev得到了一个漂亮的用户界面。有一件事需要注意,在我看来是相当值得称赞的,那就是你不需要全局安装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

我们可以看到常规的prettier.config.js、jest.config.js,还有一个redwood.toml用于配置dev-server的端口。我们有一个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,它告诉我们Redwood当然是使用Prisma。src/目录存储了我们大部分的逻辑。
  • directives/:存储我们的graphql schema指令
  • 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可能没有任何动态内容,所以我们可以静态地生成它。只要添加prerender,就可以了。

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时,服务器都会被击中。

Redwood中的前端功能

在使用Redwood的过程中,有两件事让我非常喜欢。单元和表单。

单元是一个组件,它获取并管理自己的数据和状态。你定义它要使用的查询和突变,然后导出一个函数来呈现组件的加载、空、失败和成功状态。当然,你可以使用生成器来为你创建必要的模板。

生成的单元格看起来像这样。

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--或者任何动态内容--只有它们的加载状态会被预渲染,这并没有什么帮助。没错,如果你使用Redwood,就没有getStaticProps了。

Redwood的另一个好处是它简化了表单处理,尽管他们的框架方式让我觉得有点不舒服。但首先,漂亮的部分。

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' }} 告诉表单在用户离开每个字段时进行验证。

唯一让人扫兴的是,这种模式与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文件中,而不是模板中,而JJSX则是将逻辑与视图相结合。然而,我认为一个理想的全栈框架应该允许我为输入写一次验证代码,然后让我简单地在视图中提供槽来插入错误信息,并允许我为无效的输入设置条件样式,就可以了。这将提供一种在前端编写更干净的代码的方法,即使是在使用JSX时。你可以说这违背了React的原始理念,而我的论点只是表明我对它有意见。你这样做可能是对的。但这毕竟是一篇关于有意见的框架的观点文章,所以这就是了。

RedwoodJS背后的人

归功于此。

Redwood是由GitHub的联合创始人和前CEO Tom Preston-Werner、Peter Pistorius、David Price和Rob Cameron创建的。此外,其核心团队目前由23人组成。因此,如果你害怕尝试新的工具,因为你可能永远不知道他们唯一的维护者什么时候厌倦了在空闲时间从事自由软件工具的斗争,你可以放心。Redwood将继续存在。

红木:值得一提的是

红木

最后一个功能在2022年有望实现,而可访问性功能则值得单独发表。不过,这篇文章已经太长了,我们甚至还没有提到另一个竞争者。

让我们看看BlitzJS

Blitz是建立在Next.js之上的,它受到Ruby on Rails的启发,提供了一个 "零API "的数据层抽象。没有GraphQL,向前辈们致敬......看起来我们有了一个好的开始。但它是否达到了我的厚望?算是吧。

困难的过去

与Redwood相比,Blitz的教程和文档没有那么全面和精炼。它还缺少一些方便的功能。

  • 它没有真正自动生成特定主机的配置文件。
  • Blitz 不能运行简单的 CLI 命令来设置 auth provider。
  • 它不提供可及性帮助器。
  • 它的代码生成器在生成页面时没有考虑到模型。

Blitz的首次提交是在2020年2月,比Redwood在2019年6月的提交晚了半年多,虽然Redwood有相当多的贡献者,但Blitz的核心团队仅由2-4人组成。鉴于这一切,我认为他们的工作值得称赞。

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

虽然Blitz最初包括Next.js,并且是围绕它建立的,但Brandon Bayer和其他开发者认为它的局限性太大。因此他们将其分叉,这被证明是一个相当错误的决定。很快就变得很明显,维护分叉所花费的精力比团队所能投入的多得多。

然而,一切并没有失去。这个分叉的目的是把最初的价值主张 "JavaScript on Rails with Next "变成 "JavaScript on Rails, bring your own Front-end Framework"。

我无法告诉你我有多欣慰,因为对Rails的重新创造不会迫使我使用React。

不要误会我的意思。我喜欢React带来的创造性。在过去的九年里,由于React的存在,前端开发已经有了长足的进步。其他框架如Vue和Svelte在遵循新概念方面可能会落后,但这也意味着他们有更多的时间来进一步打磨这些想法,提供更好的DevX。或者说,至少我发现他们更容易工作,而不必担心我的客户端代码的性能会停滞不前。

总而言之,我觉得这个转折是一个幸运的失误。

如何创建一个Blitz应用程序

在你创建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

当然,你会看到应用程序正在运行,用户界面告诉你要运行。

$ 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存放你的测试设置和工具。Integrations是配置你的外部服务的地方,如支付提供商或邮件发送者。说到邮件,这是你可以处理你的邮件发送逻辑的地方。Blitz生成了一个漂亮的模板,其中有丰富的注释供你开始使用,包括一个忘记密码的电子邮件模板。

正如你可能猜到的那样,app和db目录是你拥有大部分应用相关代码的地方。现在是时候按照生成的登陆页面所说的做了,运行blitz generate all project name: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

migrations目录是由Prisma处理的,所以如果你已经熟悉它,就不会感到惊讶。如果不是,我强烈建议你在使用Blitz或Redwood之前,先试试它本身,因为它们大量地、透明地依赖于它。

就像在Redwood的db dir里,我们有schema.prisma和sqlite db,所以我们有一些东西可以开始使用。但我们也有seeds.ts和index.ts。如果你看一下index.ts文件,它只是重新导出了Prisma并做了一些改进,而seeds.ts文件则是不言自明的。

现在是时候仔细看一下我们的schema.prisma了。

// 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和updateAt文件。与Redwood相比,我更喜欢Blitz的一点是它的CLI模仿了Phoenix,你真的可以从命令行端到端创建一切。

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

DB就这样了,让我们来看看应用程序目录。

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

核心目录包含了Blitz的好东西,比如预定义和参数化的Form(虽然没有Redwood或Phoenix的好东西),useCurrentUser钩子,以及Layouts目录,因为Bliz让页面之间的布局很容易持久化,这在即将到来的Next.js Layouts中会变得完全没有必要。这进一步证实了放弃分叉并转向工具包的决定可能是一个艰难但必要的决定。

auth目录包含了我们之前谈到的全功能的认证逻辑,包括所有必要的数据库突变,如注册、登录、注销和忘记密码,以及相应的页面和一个注册和登录表单组件。getCurrentUser查询在用户目录中有自己的位置,这很有意义。

然后我们来到了页面和项目目录,所有的行动都发生在这里。

Blitz创建了一个目录来存储数据库查询、突变、输入验证(使用zod),以及特定于模型的组件,如创建和更新表单,都在一个地方。你将需要经常摆弄这些东西,因为你将需要根据你的实际模型来更新它们。这一点在教程中得到了很好的阐述......请务必阅读它,就像我第一次尝试Blitz时那样。

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/index.tsx 将被呈现,访问/projects 将呈现pages/projects/index.tsx/projects/new 将呈现pages/projects/new.tsx ,以此类推。

如果一个文件名用[]-s括起来,这意味着它对应于一个路由参数。因此,/projects/15 将呈现pages/projects/[projectId].tsx 。与Next不同,你可以使用useParam(name: string, type?: string)钩子在页面中访问参数的值。要访问查询对象,使用useRouterQuery(name: string)说实话,我从来没有真正理解过为什么Next需要把这两者结合起来。

当你使用CLI生成页面时,所有的页面默认是受保护的。要使它们公开,只需删除[PageComponent].authenticate = true 行。如果用户没有登录,这将抛出一个AuthenticationError ,所以如果你宁愿把未认证的用户重定向到你的登录页面,你可能想使用[PageComponent].authenticate = {redirectTo: '/login'}

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

最后,如果你仍然需要一个合适的http API,你可以创建Express风格的处理函数,使用与你的页面相同的文件系统路由。

一个可能的光明的未来

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

Redwood与Blitz。比较和结论

我着手看看我们是否有一个Rails,或者甚至更好的,在JavaScript中的Phoenix等同物。让我们看看他们是如何衡量的。

1.CLI代码生成器

Redwood的CLI在这一点上得到了复选标记,因为它是多功能的,而且做了它需要做的事情。唯一的小缺点是,模型必须先写在文件里,而不能生成。

Blitz的CLI还在制作中,但一般来说Blitz也是如此,所以用已经准备好的东西来判断它是不公平的,只能用它将会是什么。从这个意义上说,如果Blitz功能完备(或将会有功能时),它就会赢,因为它真的可以生成端到端的页面。

裁定:平手

2.一个强大的ORM

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

裁定:平手

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

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

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

Redwood并不试图看起来像Rails的替代品。它在前端和后端之间有清晰的界限,由yarn工作区划分。它无疑提供了很好的惯例,并且--为了保持慈善--很好地重塑了Phoenix表单处理的正确部分。然而,严格依赖GraphQL感觉有点矫枉过正。对于我们一开始就选择使用全栈框架的小程序来说,肯定感觉很别扭。

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

Blitz遵循Rails的方式,但控制器层更抽象一些。但这是可以理解的,因为使用Next的基于文件系统的路由,很多对Rails有意义的东西对Blitz就没有意义了。而且总的来说,这比什么都用GraphQL感觉更自然。同时,变得与框架无关,使它比Redwood更具有通用性。

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

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

**结论。**死硬的GraphQL粉丝可能会想坚持使用Redwood。但根据我的标准,Blitz在这一领域胜出。

4.API

Blitz自动生成一个数据访问的API,如果你想使用的话,你可以使用,但你也可以明确地编写处理函数。有点尴尬,但可能性是存在的。

Redwood在前端和后端之间保持着硬性的分离,所以你有一个API,开始是很微不足道的。即使是GraphQL API,对于你的需求来说,这可能是太多的工程。

结论:平手(TBH,我觉得他们在这方面都很糟糕。)

现在再见

总之,Redwood是一个生产就绪的、基于React+GraphQL的全栈式JavaScript框架,是为边缘而生。它完全不遵循Rails所规定的模式,除了非常有主见。如果你认同它的观点,它是一个很好的工具,但在什么是有效和愉快的开发方面,我的观点与Redwood的观点大相径庭。

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

我希望你觉得这个比较有帮助。如果你同意我的结论并和我一样喜欢Blitz,请留言。如果你不同意,就和那些开明的人争论吧......他们说争论会提高访问者的数量。

The postRedwoodJS vs. BlitzJS: The Future of Fullstack JavaScript Meta-Frameworksappeared first onRisingStack Engineering.