如何使用TypeScript通过Express构建节点API

本文最初发布在Okta开发人员博客上 。 感谢您支持使SitePoint成为可能的合作伙伴。

自1995年以来,无论是否喜欢,JavaScript一直在帮助开发人员为Internet提供支持。那时,JavaScript的使用已从小型用户体验增强功能发展为使用服务器上的Node.js和客户端上的许多框架之一的复杂的全栈应用程序。例如Angular,React或Vue。

如今, 大规模构建JavaScript应用程序仍然是一个挑战。 越来越多的团队开始使用TypeScript来补充其JavaScript项目。

Node.js服务器应用程序也可以从使用TypeScript中受益。 本教程的目的是向您展示如何使用TypeScript和Express构建新的Node.js应用程序。

TypeScript案例

作为一名Web开发人员,我很久以前就停止抵制JavaScript,并且逐渐意识到它的灵活性和普遍性。 ES2015及更高版本中添加的语言功能极大地提高了其实用性并减少了编写应用程序时的常见挫败感。

但是,较大JavaScript项目要求使用诸如ESLint之类的工具来捕获常见错误,并且需要更严格的规程以使代码库充满有用的测试。 与任何软件项目一样,包括同行评审过程在内的健康团队文化可以提高质量,并防止出现可能渗入项目的问题。

使用TypeScript的主要好处是可以在错误投入生产之前捕获更多错误,并使其更易于使用代码库。

TypeScript不是另一种语言。 它是JavaScript的灵活超集 ,具有描述可选数据类型的方式。 所有“标准”和有效JavaScript也是有效的TypeScript。 您可以根据需要拨打任意数量的电话。

将TypeScript编译器或TypeScript插件添加到您最喜欢的代码编辑器后,便会立即获得安全性和生产力上的好处。 TypeScript可以警告您拼写错误的函数和属性,检测将错误类型的参数或错误数量的参数传递给函数,并提供更智能的自动完成建议。

使用TypeScript和Node.js构建吉他清单应用程序

在吉他演奏家中,有一个每个人都应该理解的笑话。

问:“您需要几把吉他?”

答:“ n +1。总是一个。”

在本教程中,您将创建一个新的Node.js应用程序来跟踪吉他库存。 简而言之,本教程使用Node.js和Express , EJS和PostgreSQL在后端,在Vue , Materialize和Axios在前端, Okta进行帐户注册和授权,并使用TypeScript来管理JavaScript!

创建您的Node.js项目

打开终端(Mac / Linux)或命令提示符(Windows)并键入以下命令:

node --version

如果出现错误,或者您拥有的Node.js版本低于版本8,则需要安装Node.js。 在Mac或Linux上,建议您首先安装nvm,然后使用nvm安装Node.js。 在Windows上,建议您使用Chocolatey 。

确保安装了最新版本的Node.js之后,为您的项目创建一个文件夹。

mkdir guitar-inventory
cd guitar-inventory

使用npm初始化package.json文件。

npm init -y

你好,世界!

在此示例应用程序中, Express用于服务网页和实现API。 使用npm安装依赖项。 使用以下命令将Express添加到您的项目中。

npm install express

免费学习PHP!

全面介绍PHP和MySQL,从而实现服务器端编程的飞跃。

原价$ 11.95 您的完全免费

免费获得这本书

接下来,在您选择的编辑器中打开项目。

如果您还没有喜欢的代码编辑器,请使用并推荐Visual Studio Code 。 VS Code对JavaScript和Node.js具有出色的支持,例如智能代码完成和调试,并且社区提供了大量免费的扩展库。

创建一个名为src的文件夹。 在此文件夹中,创建一个名为index.js的文件。 打开文件并添加以下JavaScript。

const express = require( "express" );
const app = express();
const port = 8080; // default port to listen

// define a route handler for the default home page
app.get( "/", ( req, res ) => {
    res.send( "Hello world!" );
} );

// start the Express server
app.listen( port, () => {
    console.log( `server started at http://localhost:${ port }` );
} );

接下来,更新package.json以指导npm如何运行您的应用程序。 将main属性值更改为src/index.js ,然后将start脚本添加到scripts对象。

"main": "src/index.js",
  "scripts": {
    "start": "node .",
    "test": "echo \"Error: no test specified\" && exit 1"
  },

现在,可以从终端或命令行启动应用程序。

npm run start

如果一切顺利,您应该会看到此消息写入控制台。

server started at http://localhost:8080

启动浏览器并导航到http://localhost:8080 。 您应该看到文本“ Hello world!”。

注意:要停止Web应用程序,您可以返回到终端或命令提示符,然后按CTRL+C

设置您的Node.js项目以使用TypeScript

第一步是添加TypeScript编译器。 您可以使用--save-dev标志将编译器安装为开发人员依赖项。

npm install --save-dev typescript

下一步是添加tsconfig.json文件。 此文件指示TypeScript如何将TypeScript代码编译(转换)为纯JavaScript。

在项目的根文件夹中创建一个名为tsconfig.json的文件,然后添加以下配置。

{
    "compilerOptions": {
        "module": "commonjs",
        "esModuleInterop": true,
        "target": "es6",
        "noImplicitAny": true,
        "moduleResolution": "node",
        "sourceMap": true,
        "outDir": "dist",
        "baseUrl": ".",
        "paths": {
            "*": [
                "node_modules/*"
            ]
        }
    },
    "include": [
        "src/**/*"
    ]
}

基于此tsconfig.json文件,TypeScript编译器将(尝试)编译在src文件夹中找到的以.ts结尾的任何文件,并将结果存储在名为dist的文件夹中。 Node.js使用CommonJS模块系统,因此module设置的值为commonjs 。 此外,JavaScript的目标版本是ES6(ES2015),它与Node.js的现代版本兼容。

添加tslint并创建一个tslint.json文件来指示TypeScript如何tslint.json代码也是一个好主意。 如果您不熟悉棉绒,它是一种代码分析工具,可以提醒您除语法问题之外的代码潜在问题。

tslint安装为开发人员依赖项。

npm install --save-dev typescript tslint

接下来,在名为tslint.json的根文件夹中创建一个新文件,并添加以下配置。

{
    "defaultSeverity": "error",
    "extends": [
        "tslint:recommended"
    ],
    "jsRules": {},
    "rules": {
        "trailing-comma": [ false ]
    },
    "rulesDirectory": []
}

接下来,更新您的package.json以将main更改为指向由TypeScript编译器创建的新dist文件夹。 另外,在启动Node.js服务器之前,添加几个脚本来执行TSLint和TypeScript编译器。

"main": "dist/index.js",
  "scripts": {
    "prebuild": "tslint -c tslint.json -p tsconfig.json --fix",
    "build": "tsc",
    "prestart": "npm run build",
    "start": "node .",
    "test": "echo \"Error: no test specified\" && exit 1"
  },

最后,将src/index.js文件的扩展名从.js更改为.ts ,即TypeScript扩展名,然后运行启动脚本。

npm run start

注意:无需使用npm run build即可启动TSLint和TypeScript编译器,而无需启动Node.js服务器。

TypeScript错误

不好了! 立即,您可能会看到一些类似这样的错误记录到控制台。

ERROR: /Users/reverentgeek/Projects/guitar-inventory/src/index.ts[12, 5]: Calls to 'console.log' are not allowed.

src/index.ts:1:17 - error TS2580: Cannot find name 'require'. Do you need to install type definitions for node? Try `npm i @types/node`.

1 const express = require( "express" );
                  ~~~~~~~

src/index.ts:6:17 - error TS7006: Parameter 'req' implicitly has an 'any' type.

6 app.get( "/", ( req, res ) => {
                  ~~~

您可能会看到的两个最常见的错误是语法错误和缺少类型信息。 TSLint认为使用console.log是生产代码的问题。 最好的解决方案是用winston等日志记录框架替换console.log的使用。 现在,将以下注释添加到src/index.ts以禁用该规则。

app.listen( port, () => {
    // tslint:disable-next-line:no-console
    console.log( `server started at http://localhost:${ port }` );
} );

TypeScript更喜欢使用import模块语法而不是require ,因此您将从src/index.ts的第一行src/index.ts为:

const express = require( "express" );

至:

import express from "express";

获取正确的类型

为了帮助TypeScript开发人员,库作者和社区贡献者发布了称为TypeScript声明文件的配套库。 声明文件发布到DefinitelyTyped开源存储库中,或者有时在原始JavaScript库本身中找到。

更新您的项目,以便TypeScript可以使用Node.js和Express的类型声明。

npm install --save-dev @types/node @types/express

接下来,重新运行启动脚本并确认没有其他错误。

npm run start

使用Materialize和EJS构建更好的用户界面

您的Node.js应用程序有一个良好的开端,但可能看起来还不是最好的。 此步骤添加了Materialize (一个基于Google的Material Design的现代CSS框架)和Embedded JavaScript Templates (EJS)(一种用于ExpressHTML模板语言)。 Materialize和EJS是更好的UI的良好基础。

首先,安装EJS作为依赖项。

npm install ejs

接下来,在/src下创建一个名为views的新文件夹。 在/src/views文件夹中,创建一个名为index.ejs的文件。 将以下代码添加到/src/views/index.ejs




    
    
    Guitar Inventory
    
    
    


    

Guitar Inventory

arrow_forwardGet started!

使用以下代码更新/src/index.ts

import express from "express";
import path from "path";
const app = express();
const port = 8080; // default port to listen

// Configure Express to use EJS
app.set( "views", path.join( __dirname, "views" ) );
app.set( "view engine", "ejs" );

// define a route handler for the default home page
app.get( "/", ( req, res ) => {
    // render the index template
    res.render( "index" );
} );

// start the express server
app.listen( port, () => {
    // tslint:disable-next-line:no-console
    console.log( `server started at http://localhost:${ port }` );
} );

为Typescript添加资产构建脚本

TypeScript编译器完成生成JavaScript文件并将其复制到dist文件夹的工作。 但是,它不会复制项目需要运行的其他类型的文件,例如EJS视图模板。 为此,创建一个将所有其他文件复制到dist文件夹的构建脚本。

使用这些命令安装所需的模块和TypeScript声明。

npm install --save-dev ts-node shelljs fs-extra nodemon rimraf npm-run-all
npm install --save-dev @types/fs-extra @types/shelljs

这是您刚刚安装的模块的快速概述。

  1. ts-node 。 用于直接运行TypeScript文件。
  2. shelljs 。 用于执行外壳命令,例如复制文件和删除目录。
  3. fs-extra 。 一个扩展Node.js文件系统( fs )模块的模块,具有读取和写入JSON文件等功能。
  4. rimraf 。 用于递归删除文件夹。
  5. npm-run-all 。 用于顺序或并行执行多个npm脚本。
  6. nodemon 。 在开发环境中运行Node.js的便捷工具。 Nodemon监视文件中的更改,并在检测到更改时自动重新启动Node.js应用程序。 不再停止和重新启动Node.js!

在项目的根目录中创建一个名为tools的新文件夹。 在tools文件夹中创建一个名为copyAssets.ts文件。 将以下代码复制到该文件中。

import * as shell from "shelljs";

// Copy all the view templates
shell.cp( "-R", "src/views", "dist/" );

更新npm脚本

package.jsonscripts更新为以下代码。

"scripts": {
    "clean": "rimraf dist/*",
    "copy-assets": "ts-node tools/copyAssets",
    "lint": "tslint -c tslint.json -p tsconfig.json --fix",
    "tsc": "tsc",
    "build": "npm-run-all clean lint tsc copy-assets",
    "dev:start": "npm-run-all build start",
    "dev": "nodemon --watch src -e ts,ejs --exec npm run dev:start",
    "start": "node .",
    "test": "echo \"Error: no test specified\" && exit 1"
  },

注意:如果您不熟悉使用npm脚本,那么它们对于任何Node.js项目都是非常强大和有用的。 脚本可以通过多种方式链接在一起。 将脚本链接在一起的一种方法是使用prepost前缀。 例如,如果您有一个标记为start脚本和另一个标记为prestart脚本,则在终端上执行npm run start将首先运行prestart ,并且只有在其成功完成后才start运行。

现在运行该应用程序并导航到http:// localhost:8080。

npm run dev

主页开始看起来更好! 当然,“ 入门”按钮会导致令人失望的错误消息。 别担心! 该问题的解决方案即将推出!

在Node.js中管理配置设置的更好方法

Node.js应用程序通常使用环境变量进行配置。 但是,管理环境变量可能很麻烦。 dotenv是用于管理应用程序配置数据的流行模块。

安装dotenv作为项目依赖项。

npm install dotenv
npm install --save-dev @types/dotenv

在项目的根文件夹中创建一个名为.env的文件,并添加以下代码。

# Set to production when deploying to production
NODE_ENV=development

# Node.js server configuration
SERVER_PORT=8080

注意:使用诸如git类的源代码管理系统时, 请勿.env文件添加到源代码管理中。 每个环境都需要一个自定义的.env文件。 建议您记录预计值.env项目README或一个单独的文件.env.sample文件。

现在,更新src/index.ts以使用dotenv来配置应用程序服务器端口值。

import dotenv from "dotenv";
import express from "express";
import path from "path";

// initialize configuration
dotenv.config();

// port is now available to the Node.js runtime 
// as if it were an environment variable
const port = process.env.SERVER_PORT;

const app = express();

// Configure Express to use EJS
app.set( "views", path.join( __dirname, "views" ) );
app.set( "view engine", "ejs" );

// define a route handler for the default home page
app.get( "/", ( req, res ) => {
    // render the index template
    res.render( "index" );
} );

// start the express server
app.listen( port, () => {
    // tslint:disable-next-line:no-console
    console.log( `server started at http://localhost:${ port }` );
} );

随着项目的增长,您将使用.env获取更多配置信息。

轻松向节点和Express添加身份验证

向任何应用程序添加用户注册和登录(身份验证)并不是一件容易的事。 好消息是Okta使此步骤非常容易。 首先,使用Okta创建一个免费的开发人员帐户。 首先,浏览至developer.okta.com ,然后单击“ 创建免费帐户”按钮,或单击“ 注册”按钮。

创建帐户后,单击顶部的“ 应用程序”链接,然后单击“ 添加应用程序”

接下来,选择一个Web应用程序 ,然后单击Next

输入您的应用程序的名称,例如Guitar Inventory 。 验证端口号与为本地Web应用程序配置的端口号相同。 然后,单击“完成”以完成应用程序的创建。

将以下代码复制并粘贴到您的.env文件中。

# Okta configuration
OKTA_ORG_URL=https://{yourOktaDomain}
OKTA_CLIENT_ID={yourClientId}
OKTA_CLIENT_SECRET={yourClientSecret}

在Okta应用程序控制台中,单击新应用程序的“ 常规”选项卡,然后在页面底部附近找到标题为“客户端凭据”的部分。 复制客户ID客户机密值,然后将其粘贴到您的.env文件中,分别替换{yourClientId}{yourClientSecret}

启用自助服务注册

Okta的一大功能是允许您的应用程序用户注册一个帐户。 默认情况下,此功能是禁用的,但是您可以轻松地启用它。 首先,单击“ 用户”菜单,然后选择“ 注册”

  1. 单击编辑按钮。
  2. 自助服务注册更改为已启用
  3. 单击表单底部的“ 保存”按钮。

保护您的Node.js应用程序

保护Node.js应用程序的最后一步是将Express配置为使用Okta OpenId Connect(OIDC)中间件 。

npm install @okta/oidc-middleware express-session
npm install --save-dev @types/express-session

接下来,更新您的.env文件以添加HOST_URLSESSION_SECRET值。 您可以将SESSION_SECRET值更改为所需的任何字符串。

# Node.js server configuration
SERVER_PORT=8080
HOST_URL=http://localhost:8080
SESSION_SECRET=MySuperCoolAndAwesomeSecretForSigningSessionCookies

src下创建一个名为middleware的文件夹。 将文件添加到名为sessionAuth.tssrc/middleware文件夹中。 将以下代码添加到src/middleware/sessionAuth.ts

import { ExpressOIDC } from "@okta/oidc-middleware";
import session from "express-session";

export const register = ( app: any ) => {
    // Create the OIDC client
    const oidc = new ExpressOIDC( {
        client_id: process.env.OKTA_CLIENT_ID,
        client_secret: process.env.OKTA_CLIENT_SECRET,
        issuer: `${ process.env.OKTA_ORG_URL }/oauth2/default`,
        redirect_uri: `${ process.env.HOST_URL }/authorization-code/callback`,
        scope: "openid profile"
    } );

    // Configure Express to use authentication sessions
    app.use( session( {
        resave: true,
        saveUninitialized: false,
        secret: process.env.SESSION_SECRET
    } ) );

    // Configure Express to use the OIDC client router
    app.use( oidc.router );

    // add the OIDC client to the app.locals
    app.locals.oidc = oidc;
};

此时,如果您使用的是VS Code之类的代码编辑器,则可能会看到TypeScript抱怨@okta/oidc-middleware模块。 在撰写本文时,该模块还没有正式的TypeScript声明文件。 现在,在src文件夹中创建一个名为global.d.ts的文件,并添加以下代码。

declare module "@okta/oidc-middleware";

重构路线

随着应用程序的增长,您将添加更多的路由。 最好在项目的一个区域中定义所有路线。 在src下创建一个名为routes的新文件夹。 将一个新文件添加到名为index.ts src/routes 。 然后,将以下代码添加到该新文件中。

import * as express from "express";

export const register = ( app: express.Application ) => {
    const oidc = app.locals.oidc;

    // define a route handler for the default home page
    app.get( "/", ( req: any, res ) => {
        res.render( "index" );
    } );

    // define a secure route handler for the login page that redirects to /guitars
    app.get( "/login", oidc.ensureAuthenticated(), ( req, res ) => {
        res.redirect( "/guitars" );
    } );

    // define a route to handle logout
    app.get( "/logout", ( req: any, res ) => {
        req.logout();
        res.redirect( "/" );
    } );

    // define a secure route handler for the guitars page
    app.get( "/guitars", oidc.ensureAuthenticated(), ( req: any, res ) => {
        res.render( "guitars" );
    } );
};

接下来,更新src/index.ts以使用sessionAuthroutes您创建的模块。

import dotenv from "dotenv";
import express from "express";
import path from "path";
import * as sessionAuth from "./middleware/sessionAuth";
import * as routes from "./routes";

// initialize configuration
dotenv.config();

// port is now available to the Node.js runtime
// as if it were an environment variable
const port = process.env.SERVER_PORT;

const app = express();

// Configure Express to use EJS
app.set( "views", path.join( __dirname, "views" ) );
app.set( "view engine", "ejs" );

// Configure session auth
sessionAuth.register( app );

// Configure routes
routes.register( app );

// start the express server
app.listen( port, () => {
    // tslint:disable-next-line:no-console
    console.log( `server started at http://localhost:${ port }` );
} );

接下来,在src/views/guitars.ejs为吉他列表视图模板创建一个新文件,然后输入以下HTML。




    
    
    Guitar Inventory
    
    
    


    

Guitar Inventory

Your future list of guitars!

最后,运行该应用程序。

npm run dev

注意:要验证身份验证是否按预期工作,请打开新的浏览器或使用专用/隐身浏览器窗口。

单击入门按钮。 如果一切顺利,请使用您的Okta帐户登录,Okta应该会自动将您重定向回“吉他列表”页面!

将导航菜单添加到您的Node + Typescript App

通过身份验证,您可以利用从Okta返回的用户配置文件信息。 OIDC中间件会自动将userContext对象和isAuthenticated()函数附加到每个请求。 此userContext具有一个userinfo属性,该属性包含类似于以下对象的信息。

{ 
  sub: '00abc12defg3hij4k5l6',
  name: 'First Last',
  locale: 'en-US',
  preferred_username: '[email protected]',
  given_name: 'First',
  family_name: 'Last',
  zoneinfo: 'America/Los_Angeles',
  updated_at: 1539283620 
}

第一步是获取用户配置文件对象,并将其作为数据传递给视图。 使用以下代码更新src/routes/index.ts

import * as express from "express";

export const register = ( app: express.Application ) => {
    const oidc = app.locals.oidc;

    // define a route handler for the default home page
    app.get( "/", ( req: any, res ) => {
        const user = req.userContext ? req.userContext.userinfo : null;
        res.render( "index", { isAuthenticated: req.isAuthenticated(), user } );
    } );

    // define a secure route handler for the login page that redirects to /guitars
    app.get( "/login", oidc.ensureAuthenticated(), ( req, res ) => {
        res.redirect( "/guitars" );
    } );

    // define a route to handle logout
    app.get( "/logout", ( req: any, res ) => {
        req.logout();
        res.redirect( "/" );
    } );

    // define a secure route handler for the guitars page
    app.get( "/guitars", oidc.ensureAuthenticated(), ( req: any, res ) => {
        const user = req.userContext ? req.userContext.userinfo : null;
        res.render( "guitars", { isAuthenticated: req.isAuthenticated(), user } );
    } );
};

src/views下创建一个名为partials的新文件夹。 在此文件夹中创建一个名为nav.ejs的新文件。 将以下代码添加到src/views/partials/nav.ejs

修改src/views/index.ejssrc/views/guitars.ejs文件。 在标记之后,立即插入以下代码。


    <% include partials/nav %>

完成这些更改后,您的应用程序现在在顶部具有一个导航菜单,该菜单会根据用户的登录状态进行更改。

使用Node和PostgreSQL创建API

下一步是将API添加到Guitar Inventory应用程序。 但是,在继续之前,您需要一种存储数据的方法。

创建一个PostgreSQL数据库

本教程使用PostgreSQL 。 为了使事情变得简单,请使用Docker设置PostgreSQL实例。 如果尚未安装Docker,则可以按照安装指南进行操作 。

安装Docker之后,运行以下命令以下载最新的PostgreSQL容器。

docker pull postgres:latest

现在,运行此命令以创建PostgreSQL数据库服务器的实例。 随时更改管理员密码值。

docker run -d --name guitar-db -p 5432:5432 -e 'POSTGRES_PASSWORD=p@ssw0rd42' postgres

注意:如果已经在本地安装了PostgreSQL,则需要更改-p参数以将端口5432映射到与现有PostgreSQL实例不冲突的其他端口。

这是以前的Docker参数的快速说明。

  • -d –这将以守护程序模式启动容器,因此它将在后台运行。
  • -name –这为您的Docker容器提供了一个友好的名称,这对于停止和启动容器很有用。
  • -p –这将主机(计算机)端口5432映射到容器的端口5432。默认情况下,PostgreSQL侦听TCP端口5432上的连接。
  • -e –这将在容器中设置一个环境变量。 在此示例中,管理员密码为p@ssw0rd42 。 您可以将此值更改为所需的任何密码。
  • postgres –此最终参数告诉Docker使用postgres映像。

注意:如果重新启动计算机,则可能需要重新启动Docker容器。 您可以使用docker start guitar-db命令执行此操作。

安装PostgreSQL客户端模块并使用以下命令键入声明。

npm install pg pg-promise
npm install --save-dev @types/pg

数据库配置设置

将以下设置添加到.env文件的末尾。

# Postgres configuration
PGHOST=localhost
PGUSER=postgres
PGDATABASE=postgres
PGPASSWORD=p@ssw0rd42
PGPORT=5432

注意:如果您更改了数据库管理员密码,请确保在该文件中用该密码替换默认的p@ssw0rd42

添加数据库构建脚本

您需要一个构建脚本来初始化PostgreSQL数据库。 该脚本应读取一个.pgsql文件,并对本地数据库执行SQL命令。

tools文件夹中,创建两个文件: initdb.tsinitdb.pgsql 。 将以下代码复制并粘贴到initdb.ts

import dotenv from "dotenv";
import fs from "fs-extra";
import { Client } from "pg";

const init = async () => {
    // read environment variables
    dotenv.config();
    // create an instance of the PostgreSQL client
    const client = new Client();
    try {
        // connect to the local database server
        await client.connect();
        // read the contents of the initdb.pgsql file
        const sql = await fs.readFile( "./tools/initdb.pgsql", { encoding: "UTF-8" } );
        // split the file into separate statements
        const statements = sql.split( /;\s*$/m );
        for ( const statement of statements ) {
            if ( statement.length > 3 ) {
                // execute each of the statements
                await client.query( statement );
            }
        }
    } catch ( err ) {
        console.log( err );
        throw err;
    } finally {
        // close the database client
        await client.end();
    }
};

init().then( () => {
    console.log( "finished" );
} ).catch( () => {
    console.log( "finished with errors" );
} );

接下来,将以下代码复制并粘贴到initdb.pgsql

-- Drops guitars table
DROP TABLE IF EXISTS guitars;

-- Creates guitars table
CREATE TABLE IF NOT EXISTS guitars (
    id INT NOT NULL PRIMARY KEY GENERATED ALWAYS AS IDENTITY
    , user_id varchar(50) NOT NULL
    , brand varchar(50) NOT NULL
    , model varchar(50) NOT NULL
    , year smallint NULL 
    , color varchar(50) NULL
);

接下来,向package.json添加一个新脚本。

"initdb": "ts-node tools/initdb",

现在,转到终端并运行新脚本。

npm run initdb

您应该在控制台上看到finished的消息。 现在,您的数据库中guitars一个名为guitars的新表! 每当您想重置数据库时,只需重新运行脚本即可。

在Node.js中添加API路由

要完成API,您需要向Express添加新路线以创建,查询,更新和删除吉他。 首先,在src/routes下创建一个名为api.ts的新文件。 将以下代码添加到该文件。

import * as express from "express";
import pgPromise from "pg-promise";

export const register = ( app: express.Application ) => {
    const oidc = app.locals.oidc;
    const port = parseInt( process.env.PGPORT || "5432", 10 );
    const config = {
        database: process.env.PGDATABASE || "postgres",
        host: process.env.PGHOST || "localhost",
        port,
        user: process.env.PGUSER || "postgres"
    };

    const pgp = pgPromise();
    const db = pgp( config );

    app.get( `/api/guitars/all`, oidc.ensureAuthenticated(), async ( req: any, res ) => {
        try {
            const userId = req.userContext.userinfo.sub;
            const guitars = await db.any( `
                SELECT
                    id
                    , brand
                    , model
                    , year
                    , color
                FROM    guitars
                WHERE   user_id = $[userId]
                ORDER BY year, brand, model`, { userId } );
            return res.json( guitars );
        } catch ( err ) {
            // tslint:disable-next-line:no-console
            console.error(err);
            res.json( { error: err.message || err } );
        }
    } );

    app.get( `/api/guitars/total`, oidc.ensureAuthenticated(), async ( req: any, res ) => {
        try {
            const userId = req.userContext.userinfo.sub;
            const total = await db.one( `
            SELECT  count(*) AS total
            FROM    guitars
            WHERE   user_id = $[userId]`, { userId }, ( data: { total: number } ) => {
                return {
                    total: +data.total
                };
            } );
            return res.json( total );
        } catch ( err ) {
            // tslint:disable-next-line:no-console
            console.error(err);
            res.json( { error: err.message || err } );
        }
    } );

    app.get( `/api/guitars/find/:search`, oidc.ensureAuthenticated(), async ( req: any, res ) => {
        try {
            const userId = req.userContext.userinfo.sub;
            const guitars = await db.any( `
                SELECT
                    id
                    , brand
                    , model
                    , year
                    , color
                FROM    guitars
                WHERE   user_id = $[userId]
                AND   ( brand ILIKE $[search] OR model ILIKE $[search] )`,
                { userId, search: `%${ req.params.search }%` } );
            return res.json( guitars );
        } catch ( err ) {
            // tslint:disable-next-line:no-console
            console.error(err);
            res.json( { error: err.message || err } );
        }
    } );

    app.post( `/api/guitars/add`, oidc.ensureAuthenticated(), async ( req: any, res ) => {
        try {
            const userId = req.userContext.userinfo.sub;
            const id = await db.one( `
                INSERT INTO guitars( user_id, brand, model, year, color )
                VALUES( $[userId], $[brand], $[model], $[year], $[color] )
                RETURNING id;`,
                { userId, ...req.body  } );
            return res.json( { id } );
        } catch ( err ) {
            // tslint:disable-next-line:no-console
            console.error(err);
            res.json( { error: err.message || err } );
        }
    } );

    app.post( `/api/guitars/update`, oidc.ensureAuthenticated(), async ( req: any, res ) => {
        try {
            const userId = req.userContext.userinfo.sub;
            const id = await db.one( `
                UPDATE guitars
                SET brand = $[brand]
                    , model = $[model]
                    , year = $[year]
                    , color = $[color]
                WHERE
                    id = $[id]
                    AND user_id = $[userId]
                RETURNING
                    id;`,
                { userId, ...req.body  } );
            return res.json( { id } );
        } catch ( err ) {
            // tslint:disable-next-line:no-console
            console.error(err);
            res.json( { error: err.message || err } );
        }
    } );

    app.delete( `/api/guitars/remove/:id`, oidc.ensureAuthenticated(), async ( req: any, res ) => {
        try {
            const userId = req.userContext.userinfo.sub;
            const id = await db.result( `
                DELETE
                FROM    guitars
                WHERE   user_id = $[userId]
                AND     id = $[id]`,
                { userId, id: req.params.id  }, ( r ) => r.rowCount );
            return res.json( { id } );
        } catch ( err ) {
            // tslint:disable-next-line:no-console
            console.error(err);
            res.json( { error: err.message || err } );
        }
    } );
};

更新src/routes/index.ts以包括新的api模块。

import * as express from "express";
import * as api from "./api";

export const register = ( app: express.Application ) => {
    const oidc = app.locals.oidc;

    // define a route handler for the default home page
    app.get( "/", ( req: any, res ) => {
        const user = req.userContext ? req.userContext.userinfo : null;
        res.render( "index", { isAuthenticated: req.isAuthenticated(), user } );
    } );

    // define a secure route handler for the login page that redirects to /guitars
    app.get( "/login", oidc.ensureAuthenticated(), ( req, res ) => {
        res.redirect( "/guitars" );
    } );

    // define a route to handle logout
    app.get( "/logout", ( req: any, res ) => {
        req.logout();
        res.redirect( "/" );
    } );

    // define a secure route handler for the guitars page
    app.get( "/guitars", oidc.ensureAuthenticated(), ( req: any, res ) => {
        const user = req.userContext ? req.userContext.userinfo : null;
        res.render( "guitars", { isAuthenticated: req.isAuthenticated(), user } );
    } );

    api.register( app );
};

最后,更新src/index.ts以在创建Express应用程序的行之后立即添加新的配置选项。 此代码使Express可以解析传入的JSON数据。

const app = express();

// Configure Express to parse incoming JSON data
app.use( express.json() );

使用Vue,Axios和Parcel更新用户界面

API已准备就绪。 要完成该应用程序,您需要向前端添加一些代码以使用该API。 您还可以将TypeScript与前端代码一起使用。

该项目的最后一步使用Vue进行前端渲染,使用Axios进行对后端API的HTTP调用,并使用Parcel转换TypeScript并将所有依赖项捆绑到一个JavaScript文件中。

首先,使用以下命令在控制台上安装新的依赖项。

npm install axios vue materialize-css
npm install --save-dev parcel-bundler @types/axios @types/materialize-css @types/vue

src下新建一个名为public文件夹。 在src/public下创建一个名为js的新文件夹。 在src/public/js下创建一个名为main.ts的文件,并添加以下代码。

import axios from "axios";
import * as M from "materialize-css";
import Vue from "vue";

// tslint:disable-next-line no-unused-expression
new Vue( {
    computed: {
        hazGuitars(): boolean {
            return this.isLoading === false && this.guitars.length > 0;
        },
        noGuitars(): boolean {
            return this.isLoading === false && this.guitars.length === 0;
        }
    },
    data() {
        return {
            brand: "",
            color: "",
            guitars: [],
            isLoading: true,
            model: "",
            selectedGuitar: "",
            selectedGuitarId: 0,
            year: ""
        };
    },
    el: "#app",
    methods: {
        addGuitar() {
            const guitar = {
                brand: this.brand,
                color: this.color,
                model: this.model,
                year: this.year
            };
            axios
                .post( "/api/guitars/add", guitar )
                .then( () => {
                    this.$refs.year.focus();
                    this.brand = "";
                    this.color = "";
                    this.model = "";
                    this.year = "";
                    this.loadGuitars();
                } )
                .catch( ( err: any ) => {
                    // tslint:disable-next-line:no-console
                    console.log( err );
                } );
        },
        confirmDeleteGuitar( id: string ) {
            const guitar = this.guitars.find( ( g ) => g.id === id );
            this.selectedGuitar = `${ guitar.year } ${ guitar.brand } ${ guitar.model }`;
            this.selectedGuitarId = guitar.id;
            const dc = this.$refs.deleteConfirm;
            const modal = M.Modal.init( dc );
            modal.open();
        },
        deleteGuitar( id: string ) {
            axios
                .delete( `/api/guitars/remove/${ id }` )
                .then( this.loadGuitars )
                .catch( ( err: any ) => {
                    // tslint:disable-next-line:no-console
                    console.log( err );
                } );
        },
        loadGuitars() {
            axios
                .get( "/api/guitars/all" )
                .then( ( res: any ) => {
                    this.isLoading = false;
                    this.guitars = res.data;
                } )
                .catch( ( err: any ) => {
                    // tslint:disable-next-line:no-console
                    console.log( err );
                } );
        }
    },
    mounted() {
        return this.loadGuitars();
    }
} );

更新tsconfig.json以从后端Node.js构建过程中排除src/public文件夹。

{
    "compilerOptions": {
        "module": "commonjs",
        "esModuleInterop": true,
        "target": "es6",
        "noImplicitAny": true,
        "moduleResolution": "node",
        "sourceMap": true,
        "outDir": "dist",
        "baseUrl": ".",
        "paths": {
            "*": [
                "node_modules/*"
            ]
        }
    },
    "include": [
        "src/**/*"
    ],
    "exclude": [
        "src/public"
    ]
}

src/public/js下创建一个新的tsconfig.json文件,并添加以下代码。 此TypeScript配置用于编译main.ts以在浏览器中使用。

{
    "compilerOptions": {
        "lib": [
            "es6",
            "dom"
        ],
        "noImplicitAny": true,
        "allowJs": true,
        "target": "es5",
        "strict": true,
        "module": "es6",
        "moduleResolution": "node",
        "outDir": "../../../dist/public/js",
        "sourceMap": true
    }
}

接下来,更新src/index.ts配置快递从提供静态文件public文件夹。 在将Express配置为使用EJS的代码之后添加此行。

...
// Configure Express to use EJS
app.set( "views", path.join( __dirname, "views" ) );
app.set( "view engine", "ejs" );

// Configure Express to serve static files in the public folder
app.use( express.static( path.join( __dirname, "public" ) ) );

更新src/views/guitars.ejs以添加Vue应用程序模板和对js/main.js文件的引用。




    
    
    Guitar Inventory
    
    
    


    <% include partials/nav %>
    

Guitar list

Year Brand Model Color

No guitars yet!

Add a guitar

最后,更新package.json添加新的parcel脚本,更新build脚本,并添加新的alias为Vue的部分。 alias部分将Parcel指向正确的Vue文件,以与src/public/js/main.ts捆绑在一起。

"scripts": {
    "clean": "rimraf dist/*",
    "copy-assets": "ts-node tools/copyAssets",
    "lint": "tslint -c tslint.json -p tsconfig.json --fix",
    "tsc": "tsc",
    "parcel": "parcel build src/public/js/main.ts -d dist/public/js",
    "build": "npm-run-all clean lint tsc copy-assets parcel",
    "dev:start": "npm-run-all build start",
    "dev": "nodemon --watch src -e ts,ejs --exec npm run dev:start",
    "start": "node .",
    "initdb": "ts-node tools/initdb",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "alias": {
    "vue": "./node_modules/vue/dist/vue.common.js"
  },

现在,重新启动构建并试用您的新Web应用程序!

npm run dev

了解有关Node和TypeScript的更多信息

本教程仅概述您可以使用Node.js和TypeScript进行的操作。 以下是更多探索资源。

  • Jonathan Creamer的 TypeScript学习资源
  • TypeScript Node Starter – Microsoft的一个开源项目
  • TypeScript深入研究 – Basarat Ali Syed的免费在线书
  • TypeScript文档
  • Vue TypeScript支持
  • 简单节点认证

您可以在GitHub上找到完整的Guitar Inventory项目。

关注我们以获取更多精彩内容和我们团队的最新消息! 您可以在Twitter , Facebook和LinkedIn上找到我们。 有什么问题吗 在下面的评论中打我们。

翻译自: https://www.sitepoint.com/how-to-use-typescript-to-build-a-node-api-with-express/

你可能感兴趣的:(如何使用TypeScript通过Express构建节点API)