Node深入浅出 章节总结(第二章 — 模块机制) 持续更新...

模块机制的引入与CommonJs规范

在早期,javascript 主要还是只能在浏览器端发光发热,没有模块机制的前提下,我们只能插入一个个杂乱无章的 script 的标签,也许作为一个页面上的脚本语言,这已经足够了,因为需要它做的事很少,但是我们发现如果当 javascript 想在服务器端也同样发光时,模块机制就成为了一个必不可少的东西,否则它永远只能是脚本语言而不会用于服务端项目开发,正因为如此,CommonJs 规范应运而生,node也为它的推广立下了汗马功劳,我们借书中的关系图来说明 commonJs 与 浏览器、ES、Node的划分领域。
关系图

  • 用法(略,稍微用过node的朋友应该都知道,如果不知道,各大搜索引擎搜索即可)
  • 模块分类
    1. 核心模块(Node提供的模块,Node启动时已经被加载在内存中了,如 http、fs、path等模块)
    2. 文件模块(用户编写的模块,运行时动态加载的模块,需要先进行路径分析、文件定位、编译执行等过程)
    3. 加载模块的速度: 缓存模块 > 核心模块 > 文件模块
  • 自定义模块的寻址策略(即非核心模块 => node_modules)
    1. 当前文件目录下的 node_modules 目录
    2. 父级目录下的 node_modules 目录
    3. 沿父级目录向上递归寻找 node_modules 目录
  • 引用模块场景下,自动补全模块后缀扩展名的顺序
    1. js
    2. json
    3. node
  • require(‘module_name’) 没找到文件时的处理情况
    1. 根目录下寻找 package.json 并通过 JSON.parse 解析出 main 属性指定的文件名进行标识符定位,若找到指定文件,则以指定文件为该包名目录下的运行文件
    2. 经过上述条件后仍未找到或 require 的目录下根本没有 package.json 时,依次会默认以 index.js => index.json => index.node 为执行文件,若仍然没有则会遍历该包目录下的各个子目录递归执行上述操作,若果仍没有则报错
  • module.exports 与直接 exports 的区别
    1. 原理: 在 CommonJs 的规范下,每个 js 文件都被 node 进行了一次头尾包装,最终包装在一个立即执行函数下并返回一个不会被污染的局部 function 对象,我们每次 require 时就相当于执行了这个立即执行函数,而它的参数则是我们会常用到的局部作用域下的全局变量,下面放上一个 node 环境下头尾封装的代码
      (function (exports, require, module, __filename, __dirname) {\n ...your code \n})
      这也解释了为什么我们可以直接使用 __filename、__dirname 去直接获取当前文件的 文件名 以及 文件地址
    2. 通过上面的原理分析我们就解释了为什么我们直接 exports = {...} => {},而只有 export.fn = () => {} => {fn: [Function]},因为我们不能改变形参的引用指向,exports 只是 module.exports 的一个引用,用户真正接收到的是 module.exports,所以你改变 exports 的指向是没有用的,而且不止不能 exports 一个对象,而是地址赋值的都不可以,那就是说 Object 与 Array 直接 exports 都是不起效的,故我们有时会在某些框架中看到这样的代码 exports = module.exports = {...},这样做就是为了重新指向,恢复引用逻辑。
    3. 如果想 exports 一个引用出来让 require 能接收到,我们的迂回办法就是使用 module.exports = {...} ,因为这样我们并没有改变形参 module 的引用,而是在它的基础上挂上了 exports。

核心模块的编译过程

说在前面: 这节编译过程叙述了核心模块中 javascript 与 c/c++ 之间如何互相转换编译的过程,我个人也不是非常明白,因为没有从头到尾的实践过程,所以如果中间写错了,还请各位原谅并指出学习,谢谢各位~

首先我们通过上面的模块部分,知晓了 node 其实是分为自带的 核心模块 与 我们自己写的 文件模块 的,但是我们看 node 文档其实就可以很明显的看出 核心模块也是分为2种类型的

  1. 核心部分代码由 C/C++ 编写,其他由 javascript 完成并最终封装后向外导出
  2. 全部由 C/C++ 编写,然后封装处理后向外导出

所以他们之间的依赖关系其实是这样的

  • 内建模块(C/C++) => 核心模块(javascript) => 文件模块

我们知道 C/C++ 可以说是性能最优的静态语言了,在编译时他们被编译为二进制文件直接让计算机读取,相比于 javascript 这种动态的脚本语言,性能都不在一个等级上,但是 javascript 的快速开发能力却在一定情况下可以弥补这一点,所以如何用好内建模块,需要大家自己权衡,下面有几点说明

  1. 在启动 node 时会生成一个 process 变量,众所周知这就是当前的 node 线程,而我们的内建模块就是通过 process.binding('natives')取出后放置在NativeModule._source中,我们可以通过process.moduleLoadList 查看已经加载好的核心模块列表
  2. 如何编写 C/C++ 扩展模块:
    • 首先先用 C/C++ 代码写好扩展代码
    • 调用 NODE_MODULE([filename], [C/C++主函数名])注册方法到内存中
    • 最后利用 GYP 工具编译扩展模块文件生成 filename.node,由process.dlopen([filename.node的路径], exports)帮助你 require('./filename.node') 时动态加载
    • 通过 libuv 跨平台分别在 linux与 windows 下运行
  3. 内建模块不说了,反正我暂时肯定是不可能用上的= =

编译这块的说明对不起大家,实在没办法写的很好,因为我个人也不是太懂~~~

包与NPM

  • npm 实际上是一个 node 的开源包工具管理库,初始化一个符合 CommonJS 规范的包实际上非常简单,执行 npm init 即可
  • 一个符合 CommonJS 规范的包结构
    1. package.json:包描述文件
    2. bin:用于存放可执行二进制文件的目录(在 package.json 内加入 bin 属性即可定义需要执行的 bin 目录下的可执行脚本,可以为一个去掉 js 扩展名的 js 脚本都行,加入后需要执行 npm link 进行软连接的配置,否则不生效,最后面我会给出一个我的用例,正常情况,下载某些带bin自定义命令的包时全局安装就会将脚本添加到执行路径下了,局部安装才会使用软连接)
    3. lib:用于存放 javascript 代码的目录(一般为可执行的 js 脚本)
    4. doc:用于存放文档的目录
    5. test:用于存放单元测试用例的代码
  • 如何发布一个自己的包
    • 在 npm 上注册一个自己的仓库账号然后使用 npm adduser登录
    • 使用 npm init 初始化目录结构后完成自己的代码
    • 使用 npm publish 发布,npm 会将目录打包为一个存档文件,上传到官方源仓库中
  • AMD规范、CMD规范、CommonJs规范

    • AMD(Asynchronous Module Definition)规范代表:requireJs
      异步模块加载机制基本用法
    define([id], [dependencies] ,function (...dependencies) {
      ...
    })

    遵从依赖前置,会异步预先加载依赖,即使在函数内部 require 去加载模块,也会优先加载内部模块,再执行内部代码逻辑,这一段的意思就是告诉我们,是不支持动态加载的,因为你没办法按照加载顺序来书写模块代码,不过这种异步加载的方式似乎更合乎前端的逻辑思维。

  • CMD规范代表:seaJs

    • 最早由玉伯提出,更贴近 CommonJs 规范的写法(我个人用了一下以及看了下文档,感觉基本一致了)
    define(function(require, exports, module) {
      // 模块代码
    });

    我们会看到基本例子与 node 下封装的立即执行函数基本一致,在 requireJs 的基础上支持动态加载,用法与规范基本与 CommonJs 一致,你可以在模块代码内在需要时 require('...') 后,再顺序执行模块代码,所以玉伯说出那句 RequireJS 是没有明显的 bug,SeaJS 是明显没有 bug

  • CommonJs 规范:node 下的模块机制,上面第一大点已经说了很多了,这里就不再重复论述了

最后,如果细讲 AMD 与 CMD 规范的话,真的需要花一整篇来论述并且这两者都是对于前端而言的,与 CommonJs 规范有相同的地方,但运行环境完全不相同,大家可能需要注意这一点,关于两者的代表作 RequireJsSeaJs 这方面的资料网上已经有非常多了,这里如果大家有兴趣,看我推荐的这篇就好,个人认为这是这么多篇说的最好的之一了 SeaJS与RequireJS最大的区别

package.json 中 bin 下制定全局自定义命令的用例
事先说明:此用例是我的一个玩具,用于自己学习 node 各个工具包时使用的,功能是输入 yolo init [project-name] 会自动创建一个项目,并且启动web服务,我是在阅读完 vue-init 脚本源码后加入到 bin 目录下的,以防以后玩具的升级,目前我不需要太多的配置选项,所以没像 vue-init 一样有个专门配置选项的配置文件 meta.json 或 meta.js,下面放出示例代码结构及图片。
示例

脚本原理就是复制 /template 模板目录下的代码,目前还没引入模板生成,因为并没有什么配置需要填写的~

猜你喜欢

转载自blog.csdn.net/yolo0927/article/details/79405181
今日推荐