如何将 Typescript、NodeJS 和 Express 应用程序部署到 Heroku

Han*_*kCa 4 heroku node.js express typescript

我想提供有关此的操作方法,因为我找不到任何完整的信息。我认为这在 Stackoverflow 文档中似乎最合适。然而,它已被淘汰*-) -日落文档(*)。

相反,我将把它写成 StackOverflow 问答。

如何将 Typescript、NodeJS 和 Express 应用程序部署到 Heroku

Han*_*kCa 8

我创建了一个项目(https://gitlab.com/OehmSmith_Examples/herokumovies),其中包含描述需要完成的工作的自述文件,我将在此处重现该项目。作为一个良好的 StackOverflow 实践,我还将在本文底部提供所有代码的副本。

教程 - 将 Typescript NodeJS Express 应用程序部署到 Heroku

本教程将使用https://amenallah.com/node-js-typescript-jest-express-starter/作为基础应用程序。我与该网站或作者没有任何关系。我选择它是因为它简单且有效。它也是一个好的 OO Typescript 代码的例子。

安装打字稿

大多数教程甚至https://www.typescriptlang.org/docs/handbook/typescript-in-5-minutes.html 中的官方文档都说要全局安装打字稿:

 > npm install -g typescript
Run Code Online (Sandbox Code Playgroud)

Heroku 没有打字稿的全局安装,因此需要将其保存在本地。 示例项目就是这样做的:

 > npm i -D nodemon rimraf typescript ts-node ts-jest jest @types/jest @types/node
Run Code Online (Sandbox Code Playgroud)

@类型/节点

如果您已将您@types/node的版本固定在旧版本,您将看到类似以下错误的内容:

~/AppData/Roaming/nvm/v11.15.0/node_modules/typescript/lib/lib.es2015.iterable.d.ts:41:6 - error TS2300: Duplicate identifier 'IteratorResult'.

41 type IteratorResult<T, TReturn = any> = IteratorYieldResult<T> | IteratorReturnResult<TReturn>;
        ~~~~~~~~~~~~~~

  node_modules/@types/node/index.d.ts:170:11
    170 interface IteratorResult<T> { }
                  ~~~~~~~~~~~~~~
    'IteratorResult' was also declared here.

node_modules/@types/node/index.d.ts:170:11 - error TS2300: Duplicate identifier 'IteratorResult'.

170 interface IteratorResult<T> { }
              ~~~~~~~~~~~~~~

~/AppData/Roaming/nvm/v11.15.0/node_modules/typescript/lib/lib.es2015.iterable.d.ts:41:6
    41 type IteratorResult<T, TReturn = any> = IteratorYieldResult<T> | IteratorReturnResult<TReturn>;
            ~~~~~~~~~~~~~~
    'IteratorResult' was also declared here.


Found 2 errors.
Run Code Online (Sandbox Code Playgroud)

来自TypeScript:重复标识符 'IteratorResult'。并且根据您需要更新您的@types/node. 这是我在处理旧代码时遇到的一个问题,并希望包括对它的讨论。

云服务更新端口

index.ts由于原始代码硬编码端口 5000,因此将其更改为以下内容:

app.listen(process.env.PORT, () => {
    console.log(`server started on port ${process.env.PORT}`)
});
Run Code Online (Sandbox Code Playgroud)

考虑到这一点我已经添加端口到npm scripts,包括添加start:dev,所以你可以像Heroku的从编译打字稿不运行它。

    "start:dev": "PORT=5000 node dist/index.js",
    "dev": "PORT=5000 nodemon --exec ts-node src/index.ts --watch src",
Run Code Online (Sandbox Code Playgroud)

或者它可以在 .env 文件中设置:

PORT=5000
Run Code Online (Sandbox Code Playgroud)

NPM 依赖项

Heroku 不会安装开发依赖项(任何其他云提供商也不会)。因此,您需要将一些依赖项移动到主块。例如,一个NestJS应用程序将这些作为 Dev Dependencies 并且需要移动它们:

 @nestjs/cli
Run Code Online (Sandbox Code Playgroud)

虚拟数据

我将此构造函数添加到MoviesApi.ts

constructor() {
    // setup some dummy data
    movies.push({
        name: 'Pirates of the caribbean',
        rating: 8.5
    })
    movies.push({
        name: 'Star Wars: A new hope',
        rating: 8.7
    })
}
Run Code Online (Sandbox Code Playgroud)

赫鲁库

现在部署到 Heroku

  1. 如果您还没有帐户,请设置一个帐户并在 Heroku 上创建一个应用程序
  2. 在您的终端中:

    heroku login
    heroku create moviesheroku // this needs to be unique
    
    Run Code Online (Sandbox Code Playgroud)
  3. 您可能需要将返回的 git url 添加为远程(检查git remote -v):

    git remote add heroku <git url>
    
    Run Code Online (Sandbox Code Playgroud)
  4. 查找或搜索构建包(下一步已经指定了我使用的那些):

  5. 添加构建包:

    heroku buildpacks:add zidizei/typescript
    heroku buildpacks:add heroku/nodejs
    
    Run Code Online (Sandbox Code Playgroud)
  6. 确认构建包:

    heroku buildpacks
    
    Run Code Online (Sandbox Code Playgroud)
  7. 提交到您的本地存储库

    git init  // if not already done
    git add --all
    git ci -m "Initial commit.  Test project all setup and should be ready to 'serve' but not yet ready to deploy to heroku"
    
    Run Code Online (Sandbox Code Playgroud)
  8. 运行

    npm dev # OR
    npm run start:dev # It depends on your npm scripts
    
    Run Code Online (Sandbox Code Playgroud)
  9. 使用邮递员或类似工具进行测试或从命令行运行:

    curl http://localhost:5000/movies
    
    Run Code Online (Sandbox Code Playgroud)
  10. 测试它与 npm run build

  11. 更新 npm 脚本,以便npm install在 Heroku 上安装 ( )后,它会在尝试执行之前构建它npm run start

    "postinstall": "npm run build"  # Depends on your npm scripts
    
    Run Code Online (Sandbox Code Playgroud)
  12. 提交到本地存储库:

    git add --all
    git ci -m "Now it should deploy, build and run on heroku"
    
    Run Code Online (Sandbox Code Playgroud)
  13. 部署到heroku。它应该建立并启动。

    git push heroku master
    
    Run Code Online (Sandbox Code Playgroud)
  14. 测试(假设您使用的应用程序heroku createmoviesheroku- 相应调整)

    curl https://moviesheroku.herokuapp.com/movies
    
    Run Code Online (Sandbox Code Playgroud)

变化

配置文件

我没有指定Procfile告诉 Heroku 任何有关该应用程序的信息。幸运的是,它通过确定这是一个node + npm应用程序来创建自己的默认值。但是,您可以明确定义它,如果您有多个应用程序或类似应用程序,则需要执行此操作。您可以添加一个 Procfile 来包含(这是默认设置):

web: npm start
Run Code Online (Sandbox Code Playgroud)

节点和 NPM 版本

Heroku 还默认使用这些的最新版本之一。您可以在package.json文件的顶层显式设置版本,例如:

 "engines": {
     "node": "10.x",
     "npm": "6.x"
 },
Run Code Online (Sandbox Code Playgroud)

尽管如果您不指定npm版本,那么 Heroku 将为 node 版本使用合理的默认值。

总结性思考

我只用了几个小时就搞定了。我需要解决的主要问题是打字稿必须是本地的,而不是全局的。和构建包。PORT 也是一个问题,尽管每个云提供商都需要使用,process.env.PORT所以这对我来说很明显。

Azure 是一场噩梦,需要几天时间,但这主要是因为我所在的工作场所坚持使用 Windows 服务器。说来话长,我就不讲了。

AWS 太复杂了。尝试了一天后,我没有得到我工作的实例。但是我确实需要再试一次。我尝试使用的应用程序使用了https://tsed.io/库。简单的 Node / Typescript / Express 应用程序应该很容易完成。

(*) - 文档的废弃有点令人惊讶,尽管它发生在 2 年前,我想这不是我使用的东西。而且我一直认为问答是最容易记录文档的地方。

代码

.gitignore

    node_modules
    dist
    coverage
Run Code Online (Sandbox Code Playgroud)

.jest.config.js

    module.exports = {
        preset: 'ts-jest',
        testEnvironment: 'node'
    };
Run Code Online (Sandbox Code Playgroud)

package.json

    {
      "name": "movies",
      "version": "1.0.0",
      "description": "Example from https://amenallah.com/node-js-typescript-jest-express-starter/ but then modify and / or show steps for how to deploy this Typescript NodeJS Express RESTful app to Heroku.",
      "main": "index.js",
      "scripts": {
        "build": "rimraf dist && tsc",
        "postinstall": "npm run build",
        "start": "node dist/index.js",
        "start:dev": "PORT=5000 node dist/index.js",
        "dev": "PORT=5000 nodemon --exec ts-node src/index.ts --watch src",
        "test": "jest --watch",
        "coverage": "jest --coverage"
      },
      "keywords": [],
      "author": "",
      "license": "ISC",
      "devDependencies": {
        "@types/express": "^4.17.2",
        "@types/jest": "^24.0.25",
        "@types/node": "^13.1.2",
        "jest": "^24.9.0",
        "nodemon": "^2.0.2",
        "rimraf": "^3.0.0",
        "ts-jest": "^24.2.0",
        "ts-node": "^8.5.4",
        "typescript": "^3.7.4"
      },
      "dependencies": {
        "body-parser": "^1.19.0",
        "express": "^4.17.1"
      }
    }
Run Code Online (Sandbox Code Playgroud)

tsconfig.json

    {
      "compilerOptions": {
        "target": "es5",
        "module": "commonjs",
        "outDir": "dist",
        "sourceMap": false,
        "allowSyntheticDefaultImports": true,
        "baseUrl": ".",
        "paths": {
          "*": [
            "node_modules/",
          ],
          "typings/*": [
            "src/typings/*"
          ]
        },
      },
      "include": [
        "src/**/*.ts"
      ],
      "exclude": [
        "src/test/**/*.spec.ts"
      ]
    }
Run Code Online (Sandbox Code Playgroud)

src/api/MoviesApi.ts

    import IResource from "typings/IResource";

    let movies: object[] = []

    export default class MoviesApi implements IResource {
        constructor() {
            // setup some dummy data
            movies.push({
                name: 'Pirates of the caribbean',
                rating: 8.5
            })
            movies.push({
                name: 'Star Wars: A new hope',
                rating: 8.7
            })
        }

        create(data: any): any {
            movies.push(data)
            return data
        }

        findMany(): any[] {
            return movies;
        }
    }
Run Code Online (Sandbox Code Playgroud)

src/test/api/Movies.spec.ts

    import IResource from '../typings/IResource'
    import MoviesApi from '../api/MoviesApi'

    const moviesApi: IResource = new MoviesApi()

    describe('Movies API', () => {
        it('should create a new movie', () => {
            const movieData: object = {
                name: 'Pirates of the caribbean',
                rating: 8.5
            };

            const movie: object = moviesApi.create(movieData);

            expect(movie).toEqual(movieData)
        })
    });
Run Code Online (Sandbox Code Playgroud)

src/typings/IResource/index.d.ts

    export default interface IResource {
        create(data: any): any
        findMany(): any[]
    }
Run Code Online (Sandbox Code Playgroud)

src/index.ts

    import * as express from 'express'
    import * as bodyParser from 'body-parser'

    import MoviesApi from './api/MoviesApi'

    const app = express();
    const moviesApi = new MoviesApi();

    app.use(bodyParser.json());

    app.post('/movies', (req: express.Request, res: express.Response) => {
        res.json(moviesApi.create(req.body))
    });

    app.get('/movies', (req: express.Request, res: express.Response) => {
        res.json(moviesApi.findMany())
    });

    app.listen(process.env.PORT, () => {
        console.log(`server started on port ${process.env.PORT}`)
    });
Run Code Online (Sandbox Code Playgroud)