谈谈对模块化的理解

重要的模块化规范有几个:commonjs,ES6模块机制,AMD,CMD。因为业务中一直接触的都是Vue+webpack+babel架构的项目,在封装代码时用的比较的多仍是ES6规范,对其余模块化规范不熟悉,所以在这里记录一下学习过的模块化知识。javascript


CommonJS

模块化的目的在于营造安全封闭的做用域、且具备易于引用接口,按个人理解可分为模块定义、模块引入两部分。java

在模块中存在着一个module对象,它表明着模块自己,将须要导出的api挂载于其中的exports属性上便可以定义导出的接口;CommonJS规范中存在require()方法,用于接受模块标识,引入某个模块到当前的上下文。node

1. 模块定义


要理解模块如何定义,那必需要先理解module对象。在Node中,每个文件模块都是一个对象,即module对象。它的定义以下:webpack

function Module(id, parent){
    this.id = id    //模块标识符
    this.exports = {}    //模块对外输出的值
    this.parent = parent    //调用该模块的模块。parent为null时意味着模块为入口模块
    if(parent && parent.children){
        parent.children.push(this)
    }    
    this.filename = filename    //文件名
    this.loaded = false    //是否已加载
    this.children = []    //表示该模块调用的其余模块
}

定义模块的目的其实在于定义输出的值。写法很是简单,举个?git

function sayHello(){
    console.log('hello')
}
module.exports = sayHello    //或exports.sayHello = sayHello

为了方便导出接口,Node还定义了一个exports变量,但有个容易踩的坑是,exports只是一个引用,原本指向module.exports,假如只是给exports变量赋值则exports变量会失去对module.exports的指向。说到底,必须对module.exports定义接口才能真正导出值。github

先说解决方法,常见的写法为:web

exports = module.exports = sayHello
//或严格地只给exports变量添加属性
exports.sayHello = sayHello

再举个例子说一下犯错的具体场景:json

//a.js
exports.name = 'kent'
exports.sayHi = function(){
    console.log('hi')
}

console.log(module)// { exports: { name: 'kent', sayHi: function(){ console.log('hi') } } }

//假如给exports从新赋值 =_=
exports = {
    name: 'nicolas',
    sayBye: function(){
        console.log('bye')
     }
}

//module中的exports属性不会有任何变化
console.log(module)// { exports: { name: 'kent', sayHi: function(){ console.log('hi') } } }
console.log(exports)// { name: 'nicolas', sayBye: function(){ console.log('bye') } }
//b.js
//所以require的时候读取的name仍然为kent
var person = require('a.js')
console.log(person.name)//kent

具体的缘由也能够从模块机制中看出来api

function require(...) {
  var module = { exports: {} };
  ((module, exports) => {
    // Your module code here. In this example, define a function.
    function some_func() {};
    exports = some_func;
    // At this point, exports is no longer a shortcut to module.exports, and
    // this module will still export an empty default object.
    module.exports = some_func;
    // At this point, the module will now export some_func, instead of the
    // default object.
  })(module, module.exports);
  return module.exports;
}

2. 模块引入


模块引入的语法也很是简单。上一节也简单提过。这里再举个?缓存

//book.js
exports.name = 'javascript'
exports.logName = function(){
    console.log('javascript')
}
//main.js
var book = require('./book.js')//require的参数即模块标识符
console.log(book.name)//'javascript'
book.logName()//'javascript'

下面详情谈谈模块引入经历哪些步骤。但在此以前须要先了解两个概念:核心模块与文件模块。

在Node中,有一部分模块由Node提供,称之为核心模块。在Node进程启动的时候,核心模块就直接加载至内存中。所以引入核心模块只须要走路径分析一个步骤,其加载速度最快。

另外一部分则是运行时动态加载,常见的有用户定义带路径标识符的模块,或自定义模块(如三方提供的包)。这类模块须要完整地走完如下三个步骤:路径分析、文件定位与编译执行。

①路径分析:
路径分析能够理解为模块标识符的分析。模块标识符在Node中主要有:

·核心模块,如:http, fs等等;
    ·以"./"或"../"开头的相对路径模块,相对于当前的目录位置;
    ·以"/"开头的绝对路径模块;
    ·非路径形式的文件模块,与核心模块的标识符相似。Node会搜索各级的node_modules目录。

· 核心模块:核心模块通过路径分析以后会直接加载。须要注意的是,自定义的文件模块不能与核心模块标识符相同,要不更换不一样的标识符要么使用相对路径或绝对路径标识符。

· 路径形式的文件模块:在分析文件模块的时候,require方法将会把路径转换为真实路径并以此为索引编译模块并存放到缓存中(缓存加载将在下文介绍)。

· 非路径形式的文件模块(自定义模块):自定义模块的路径分析在咱们引用三方库的时候常常会碰到。这类非路径形式的文件模块加载时将会以模块路径为线索逐级搜索。举个? :

//在"/Users/zhazheng/Documents/www"下新建一个module_path.js

//module_path.js
console.log(module.paths)

//再执行module_path.js
node module_path

//得出如下log
[ '/Users/zhazheng/Documents/www/node_modules',
  '/Users/zhazheng/Documents/node_modules',
  '/Users/zhazheng/node_modules',
  '/Users/node_modules',
  '/node_modules' ]

可见,这类模块会从当前文件目录往上逐级递归直到根目录下的node_modules目录。所以这类模块的路径分析是最费时的。

②文件定位:文件定位主要包括文件扩展名分析、目录和包的处理。

·文件扩展名分析:分析标识符的过程当中出现不包含文件扩展名的状况很是常见。在标识符不包含文件扩展名的状况下,Node会依次尝试如下三种扩展名:.js、.json、.node。因为尝试解析的过程是同步阻塞进行的,所以大量的分析文件扩展名会产生性能问题,这种状况下能够尝试添加扩展名或充分利用缓存加载的优点。

·目录分析与包的处理: 假如分析完扩展名后仍然没有找到对应的文件而只得出一个目录,那么Node会将此目录当作一个包来处理。首先会查找当前目录下是否有package.json文件,假若有则检查是否具备main属性(main属性即指向入口文件)。假如没有package.json文件或package.json中不具有main属性,那么Node则按index为默认的文件名,最后再重复“文件扩展名分析”这个步骤。

3.缓存加载


事实上Node的模块,不管是核心模块仍是文件模块,第一次加载以后都会被缓存。require()方法将会对二次加载的模块进行缓存。所以假若有屡次加载模块的需求,那么就须要记得先从缓存中删除模块。

缓存均保存在require.cache对象中,须要删除单个模块或所有模块的缓存能够这样写:

//删除单个模块缓存
delete require.cache[moduleName]

//删除所有模块缓存
Object.getOwnPropertyNames(require.cache).forEach(key => {
    delete require.cache[key]
})

固然,通常状况下缓存是能够带来性能优点的。对于路径套得很是深的自定义文件模块来讲尤甚。

4.循环加载


循环加载是避免不了的问题。在Node中须要了解一下循环加载的表现。首先要理解的是,require是一个同步加载的过程,读取的接口仅仅是指向exports对象中的属性,举个? :(如下三个模块均在同一目录下)

//a.js
exports.name = 'a1'
console.log(`a.js, ${require('./b.js').name}`)
exports.name = 'a2'
//b.js
exports.name = 'b1'
console.log(`b.js, ${require('./a.js').name}`)
exports.name = 'b2'
//main.js
console.log(`main.js, ${require('./a.js').name}`)
console.log(`main.js, ${require('./b.js').name}`)

nvm run node而后.load main.js得出如下的结果

b.js, a1
a.js, b2//这两行结果应该大体能够理解两个模块的require方法发生了什么
main.js a2
main.js b2

再次执行.load main.js会读取缓存结果

main.js a2
main.js b2

循环加载示例代码可到个人github查看

AMD

...未完待续
相关文章
相关标签/搜索