初识nodejs,简易搭建express服务器

1,512 阅读7分钟

前言

nodejs是一个构建网络应用的基础框架,开发者可以在nodejs的基础上构建服务器、客户端、命令行工具等应用。如今nodejs已然成为了前端中必要的一门技术,接下来笔者就带你了解和入门这项技术,并利用express来搭建服务器应用。

nodejs的特点

异步I/O

在nodejs中,绝大多数的操作都是以事件循环的方式,异步去进行的。这就使得每个I/O调用之间无需等待上一个I/O执行完毕再执行,所以nodejs很适合应用在I/O任务密集的场景。个人理解尽管有async/await这样同步的编程方式,也应该少用同步阻塞来写业务逻辑。具体可以再用一篇文章来说说。(挖坑)

事件与回调函数

在js中,函数是第一公民,可以常常看到使用函数作为参数传入,这种方式我们称为回调函数。使用回调函数可以使多个异步任务场景下各事件独立(每个事件都是一个独立的函数),使得我们的代码可以进行松耦合。

但这种模式写代码还是会有弊端。使用事件回调函数方式来写代码必定会使代码的编写顺序和执行顺序不一一对应,不易于代码阅读。另一方面回调函数嵌套变多后会造成回调地狱的问题(可以用promise来解决)。

单线程

node保留了js在浏览器中的单线程特点。这就意味着在node.js中不会遇到像多线程语言会遇到的线程锁问题(死锁问题),共享状态和交换context(上下文)所带来的性能上的开销。但同时,单线程也会有以下缺点

  • 无法利用多核CPU进行计算。
  • 错误会导致整个应用退出。
  • 大量计算占用CPU导致无法继续调用异步I/O。

对于大量计算占用CPU的问题,node采用了子线程来解决——通过把大量计算任务分发给多个子进程,等计算完成后通过事件消息来传递结果。

跨平台

为了使事件驱动能在跨平台上使用,nodejs的上层模块与操作系统之间还弄了一层平台层架构——libuv。如下图所示(图出自《深入浅出Node.js》):

libuv是为了让nodejs能在多平台使用异步I/O而写的库。libuv中的事件循环算法对应不同平台会有不同的实现,如一下表格:

系统事件循环算法
linuxepoll
OSXkqueue 或者 BSD
SunOSevent ports
WindowsIOCP

应用场景

I/O密集型

node利用事件循环的处理能力进行I/O处理,资源占用极少。例如web应用,聊天应用等。

对于CPU密集型应用场景

可以使用子线程的方式,将一部分node进程当做常驻服务进程用于计算,然后利用进程间的消息传递结果,分离计算与I/O,这样还能充分利用多CPU。所以由此看出,对于CPU密集型的业务,node也能胜任,关键在于如何合理安排调度。

nodejs在B/S中的定位

在B/S架构中,nodejs可以用于I/O频繁的地方,比如接收请求→发送请求→异步获取数据进行组装→返回给前端。大致如下图所示。

test.png

nodejs中的模块机制

nodejs在13.2.0前的版本中只支持一套参考了CommonJS规范后,实现的模块引用标准。13.2.0以后支持ES规范中的import引入方式。

nodejs中,主要通过三个步骤来引入模块:

  1. 路径分析。
  2. 文件定位。
  3. 编译执行。

nodejs中模块分为核心模块文件模块。

核心模块

核心模块中分为js编写的核心模块和C/C++编写的核心模块。

  • js编写的核心模块会通过V8自带的js2c.py来进行编译生成C/C++代码。
  • C/C++编写的核心模块则会编译成二进制文件,一旦nodejs开始执行,就直接加载到内存中。buffer、crypto、evals、fs、os等模块都是部分通过过C/C++编写的。

搭建express服务器

聊了一些对nodejs的基本概念和特点,那么就开始一步步简单搭建一个express服务。

初始化项目

先安装typescript。

npm install -g typescript

创建文件架node-server,进入文件夹内使用npm init来初始化工程。然后使用tsc —init来生成tsconfig.json。详细配置可以看这里,以下是笔者的配置。

{
  "compilerOptions": {
    /* Basic Options */
    "target": "es6",                                /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */
    "module": "commonjs",                           /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
    // "lib": [],                                   /* Specify library files to be included in the compilation. */
    "allowJs": true,                             /* Allow javascript files to be compiled. */
    "outDir": "dist",                              /* Redirect output structure to the directory. */
    /* Strict Type-Checking Options */
    "strict": true,                                 /* Enable all strict type-checking options. */
    "noImplicitAny": true,                       /* Raise error on expressions and declarations with an implied 'any' type. */
    "noImplicitThis": true,                      /* Raise error on 'this' expressions with an implied 'any' type. */
    /* Module Resolution Options */
    "moduleResolution": "node",                  /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
    "baseUrl": "./",                             /* Base directory to resolve non-absolute module names. */
    "paths": {
      "*": ["node_modules/*", "src/types/*"],
      "@/*": ["src/*"]
    },                                 /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
    "esModuleInterop": true,                        /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
    "skipLibCheck": true,                           /* Skip type checking of declaration files. */
    "forceConsistentCasingInFileNames": true        /* Disallow inconsistently-cased references to the same file. */
  },
  "include": ["src"], // 需要编译的 ts 文件,这里设置为 src 目录下的所有文件
  "exclude": ["node_modules"], // 编译需要排除的文件目录
}

安装依赖

先安装express、body-parser、compression和typescript

npm install --save-dev typescript
npm install --save body-parser
npm install --save-dev @types/body-parser
npm install --save compression
npm install --save-dev @types/compression
npm install --save express
npm install --save-dev @types/express
  • body-parser:用来解析用户端发送的请求中带的JSON数据和表单数据。
  • compression:压缩请求和响应。

接下来我们新建一些文件和文件夹,来构建整个应用。

配置eslint

用eslint来约束代码风格,先安装依赖

npm install --save-dev @types/eslint eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin

编写.eslintrc文件

{
  "parser": "@typescript-eslint/parser",
  "extends": ["plugin:@typescript-eslint/recommended"],
  "parserOptions": {
    "ecmaVersion": 2018,
    "sourceType": "module"
  },
  "rules": {
    "semi": ["error", "always"],
    "quotes": ["error", "single"],
    "@typescript-eslint/explicit-function-return-type": "off",
    "@typescript-eslint/explicit-module-boundary-types": "off",
    "@typescript-eslint/no-explicit-any": 1,
    "@typescript-eslint/no-inferrable-types": [
      "warn", {
        "ignoreParameters": true
      }
    ],
    "@typescript-eslint/no-unused-vars": "warn"
  }
}

编写.eslintignore

# /node_modules/* in the project root is ignored by default
# build artefacts
dist/*
coverage/*
# data definition files
**/*.d.ts
# 3rd party libs
/src/public/
# custom definition files
/src/types/

编写app.ts文件

app.ts文件主要用于注册路由和中间件。

import bodyParse from 'body-parser';
import express from 'express';
import { NextFunction, Request, Response } from 'express';
import compression from 'compression';

const app = express();

/*中间件*/

// 压缩
app.use(compression());

// 请求解析json
app.use(bodyParse.json({ limit: '20mb' }));
// 请求解析formData
app.use(
  bodyParse.urlencoded(
    {
      limit: '20mb',
      extended: false
    }
  )
);
/* 路由 */
app.get('/test', function (req: Request, res: Response) {
  res.json({
    hello: 'www'
  });
});

export default app;

配置server

在config文件夹里面新建index.ts,来负责服务的端口设置。

export type SystemConfig = {
  port: number
}
export const systemConfig: SystemConfig = {
  port: 8003
};

在server.ts中引入。

import app from './app';

import { systemConfig } from './config';

const server = app.listen(systemConfig.port, function() {
  console.log(`server is listening at port ${systemConfig.port}`);
});

export default server;

编译与热更新

ts是无法直接运行的,所以要用ts-node来进行编译。但每次修改代码后都要ts-node来编译就很麻烦,所以这里使用nodemon来检测并热更新。为了在runtime的情况下加载目录中的tsconfig.json,则必须用到tsconfig-paths。

先安装依赖。

npm install --save-dev tsconfig-paths ts-node nodemon cross-env

在package.json的script中写下命令。

{
	"scripts": {
    "dev": "cross-env nodemon --watch 'src/' -e ts --exec 'ts-node' -r tsconfig-paths/register ./src/server.ts --files"
  }
}

运行

直接npm run dev运行,运行后访问http://localhost:8003/test便可以出现以下结果。

{ hello: 'www' }

总结&参考源码

这次搭建express比较简单,想要真的api和middleware文件夹要写些什么可以参考一下笔者用express搭建的项目github.com/Lstmxx/easy…

参考