细数js模块机制内涵

说个事, 亲,你知道当你运行js的时候,发生错误的时候下面的信息表明的是什么吗?html

like前端

SyntaxError: Unexpected token .
    at exports.runInThisContext (vm.js:53:16)
    at Module._compile (module.js:414:25)
    at Object.Module._extensions..js (module.js:442:10)
    at Module.load (module.js:356:32)
    at Function.Module._load (module.js:313:12)
    at Function.Module.runMain (module.js:467:10)
    at startup (node.js:136:18)

母鸡吧,实际上,这个nodeJS中Module模块的内容。 说道模块,这就牵扯到整个js现存的那些优秀的模块机制呢。首当其冲的要数,AMD,而后是NodeJS的加载模块.下面,咱们来了解一下内部的机制,肯定上面的错误,到底表明什么.node

AMD

AMD 全称为: Asynchronous Module Definition(异步模块定义)jquery

AMD规范应该是之前 前端 最常使用的一个模块化规范. 他经过异步的方式加载js 脚本,而且执行相关的内容.
咱们先来看一下基本的AMD格式. 他其实就提供了一个全局函数-define.json

define([arr], function(para){//...});

该函数接受两个参数segmentfault

  • 第一个参数: 接受的是数组类型, 里面定义的参数是相关的js文件的路径或者js文件的alias. 路径或者alias 表明着一个引入的js模块. 好比: ['/arr/script.js','/js/demo.js','jquery'],不论是定义路径仍是定义alias, 最终, 都会被插件解析为 实际的js代码. 这个具体就涉及路径的解析了, 咱们下文在说一下api

  • 第二个参数: 接受的是函数. 而且函数能够带入参数. 参数的位置, 和前面数组引入的模块位置一致. 而后咱们就能够直接使用模块内容。define(['/arr/isArray.js','jquery'],function(isArray,$){//...}) 咱们接着就能够直接使用定义好的模块alias就能够了。数组

NodeJS 的模块化

因为AMD 仅仅提出了一个define函数用来异步加载脚本. 可是服务端的场景,这显然就有点鸡肋了. 因此, nodeJS 基本上参考了 CommonJS Modules/1.1 proposal. 他为了更精确的表达 server 端模块化的机制. 定义了3个全局的变量 exports , require , module. 须要注意的是... 其实exports 并非单一的一块,他实际上是.缓存

var exports = module.exports = {};

即, 其实nodeJS模块交互只有require 和 module 在进行。 咱们来具体看一下 , nodeJS 端 进行模块化的机制吧.
首先,咱们得明白什么叫作模块?app

什么是模块

A module encapsulates related code into a single unit of code.

看到这段话后,更觉更懵逼了. 能不能说人话~
其实, 模块就是可以完成必定工做的函数,对象,甚至基本的数据类型(好比:String,Number等);
来, 咱们能够写一个demo:

var sayHello = function(){
    return 'hello';
}
var move = function(){
    return 'Now, I am moving';
}

上面两个函数咱们就能够说,是模块.
Ok~ 如今咱们已经写了一个简单的模块了, 那接下来该怎么导出这个模块呢?

导出模块

这里咱们就须要使用exports方法进行导出便可.

//dist.js
// var exports = module.exports = {};
exports.sayHello = function(){
    return 'hello';
}
exports.move = function(){
    return 'Now, I am moving';
}

这里,咱们须要注意一下:exports = module.exports.
因为是对象,咱们还能够利用对象原本的特征, 经过字面量形式书写

//dist.js
//下面的module.exports 不能使用exports代替

module.exports = {
    sayHello : function(){
    return 'hello';
    }
    move : function(){
    return 'Now, I am moving';
    }
}
//若是你写成以下
exports = {//...} //那么你的exports关键字已经和module.exports断开联系了.

可是, 现实状况是, 不推荐这样直接 将 exports 用字面量表达. 由于这样形成将一开始写入的内容给覆盖掉.

exports.getName = function(){
    return "jimmy";
};
exports.flag = "It will be overloaded";
//上面全部的都将会被覆盖掉
module.exports = {  //这里只能使用
  getName : function(){
    return "sam"
  },
  sayName :function(){
    return "Michael"
  }
}

因此,推荐的两点:

  • 若是一开始使用exports.xxx 导出的话, 后面就不要使用exports = {} 导出.

  • 能够在最后部分直接使用 exports = {} 进行导出, 这样的, 可以让你的代码更清晰.

OK, 基本模块样式咱们已经写完了. 如今就轮到如何引用模块了.

引用模块

最后上面3个关键字,就只剩下了 require. 那require的工做机制是怎样的呢?

var require = function(path) {

  // 经过路径查找文件, 并解析

  return module.exports;
};

因此, 如今模块机制的难点不在是 模块是怎么 引用的, 而变成了 路径解析的问题, 咱们能够放到后面再进行讨论。 咱们如今能够梳理一下, 模块内容传递的 Process.

  • app.js => module.exports => require => 自定义变量

因此,一个模块 就经历了以上的流程传递到你最后引用的变量里面了。 咱们来看一下总体的demo.

//dist.js
module.exports = {  
    sayHello : function(){
    return 'hello';
    }
    move : function(){
    return 'Now, I am moving';
    }
}
//main.js
var action = require('../dist.js');
console.log(action.sayHello()); //hello
console.log(action.move()); //Now, I am moving

经过模块机制, 咱们能够很容易的了解到. require其实就是一个包装函数. 在函数体内部进行 一些列的路径转换. 好比, 路径解析, 包的缓存,模块的加载,内置模块等等。
咱们稍微肤浅一点,看一下. require是怎样进行路径加载的吧.

require 路径解析规则

这里,咱们依照官方的说明.前提是:
在Y路径下,使用require(X) 引用. 会按一下步骤进行解析

  1. 若是X是内置的模块,好比http,net等. 直接返回. over

  2. 若是X带上'/'或者'./'或者'../'.

    • 会根据X所在的父目录,肯定X所在的绝对位置.

    • 先假设X是文件,而后按照顺序依次查找下列文件

      • x

      • x.js

      • x.json

      • x.node
        若是找到则返回

    • 若是X是目录,则依次查找下列文件:

      • X/package.json(main 字段)

      • X/index.js

      • X/index.json

      • X/index.node
        若是找到则返回.

  3. 若是X不是以'/'或'./'或'../'开头. 则会根据X所在的父目录,对node_modules进行回朔遍历. 接着,经过上述肯定X为文件或者目录的方式,进行查找.

  4. 若是上述的流程都没有找到则会抛出错误(Not Found)

这里,咱们具体来看一下 node_modules的查找. 假设在路径/usr/app/shop 下运行 require('bar'); 以后, 程序遍历的结果是.

  • 首先, 假设bar是文件,查找路径为

    • /usr/app/shop/node_modules/bar

    • /usr/app/shop/node_modules/bar.js

    • /usr/app/shop/node_modules/bar.json

    • /usr/app/shop/node_modules/bar.node

  • 若是,在该目录下没有找到,则会进行回朔(../).则遍历路径为:

    • /usr/app/shop/node_modules

    • /usr/app/node_modules

    • /usr/node_modules

    • /node_modules

  • 若是假设为目录. 相似,查找为:

    • bar/package.json(main)

    • bar/index.js

    • bar/index.json

    • bar/index.node

  • 一样,也有路径回朔(../). 如上,这里就不赘述了

require() 运行的内部机制

实际上, nodeJS的壮大, 其一是其自己的异步机制和事件mode 优点带动的, 其二就是其自己优秀的模块机制. 经过Modules 模块, nodeJS将其自己的扩展性,提的老高老高. 上述路径解析,其实就是nodeJS Modules机制中的一部分. 详情能够参考一下:modules详情
其实,咱们写的每个js文件,在run的时候,都会包裹一层Modules.具体情形就是:

(function (exports, require, module, __filename, __dirname) {
  // 模块源码
  return exports;
});

实际上,module其实就是Modules的一个实例,在源码中定义的Modules函数实际内容,并不复杂:

function Module(id, parent) {
  this.id = id;
  this.exports = {};
  this.parent = parent;
  if (parent && parent.children) {
    parent.children.push(this);
  }

  this.filename = null;
  this.loaded = false;
  this.children = [];
}
module.exports = Module;

能够说,咱们全部的模块都是创建在Module这一个构造函数上的. 那这些对象,咱们应该怎么获取呢?
实际上,clever的童鞋,已经意识到了, module在运行的时候已经传进来了,咱们能够直接调用.
一个简单的demo:
app.js

console.log('module.id: ', module.id);
console.log('module.exports: ', module.exports);
console.log('module.parent: ', module.parent);
console.log('module.filename: ', module.filename);
console.log('module.loaded: ', module.loaded);
console.log('module.children: ', module.children);
console.log('module.paths: ', module.paths);

运行: ndoe app.js
结果,为:

module.id:  .
module.exports:  {}
module.parent:  null
module.filename:  /Users/jimmy_thr/Documents/code/shopping/app/sam.js
module.loaded:  false
module.children:  []
module.paths:  [ //内容过多忽略 ]

有兴趣的童鞋,能够本身运行试一试.
那每一个属性对应的是什么内容呢?

property effect
id 引用的模块名--当没有父模块时为:. 有则为绝对路径
exports 就是使用module.exports导出的方法或者变量
parent 很简单,就是父模块.也就是另一个module实例
filename 模块的绝对路径
loaded 用来表示,模块是否已经所有加载(没太多用处)
children 数组类型,表示子模块
paths 包含模块可能存在的位置,以备下次require的时候搜索

能够看出,经过run以后, 有3个global对象,分别为,require,module,exports. 那实际上,他们3者的关系是什么呢?

咱们来看一下源码里面是怎么作的吧.

Module内部细节

这是require 方法的具体细节:

Module.prototype.require = function(path) {
  return Module._load(path, this);
};

实际上, require 只是一层皮, 里面套的是Module的_load方法.
代码内有不少debug和alert, 去掉检测的内容,咱们来看一下内部机理.

Module._load = function(request, parent, isMain) {

  //  计算绝对路径
  var filename = Module._resolveFilename(request, parent);

  //  第一步:若是有缓存,取出缓存
  var cachedModule = Module._cache[filename];
  if (cachedModule) {
    return cachedModule.exports;

  // 第二步:是否为内置模块
  if (NativeModule.exists(filename)) {
    return NativeModule.require(filename);
  }

  // 第3.1步:加载模块,生成模块实例,存入缓存
  var module = new Module(filename, parent);
  Module._cache[filename] = module;

  // 第3.2步: 载入模块内容
  try {
    module.load(filename);
    hadException = false;
  } finally {
    if (hadException) {
      delete Module._cache[filename];
    }
  }

  // 第四步:输出模块的exports属性
  return module.exports;
};

这下大概清楚了,实际上, 在路径解析以前,其实Module 还会对内置模块进行其余的检测.
实际顺序为:

  • 是否已经缓存

  • 是否为内置模块

  • 加载模块

    • 生成模块实例,存入缓存

    • 路径解析

  • 最终返回module.exports

这里,咱们也能够看到NodeJS 模块加载的另一个机制.
只要require事后的模块都会被保存在缓存当中. 当须要再次引用的时候,则会直接从缓存中获取.

Module里面自定义了不少路径的处理和缓存的处理。 咱们这里, 只关注一下. module.load的内容. 源码以下

Module.prototype.load = function(filename) {
  this.filename = filename;
  this.paths = Module._nodeModulePaths(path.dirname(filename));

  var extension = path.extname(filename) || '.js';
  if (!Module._extensions[extension]) extension = '.js';
  Module._extensions[extension](this, filename);
  this.loaded = true;
};

这里很简单,用来肯定文件后缀的加载:

  • X

  • X.js

  • X.json

  • X.node

首先,在理解内部机制以前,咱们须要了解一下关于path 模块。 该模块一般使用来处理文件路径的.

  • path.basename(p[, ext])
    返回基本的文件名. 若是ext有参数,则表示不带指定尾缀返回. 好比:usr/home/app.js=>app.js. 若是指定ext为.js则返回app . 更好的理解方式为: p-ext

  • path.dirname(p)
    返回目录名. usr/home/app.js=>usr/home

  • path.extname(p)
    返回文件名的后缀。一般是最后一个'.'到字符串最后. index.html=>.html。若是没有'.'则会返回一个空字符.index=>''

  • path.format(pathObject)
    将路径对象转化为字符串路径. 即.path.format({//...}). Object能够带的属性有:

    • root

    • dir

    • base

    • ext

    • name
      一个简单的demo:

      path.format({
          root : "/",
          dir : "/home/user/dir", //后面不用加`/`系统会自动补充
          base : "file.txt",
          name : "file",
          ext : ".txt"
      });
      // returns '/home/user/dir/file.txt'

    实际上, 咱们只须要使用一部分便可。
    俺,经常使用的组合为: dir + base. 或者 dir+name+ext

  • path.isAbsolute(path)
    用来检查路径是否为绝对路径。绝对路径很好理解, 1. 看你的路径是否在根目录上. 2. 看你的路径的开头是不是//usr/path=> true, shop/app.js=>false

  • path.join(path1[, ...])
    使用/来链接多个字符,并对..或者.进行路径转化. 这是一个比较重要的方法. 经常用在路径处理.

    path.join('/foo', 'bar', 'baz/sam', 'quux', '..');
    返回为: '/foo/bar/baz/sam'
  • path.normalize(p)
    对路径字符串解释, 会处理...

    path.normalize('/usr/home/../sam');
    返回: '/usr/sam'
  • path.parse(pathString)
    该方法和path.format相反,是将路径字符串转化为路径对象

    path.parse('/home/user/dir/file.txt')
       // returns
       // {
       //    root : "/",
       //    dir : "/home/user/dir",
       //    base : "file.txt",
       //    ext : ".txt",
       //    name : "file"
       // }
  • path.resolve([from ...], to)
    组合全部的路径,找出绝对路径. 若是路径中不存在以/开头,或者根目录的话,则以当前js文件所在的目录为起始参考路径. NodeJS官方给出一种更好理解的方式:

    path.resolve('foo/bar', '/tmp/file/', '..', 'a/../subfile')
    // cd foo/bar
    // cd /tmp/file/
    // cd ..
    // cd a/../subfile
    最后返回: /tmp/subfile
  • path.relative(fromPath, toPath)
    计算出,相对于fromPath 到 toPath的相对路径。两个参数须要是绝对路径. 在MAC下面开头须要为/. 若是不是, 则会默认以执行的js文件所在目录进行转化.

    path.relative('/usr/home/sam','/usr/app')
    返回: ../../app

总结一下:

回到load方法。 该方法主要就是对尾缀进行不一样的处理策略:

var extension = path.extname(filename) || '.js';
  if (!Module._extensions[extension]) extension = '.js';
  Module._extensions[extension](this, filename);
  this.loaded = true;

再反观,源码对不一样后缀的处理

Module._extensions['.js'] = function(module, filename) {...}
Module._extensions['.json'] = function(module, filename) {...}
Module._extensions['.node'] = function(module, filename) {...}

找到文件以后,再经过vm模块,进行编译处理.
最后, 在_compile函数里, 对scope和sandbox进行处理后,争取运行文件.

Module.prototype._compile = function(content, filename) {
  var self = this;
  var args = [self.exports, require, self, filename, dirname];
  return compiledWrapper.apply(self.exports, args);
};

最后就编译为,咱们前文所述的那样:

(function (exports, require, module, __filename, __dirname) {
  // 模块源码
  return exports;
});

经过上文,咱们也可以很好地理解。 出错的时候,下面的信息到底意味着什么了.

SyntaxError: Unexpected token .
    at exports.runInThisContext (vm.js:53:16)
    at Module._compile (module.js:414:25)
    at Object.Module._extensions..js (module.js:442:10)
    at Module.load (module.js:356:32)
    at Function.Module._load (module.js:313:12)
    at Function.Module.runMain (module.js:467:10)
    at startup (node.js:136:18)

转载请注明原文连接和做者: https://segmentfault.com/a/1190000004868...

相关文章
相关标签/搜索