序言
模块化,大家用
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定义的模块分为:
- 模块引用(
)require
- 模块定义(
exports
- 模块标识(
module
他可以做到:
- 服务器端JavaScript应用程序
- 命令行工具
- 图形界面应用程序
- 混合应用程序(如,Titanium或Adobe AIR)
CommonJS模块的特点如下
- 所有代码都运行在模块作用域,不会污染全局作用域。
- 模块可以多次加载,但是只会在第一次加载时运行一次,然后运行结果就被缓存了,以后再加载,就直接读取缓存结果。要想让模块再次运行,必须清除缓存。
- 模块加载的顺序,按照其在代码中出现的顺序。
先谈一谈包的概念
前面给大家说过,
node.js
是基于
CommonJS
的规范实现的,
NPM
大家一定都很熟悉,它实践了
CommonJS
的包规范。
包规范
关于包规范,类比于
git
仓库,我们可以这么理解:
-
在当前文件夹中生成了隐藏文件git init
,我们把它叫做.git
。git仓库
-
命令在当前文件夹中生成了配置文件npm init
,它描述了当前这个包,我们管这个文件叫做包(概念不准确,可以这么理解)。package.json
包结构
严格按照
CommonJS
规范来的话,包的目录应当包含以下文件或目录。
-
:包描述文件,存在于包顶级目录下package.json
-
:存放可执行二进制文件的目录bin
-
:存放js代码的目录lib
-
:存放文档的目录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
属性),Node默认加载该文件夹下的index.js文件(main
属性其实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
到底是个什么东西(写的详细些,水平高的自行滤过);
- 新建一个文件夹,名为
modulePractice
- 命令行进入
文件夹cd modulePractive/
-
,输入信息,此时我们相当于建立了一个包npm init
-
,安装npm install jquery
来做测试jquery
- 新建
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')
- 则将先找到
的位置,然后再以它为参数,找到后续路径。example-module
- 查找是否有
file
- 若找到,则尝试找
,并以其package.json
属性指定的目录作为入口文件,否则便以当前目录下的main
作为入口文件index.js | index.node
- 若未找到,则
会尝试为文件名添加Node
、.js
.json
后,再去搜索。.node
件会以文本格式的.js
脚本文件解析,JavaScript
文件会以.json
格式的文本文件解析,JSON
文件会以编译后的二进制文件解析。.node
- 若找到,则尝试找
- 若还没有发现发现,则报错。
第一次加载某个模块时,Node会缓存该模块。以后再加载该模块,就直接从缓存取出该模块的module.exports属性。
CommonJS模块载入方式
CommonJS规范加载模块是同步的,也就是说,只有加载完成,才能执行后面的操作。所以一般来说,CommonJS规范不适用于浏览器环境。然而,对服务器端不是一个问题,因为所有的模块都存放在本地硬盘,可以同步加载完成,等待时间就是硬盘的读取时间。但是,对于浏览器,这却是一个大问题,因为模块都放在服务器端,等待时间取决于网速的快慢,可能要等很长时间,浏览器处于"假死"状态。
因此,浏览器端的模块,不能采用"同步加载"(
synchronous
),只能采用"异步加载"(
asynchronous
)。这就是AMD规范诞生的背景。
下一章将会给大家讲一下
AMD
规范。
原文发布时间为:2018年06月24日
原文作者:掘金
本文来源:
https://juejin.im/entry/5b2afc3551882574e321dcf1 掘金 https://juejin.im/entry/5b3a29f95188256228041f46如需转载请联系原作者