[译] 如何编写全栈 JavaScript 应用

如何编写全栈 JavaScript 应用

咱们的 GitHub 仓库最近在 GitHub 上得到了 10,000 颗星。它在 HackerNews、GitHub Trending 上排名第一,并在 Reddit 上得到了 2 万个赞。javascript

这篇文章是我这一段时间以来一直想写的,随着咱们的仓库快速上升,我认为如今是写它的最佳时间。css

No. 1 Trending on GitHub

我是自由职业者团队的一员,咱们使用 React/React Native、Node.js、GraphQL 等典型项目。这篇文章既是写给那些有兴趣了解咱们如何构建完整的全栈应用程序的人,也是那些未来打算加入咱们的人的入职工具。html

如下是咱们的核心原则。前端

保持简单易读

提及来容易作起来难。大多数开发人员都明白简单易读是一个重要的原则,可是这并不那么容易就作到的。简单易读的代码使维护更容易,还使全部团队成员更容易作出贡献。它还将帮助您在往后管理本身的代码。java

我看到的一些错误:node

  • 过于聪明。复制粘贴代码有时是挺好的。您不须要抽象每两段看起来有些类似的代码。我本身就犯过这个错误。人人都这样。DRY(Don't Repeat Yourself)是一个很好的原则,可是选择错误的抽象可能会很糟糕,并使代码库复杂化。若是您想了解更多相关内容,我推荐:AHA Programming.
  • 拒绝使用现成的工具。好比放着 mapfilter 不用,反而去用 reduce。固然您能够mapreduce,但它可能会有更多的代码行数,并且其余人也更难理解。
    固然,简单易读是主观的。您将看到经验丰富的开发人员在他们不须要使用 reduce 的地方使用 reduce。 有时您须要使用 reduce,若是您曾经束缚于 mapfilterreduce 可能会有更好的表现,由于您只须要将集合传递一次而不是两次。这是一个性能与简单易懂性的抉择。总的来讲,我倾向于简单易读,避免过早的优化。若是使用两层的 map/filter 成为了您的瓶颈,您能够将代码切换为使用 reduce

下面的许多原则也旨在使代码库尽量简单易读。react

物以类聚(主机托管)

这一原则适用于应用程序的许多部分。客户端和服务器文件夹结构,以及在每一个文件中的代码,保持在相同的仓库。android

仓库

将客户端和服务器文件夹保存在同一个 Monorepo 中。(译者注:Monorepo 是用一个仓库来管理全部的源代码,Multirepo 是用多个仓库来管理本身的源代码)这很简单。别把事情复杂化。人人都是用这种方式同步的。在使用 Multirepo 的项目中工做,也并非世界末日,可是使用 Monorepo 会让生活变得更简单。您不会意外地拥有不一样步的客户端和服务器。ios

客户端结构

一个常见的客户端文件夹结构是按文件类型分组。该结构使用不一样的文件夹:components,containers,actions,reducers 和 routes(actions 和 reducers 是使用 redux 才有的,而我会尽可能避免用它)。components 文件夹将包含 BlogPostProfile 之类的内容,而 containers 文件夹将包含 BlogPostContainerProfileContainer 文件。容器将从服务器获取数据并将其传递给 Dumb 子组件,Dumb 子组件的工做是将数据呈现到屏幕上。(译者注:React 中能够将组件分为 Smart 和 Dumb 两类,方便组件复用)git

这个结构是可行的。至少它是一致的,这是很重要的,一个新加入代码库的人会明白发生了什么,在哪发生的。但这种结构的缺点,也是我我的如今避免使用它的缘由是,您必须常常跳转代码库。好比,ProfileContainerBlogPostContainer 它们之间没有任何关系,可是文件就在彼此的旁边,而且远离它们实际要使用的地方。

我更喜欢将要一块儿使用的文件分为一组 —— 一种基于功能的方法。将 Smart 父组件和 Dumb 子组件放在同一个文件夹中。这会让您的生活更容易。

咱们一般使用 routes / screens 文件夹和 components 文件夹。组件将包含能够在应用程序的任何页面上使用的 ButtonInput 等内容。route 文件夹中的每一个文件夹表明着应用程序的不一样页面,与该路由相关的全部组件和业务逻辑都放在该文件夹中。在多个屏幕上使用的组件放在 components 文件夹中。

在每一个 route 文件夹中,您能够在其中建立更多文件夹,对页面的某些部分进行分组。因此若是 route 文件夹中包含了不少内容,这是能够理解的。可是我要警告的一件事是,不要嵌得太深。这将使咱们这个项目在这个项目中更难地跳转。这是没必要要的事情过于复杂的另外一个迹象(顺便说一句,使用 command-p 和搜索也是在项目找到所需内容的好方法,但文件结构会有所影响)。

相似的方法是按功能分组,而不是按路由分组。在一个使用 Mobx State Tree 而且包含许多特性的单页面的项目中,这种方法对我很是有效。按常规方法分组很简单,并且不须要花费太多脑力来找出应该分组的内容和在哪里找到项目。按功能分组的一个麻烦之处在于决定它属于哪里。功能的边界可能很模糊。

更进一步,您甚至可能喜欢将容器和组件放在同一个文件中。或者更进一步,把两部分合为一。我知道您在想什么。“这家伙在说些什么?这是亵渎。”实际上,它并不像听起来那么糟糕,实际上很是好,若是您正在使用 React Hook 和/或生成的代码,我推荐使用这种方法。

真正的问题是,为何要将组件分红 Smart 和 Dumb 组件?对此有几个答案:

  1. 易于测试
  2. 易于工具的使用,如 Storybook
  3. 可使用相同的 Dumb 组件 与多个不一样的 Smart 组件(反之亦然)。
  4. 能够跨平台共享 Smart 组件(例如 React 和 React Native)。

这些都是正当的理由,但每每可有可无。在咱们的代码库中,咱们常用带有 hook 的 Apollo Client。它用来进行测试,您能够模拟 Apollo 响应,也能够模拟 hook。Storybook 也是如此。至于混合和匹配 Smart 和 Dumb 组件,我从未在实践中看到过这种状况。至于跨平台使用,有一个项目我打算这么作,但最终没有实践。那个项目应该是 Lerna 管理的一个 Monorepo。今天,不管如何您都极可能选择 React Native Web 而不是这种方法。

所以,区分 Smart 组件和 Dumb 组件是有正当理由的。这是一个须要注意的重要概念,但一般不须要像您想象的那样担忧,特别是最近 React 添加了 hook 新特性。

在同一个组件中组合 Smart 组件和 Dumb 组件的好处是,它加快了开发时间,并且更简单。

此外,若是未来有须要,您也是能够将组件分红两个单独的组件的。

样式

咱们使用 emotion/styled components 进行样式管理。人们倾向于将样式拆分为单独的文件。我见过有人这样作,但在尝试了这两种方法以后,我认为没有任何理由将样式放在不一样的文件中。与这里列出的其余全部内容同样,若是您将样式与它们所关联的组件放在同一个文件中,那么您的生活会更容易。

React 官方文档中包含了一些关于结构的简明说明,我也推荐你们通读一遍。其中最大的收获:

通常来讲,将常常更改的文件放在一块儿是一个好主意。这一原则被称为“托管”。

服务器结构

服务器也是如此。我我的避免使用的典型结构是这样的

src
│ app.js # App 入口点
└───api # 表示 app 的全部后端路由控制器
└───config # 环境变量和配置相关的东西
└───jobs # agenda.js 的做业定义
└───loaders # 将启动过程分红模块
└───models # 数据库模型
└───services # 全部的业务逻辑都在这里
└───subscribers # 异步任务的事件处理程序
└───types # Typescript 的类型声明文件(d.ts)

咱们一般在咱们的项目中使用 GraphQL。有模型、服务和解析器文件。与其把这三个文件分散在应用程序中,不如把它们都放在同一个文件夹中。绝大多数状况下,它们会一块儿使用,若是它们放在一块儿,您会更容易找到它们。

在这里看一个示例服务器结构:elie222/bike-sharing

不重写类型

咱们在项目中使用了不少类型系统:TypeScript,GraphQL,数据库模式,有时候还有 Mobx State Tree。

您可能会写一样的类型 3 或 4 次。避免这种状况。使用自动生成类型的工具。

在服务器上,您可使用 TypeORM/Typegoose 和 TypeGraphQL 的组合来覆盖全部类型。TypeORM/Typegoose 将定义数据库模式 以及它们的 TypeScript 类型。TypeGraphQL 将生成 GraphQL 类型和 TypeScript 类型。

在一个文件中定义 TypeORM(MongoDB)和 TypeGraphQL 类型的一个例子:

import { Field, ObjectType, ID } from 'type-graphql'
import {
  Entity,
  ObjectIdColumn,
  ObjectID,
  Column,
  CreateDateColumn,
  UpdateDateColumn,
} from 'typeorm'

@ObjectType()
@Entity()
export default class Policy {
  @Field(type => ID)
  @ObjectIdColumn()
  _id: ObjectID

  @Field()
  @CreateDateColumn({ type: 'timestamp' })
  createdAt: Date

  @Field({ nullable: true })
  @UpdateDateColumn({ type: 'timestamp', nullable: true })
  updatedAt?: Date

  @Field()
  @Column()
  name: string

  @Field()
  @Column()
  version: number
}
复制代码

GraphQL Code Generator 可以生成许多不一样类型。咱们使用它在客户端上生成 TypeScript 类型,并使用 React Hook 调用服务器。

若是您使用 Mobx State Tree,能够经过添加 2 行代码自动从中获取 TypeScript 类型,若是将它与 GraphQL 一块儿使用,则会有一个名为 MST-GQL 的新包,它将从 GQL 模式中生成状态树。

将这些包一块儿使用将节省您重写大量代码并帮助您避免潜在的 bug。

其余解决方案 PrismaHasuraAWS AppSync 也能够帮助避免类型复制。使用这些工具备利有弊。对于咱们所作的项目,这些也不老是一个选项,由于咱们须要将代码部署到提早预置好的服务器上。

尽量地生成代码

除了使用上面的代码生成工具,您还会发现本身一次又一次地编写相同的代码。我在这里能够给您的第一个技巧是为您常用的全部东西添加 snippet。若是您写了大量的 console.log,确保您有一个 cl snippet 将 cl 展开为 console.log()。若是您不这样作,还请我帮忙调试您的代码,我会生气的。

尽管有不少 snippet 的包,可是您也能够很容易地在这里生成您本身的:snippet generator

一些我喜欢的 snippet:

  • cl — console.log
  • React component/hooks snippets
  • imes — import emotion/styled
  • sc — emotion/styled component
  • fn — 打印当前所在文件的文件名。

若是您想手动将它们添加到 VS Code 中,下面是代码:

{
  "Export default": {
    "scope": "javascript,typescript,javascriptreact,typescriptreact",
    "prefix": "eid",
    "body": [
      "export { default } from './${TM_DIRECTORY/.*[\\/](.*)$$/$1/}'",
      "$2"
    ],
    "description": "Import and export default in a single line"
  },
  "Filename": {
    "prefix": "fn",
    "body": ["${TM_FILENAME_BASE}"],
    "description": "Print filename"
  },

  "Import emotion styled": {
    "prefix": "imes",
    "body": ["import styled from '@emotion/styled'"],
    "description": "Import Emotion js as styled"
  },
  "Import emotion css only": {
    "prefix": "imec",
    "body": ["import { css } from '@emotion/styled'"],
    "description": "Import Emotion css only"
  },
  "Import emotion styled and css only": {
    "prefix": "imesc",
    "body": ["import styled, { css } from ''@emotion/styled'"],
    "description": "Import Emotion js and css"
  },
  "Styled component": {
    "prefix": "sc",
    "body": ["const ${1} = styled.${2}`", " ${3}", "`"],
    "description": "Import Emotion js and css"
  },

  "TypeScript React Function Component": {
    "prefix": "rfc",
    "body": [
      "import React from 'react'",
      "",
      "interface ${1:ComponentName}Props {",
      "}",
      "",
      "const ${1:ComponentName}: React.FC<${1:ComponentName}Props> = props => {",
      " return (",
      " <div>",
      " ${1:ComponentName}",
      " </div>",
      " )",
      "}",
      "",
      "export default ${1:ComponentName}",
      ""
    ],
    "description": "TypeScript React Function Component"
  },
  
  "console.log": {
    "prefix": "clg",
    "body": [
      "console.log('$1', $1)"
    ],
    "description": "console.log"
  },
  "console.log JSON": {
    "prefix": "clgj",
    "body": [
      "console.log('$1', JSON.stringify($1, null, 2))"
    ],
    "description": "console.log JSON"
  }
}
复制代码

除了 snippet,编写代码生成器也能够节省大量时间。我喜欢使用 plop

Angular 有本身的生成器,能够经过命令行建立一个新的组件,每一个 Angular 组件都有 4 个文件。很遗憾 React 没有这样开箱即用的功能,可是您可使用 plop 本身建立它。若是您建立的每一个新组件都应该是一个包含组件、测试和 Storybook 文件的文件夹,那么生成器能够在一行中为您建立。在不少状况下,这会让咱们的生活变得轻松。例如,在服务器上添加新特性是命令行中的一行,它建立一个实体、服务和解析器文件,全部核心部分都自动填写。

生成器的另外一个好处是它推进您的团队以一致的方式工做。若是每一个人都使用相同的 plop 生成器,代码将具备很是一致的感受。

看一下在这个项目中咱们使用的生成器的例子:elie222/bike-sharing

自动格式化代码

这很简单,但不幸的是并不老是这样。不要浪费时间在缩进代码和添加或删除分号上。在每次提交时,使用 Prettier 自动格式化代码:azz/pretty-quick


总结

咱们讨论了多年来咱们从尝试不一样方法中学到的一些技巧。有不少方法能够构造代码库,可是没有一种方法是绝对“正确的”。

核心思想是保持事物的简单、一致、结构化和易于遍历。这将方便许多人参与到项目中工做,并且立刻就有种在读本身代码的感受。

若是发现译文存在错误或其余须要改进的地方,欢迎到 掘金翻译计划 对译文进行修改并 PR,也可得到相应奖励积分。文章开头的 本文永久连接 即为本文在 GitHub 上的 MarkDown 连接。


掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 AndroidiOS前端后端区块链产品设计人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划官方微博知乎专栏

相关文章
相关标签/搜索