细数js模块机制内涵

ufoe7070 8年前
   <p>说个事, 亲,你知道当你运行js的时候,发生错误的时候下面的信息代表的是什么吗?like</p>    <pre>  <code class="language-javascript">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)  </code></pre>    <p>母鸡吧,实际上,这个nodeJS中Module模块的内容。 说道模块,这就牵扯到整个js现存的那些优秀的模块机制呢。首当其冲的要数,AMD,然后是NodeJS的加载模块.下面,我们来了解一下内部的机制,确定上面的错误,到底代表什么.</p>    <h2>AMD</h2>    <p>AMD 全称为: Asynchronous Module Definition(异步模块定义)</p>    <p>AMD规范应该是以前 前端 最常使用的一个模块化规范. 他通过异步的方式加载js 脚本,并且执行相关的内容.</p>    <p>我们先来看一下基本的AMD格式. 他其实就提供了一个全局函数-define.</p>    <pre>  <code class="language-javascript">define([arr], function(para){//...});</code></pre>    <p>该函数接受两个参数</p>    <ul>     <li> <p>第一个参数: 接受的是数组类型, 里面定义的参数是相关的js文件的路径或者js文件的alias. 路径或者alias 代表着一个引入的js模块. 比如:</p> ['/arr/script.js','/js/demo.js','jquery']</li>    </ul>    <p>不管是定义路径还是定义alias, 最终, 都会被插件解析为 实际的js代码. 这个具体就涉及路径的解析了, 我们下文在说一下</p>    <ul>     <li> <p>第二个参数: 接受的是函数. 并且函数可以带入参数. 参数的位置, 和前面数组引入的模块位置一致. 然后我们就可以直接使用模块内容。</p> define(['/arr/isArray.js','jquery'],function(isArray,$){//...})</li>    </ul>    <p>我们接着就可以直接使用定义好的模块alias就可以了。</p>    <h2>NodeJS 的模块化</h2>    <p>由于AMD 仅仅提出了一个define函数用来异步加载脚本. 但是服务端的场景,这显然就有点鸡肋了. 所以, nodeJS 基本上参考了 <a href="/misc/goto?guid=4959639286796770941" rel="nofollow,noindex">CommonJS Modules/1.1 proposal</a> . 他为了更精确的表达 server 端模块化的机制. 定义了3个全局的变量 exports , require , module . 需要注意的是... 其实exports 并不是单一的一块,他其实是.</p>    <pre>  <code class="language-javascript">var exports = module.exports = {};</code></pre>    <p>即, 其实nodeJS模块交互只有require 和 module 在进行。 我们来具体看一下 , nodeJS 端 进行模块化的机制吧.首先,我们得明白什么叫做模块?</p>    <h3>什么是模块</h3>    <p>A module encapsulates related code into a single unit of code.</p>    <p>看到这段话后,更觉更懵逼了. 能不能说人话~</p>    <p>其实, 模块就是能够完成一定工作的函数,对象,甚至基本的数据类型(比如:String,Number等);</p>    <p>来, 我们可以写一个demo:</p>    <pre>  <code class="language-javascript">var sayHello = function(){      return 'hello';  }  var move = function(){      return 'Now, I am moving';  }</code></pre>    <p>上面两个函数我们就可以说,是模块.Ok~ 现在我们已经写了一个简单的模块了, 那接下来该怎么导出这个模块呢?</p>    <h3>导出模块</h3>    <p>这里我们就需要使用exports方法进行导出即可.</p>    <pre>  <code class="language-javascript">//dist.js  // var exports = module.exports = {};  exports.sayHello = function(){      return 'hello';  }  exports.move = function(){      return 'Now, I am moving';  }</code></pre>    <p>这里,我们需要注意一下:exports = module.exports.由于是对象,我们还可以利用对象本来的特征, 通过字面量形式书写</p>    <pre>  <code class="language-javascript">//dist.js  //下面的module.exports 不能使用exports代替    module.exports = {      sayHello : function(){      return 'hello';      }      move : function(){      return 'Now, I am moving';      }  }  //如果你写成如下  exports = {//...} //那么你的exports关键字已经和module.exports断开联系了.</code></pre>    <p>但是, 现实情况是, 不推荐这样直接 将 exports 用字面量表达. 因为这样造成将一开始写入的内容给覆盖掉.</p>    <pre>  <code class="language-javascript">exports.getName = function(){      return "jimmy";  };  exports.flag = "It will be overloaded";  //上面所有的都将会被覆盖掉  module.exports = {  //这里只能使用    getName : function(){      return "sam"    },    sayName :function(){      return "Michael"    }  }</code></pre>    <p>所以,推荐的两点:</p>    <ul>     <li> <p>如果一开始使用 exports.xxx 导出的话, 后面就不要使用 exports = {} 导出.</p> </li>     <li> <p>可以在最后部分直接使用 exports = {} 进行导出, 这样的, 能够让你的代码更清晰.</p> </li>    </ul>    <p>OK, 基本模块样式我们已经写完了. 现在就轮到如何引用模块了.</p>    <h3>引用模块</h3>    <p>最后上面3个关键字,就只剩下了 require . 那require的工作机制是怎样的呢?</p>    <pre>  <code class="language-javascript">var require = function(path) {      // 通过路径查找文件, 并解析      return module.exports;  };</code></pre>    <p>所以, 现在模块机制的难点不在是 模块是怎么 引用的, 而变成了 路径解析的问题, 我们可以放到后面再进行讨论。 我们现在可以梳理一下, 模块内容传递的 Process.</p>    <ul>     <li> <p>app.js => module.exports => require => 自定义变量</p> </li>    </ul>    <p>所以,一个模块 就经历了以上的流程传递到你最后引用的变量里面了。 我们来看一下整体的demo.</p>    <pre>  <code class="language-javascript">//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</code></pre>    <p>通过模块机制, 我们可以很容易的了解到. require其实就是一个包装函数. 在函数体内部进行 一些列的路径转换. 比如, 路径解析, 包的缓存,模块的加载,内置模块等等。我们稍微肤浅一点,看一下. require是怎样进行路径加载的吧.</p>    <h2>require 路径解析规则</h2>    <p>这里,我们依照官方的说明.前提是:在Y路径下,使用require(X) 引用. 会按一下步骤进行解析</p>    <ol>     <li> <p>如果X是内置的模块,比如http,net等. 直接返回. over</p> </li>     <li> <p>如果X带上'/'或者'./'或者'../'.</p>      <ul>       <li> <p>会根据X所在的父目录,确定X所在的绝对位置.</p> </li>       <li> <p>先假设X是文件,然后按照顺序依次查找下列文件</p>        <ul>         <li> <p>x</p> </li>         <li> <p>x.js</p> </li>         <li> <p>x.json</p> </li>         <li> <p>x.node如果找到则返回</p> </li>        </ul> </li>       <li> <p>如果X是目录,则依次查找下列文件:</p>        <ul>         <li> <p>X/package.json(main 字段)</p> </li>         <li> <p>X/index.js</p> </li>         <li> <p>X/index.json</p> </li>         <li> <p>X/index.node如果找到则返回.</p> </li>        </ul> </li>      </ul> </li>     <li> <p>如果X不是以'/'或'./'或'../'开头. 则会根据X所在的父目录,对node_modules进行回朔遍历. 接着,通过上述确定X为文件或者目录的方式,进行查找.</p> </li>     <li> <p>如果上述的流程都没有找到则会抛出错误(Not Found)</p> </li>    </ol>    <p>这里,我们具体来看一下 node_modules的查找. 假设在路径/usr/app/shop 下运行 require('bar'); 之后, 程序遍历的结果是.</p>    <ul>     <li> <p>首先, 假设bar是文件,查找路径为</p>      <ul>       <li> <p>/usr/app/shop/node_modules/bar</p> </li>       <li> <p>/usr/app/shop/node_modules/bar.js</p> </li>       <li> <p>/usr/app/shop/node_modules/bar.json</p> </li>       <li> <p>/usr/app/shop/node_modules/bar.node</p> </li>      </ul> </li>     <li> <p>如果,在该目录下没有找到,则会进行回朔(../).则遍历路径为:</p>      <ul>       <li> <p>/usr/app/shop/node_modules</p> </li>       <li> <p>/usr/app/node_modules</p> </li>       <li> <p>/usr/node_modules</p> </li>       <li> <p>/node_modules</p> </li>      </ul> </li>     <li> <p>如果假设为目录. 类似,查找为:</p>      <ul>       <li> <p>bar/package.json(main)</p> </li>       <li> <p>bar/index.js</p> </li>       <li> <p>bar/index.json</p> </li>       <li> <p>bar/index.node</p> </li>      </ul> </li>     <li> <p>同样,也有路径回朔(../). 如上,这里就不赘述了</p> </li>    </ul>    <h2>require() 运行的内部机制</h2>    <p>实际上, nodeJS的壮大, 其一是其本身的异步机制和事件mode 优势带动的, 其二就是其本身优秀的模块机制. 通过 <a href="/misc/goto?guid=4959639286796770941" rel="nofollow,noindex">Modules</a> 模块, nodeJS将其本身的扩展性,提的老高老高. 上述路径解析,其实就是nodeJS Modules机制中的一部分. 详情可以参考一下: <a href="/misc/goto?guid=4959637721494735448" rel="nofollow,noindex">modules详情</a></p>    <p>其实,我们写的每一个js文件,在run的时候,都会包裹一层Modules.具体情形就是:</p>    <pre>  <code class="language-javascript">(function (exports, require, module, __filename, __dirname) {    // 模块源码    return exports;  });</code></pre>    <p>实际上,module其实就是Modules的一个实例,在源码中定义的Modules函数实际内容,并不复杂:</p>    <pre>  <code class="language-javascript">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;</code></pre>    <p>可以说,我们所有的模块都是建立在Module这一个构造函数上的. 那这些对象,我们应该怎么获取呢?</p>    <p>实际上,clever的童鞋,已经意识到了, module在运行的时候已经传进来了,我们可以直接调用.</p>    <p>一个简单的demo:</p>    <p>app.js</p>    <pre>  <code class="language-javascript">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);</code></pre>    <p>运行: ndoe app.js<br> 结果,为:</p>    <pre>  <code class="language-javascript">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:  [ //内容过多忽略 ]</code></pre>    <p>有兴趣的童鞋,可以自己运行试一试.那每个属性对应的是什么内容呢?</p>    <table>     <thead>      <tr>       <th>property</th>       <th>effect</th>      </tr>     </thead>     <tbody>      <tr>       <td>id</td>       <td>引用的模块名--当没有父模块时为: . 有则为绝对路径</td>      </tr>      <tr>       <td>exports</td>       <td>就是使用 module.exports 导出的方法或者变量</td>      </tr>      <tr>       <td>parent</td>       <td>很简单,就是父模块.也就是另外一个module实例</td>      </tr>      <tr>       <td>filename</td>       <td>模块的绝对路径</td>      </tr>      <tr>       <td>loaded</td>       <td>用来表示,模块是否已经全部加载(没太多用处)</td>      </tr>      <tr>       <td>children</td>       <td>数组类型,表示子模块</td>      </tr>      <tr>       <td>paths</td>       <td>包含模块可能存在的位置,以备下次require的时候搜索</td>      </tr>     </tbody>    </table>    <p>可以看出,通过run之后, 有3个global对象,分别为,require,module,exports. 那实际上,他们3者的关系是什么呢?</p>    <p><img src="https://simg.open-open.com/show/9c78408bfe723c71cffb2a5370dbd49e.png"></p>    <p>我们来看一下源码里面是怎么做的吧.</p>    <h3>Module内部细节</h3>    <p>这是require 方法的具体细节:</p>    <pre>  <code class="language-javascript">Module.prototype.require = function(path) {    return Module._load(path, this);  };</code></pre>    <p>实际上, require 只是一层皮, 里面套的是Module的_load方法.代码内有很多debug和alert, 去掉检测的内容,我们来看一下内部机理.</p>    <pre>  <code class="language-javascript">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;  };</code></pre>    <p>这下大概清楚了,实际上, 在路径解析之前,其实Module 还会对内置模块进行其他的检测.实际顺序为:</p>    <ul>     <li> <p>是否已经缓存</p> </li>     <li> <p>是否为内置模块</p> </li>     <li> <p>加载模块</p>      <ul>       <li> <p>生成模块实例,存入缓存</p> </li>       <li> <p>路径解析</p> </li>      </ul> </li>     <li> <p>最终返回module.exports</p> </li>    </ul>    <p>这里,我们也可以看到NodeJS 模块加载的另外一个机制.</p>    <p>只要require过后的模块都会被保存在缓存当中. 当需要再次引用的时候,则会直接从缓存中获取.</p>    <p>Module里面自定义了很多路径的处理和缓存的处理。 我们这里, 只关注一下. module.load的内容. 源码如下</p>    <pre>  <code class="language-javascript">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;  };</code></pre>    <p>这里很简单,用来确定文件后缀的加载:</p>    <ul>     <li> <p>X</p> </li>     <li> <p>X.js</p> </li>     <li> <p>X.json</p> </li>     <li> <p>X.node</p> </li>    </ul>    <p>首先,在理解内部机制之前,我们需要了解一下关于path 模块。 该模块通常使用来处理文件路径的.</p>    <ul>     <li> <p>path.basename(p[, ext])</p> </li>    </ul>    <p>返回基本的文件名. 如果ext有参数,则表示不带指定尾缀返回. 比如: usr/home/app.js => app.js . 如果指定ext为 .js 则返回 app . 更好的理解方式为: p-ext</p>    <ul>     <li> <p>path.dirname(p)返回目录名. usr/home/app.js => usr/home</p> </li>    </ul>    <ul>     <li> <p>path.extname(p)</p> <p>返回文件名的后缀。通常是最后一个'.'到字符串最后. index.html => .html 。如果没有'.'则会返回一个空字符. index => ''</p> </li>    </ul>    <ul>     <li> <p>path.format(pathObject)</p> <p>将路径对象转化为字符串路径. 即. path.format({//...}) . Object可以带的属性有:</p>      <ul>       <li> <p>root</p> </li>       <li> <p>dir</p> </li>       <li> <p>base</p> </li>       <li> <p>ext</p> </li>       <li> <p>name一个简单的demo:</p> </li>      </ul> </li>    </ul>    <pre>  <code class="language-javascript">path.format({      root : "/",      dir : "/home/user/dir", //后面不用加`/`系统会自动补充      base : "file.txt",      name : "file",      ext : ".txt"  });  // returns '/home/user/dir/file.txt'</code></pre>    <p>实际上, 我们只需要使用一部分即可。俺,常用的组合为: dir + base. 或者 dir+name+ext</p>    <ul>     <li> <p>path.isAbsolute(path)用来检查路径是否为绝对路径。绝对路径很好理解, 1. 看你的路径是否在根目录上. 2. 看你的路径的开头是否是 / 。</p> </li>    </ul>    <p>/usr/path => true, shop/app.js =>false</p>    <ul>     <li> <p>path.join(path1[, ...])使用 / 来连接多个字符,并对 .. 或者 . 进行路径转化. 这是一个比较重要的方法. 常常用在路径处理.</p> </li>    </ul>    <pre>  <code class="language-javascript">path.join('/foo', 'bar', 'baz/sam', 'quux', '..');  返回为: '/foo/bar/baz/sam'</code></pre>    <ul>     <li> <p>path.normalize(p)对路径字符串解释, 会处理 .. 和 . 。</p> </li>    </ul>    <pre>  <code class="language-javascript">path.normalize('/usr/home/../sam');  返回: '/usr/sam'</code></pre>    <ul>     <li> <p>path.parse(pathString)</p> <p>该方法和path.format相反,是将路径字符串转化为路径对象</p> <pre>  <code class="language-javascript">path.parse('/home/user/dir/file.txt')  // returns  // {  //    root : "/",  //    dir : "/home/user/dir",  //    base : "file.txt",  //    ext : ".txt",  //    name : "file"  // }</code></pre> </li>     <li> <p>path.resolve([from ...], to)组合所有的路径,找出绝对路径. 如果路径中不存在以 / 开头,或者根目录的话,则以当前js文件所在的目录为起始参考路径. NodeJS官方给出一种更好理解的方式:</p> </li>    </ul>    <pre>  <code class="language-javascript">path.resolve('foo/bar', '/tmp/file/', '..', 'a/../subfile')  // cd foo/bar  // cd /tmp/file/  // cd ..  // cd a/../subfile  最后返回: /tmp/subfile</code></pre>    <ul>     <li> <p>path.relative(fromPath, toPath)计算出,相对于fromPath 到 toPath的相对路径。两个参数需要是绝对路径. 在MAC下面开头需要为 / . 如果不是, 则会默认以执行的js文件所在目录进行转化.</p> </li>    </ul>    <pre>  <code class="language-javascript">path.relative('/usr/home/sam','/usr/app')  返回: ../../app</code></pre>    <p>总结一下:</p>    <p><img src="https://simg.open-open.com/show/6ed41bd2e5b2bf2afd82919276eb8e2e.png"></p>    <p>回到load方法。 该方法主要就是对尾缀进行不同的处理策略:</p>    <pre>  <code class="language-javascript">var extension = path.extname(filename) || '.js';    if (!Module._extensions[extension]) extension = '.js';    Module._extensions[extension](this, filename);    this.loaded = true;</code></pre>    <p>再反观,源码对不同后缀的处理</p>    <pre>  <code class="language-javascript">Module._extensions['.js'] = function(module, filename) {...}  Module._extensions['.json'] = function(module, filename) {...}  Module._extensions['.node'] = function(module, filename) {...}</code></pre>    <p>找到文件之后,再通过vm模块,进行编译处理.最后, 在_compile函数里, 对scope和sandbox进行处理后,争取运行文件.</p>    <pre>  <code class="language-javascript">Module.prototype._compile = function(content, filename) {    var self = this;    var args = [self.exports, require, self, filename, dirname];    return compiledWrapper.apply(self.exports, args);  };</code></pre>    <p>最后就编译为,我们前文所述的那样:</p>    <pre>  <code class="language-javascript">(function (exports, require, module, __filename, __dirname) {    // 模块源码    return exports;  });</code></pre>    <p>通过上文,我们也能够很好地理解。 出错的时候,下面的信息到底意味着什么了.</p>    <pre>  <code class="language-javascript">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)</code></pre>    <p>来自: <a href="/misc/goto?guid=4959670034148753670" rel="nofollow">https://segmentfault.com/a/1190000004868777</a></p>