天天看点

前端模块化(一)nodeJS中的CommonJS规范

序言

模块化,大家用

vue

react

等东西,都会接触到像

exports

module.exports

export

export default

require

define

import

等等字段,感觉很多人对于这些东西还是分不清,概念非常的模糊,便想着写这么一篇文章,一是帮助自己梳理知识点,二是跟大家一起成长。其中有写得不对的,请及时提出来 ,我及时更正。

刚开始写的时候有些无从下手,一是因为知识点太多,二是因为自己的经验还不足以帮助大家从深层次剖析js的模块化中的区别,以及其实现原理、思想。这是一篇自己的学习笔记整理,我只能带大家了解前端模块化,区分他们并正确的使用他们。

先给大家扔出几条知识:

  • CommonJS

    NodeJS

    模块系统具体实现的基石。
  • AMD

    :异步模块规范,是

    RequireJS

    在推广过程中对模块定义的规范化产出的,推崇依赖前置;
  • UMD

    :兼容

    AMD

    commonJS

    规范的同时,还兼容全局引用的方式;
  • CMD

    :是

    SeaJS

     在推广过程中对模块定义的规范化产出的,推崇依赖就近;
  • ES6

    :ES6模块的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量;

CommonJS规范

CommonJS官网上写道,它希望js不仅仅可以在浏览器上运行,而是可以在任何地方运行,使其具备开发大型应用的能力。

javascript: not just for browsers any more!

CommonJS定义的模块分为:

  1. 模块引用(

    require

    )
  2. 模块定义(

    exports

    )
  3. 模块标识(

    module

    )

他可以做到:

  • 服务器端JavaScript应用程序
  • 命令行工具
  • 图形界面应用程序
  • 混合应用程序(如,Titanium或Adobe AIR)

CommonJS模块的特点如下

  • 所有代码都运行在模块作用域,不会污染全局作用域。
  • 模块可以多次加载,但是只会在第一次加载时运行一次,然后运行结果就被缓存了,以后再加载,就直接读取缓存结果。要想让模块再次运行,必须清除缓存。
  • 模块加载的顺序,按照其在代码中出现的顺序。

先谈一谈包的概念

前面给大家说过,

node.js

是基于

CommonJS

的规范实现的,

NPM

大家一定都很熟悉,它实践了

CommonJS

的包规范。

包规范

关于包规范,类比于

git

仓库,我们可以这么理解:

  • git init

    在当前文件夹中生成了隐藏文件

    .git

    ,我们把它叫做

    git仓库

  • npm init

    命令在当前文件夹中生成了配置文件

    package.json

    ,它描述了当前这个包,我们管这个文件叫做包(概念不准确,可以这么理解)。

包结构

严格按照

CommonJS

规范来的话,包的目录应当包含以下文件或目录。

  • package.json

    :包描述文件,存在于包顶级目录下
  • bin

    :存放可执行二进制文件的目录
  • lib

    :存放js代码的目录
  • doc

    :存放文档的目录
  • test

    :存放单元测试用例代码的目录

package.json

则是一个配置文件,它描述了包的相关信息。

NodeJS模块

既然

node.js

是基于

CommonJS

实现的,那么我们先来简单看看

NodeJS

的模块原理。

最近参加了公司开展的一次培训,结构性思维培养。任何东西都能够进行分类,事物一旦进行分类,更利于大家对此事物的认知,也能方便大家记忆。所以我们先来看看

Node

的模块分类。

通常分类

先给大家讲讲模块的分类

  • 核心模块
    • 核心模块指的是那些被编译进Node的二进制模块
    • 预置在Node中,提供Node的基本功能,如fs、http、https等。
    • 核心模块使用C/C++实现,外部使用JS封装
  • 第三方模块
    • Node

      使用

      NPM

      (

      Node Package Manager

      )安装第三方模块
    • NPM

      会将模块安装(可以说是下载到)到应用根目录下的

      node_modules

      文件夹中
    • 模块加载时,

      node

      会先在核心模块文件夹中进行搜索,然后再到

      node_modules

      文件夹中进行搜索
  • 文件模块
    • 文件可放在任何位置
    • 加载模块文件时加上路径即可
  • 文件夹模块(后续的

    nodeJS的加载规则

    将会详细介绍)
    • Node

      首先会在该文件夹中搜索

      package.json

      文件,
      • 存在,Node便尝试解析它,并加载main属性指定的模块文件
      • 不存在(或者

        package.json

        没有定义

        main

        属性),Node默认加载该文件夹下的index.js文件(

        main

        属性其实

        NodeJS

        的一个拓展,

        CommonJS

        标准定义中其实并不包括此字段)

估计大家对于文件夹模块概念都比较模糊,它其实相当于一个自定义模块,给大家举一个栗子🤡:

在根目录下的

/main.js

中,我们需要使用一个自定义文件夹模块。我们将所有的自定义文件夹模块存放在根目录下的

/module

下,其中有一个

/module/demo

文件夹,是我们需要引入的文件夹模块;

|—— main.js
|—— module
    |—— demo
        |—— package.json
        |—— demo.js           

package.json

文件的信息如下:

{
    "name": "demo",
    "version": "1.0.0",
    "main": "./demo.js"
}           

/main.js

中:

let demo = require("./modules/demo");           

此时,

Node

将会根据

package.json

中指定的

main

属性,去加载

./modules/demo/demo.js

;

这就是一个最简单的包,以一个文件夹作为一个模块。

nodeJS模块与CommonJS

module属性
  • module.id

     模块的识别符,通常是带有绝对路径的模块文件名。
  • module.filename

     模块的文件名,带有绝对路径。
  • module.loaded

     返回一个布尔值,表示模块是否已经完成加载。
  • module.parent

     返回一个对象,表示调用该模块的模块。
  • module.children

     返回一个数组,表示该模块要用到的其他模块。
  • module.exports

     表示模块对外输出的值。

来做一个测试,看看

module

到底是个什么东西(写的详细些,水平高的自行滤过);

  1. 新建一个文件夹,名为

    modulePractice

  2. 命令行进入

    cd modulePractive/

    文件夹
  3. npm init

    ,输入信息,此时我们相当于建立了一个包
  4. npm install jquery

    ,安装

    jquery

    来做测试
  5. 新建

    modulePractice/test.js

|—— modulePractice
    |—— node_module
    |—— package.json
    |—— test.js
// test.js
var jquery = require('jquery');
exports.$ = jquery;
console.log(module);        //module就是当前模块内部中的一个对象,代表当前对象           

终端执行这个文件

node test.js           

命令行会输出如下信息:

Module {
  id: '.',
  exports: { '$': [Function] },
  parent: null,
  filename: '/Applications/practice/nodepractice/modulePratice/test.js',
  loaded: false,
  children:
   [ Module {
       id: '/Applications/practice/nodepractice/modulePratice/node_modules/jquery/dist/jquery.js',
       exports: [Function],
       parent: [Circular],
       filename: '/Applications/practice/nodepractice/modulePratice/node_modules/jquery/dist/jquery.js',
       loaded: true,
       children: [],
       paths: [Array] } ],
  paths:
   [ '/Applications/practice/nodepractice/modulePratice/node_modules',
     '/Applications/practice/nodepractice/node_modules',
     '/Applications/practice/node_modules',
     '/Applications/node_modules',
     '/node_modules' ] }           

现在我们可以看到,当前这个模块的

parent

属性为

null

,这证明当前这个模块是一个入口脚本。

我们来看看在

test.js

中引入别的文件模块,

module

会输出什么

6.新建一个

modulePractice/child.js

|—— modulePractice
    |—— node_module
    |—— package.json
    |—— test.js
    |—— child.js
//child.js
var str = "I'm child";
exports.str = str;
console.log(module);           

再一次执行:

node test.js           

我们再来分别看看

child.js

中的

module

test.js

中的

module

分别是什么样子

//这个是child.js中输出的信息
Module {
  id: '/Applications/practice/nodepractice/modulePratice/child.js',
  exports: { str: 'I\'m child' },
  parent:
   Module {
     id: '.',
     exports: {},
     parent: null,
     filename: '/Applications/practice/nodepractice/modulePratice/test.js',
     loaded: false,
     children: [ [Circular] ],
     paths:
      [ '/Applications/practice/nodepractice/modulePratice/node_modules',
        '/Applications/practice/nodepractice/node_modules',
        '/Applications/practice/node_modules',
        '/Applications/node_modules',
        '/node_modules' ] },
  filename: '/Applications/practice/nodepractice/modulePratice/child.js',
  loaded: false,
  children: [],
  paths:
   [ '/Applications/practice/nodepractice/modulePratice/node_modules',
     '/Applications/practice/nodepractice/node_modules',
     '/Applications/practice/node_modules',
     '/Applications/node_modules',
     '/node_modules' ] }

//这个是test.js中输出的module信息
Module {
  id: '.',
  exports: { '$': [Function] },
  parent: null,
  filename: '/Applications/practice/nodepractice/modulePratice/test.js',
  loaded: false,
  children:
   [ Module {
       id: '/Applications/practice/nodepractice/modulePratice/child.js',
       exports: [Object],
       parent: [Circular],
       filename: '/Applications/practice/nodepractice/modulePratice/child.js',
       loaded: true,
       children: [],
       paths: [Array] },
     Module {
       id: '/Applications/practice/nodepractice/modulePratice/node_modules/jquery/dist/jquery.js',
       exports: [Function],
       parent: [Circular],
       filename: '/Applications/practice/nodepractice/modulePratice/node_modules/jquery/dist/jquery.js',
       loaded: true,
       children: [],
       paths: [Array] } ],
  paths:
   [ '/Applications/practice/nodepractice/modulePratice/node_modules',
     '/Applications/practice/nodepractice/node_modules',
     '/Applications/practice/node_modules',
     '/Applications/node_modules',
     '/node_modules' ] }           

大家可以看到

  • child.js

    中的

    parent

    属性输出的是

    test.js

    module

    信息,
  • test.js

    中的

    children

    属性,包括了

    jquery

    child.js

    两个

    module

    信息
  • test.js

    中的

    parent

    属性为

    null

由此,我们可以以

module.parent

来判断当前模块是否是入口脚本

当然,也有别的办法可以判断入口脚本,比如使用

require.main

child.js

修改如下:

//child.js
var str = "I'm child";
exports.str = str;
console.log(require.main);
node test.js
Module {
  id: '.',
  exports: {},
  parent: null,
  filename: '/Applications/practice/nodepractice/modulePratice/test.js',
  loaded: false,
  children:
   [ Module {
       id: '/Applications/practice/nodepractice/modulePratice/child.js',
       exports: [Object],
       parent: [Circular],
       filename: '/Applications/practice/nodepractice/modulePratice/child.js',
       loaded: false,
       children: [],
       paths: [Array] } ],
  paths:
   [ '/Applications/practice/nodepractice/modulePratice/node_modules',
     '/Applications/practice/nodepractice/node_modules',
     '/Applications/practice/node_modules',
     '/Applications/node_modules',
     '/node_modules' ] }           

可以看到,

require.main

直接输出的是入口脚本,由于我们是在

child.js

中打印的

require.main

,所以我们拿不到

test.js

这个入口脚本的

exports

,且只能看到当前入口脚本的

children

仅有

child.js

一个模块;

换一种方式进行测试,我们在

test.js

中打印

require.main

看一下会输出什么东西;

test.js

修改如下:

var child = require("./child.js");
var jquery = require('jquery');
exports.$ = jquery;
console.log(require.main);           

执行

node test.js           

拿到如下信息:

Module {
  id: '.',
  exports: { '$': [Function] },
  parent: null,
  filename: '/Applications/practice/nodepractice/modulePratice/test.js',
  loaded: false,
  children:
   [ Module {
       id: '/Applications/practice/nodepractice/modulePratice/child.js',
       exports: [Object],
       parent: [Circular],
       filename: '/Applications/practice/nodepractice/modulePratice/child.js',
       loaded: true,
       children: [],
       paths: [Array] },
     Module {
       id: '/Applications/practice/nodepractice/modulePratice/node_modules/jquery/dist/jquery.js',
       exports: [Function],
       parent: [Circular],
       filename: '/Applications/practice/nodepractice/modulePratice/node_modules/jquery/dist/jquery.js',
       loaded: true,
       children: [],
       paths: [Array] } ],
  paths:
   [ '/Applications/practice/nodepractice/modulePratice/node_modules',
     '/Applications/practice/nodepractice/node_modules',
     '/Applications/practice/node_modules',
     '/Applications/node_modules',
     '/node_modules' ] }           

也就是说,在真正的入口文件中,打印的

require.main

信息,才是完全的信息;

同样也可以用

require.main

输出的

module

信息中的

parent

属性,来判断是否是入口脚本;

当然也可以在当前模块中判断

require.main === module

,若为真,则代表它是被直接执行的(

node xxx.js

exports属性

现在我们了解了

module

属性,那么

module.exports

exports

都是什么呢?

从以上的测试,我们可以看到,

module

中其实带有的

exports

属性,就是我们对外的接口。也就是说,

module.exports

属性表示当前模块对外输出的接口,其他文件加载该模块,实际上就是读取

module.exports

变量。

exports

变量,实际上是

nodeJS

为了方便,为每个模块提供一个

exports

变量,指向

module.exports

。这等同在每个模块头部,有一行这样的命令。

var exports = module.exports;           

因此,我们可以直接向

exports

对象添加方法

exports.area = function (r) {
  return Math.PI * r * r;
};

exports.circumference = function (r) {
  return 2 * Math.PI * r;
};           

注意点:

  • 不能直接将

    exports

    变量指向一个值,等于切断了

    exports

    module.exports

    的联系,他将不再是一个接口,而仅仅当前模块中的一个局部变量。此时你在当前模块中写的所有其他的

    exports

    导出的接口,都将失效。而只有

    module.exports

    能够暴露出去当前模块的对外接口。

其实说简单点,

nodeJS

仅仅为了方便,用了一个变量

exports

直接指向了

module.exports

了,你只要注意

exports

变量,正确指向

module.exports

属性即可。最终我们导出去的接口,就是

module.exports

属性。

加载规则,require方法

require命令的基本功能是,读入并执行一个JavaScript文件,然后返回该模块的exports对象。如果没有发现指定模块,会报错。

require

命令是

CommonJS

规范之中,用来加载其他模块的命令。它其实不是一个全局命令,而是指向当前模块的

module.require

命令,而后者又调用

Node

的内部命令

Module._load

  • require()

    : 加载外部模块
  • require.resolve()

    :将模块名解析到一个绝对路径
  • require.main

    :指向主模块
  • require.cache

    :指向所有缓存的模块
  • require.extensions

    :根据文件的后缀名,调用不同的执行函数

require命令用于加载文件,后缀名默认为.js。

var foo = require('foo');
//  等同于
var foo = require('foo.js');           

而这种方式的引入(不是绝对路径,且不是相对路径),将会以如下规则进行搜索加载;

/usr/local/lib/node/foo.js
/home/user/projects/node_modules/foo.js
/home/user/node_modules/foo.js
/home/node_modules/foo.js
/node_modules/foo.js           

也就是说,将会先搜索默认的核心模块(

node

),再层级往上找

node_modules

中的当前模块。这样使得不同的模块可以将所依赖的模块本地化。

而如果是一个:

require('example-module/path/to/file')           
  1. 则将先找到

    example-module

    的位置,然后再以它为参数,找到后续路径。
  2. 查找是否有

    file

    文件夹
    • 若找到,则尝试找

      package.json

      ,并以其

      main

      属性指定的目录作为入口文件,否则便以当前目录下的

      index.js | index.node

      作为入口文件
    • 若未找到,则

      Node

      会尝试为文件名添加

      .js

      .json

      .node

      后,再去搜索。

      .js

      件会以文本格式的

      JavaScript

      脚本文件解析,

      .json

      文件会以

      JSON

      格式的文本文件解析,

      .node

      文件会以编译后的二进制文件解析。
  3. 若还没有发现发现,则报错。

第一次加载某个模块时,Node会缓存该模块。以后再加载该模块,就直接从缓存取出该模块的module.exports属性。

CommonJS模块载入方式

CommonJS规范加载模块是同步的,也就是说,只有加载完成,才能执行后面的操作。所以一般来说,CommonJS规范不适用于浏览器环境。然而,对服务器端不是一个问题,因为所有的模块都存放在本地硬盘,可以同步加载完成,等待时间就是硬盘的读取时间。但是,对于浏览器,这却是一个大问题,因为模块都放在服务器端,等待时间取决于网速的快慢,可能要等很长时间,浏览器处于"假死"状态。

因此,浏览器端的模块,不能采用"同步加载"(

synchronous

),只能采用"异步加载"(

asynchronous

)。这就是AMD规范诞生的背景。

下一章将会给大家讲一下

AMD

规范。

原文发布时间为:2018年06月24日

原文作者:掘金

本文来源: 

掘金 https://juejin.im/entry/5b3a29f95188256228041f46

如需转载请联系原作者

继续阅读