[譯] 如何編寫全棧 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前端後端區塊鏈產品設計人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章
相關標籤/搜索