05-node模块化

node 的模块运行机制

1.CommonJS 的规范

img

CommonJS 的规范,包括模块引用模块定义模块标识,3个部分

模块引用: 模块通过require方法来同步加载所依赖的模块

模块定义: 在node中一个文件就是一个模块,提供exports对象导出当前模块的方法或变量

模块标识: 模块标识传递给require()方法的参数,可以是按小驼峰(camelCase)命名的字符串,也可以是文件路径。

1.1.node 模块中CommonJS 的应用

模块内容导出两种方式:

a.js的内容如下,

**方式一:**可将需要导出的变量或函数挂载到 exports 对象的属性上

// node.js 每一个文件都是一个单独模块
// Node对获取的Javascript文件的内容进行了包装,以传入如下变量
console.log(exports, require, module, __filename, __dirname);
// 可将需要导出的变量或函数挂载到 exports 对象的属性上,
exports.name = 'luoxiaobu';
exports.age = '18'

**方式二:**使用 module.exports 对象整体导出一个变量对象或者函数

// node.js 每一个文件都是一个单独模块
// Node对获取的Javascript文件的内容进行了包装,以传入如下变量
console.log(exports, require, module, __filename, __dirname);
let name = 'luoxiaobu';
let age = '18'
// 使用 module.exports 对象整体导出一个变量对象或者函数,
module.exports = {name,age};

模块的引用的方式: 按照引用模块的来源来区分

// 核心模块的引入 node自己的模块
let crypto = require('crypto')

// 用户自己编写的模块引入
let aModule = require('./a.js')
// 第三方,别人实现发布的模块(其实也是其他用户编写)
let proxy = require('http-proxy');

2.node 模块加载过程

node.js 每一个文件都是一个单独模块,每个模块都用一个module对象来表示自身。

非 node NativeModule

// 非 node NativeModule
function Module(id = '', parent) {
  this.id = id;
  this.path = path.dirname(id);
  this.exports = {};
  this.parent = parent;
  updateChildren(parent, this, false);
  this.filename = null;
  this.loaded = false;
  this.children = [];
}

NativeModule

// Set up NativeModule.
function NativeModule(id) {
  this.filename = `${id}.js`;
  this.id = id;
  this.exports = {};
  this.module = undefined;
  this.exportKeys = undefined;
  this.loaded = false;
  this.loading = false;
  this.canBeRequiredByUsers = !id.startsWith('internal/');
}

2.1 node 模块加载简述

加载过程大概流程:(Module._load 加载函数)

img

参考源码:node/lib/internal/modules/cjs/loader.js

代码略微删减

// Check the cache for the requested file.
// 1. If a module already exists in the cache: return its exports object.
// 2. If the module is native: call
//    `NativeModule.prototype.compileForPublicLoader()` and return the exports.
// 3. Otherwise, create a new module for the file and save it to the cache.
//    Then have it load  the file contents before returning its exports
//    object.
Module._load = function(request, parent, isMain) {
    let relResolveCacheIdentifier;
    if (parent) {
      debug('Module._load REQUEST %s parent: %s', request, parent.id);
      ...
    }
    // 查找文件具体位置
    const filename = Module._resolveFilename(request, parent, isMain);
    // 存在缓存,则不需要再次执行 返回缓存
    const cachedModule = Module._cache[filename];
    if (cachedModule !== undefined) {
      updateChildren(parent, cachedModule, true);
      if (!cachedModule.loaded)
        return getExportsForCircularRequire(cachedModule);
      return cachedModule.exports;
    }
    // 加载node原生模块,原生模块loadNativeModule  
    // 如果有 且能被用户引用 返回 mod.exports(这包括node模块的编译创建module对象,将模块运行结果保存在module对象上)
    const mod = loadNativeModule(filename, request);
    if (mod && mod.canBeRequiredByUsers) return mod.exports;
  
    // 创建一个模块
    // Don't call updateChildren(), Module constructor already does.
    const module = new Module(filename, parent);
  
    if (isMain) {
      process.mainModule = module;
      module.id = '.';
    }
    // 缓存模块
    Module._cache[filename] = module;
    if (parent !== undefined) {
      relativeResolveCache[relResolveCacheIdentifier] = filename;
    }
    // 加载执行新的模块
    module.load(filename);  
      
    return module.exports;
  };

node 缓存的是编译和执行后的对象

相同:

node模块和非node模块经历的过程都是,有执行后的缓存对象,返回缓存对象

没有执行后的缓存对象,创建module对象,执行模块,存储执行后得到的对象,返回执行后的结果exports

不同:

缓存对象不同

加载模块文件方式不同

2.2 node 源码目录

大概源码结构:(只标注了部分感兴趣的)

img

我们可以看到 node 库的目录,其中:

deps: 包含了node所依赖的库,如v8,libuv,zlib 等,

lib: 包含了用 javascript 定义的函数和模块(可能会通过internalBinding调用c++模块,c++ 模块实现在目录src 下),

src: 包括了lib 库对应的C++实现,其中很多 built-in(C++实现) 模块都在这里

会有所困惑,js跟c++ 之间的相互调用?

Node.js主要包括这几个部分,Node Standard Library,Node Bindings,V8,Libuv,架构图如下:

img

Node Bindings: 是沟通JS 和 C++的桥梁,将V8 引擎暴露的c++ 接口转换成JS API

V8: JavaScript的引擎,提供JavaScript运行环境

c++ 模块的引用大概流程

img

C++ 和 JS 交互 参考文章:(感兴趣可以了解一下)

C++ 和 JS 交互

使用 Google V8 引擎开发可定制的应用程序

2.3 node 模块分类

参考:node/lib/internal/bootstrap/loaders.js

// This file creates the internal module & binding loaders used by built-in

// modules. In contrast, user land modules are loaded using

// lib/internal/modules/cjs/loader.js (CommonJS Modules) or

// lib/internal/modules/esm/* (ES Modules).

//

// This file is compiled and run by node.cc before bootstrap/node.js

// was called, therefore the loaders are bootstraped before we start to

// actually bootstrap Node.js. It creates the following objects:

node.cc编译和运行的node/lib/internal/bootstrap/loaders.js 文件的时候会,创建内部模块, 绑定内置模块使用的加载程序。

而用户的模块加载运行依靠lib/internal/modules/cjs/loader.js 或者 lib/internal/modules/esm/*

所以node的模块大致分为两类:

  • node的核心模块
    • node的核心模块js实现
    • node核心模块c++实现,js包裹调用c++模块
  • 第三方模块,或者用户自己编写模块
    • JavaScript 模块,我们开发写的JavaScript 模
    • JSON 模块,一个 JSON 文件
    • C/C++ 扩展模块,使用 C/C++ 编写,编译后后缀名为 .node(感兴趣可以了解动态链接库)
2.3.1 node的核心模块

⑴ C++ binding loaders: (对c++核心模块的引入,暴露的c++ 接口)

process.binding(): 旧版C ++绑定加载程序,可从用户空间访问,因为它是附加到全局流程对象的对象。这些C ++绑定是使用NODE_BUILTIN_MODULE_CONTEXT_AWARE()创建的,并且其nm_flags设置为NM_F_BUILTIN。我们无法确保这些绑定的稳定性,但是仍然必须时时解决由它们引起的兼容性问题。

process._linkedBinding(): 在应用程序中添加额外的其他C ++绑定。可以使用带有标志NM_F_LINKED 的 NODE_MODULE_CONTEXT_AWARE_CPP()创建这些C ++绑定。

internalBinding(): 私有内部C ++绑定加载程序,(除非通过require('internal / test / binding'),否则无法从用户区域访问)。 这些C ++绑定是使用NODE_MODULE_CONTEXT_AWARE_INTERNAL()创建的,其nm_flags设置为NM_F_INTERNAL。

⑵Internal JavaScript module loader:

该模块是用于加载 lib.js 和 deps.js 中的JavaScript核心模块的最小模块系统。

所有核心模块都通过由 js2c.py 生成的 node_javascript.cc 编译成 Node 二进制文件,这样可以更快地加载它们,而不需要I/O成本。

此类使lib / internal / ,deps / internal /模块和internalBinding()在默认情况下对核心模块可,并允许核心模块通require(‘internal / bootstrap / loaders’)来引用自身,即使此文件不是以CommonJS风格编写的。

核心模块的加载大概如下:(internalBinding是process.binding的替代可以简单这样理解)

img

Process.binding / InternalBinding 实际上是C++函数,是用于将Node标准库中C++端和Javascript端连接起来的桥梁。

2.3.2 node的非 核心模块
  • JavaScript 模块,我们开发写的JavaScript 模(或着第三方模块)
  • JSON 模块,一个 JSON 文件
  • C/C++ 扩展模块,使用 C/C++ 编写,编译后后缀名为 .node(感兴趣可以了解动态链接库)

此类模块的大概加载流程:

img

路径分析

const filename = Module._resolveFilename(request, parent, isMain);复制代码

是否有缓存

 const cachedModule = Module._cache[filename];
  if (cachedModule !== undefined) {
    updateChildren(parent, cachedModule, true);
    return cachedModule.exports;
  }

创建module对象

  const module = new Module(filename, parent);
// 缓存 module 对象
  Module._cache[filename] = module;复制代码

文件定位根据后缀编译执行

// Native extension for .js
Module._extensions['.js'] = function(module, filename) {
  if (experimentalModules && filename.endsWith('.js')) {
    const pkg = readPackageScope(filename);
    if (pkg && pkg.type === 'module') {
      throw new ERR_REQUIRE_ESM(filename);
    }
  }
  const content = fs.readFileSync(filename, 'utf8');
  module._compile(stripBOM(content), filename);
};


// Native extension for .json
Module._extensions['.json'] = function(module, filename) {
  const content = fs.readFileSync(filename, 'utf8');

  if (manifest) {
    const moduleURL = pathToFileURL(filename);
    manifest.assertIntegrity(moduleURL, content);
  }

  try {
    module.exports = JSON.parse(stripBOM(content));
  } catch (err) {
    err.message = filename + ': ' + err.message;
    throw err;
  }
};


// Native extension for .node
Module._extensions['.node'] = function(module, filename) {
  if (manifest) {
    const content = fs.readFileSync(filename);
    const moduleURL = pathToFileURL(filename);
    manifest.assertIntegrity(moduleURL, content);
  }
  // Be aware this doesn't use `content`
  return process.dlopen(module, path.toNamespacedPath(filename));
};

返回module.exports 对象。

模块路径解析规则

我们已经知道,require函数支持斜杠(/)或盘符(C:)开头的绝对路径,也支持./开头的相对路径。但这两种路径在模块之间建立了强耦合关系,一旦某个模块文件的存放位置需要变更,使用该模块的其它模块的代码也需要跟着调整,变得牵一发动全身。因此,require函数支持第三种形式的路径,写法类似于foo/bar,并依次按照以下规则解析路径,直到找到模块位置。

1. 内置模块

如果传递给 require 函数的是 NodeJS 内置模块名称,不做路径解析,直接返回内部模块的导出对象,例如require(‘fs’)。

2. node_modules 目录

NodeJS 定义了一个特殊的 node_modules 目录用于存放模块。例如某个模块的绝对路径是 /home/user/hello.js,在该模块中使用 require(‘foo/bar’) 方式加载模块时,则 NodeJS 依次尝试使用以下路径。

 /home/user/node_modules/foo/bar
 /home/node_modules/foo/bar
 /node_modules/foo/bar

3. NODE_PATH 环境变量

与 PATH 环境变量类似,NodeJS 允许通过 NODE_PATH 环境变量来指定额外的模块搜索路径。NODE_PATH 环境变量中包含一到多个目录路径,路径之间在 Linux 下使用:分隔,在 Windows 下使用;分隔。例如定义了以下 NODE_PATH 环境变量:

 NODE_PATH=/home/user/lib:/home/lib

当使用 require(‘foo/bar’)的方式加载模块时,则 NodeJS 依次尝试以下路径。

 /home/user/lib/foo/bar
 /home/lib/foo/bar

Node.js包

Node组织了自身的核心模块,也使得第三方文件模块可以有序地编写和使用。但是在第三方模块中,模块与模块之间仍然是散列在各地的,相互之间不能直接引用。而在模块之外,包和NPM则是将模块联系起来的一种机制。JavaScript不似Java或者其他语言那样,具有模块和包结构。Node对模块规范的实现,一定程度上解决了变量依赖、依赖关系等代码组织性问题。包的出现,则是在模块的基础上进一步组织JavaScript代码。CommonJS的包规范的定义其实也十分简单,它由包结构和包描述文件两个部分组成,前者用于组织包中的各种文件,后者则用于描述包的相关信息,以供外部读取分析。

1. 包结构

包实际上是一个存档文件,即一个目录直接打包为.zip或tar.gz格式的文件,安装后解压还原为目录。完全符合CommonJS规范的包目录应该包含如下这些文件。
 1、package.json:包描述文件
 2、bin:用于存放可执行二进制文件的目录
 3、lib:用于存放JavaScript代码的目录
 4、doc:用于存放文档的目录
 5、test:用于存放单元测试用例的代码

2. 包描述文件package.json

初始化,生成包,依次输入相关字段

npm init

package.json文件,定义了项目所需要的各种模块,以及项目的配置信息(比如名称、版本、许可证等元数据)。npm install命令根据这个配置文件,自动下载所需的模块,也就是配置项目所需的运行和开发环境。
 有了package.json文件,直接使用npm installyarn install,就会在当前目录中安装所需要的模块。
 如果一个模块不在package.json文件之中,可以单独安装这个模块,并使用相应的参数,将其写入package.json文件之中。

 npm install koa --save
 npm install koa --save-dev

上面代码表示单独安装koa模块,–save参数表示将该模块写入dependencies属性,–save-dev表示将该模块写入devDependencies属性,我更喜欢用yarn做包管理工具。

yarn add koa 
yarn add koa --dev

【基本字段】

{
  "name": "api",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

1、name——包名。规范定义它需要由小写的字母和数字组成,可以包含.、_和-,但不允许出现空格。包名必须是唯一的,以免对外公布时产生重名冲突的误解。除此之外,NPM还建议不要在包名中附带上node或js来重复标识它是JavaScript或Node模块 。
 2、version——版本号。一个语义化的版本号,这在http://semver.org/上有详细定义,通常为major.minor.revision格式。该版本号十分重要,常常用于一些版本控制的场合。

【必需字段】
 1、description——包简介。方便别人了解该模块作用,搜索的时候也有用
“description”: “包介绍”
 2、keywords——关键词数组,NPM中主要用来做分类搜索。一个好的关键词数组有利于用户快速找到该包。
 3、maintainers——包维护者列表。每个维护者由name、email和web地址这3个属性组成。
 4、contributors——贡献者列表。在开源社区中,为开源项目提供代码是经常出现的事情,如果名字能出现在知名项目的contributors列表中,是一件比较有荣誉感的事。列表中的第一个贡献应当是包的作者本人。它的格式与维护者列表相同。
 5、bugs——一个可以反馈bug的网页地址或邮件地址。
 6、licenses——当前包所使用的许可证列表,表示这个包可以在哪些许可证下使用。

"licenses": [
    {
      "type": "MIT",
      "url": "https://github.com/napcs/node-livereload/blob/master/LICENSE"
    }
  ]

7、repositories——托管源代码的位置列表,表明可以通过哪些方式和地址访问包的源代码。

  "repository": {
    "type": "git",
    "url": "git+ssh://git@github.com/napcs/node-livereload.git"
  }

8、dependencies——使用当前包所需要依赖的包列表。这个属性十分重要,NPM会通过这个属性帮助自动加载依赖的包。详细介绍请见npm简单使用

【可选字段

1、homepage——当前包的网站地址

  "homepage": "https://github.com/napcs/node-livereload#readme"

2、os——操作系统支持列表。这些操作系统的取值包括aix、freebsd、linux、macos、solaris、vxworks、windows。如果设置了列表为空,则不对操作系统做任何假设。
 3、cpu——CPU架构的支持列表,有效的架构名称有arm、mips、ppc、sparc、x86和x86_64。同os一样,如果列表为空,则不对CPU架构做任何假设。
 4、engine——支持的JavaScript引擎列表,有效的引擎取值包括ejs、flusspferd、gpsee、jsc、spidermonkey、narwhal、node和v8。

"engines": {
    "node": ">=0.4.0"
  }

5、builtin——标志当前包是否是内建在底层系统的标准组件。
 6、directories——包目录说明

"directories": {}

7、implements——实现规范的列表。标志当前包实现了CommonJS的哪些规范。
 8、scripts——脚本说明对象。它主要被包管理器用来安装、编译、测试和卸载包。scripts指定了运行脚本命令的npm命令行缩写,比如start指定了运行npm run start时,所要执行的命令

  "scripts": {
    "test": "mocha"
  }

【其他字段】
 1、author——包作者
 2、bin——指定各个内部命令对应的可执行文件的位置,这样就不需要配置环境变量啦。

  "bin": {
    "livereload": "./bin/livereload.js"
  }

3、main——加载的入口文件。模块引入方法require()在引入包时,会优先检查这个字段,并将其作为包中其余模块的入口。如果不存在这个字段,require()方法会查找包目录下的index.js、index.node、index.json文件作为默认入口

"main": "./lib/livereload.js"

4、devDependencies——项目开发所需要的模块。一些模块只在开发时需要依赖。配置这个属性,可以提示包的后续开发者安装依赖包。类比于dependencies字段

"devDependencies": {
    "coffee-script": ">= 1.8.0",
    "mocha": ">= 1.0.3",
    "request": ">= 2.9.203",
    "should": ">= 0.6.3",
    "sinon": "^1.17.4"
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值