Nodal:在Node.js中轻松创建API服务的教程

If you haven't heard about Nodal yet, or you have but you're unsure of where to begin, this tutorial is for you! Make sure you keep up with Nodal on GitHub to follow project updates.

如果您还没有听说过Nodal ,或者您已经听说过Nodal ,但不确定从哪里开始,那么本教程适合您! 确保您与GitHub上的Nodal保持同步,以跟踪项目更新。

Nodal is a Node.js server platform and framework that enables you to develop API services easily. With products growing increasingly multi-platform (web, mobile, IoT) we need to start thinking about our backend architecture using a service-oriented approach to begin with instead of as an afterthought.

Nodal是一个Node.js服务器平台和框架,可让您轻松开发API服务。 随着产品越来越多平台(Web,移动,IoT)的发展,我们需要开始使用面向服务的方法来思考后端架构,而不是事后才想到。

The goal of Nodal is to build an encyclopedia around Node.js that allows any developer — newbie or veteran, back-end or front-end — to join in and begin creating web applications effortlessly.

Nodal的目标是围绕Node.js构建一个百科全书,使任何开发人员(无论是新手还是老手,后端还是前端)都可以加入其中并开始轻松地创建Web应用程序。

Nodal has built-in PostgreSQL support, makes frequent use of modern ES6 syntax and idioms, and encourages fairly rigid software design patterns. This allows Nodal to make a bunch of decisions for you so that you can get your applications built and deployed quickly, with more confidence. Get to writing the code that powers your product faster and let Nodal do the heavy lifting.

Nodal具有内置的PostgreSQL支持,经常使用现代的ES6语法和习惯用法,并鼓励相当严格的软件设计模式。 这使Nodal可以为您做出一系列决策,以便您可以更加自信地快速构建和部署应用程序。 开始编写能更快地为您的产品提供动力的代码,让Nodal承担繁重的工作。

While Nodal's core competency is not being used as a traditional monolithic webserver, it can still be used for that purpose. It's out of scope of this tutorial, but by sniffing around documentation you'll be able to find out how to get Nodal to do whatever you'd like - serving a static branding website, template support, etc.

虽然交点的核心竞争力是不是被用来作为一个传统的单片式网络服务器,它仍然可以用于这一目的。 它不在本教程的讨论范围之内,但是通过四处寻找文档,您将能够找到如何使Nodal做您想做的一切-提供静态品牌网站,模板支持等。

我们的第一个节点项目 (Our First Nodal Project)

While a lot of Nodal will be familiar to you if you've worked with an MVC framework such as Django or Ruby on Rails before, we'll start with getting a basic API Server set up and generate a few models. It's good to start with a sample project so let's make a Twitter clone called Instatweet.

如果您以前使用过Django或Ruby on Rails之类的MVC框架,虽然很多Nodal会为您所熟悉,但我们将从设置基本的API Server并生成一些模型开始。 最好从一个示例项目开始,所以让我们创建一个名为Instatweet的Twitter克隆。

For reference, you can find a completed version of the project used for this tutorial at keithwhor/instatweet-api on GitHub.

作为参考,您可以在GitHub上的keithwhor / instatweet-api中找到本教程使用的项目的完整版本。

设置节点 (Setting Up Nodal)

In order to install Nodal and get it working with a database we'll need to do the following:

为了安装Nodal并使其与数据库一起使用,我们需要执行以下操作:

  1. Install Node.js 4.x or higher

    安装Node.js 4.x或更高版本
  2. Install PostgreSQL

    安装PostgreSQL
  3. Install Nodal

    安装节点

安装Node.js (Installing Node.js)

To make sure you're running a recent version of Node.js, just head over to Nodejs.org and download the most recent 4.x version or higher. Nodal has been developed explicitly for 4.x, so that's what's recommended at present time.

为确保您正在运行最新版本的Node.js,只需转到Nodejs.org,然后下载最新的4.x版本或更高版本即可。 Nodal已针对4.x明确开发,因此目前建议这样做。

安装PostgreSQL (Installing PostgreSQL)

If you're using Mac OS X I strongly recommend using Postgres.app to get PostgreSQL up and running on your machine. Make sure you configure your $PATH to get access to the command line tools. Once Postgres.app is installed and you've followed the instructions to set up the CLI, make sure there's a postgres superuser named postgres (no password) with:

如果您使用的是Mac OS XI,强烈建议您使用Postgres.app来启动PostgreSQL并在您的计算机上运行。 确保将$ PATH配置为可以访问命令行工具。 一旦安装了Postgres.app,并且按照说明设置了CLI,请确保存在一个名为postgres的postgres超级用户(无密码),并具有:


$ createuser postgres -s


For a Windows installation, you can check out the PostgreSQL website.

对于Windows安装,您可以访问PostgreSQL网站

安装节点 (Installing Nodal)

You're almost ready to start setting up API servers in a flash. :)

您几乎可以立即开始设置API服务器。 :)

To install Nodal, simply open up your Terminal command line and type:

要安装Nodal,只需打开您的终端命令行并输入:


$ npm install nodal -g


This will install the Nodal command line tools and the current version of the Nodal core. You're all ready to begin!

这将安装Nodal命令行工具和当前版本的Nodal内核。 你们都准备好开始了!

建立专案 (Creating Your Project)

Project setup is easy. Go to the directory in which you'd like to create your project folder and type:

项目设置很容易。 转到要在其中创建项目文件夹的目录,然后键入:


$ nodal new


You'll see a prompt...

您会看到提示...


Welcome to Nodal! v0.7.x

? Name (my-nodal-project)


You can name it whatever you'd like, but I'll be using instatweet-api for this tutorial. You'll also be asked to enter in your name. Nodal will create your project directory for you, and copy all necessary packages (node_modules) from your global installation of nodal.

您可以随意命名,但是在本教程中我将使用instatweet-api 。 您还将被要求输入您的姓名。 Nodal将为您创建项目目录,并从您的nodal全局安装中复制所有必需的软件包(node_modules)。

启动服务器 (Starting Your Server)

Boot your server with:

使用以下方式引导服务器:


$ nodal s


You'll see something like:

您会看到类似以下内容:


[Nodal.Daemon] Startup: Initializing
Initializer Ready
[Nodal.Daemon] Startup: Spawning HTTP Workers
[Nodal.27454] Startup: Starting HTTP Worker
[Nodal.27455] Startup: Starting HTTP Worker
[Nodal.27455] Ready: HTTP Worker listening on port 3000
[Nodal.27454] Ready: HTTP Worker listening on port 3000


In fact, you'll see a new [Nodal.XXXX] process spawned for each one of the cores on your processor. So if you see 16 messages here (2 for each core), don't worry about it. That's just the Daemon doing its job. :)

实际上,您会看到为处理器上的每个内核产生了一个新的[Nodal.XXXX]进程。 因此,如果您在此处看到16条消息(每个核心2条消息),请不必担心。 那只是守护进程的工作。 :)

Nodal does a good job of rebooting itself on file changes, so just leave the server running and open another Terminal window and navigate back to your instatweet-api project directory.

Nodal很好地完成了文件更改后自动重启,因此只需让服务器运行并打开另一个“终端”窗口,然后导航回到您的instatweet-api项目目录。

创建您的第一个模型 (Creating your first Model)

Creating a Model is simple. Fire up your terminal and type:

创建模型很简单。 启动您的终端并输入:


$ nodal g:model Tweet user_id:int body:string


You'll see something like:

您会看到类似以下内容:


Create: ./app/models/tweet.js
Create: ./db/migrations/2016022003113671__create_tweet.js


A Model file and a Migration have automatically been created for you.

将自动为您创建一个模型文件和一个迁移文件。

The Model file contains information about the Tweet object in your project, which contains an integer field called user_id and a body which is a string.

Model文件包含有关项目中Tweet对象的信息,该信息包含一个名为user_id的整数字段和一个字符串主体。

The Migration file is a set of commands for creating a table in the Postgres database to hold Tweet data, with the fields as columns in the table.

迁移文件是一组命令,用于在Postgres数据库中创建一个表以保存Tweet数据,并将字段作为表中的列。

创建您的第一个控制器 (Creating your first Controller)

Now that we have a Model, we want a Controller for that model. We can use the command line again to make this easy for us:

现在我们有了一个模型,我们想要一个用于该模型的控制器。 我们可以再次使用命令行来简化此操作:


$ nodal g:controller v1 --for:Tweet


The v1 tells us a namespace and the --for:Tweet tells us the Controller is a Create-Read-Update-Destroy (CRUD) controller for a Model resource (will do some things for us). Note that something like:

v1告诉我们一个名称空间,而--for:Tweet告诉我们该Controller是Model资源的Create-Read-Update-Destroy(CRUD)控制器(将为我们做一些事情)。 注意类似:


$ nodal g:controller v1/Tweet


is also acceptable, but it will create an empty Controller template without the CRUD commands, so you'll need to write all the functions yourself.

也可以接受,但是它将创建一个没有CRUD命令的空Controller模板,因此您需要自己编写所有功能。

From this command you should see:

通过此命令,您应该看到:


Create: ./app/controllers/v1/tweets_controller.js
Modify: ./app/router.js


The Nodal command line tools have automatically modified your routes and created your controllers for you. The router has automatically added certain standard paths and HTTP methods such as GET and POST. to serve up the API for tweets -- listing, creating, updating, deleting tweets, etc.

Nodal命令行工具已自动修改了您的路线并为您创建了控制器。 路由器自动添加了某些标准路径和HTTP方法,例如GET和POST。 提供推文的API-列出,创建,更新,删除推文等

运行迁移 (Running Migrations)

Now that you have a Tweet Model and a Migration, before we start interfacing with our Tweet Model we'll want to make sure the database is ready to handle it. Create the database specified in config/db.json with:

现在您已经有了一个Tweet模型和一个Migration,在开始与Tweet模型进行交互之前,我们将要确保数据库已准备好处理它。 使用以下命令创建在config/db.json指定的数据库:


$ nodal db:create


Now, prepare it for migrations and then run those migrations with:

现在,为迁移做好准备,然后使用以下命令运行这些迁移:


$ nodal db:prepare
$ nodal db:migrate


与我们的推文接口 (Interfacing With our Tweets)

Make sure your server is running again on localhost with nodal s.

确保您的服务器通过nodal s再次在localhost上运行。

Open http://localhost:3000/v1/tweets in your browser. This is the route that was created automatically by creating the Tweet model. Note that we've automatically pluralized the Tweet model to make the route be "tweets". You should see:

在浏览器中打开http://localhost:3000/v1/tweets 。 这是通过创建Tweet模型自动创建的路由。 请注意,我们已自动将Tweet模型多元化,以使路线成为“ tweets”。 您应该看到:


{
  "meta": {
    "total": 0,
    "count": 0,
    "offset": 0,
    "error": null
  },
  "data": []
}


To create a tweet, simply send a POST request to the same endpoint with JSON data or urlencoded data. You can use curl for this. (Also checkout the Postman Chrome Plugin, a visual tool that is great for poking data to and from APIs.)

要创建推文,只需将带有JSON数据或urlencoded数据的POST请求发送到同一端点。 您可以为此使用curl 。 (还可以检查Postman Chrome插件 ,这是一个可视化工具,非常适合在API之间来回发送数据。)


$ curl --data "user_id=1&body=Testing" http://localhost:3000/v1/tweets


Your response should look something like:

您的回复应类似于:


{
  "meta": {
    "total": 1,
    "count": 1,
    "offset": 0,
    "error": null
  },
  "data": [
    {
      "id": 1,
      "user_id": 1,
      "body": "Testing",
      "created_at": "2016-02-20T03:21:30.879Z",
      "updated_at": "2016-02-20T03:21:30.882Z"
    }
  ]
}


Refresh your browser page at http://localhost:3000/v1/tweets and you should see the tweet there.

http://localhost:3000/v1/tweets刷新浏览器页面,您应该在那看到推文。

创建用户模型 (Creating a User Model)

Great! We now have Tweets, but we want some users. In order to handle password encryption (so you don't have to write it yourself), Nodal comes with a pre-baked User model that you can generate with:

大! 现在我们有推文,但我们需要一些用户。 为了处理密码加密(因此您不必自己编写),Nodal附带了可以使用以下方法生成的预烘焙用户模型:


$ nodal g:model --user


This User model will be generated with username, email and password fields automatically, along with the bcrypt package for password encryption.

该用户模型将自动使用usernameemailpassword字段以及用于密码加密的bcrypt包生成。

Generate a Controller for your user models with:

通过以下方式为您的用户模型生成控制器:


$ nodal g:controller v1 --for:User


Migrate your database with:

使用以下方法迁移数据库:


$ nodal db:migrate


Run your server with nodal s, and send the following POST request to create a user:

使用nodal s运行服务器,并发送以下POST请求以创建用户:


$ curl --data "username=test_user&email=test@test.com&password=password" http://localhost:3000/v1/users


You'll get a response like:

您会收到类似的响应:


{
  "meta": {
    "total": 1,
    "count": 1,
    "offset": 0,
    "error": null
  },
  "data": [
    {
      "id": 1,
      "email": "test@test.com",
      "password": "$2a$10$/pXLNrp9afneJtImvNTBO.79CIsd8N39fko4sF3CaXZyoaxpctQZS",
      "username": "test_user",
      "created_at": "2016-02-20T03:27:58.152Z",
      "updated_at": "2016-02-20T03:27:58.255Z"
    }
  ]
}


Wonderful! We have users, and they have encrypted passwords.

精彩! 我们有用户,他们有加密的密码。

隐藏敏感字段和模型验证 (Hiding Sensitive Fields and Model Validations)

Visit http://localhost:3000/v1/users in your browser to see a list of all users - you may notice a problem. The API returns the encrypted password. This isn't something we want. To fix this we'll open app/models/user.js:

在浏览器中访问http://localhost:3000/v1/users以查看所有用户的列表-您可能会注意到一个问题。 API返回加密的密码。 这不是我们想要的东西。 为了解决这个问题,我们将打开app/models/user.js

Find the lines:

查找行:


User.validates('email', 'must be valid', v => v && (v + '').match(/.+@.+\.\w+/i));
User.validates('password', 'must be at least 5 characters in length', v => v && v.length >= 5);


Underneath them, add:

在它们下面添加:


User.hides('password');


Open http://localhost:3000/v1/users in your browser and voila! Password gone.

在浏览器中打开http://localhost:3000/v1/users ,瞧! 密码不见了。

You may be wondering what the User.validates(...) calls are about. Well, let's try a new curl request with a password...

您可能想知道User.validates(...)调用是关于什么的。 好吧,让我们尝试一个带有密码的新curl请求...


$ curl --data "username=test_user&email=test@test.com" http://localhost:3000/v1/users


Awesome! Our validations work as expected. You can try playing along with them on your own.

太棒了! 我们的验证按预期工作。 您可以尝试自己玩。


{
  "meta": {
    "total": 0,
    "count": 0,
    "offset": 0,
    "error": {
      "message": "Validation error",
      "details": {
        "password": [
          "must be at least 5 characters in length"
        ]
      }
    }
  },
  "data": []
}


将用户加入API响应中的推文 (Joining Users to Tweets in API Responses)

You'll notice that in our first Tweet we specified a user_id. We can make sure we join users to Tweets in our API response by doing the following.

您会注意到,在我们的第一个Tweet我们指定了一个user_id 。 通过执行以下操作,我们可以确保在API响应中将用户加入推文。

First, open app/models/tweet.js:

首先,打开app/models/tweet.js


module.exports = (function() {

  'use strict';

  const Nodal = require('nodal');

  class Tweet extends Nodal.Model {}

  Tweet.setDatabase(Nodal.require('db/main.js'));
  Tweet.setSchema(Nodal.my.Schema.models.Tweet);

  return Tweet;

})();


Before return Tweet, add the lines:

return Tweet之前,添加以下行:


const User = Nodal.require('app/models/user.js');
Tweet.joinsTo(User, {multiple: true});


Now, open app/controllers/v1/tweets_controllers.js and find index():

现在,打开app/controllers/v1/tweets_controllers.js并找到index()


index() {

  Tweet.query()
    .where(this.params.query)
    .end((err, models) => {

      this.respond(err || models);

  });
}


Change this to:

更改为:


index() {

  Tweet.query()
    .where(this.params.query)
    .join('user')
    .end((err, models) => {
      this.respond(err || models, ['id', 'body', 'created_at', 'user']);
    });
}


Refresh http://localhost:3000/v1/tweets in your browser and you should see:

在浏览器中刷新http://localhost:3000/v1/tweets ,您应该看到:


{
  "meta": {
    "total": 1,
    "count": 1,
    "offset": 0,
    "error": null
  },
  "data": [
    {
      "id": 1,
      "body": "Testing",
      "created_at": "2016-02-20T03:21:30.879Z",
      "user": {
        "id": 1,
        "email": "test@test.com",
        "username": "test_user",
        "created_at": "2016-02-20T03:27:58.152Z",
        "updated_at": "2016-02-20T03:27:58.255Z"
      }
    }
  ]
}


You may have noticed we passed a second parameter to this.respond(...). That parameter is known as the Model Interface and it tells the Controller which fields of the model to actually display in the API response. By default, the Controller will display all fields with the exception of those you've marked as sensitive / hidden (from above). It will not, however, display any joined models. You'll need to specify these manually (like we've done here, with 'user'). If you want to restrict which fields from the User model show, do the following:

您可能已经注意到我们向this.respond(...)传递了第二个参数。 该参数称为模型接口 ,它告诉控制器在API响应中实际显示模型的哪些字段。 默认情况下,控制器将显示所有字段,但您已将其标记为敏感/隐藏的字段除外。 但是,它不会显示任何加入的模型。 您需要手动指定它们(就像我们在此处使用'user' )。 如果要限制显示User模型中的哪些字段,请执行以下操作:


this.respond(err || models, ['id', 'body', 'created_at', {user: ['username']}]);


将推文加入用户 (Joining Tweets to Users)

Joining Tweets to Users is a similar process. We'll add to app/models/user.js:

将推文添加到用户是一个类似的过程。 我们将添加到app/models/user.js


const Tweet = Nodal.require('app/models/tweet.js');
User.joinedBy(Tweet, {multiple: true});


Note: Make sure that joinsTo should always be specified on the child table (whichever table / model has the parent_id field), and joinedBy is always specified on the parent table. These conventions are important.

注意 :确保应该始终在表上指定joinsTo (无论哪个表/模型都有parent_id字段),而joinedBy总是在表上指定。 这些约定很重要。

Similarly, in app/controllers/v1/user_controller.js change your index() method to:

同样,在app/controllers/v1/user_controller.jsindex()方法更改为:


index() {

  User.query()
    .where(this.params.query)
    .join('tweets')
    .end((err, models) => {

      this.respond(
          err || models,
          [
            'id',
            'username',
            'email',
            'tweets'
          ]
        );
    });
}


and you'll see the response:

然后您将看到响应:


{
  "meta": {
    "total": 1,
    "count": 1,
    "offset": 0,
    "error": null
  },
  "data": [
    {
      "id": 1,
      "username": "test_user",
      "email": "test@test.com",
      "tweets": [
        {
          "id": 1,
          "user_id": 1,
          "body": "Testing",
          "created_at": "2016-02-20T03:21:30.879Z",
          "updated_at": "2016-02-20T03:21:30.882Z"
        }
      ]
    }
  ]
}


播种数据库 (Seeding Your Database)

Alright, this is cool, but let's generate some test data!

好的,这很酷,但是让我们生成一些测试数据!

Open up config/seed.json:

打开config/seed.json


{
  "development": {},

  "test": {},

  "production": {}

}


Modify this to:

修改为:


{

  "development": {
    "User": [
      {
        "username": "Rihanna",
        "email": "rihanna@r.com",
        "password": "password"
      },
      {
        "username": "The Weeknd",
        "email": "weeknd@w.com",
        "password": "password"
      },
      {
        "username": "Drake",
        "email": "drake@d.com",
        "password": "password"
      }
    ],
    "Tweet": [
      {
        "userid": 1,
        "body": "Hello, world"
      },
      {
        "userid": 2,
        "body": "hello, world!"
      },
      {
        "user_id": 3,
        "body": "You used to call me on my cell phone, world"
      }
    ]
  },

"test": {},

"production": {}

}


We'll now run:

现在运行:


$ nodal db:bootstrap


And visit http://localhost:3000/v1/tweets (make sure server is running):

并访问http://localhost:3000/v1/tweets (确保服务器正在运行):


{
  "meta": {
    "total": 3,
    "count": 3,
    "offset": 0,
    "error": null
  },
  "data": [
    {
      "id": 1,
      "body": "Hello, world",
      "created_at": "2016-02-20T04:08:38.762Z",
      "user": {
        "id": 1,
        "email": "rihanna@r.com",
        "username": "Rihanna",
        "created_at": "2016-02-20T04:08:38.765Z",
        "updated_at": "2016-02-20T04:08:38.765Z"
      }
    },
    {
      "id": 2,
      "body": "hello, world!",
      "created_at": "2016-02-20T04:08:38.764Z",
      "user": {
        "id": 2,
        "email": "weeknd@w.com",
        "username": "The Weeknd",
        "created_at": "2016-02-20T04:08:38.767Z",
        "updated_at": "2016-02-20T04:08:38.767Z"
      }
    },
    {
      "id": 3,
      "body": "You used to call me on my cell phone, world",
      "created_at": "2016-02-20T04:08:38.764Z",
      "user": {
        "id": 3,
        "email": "drake@d.com",
        "username": "Drake",
        "created_at": "2016-02-20T04:08:38.767Z",
        "updated_at": "2016-02-20T04:08:38.767Z"
      }
    }
  ]
}


Awesome! What we've done here is set a database seed by specifying an array of values with which we'll populate each table with in the database. The key for each array (like "User") is just the model name. db:bootstrap is a command that runs nodal db:prepare, nodal db:migrate and nodal db:seed, in that order.

太棒了! 我们在这里所做的是通过指定一个值数组来设置数据库种子,我们将使用这些值填充数据库中的每个表。 每个数组的键(例如"User" )只是模型名称。 db:bootstrapnodal db:prepare顺序运行nodal db:preparenodal db:migratenodal db:seed命令。

查询端点 (Querying Your Endpoint)

The last thing we'll do is start asking our endpoint for different types of results. You may have noticed a .where(this.params.query) on the index() method for both of our controllers. This is creating a filter by through which we selectively choose what results we'd like based on the HTTP query parameters.

我们要做的最后一件事是开始向端点询问不同类型的结果。 您可能已经注意到我们两个控制器的index()方法上都有一个.where(this.params.query) 。 这是在创建一个过滤器 ,通过该过滤器我们可以根据HTTP查询参数有选择地选择想要的结果。

For example, try opening these in your browser:

例如,尝试在浏览器中打开它们:


http://localhost:3000/v1/tweets?body=Hello,%20world
http://localhost:3000/v1/tweets?body__is=Hello,%20world
http://localhost:3000/v1/tweets?body__not=Hello,%20world
http://localhost:3000/v1/tweets?body__startswith=Hello
http://localhost:3000/v1/tweets?body__istartswith=Hello
http://localhost:3000/v1/tweets?body__endswith=world
http://localhost:3000/v1/tweets?user__username=Drake


Supported comparators by the PostgreSQL adapter (required __ before them) are:

PostgreSQL适配器支持的比较器(在它们之前需要__ )是:


is
not
lt
lte
gt
gte
contains
icontains
startswith
istartswith
endswith
iendswith
like
ilike
is_null
not_null
in
not_in
json
jsoncontains


Keep note that the default is __is, and you can query joined models by using the join name (i.e. user) and separating the field with double underscores as well (__).

请注意,默认值为__is ,并且您可以通过使用联接名称(即user )并用双下划线( __ )分隔字段来查询联接的模型。

享受,并继续探索! (Enjoy, and Keep Exploring!)

That ends the tutorial for now. Remember you can find a completed version of everything outlined here at keithwhor/instatweet-api. There's a lot more you can do with Nodal, including this GraphQL demo, but there should be enough material here to get you started.

教程到此结束。 记住,您可以在keithwhor / instatweet-api中找到此处概述的所有内容的完整版本。 Nodal可以做很多事情, 包括这个GraphQL演示 ,但是这里应该有足够的材料来帮助您入门。

Check out the Nodal website and Star the repository on GitHub to keep up to date as the project grows and progresses. We welcome you to join the community! Our Gitter channel is a great place to get quick responses and pointers.

请访问Nodal网站,在GitHub上存储库加注星标 ,以随着项目的发展和进展保持最新状态。 欢迎您加入社区! 我们的Gitter渠道是获得快速响应和指示的好地方。

Additionally, you can follow along with a set of screencasts that walk through very similar material based on Nodal 0.6.

此外,您还可以按照一系列截屏视频进行操作 ,这些截屏视频将浏览基于Nodal 0.6的非常相似的资料。

Thanks for reading. :) You can follow me on Twitter at @keithwhor.

谢谢阅读。 :)您可以通过@keithwhor在Twitter上关注我。

翻译自: https://davidwalsh.name/nodejs-services

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值