Node.js

183 阅读9分钟

Node.js

Node.js是什么?

浏览器端JS:

1)js核心(ECMAScript):描述了JS的语法和基本对象。

2)文档对象模型 (DOM):处理网页内容的方法和接口,document、createElement、getElementById、appendChild等

3)浏览器对象模型(BOM):与浏览器交互的方法和接口,window、location、navigator等

Node.js

1)js核心(ECMAScript)

2)各种类库:文件系统fs、path,网络net、http、url

image.png

Node.js® is a JavaScript runtime built on Chrome's V8 JavaScript engine.

Node.js是一个JS运行环境(解释器),同时在这个环境中提供了一些内置库。

chrome中用js来控制浏览器,Node.js用来控制整个计算机。

在 Node.js 中,可以控制运行环境,因为代码只跑在Server的固定环境中,但浏览器中的js环境各异,兼容性问题多。

CommonJS规范

以前通过script标签引入多个JS,存在无法处理模块依赖的问题,

解决办法:

1)手工维护js顺序

2)不同脚本间调用使用全局变量

Node.js没有script标签,该怎么引入别的js文件?

每个文件就是一个模块,有自己的作用域。在一个文件里面定义的变量、函数、类,都是私有的,对其他文件不可见。

每个模块内部,module变量代表当前模块。这个变量是一个对象,它的exports属性(即module.exports,exports和module.exports是同一个引用)是对外的接口。加载某个模块,其实是加载该模块的module.exports属性。

// index.js
console.log('start lib');
var lib = require('./lib');
console.log(lib);
console.log('end lib');

// lib.js
console.log(module)
console.log('module.exports === exports', module.exports === exports)

执行node index.js后输出为:
start lib
Module {
  id: 'D:\\yangzongjun\\temp\\node\\share\\01_commonjs\\src\\lib.js',      
  path: 'D:\\yangzongjun\\temp\\node\\share\\01_commonjs\\src',
  exports: {},
  filename: 'D:\\yangzongjun\\temp\\node\\share\\01_commonjs\\src\\lib.js',
  loaded: false,
  children: [],
  paths: [
    'D:\\yangzongjun\\temp\\node\\share\\01_commonjs\\src\\node_modules',
    'D:\\yangzongjun\\temp\\node\\share\\01_commonjs\\node_modules',
    'D:\\yangzongjun\\temp\\node\\share\\node_modules',
    'D:\\yangzongjun\\temp\\node\\node_modules',
    'D:\\yangzongjun\\temp\\node_modules',
    'D:\\yangzongjun\\node_modules',
    'D:\\node_modules'
  ]
}
module.exports === exports true
{}
end lib

lib.js中可以通过下面3中方式对外暴露方法。常见误区:直接改写exports引用,最后一种不行,因为修改了exports的引用,但module.exports还是指向之前的内存地址。由于模块最终导出的是module.exports,所以修改exports引用不行

module.exports.add = (a, b) => a + b;
exports.add = (a, b) => a + b;
module.exports = (a, b) => a + b

exports = (a, b) => a + b // x

再看一下webpack是如何模拟实现ComonJS的:npx webpack --mode development --devtool inline-source-map,打包后dist产出为:

/******/ (() => {
  // webpackBootstrap
  var __webpack_modules__ = {
    /***/ './src/lib.js':
      /*!********************!*\
  !*** ./src/lib.js ***!
  \********************/
      /***/ (module, exports, __webpack_require__) => {
        /* module decorator */ module = __webpack_require__.nmd(module);
        // exports = function () {
        //     console.log('foo')
        // }

        exports = (a, b) => a + b;

        console.log(module);
        console.log('module.exports === exports', module.exports === exports);

        /***/
      },
  };
  /************************************************************************/
  // The module cache
  var __webpack_module_cache__ = {};

  // The require function
  function __webpack_require__(moduleId) {
    // Check if module is in cache
    var cachedModule = __webpack_module_cache__[moduleId];
    if (cachedModule !== undefined) {
      return cachedModule.exports;
    }
    // Create a new module (and put it into the cache)
    __webpack_module_cache__[moduleId] = {
      id: moduleId,
      loaded: false,
      exports: {},
    };
    var module = __webpack_module_cache__[moduleId]
    // Execute the module function
    __webpack_modules__[moduleId](module, module.exports, __webpack_require__);

    // Flag the module as loaded
    module.loaded = true;

    // Return the exports of the module
    return module.exports;
  }

  /************************************************************************/
  /* webpack/runtime/node module decorator */
  (() => {
    __webpack_require__.nmd = (module) => {
      module.paths = [];
      if (!module.children) module.children = [];
      return module;
    };
  })();

  /************************************************************************/
  var __webpack_exports__ = {};
  // This entry need to be wrapped in an IIFE because it need to be isolated against other modules in the chunk.
  (() => {
    /*!**********************!*\
  !*** ./src/index.js ***!
  \**********************/
    console.log('start lib');
    var lib = __webpack_require__(/*! ./lib */ './src/lib.js');
    console.log(lib);
    // lib.add(1, 2)
    console.log('end lib');

    // npx webpack --mode development --devtool inline-source-map
  })();

  /******/
})();

可以简化为如下demo:

function foo (module, exports) {
    // xxx
    exports = {'key': 1}
    return module.exports;
}

const testModule = {
    exports: {}
}
const requredFoo = foo(testModule, testModule.exports)
console.log(testModule) // 输出的仍旧是 {}

命令行工具 –内置API初探

  1. 如何执行:

    方式一:node filename.js

    方式二:文件第一行加上shebang#!/usr/bin/env node执行./filename.js

  2. 全局变量:

    module、module.exports、require

    __dirname 当前运行的脚本所在目录,注意和运行目录无关,无论在哪里运行,此地址都是固定的,是脚本存放目录

    __filename 当前运行的脚本文件地址+文件名

    process Node所处的当前进程

    global 全局环境 类型浏览器的window

    setTimeout、setInterval、setImediate…

    重点关注process.argv,是个数组,前两个参数分别表示node解释器地址、入口文件地址,后面才是用户传入的参数。

    // index.js
    console.log('process', process.argv)
    
    $ node index.js 
    process [
      'C:\\Program Files\\nodejs\\node.exe',
      'D:\\yangzongjun\\temp\\node\\share\\02_apis\\index.js'
    ]
    
    $ node index.js key=1 isInit
    process [
      'C:\\Program Files\\nodejs\\node.exe',
      'D:\\yangzongjun\\temp\\node\\share\\02_apis\\index.js',
      'key=1',
      'isInit'
    ]
    
  3. 模块:

    path

    fs

    child_process

    http

    net

    demo演示:

    const fs = require('fs')
    const path = require('path')
    
    console.log(path.join('foo', 'bar'))
    console.log(path.join('/foo', 'bar'))
    
    console.log(path.resolve('foo', 'bar'))
    console.log(path.resolve('/foo', 'bar'))
    
    // 读取文件的异步、同步方法:
    fs.readFile('index.js', 'utf-8', (err, data) => {
        if (err) {
            console.log(err);
            return;
        }
        console.log(data)
    })
    
    console.log(fs.readFileSync('index.js', 'utf-8'))
    
    // 写文件
    fs.writeFileSync('write.json', '{"key":1}')
    

    递归读取文件:

    const fs = require('fs')
    const path = require('path')
    
    function getAllFiles(baseDir) {
        let  res = [];
        const files = fs.readdirSync(baseDir);
    
        files.forEach(filePath => {
            const fullPath = path.join(baseDir, filePath);
        
            const stat = fs.statSync(fullPath);
            if (stat.isDirectory()) {
                res = res.concat(getAllFiles(fullPath));
            } else {
                res.push(fullPath)
            }
        });
    
        return res;
    }
                
    console.log(getAllFiles('/mnt/d/yangzongjun/temp/node/share'))
    

    child_process 可以新起一个进程来执行shell脚本:

    const {execSync} = require('child_process')
    
    console.log(execSync('ls', {encoding: 'utf-8'}))
    

异步

同步的方式更直观,更符合直觉,为啥还需要异步回调?

// 读取文件的异步、同步方法:
fs.readFile('index.js', 'utf-8', (err, data) => {
    if (err) {
        console.log(err);
        return;
    }
    console.log(data)
})

console.log(fs.readFileSync('index.js', 'utf-8'))

I/O是昂贵的:网络请求、磁盘、数据库读写。

image.png

下面的demo中花费时间分别为:

同步:M + N + ...

异步:max(M, N, ...)cpu不会因为io而阻塞

image.png

Node社区有言:

In node everything runs in parallel, except your code. 除了用户代码无法并行执行外,所有的I/O(磁盘I/O和网络I/O等)则是可以并行起来的。

如何理解这句话?

先来看一个Node的内置api:

// index.js
const os = require('os')
console.log('os.cpus()', os.cpus())

// node index.js
os.cpus() [
  {
    model: 'Intel(R) Core(TM) i7-9700 CPU @ 3.00GHz',
    speed: 3000,
    times: {
      user: 32661796,
      nice: 0,
      sys: 31846515,
      idle: 848752187,
      irq: 3762812
    }
  },
  {
    model: 'Intel(R) Core(TM) i7-9700 CPU @ 3.00GHz',
    speed: 3000,
    times: {
      user: 44414375,
      nice: 0,
      sys: 39157203,
      idle: 829688593,
      irq: 453015
    }
  },
  {
    model: 'Intel(R) Core(TM) i7-9700 CPU @ 3.00GHz',
    speed: 3000,
    times: {
      user: 37623203,
      nice: 0,
      sys: 27436328,
      idle: 848200640,
      irq: 312625
    }
  },
  {
    model: 'Intel(R) Core(TM) i7-9700 CPU @ 3.00GHz',
    speed: 3000,
    times: {
      user: 39422125,
      nice: 0,
      sys: 26997656,
      idle: 846840390,
      irq: 246250
    }
  },
  {
    model: 'Intel(R) Core(TM) i7-9700 CPU @ 3.00GHz',
    speed: 3000,
    times: {
      user: 31362234,
      nice: 0,
      sys: 21248750,
      idle: 860649187,
      irq: 221843
    }
  },
  {
    model: 'Intel(R) Core(TM) i7-9700 CPU @ 3.00GHz',
    speed: 3000,
    times: {
      user: 46002531,
      nice: 0,
      sys: 28078234,
      idle: 839179406,
      irq: 285781
    }
  },
  {
    model: 'Intel(R) Core(TM) i7-9700 CPU @ 3.00GHz',
    speed: 3000,
    times: {
      user: 56157078,
      nice: 0,
      sys: 38666328,
      idle: 818436765,
      irq: 235015
    }
  },
  {
    model: 'Intel(R) Core(TM) i7-9700 CPU @ 3.00GHz',
    speed: 3000,
    times: {
      user: 77564515,
      nice: 0,
      sys: 49278562,
      idle: 786417078,
      irq: 204968
    }
  }
]

那Node是如何读取到CPU信息的呢,在浏览器端js是没有这个能力的,Node让js能够与操作系统进行交互,机制在于js会通过Node.js bindings的一种桥来调用C++(libuv)代码,最终C++执行结果通过异步回调方式调用js callback。

Node github源码可以看到:github.com/nodejs/node…

Node调用os.cups()调用的是getCPUs方法,这方法是在这里桥接,将getCPUs方法绑定到C++的GetCPUInfo方法,C++执行可以是异步的,像是fs.readFile等api,得到结果后调用js的callback。

至此,我们知道Node的内置api多是通过:js代码调用C++代码,C++再通过callback异步回调js

同步写法时,execSync会阻塞后面的js执行,只有等待execSync这个io操作执行完毕才会执行后面的js:

const {exec, execSync} = require('child_process')

console.time('fs')

execSync('find /d/yangzongjun/temp', {encoding: 'utf-8', maxBuffer: 2000000000})
console.log('execSync done')

console.log(1 + 1)
console.timeEnd('fs')

但异步写法时,exec执行后,js调用到C++代码,C++代码异步执行,js会接着执行下面的代码,很快打印出2,等到C++的异步操作完成后才会进入callback函数

const {exec, execSync} = require('child_process')

console.time('fs')

exec('find /d/yangzongjun/temp/', {encoding: 'utf-8', maxBuffer: 2000000000}, (err, data) => {
    if (err) {
        console.error('err: ', err)
        return;
    }
    console.log('exec callback done')
})
console.log(1 + 1)
console.timeEnd('fs')

所以说,用户写的js都是同步的,但调用Node.js内置api可以是异步的。

但异步回调有个问题:如果C++代码出错了,改如何通知js?直接throw new Error()?答案是不行

(() => {
    function readFile(callback) {
        const res = 1 + 1;
        
        setTimeout(() => {
            // callback(new Error('error'))
            throw new Error('error');
        }, 500);
    }
    
    try {
        readFile(function callback(data) {
            console.log('callback done', data)
        });
    } catch (error) {
        console.log('catched error', error) // catch不了,因为不在一个事件循环
    }
})()

原因在于try-catch不能捕获非同一事件循环的error,于是Node.js提出了error-first风格的callback函数

(() => {
    function readFile(callback) {
        const res = 1 + 1;
        
        setTimeout(() => {
            callback(new Error('error'))
        }, 500);
    }
    
    readFile(function callback(err, data) {
        if (err) {
            console.log('error', err);
            return;
        }
        console.log('callback done', data)
    });
})()

也可以通过适配为Promise,实现更直观的同步写法

(() => {
    function readFile() {
        return new Promise((resolve, reject) => {
            const res = 1 + 1;
    
            setTimeout(() => {
                if (Math.random() < 0.5) {
                    reject('err')
                } else {
                    resolve(res)
                }
            }, 500);
        })
    }
    
    readFile()
        .then(data => {
            console.log('catched done', data)
        })
        .catch (error=> {
            console.log('catched error', error)
        })


    // 利用Promise,结合async、await可以实现更符合程序员直觉的同步写法
    Promise.all([readFile(), readFile()])
        .then(data => {
            
            console.log('Promise.all catched done', data)
        })
        .catch (error=> {
            console.log('Promise.all catched error', error)
        })
})()

作为webServer

应用场景:

1)作为BFF层,在服务端渲染好html,提高首屏渲染速度、SEO友好。 腾讯视频等内容型网站。注意和SPA区别

image.png

2)前后端同构网站,后台也全部用node.js。优势在于代码复用,比如rest接口中的某个status状态,编辑中、已编辑,id=1/2,不需要前后端同时定义2份枚举。mongodb+ORM

3)其它场景如爬虫等,demo 参考

由于js是单线程的,如果代码抛异常了会导致整个服务挂掉,这是非常严重的问题,所以在生产环境社区也有解决方案,forever或pm2进行进程守护,当Node.js进程挂了会重启。但我们应当把此工具作为最后一道屏障,尽量不让代码抛异常,否则频繁启动node进程对性能损耗很大。

pm2: pm2 start/stop index.js pm2 ls

// index.js
const http = require('http');
const url = require('url');
const hostname = '127.0.0.1';
const port = 3000;

const server = http.createServer((req, res) => {
    const pathname = url.parse(req.url).pathname;
    if (pathname === '/') {
        res.statusCode = 200;
        res.setHeader('Content-Type', 'text/plain');
        res.end('Home page! \n');
    }
    if (pathname === '/list') {
        console.log(a.b)
        res.statusCode = 200;
        res.setHeader('Content-Type', 'text/plain');
        res.end('list page! \n');
    }
});

server.listen(port, hostname, () => {
  console.log(`server listening at: http://${hostname}:${port}/`);
});

// 如果直接执行 node index.js,访问 /list 会挂掉,可通过 pm2 start index.js

学习资源