JavaScript模块化

为什么要使用模块化?

当我们一个项目越做越大的时候,维护起来肯定没那么方便,且多人协作的去进行开发,当中肯定会遇到很多的问题,例如:

  1. 方法的覆盖: 很有可能你定义的一些函数会覆盖公共类中同名的函数,因为你可能根本就不知道公共类中有哪些函数,也不知道是如何命名的。
  2. 这些公共的组件: 但是你又不知道这些组件又会依赖哪些模块,同时在维护这些公共方法的时候,会新增一些依赖或者删除一些依赖,那么每个引入这些公共方法的地方都需要去对应的新增或者删除。等等,还会存在很多的问题。

我们使用模块化就是为了让各个模块之间相对独立,可能每个文件就是一个功能块,能满足于某项特定的功能,这样我们在引用某项功能的时候就会很方便。

CommonJS

Node 应用由模块组成,采用 CommonJS 模块规范。

每个文件就是一个模块,有自己的作用域。在一个文件里面定义的变量、函数、类,都是私有的,对其他文件不可见,CommonJS规范加载模块是同步的,也就是说,加载完成才可以执行后面的操作,Node.js主要用于服务器编程,模块一般都是存在本地硬盘中,加载比较快,所以Node.js采用CommonJS规范。且CommonJS模块输出的是值的缓存, 运行时加载。
CommonJS规范规定,每个模块内部,module变量代表当前模块。这个变量是一个对象,它的exports属性(即module.exports)是对外的接口。加载某个模块,其实是加载该模块的module.exports属性。

// tools.ts 
const add = (a: number, b: number) =>{
  return a + b
}

const reduce = (a: number, b: number) => {
  return a - b
}

const multy = (a: number, b: number) => {
    return a * b
}

exports.add = add
exports.reduce = reduce

export default multy

// 等价于
module.exports = {
  add,
  reduce
}
// app.ts
const tools = require('./tools.ts')
tools.add(2, 3)       // 5
tools.reduce(3, 2)    // 1

Node内部提供一个Module构建函数。所有模块都是Module的实例。

// Module 构造函数
function Module(id, parent) {
  this.id = id          //模块的识别符,通常是带有绝对路径的模块文件名
  this.exports = {}     //表示模块对外输出的值
  this.parent = parent  //返回一个对象,表示调用该模块的模块
  ...
}
// tools.ts 
const add = (a: number, b: number) =>{
  return a + b
}

exports.add = add
console.log(module)

// 输出
Module {
  id: '.',
  exports: { add: [Function: add] },
  parent: null,
  filename: 'C:\\Users\\viruser.v-desktop\\Desktop\\zz\\aa.js', //模块的文件名,带有绝对路径
  loaded: false,      //返回一个布尔值,表示模块是否已经完成加载
  children: [],       //返回一个数组,表示该模块要用到的其他模块
  paths: [ 
    'C:\\Users\\viruser.v-desktop\\Desktop\\zz\\node_modules',
    'C:\\Users\\viruser.v-desktop\\Desktop\\node_modules',
    'C:\\Users\\viruser.v-desktop\\node_modules',
    'C:\\Users\\node_modules',
    'C:\\node_modules' 
  ] 
}

如果在命令行下调用某个模块,比如node tools.js,那么module.parent就是null。如果是在脚本之中调用,比如require('./tools.js'),那么module.parent就是调用它的模块。利用这一点,可以判断当前模块是否为入口脚本。

if (!module.parent) {
  // ran with `node something.js`
  app.listen(8088, function() {
    console.log('app listening on port 8088')
  })
} else {
  // used with `require('/.something.js')`
  module.exports = app
}

为了方便,Node为每个模块提供一个exports变量,指向module.exports。这等同在每个模块头部,有一行这样的命令。造成的结果是,在对外输出模块接口时,可以向exports对象添加方法。注意,不能直接将exports变量指向一个值,因为这样等于切断了exports与module.exports的联系。

const exports = module.exports

AMD

大多数的同学都应该了解RequireJS,而且RequireJS是基于AMD规范的。AMD是"Asynchronous Module Definition"的缩写,意思就是"异步模块定义"。它采用异步方式加载模块,模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。且同样是运行时加载的,用require.config()指定引用路径等,用define()定义模块,用require()加载模块, 但是不同于CommonJS,它要求两个参数:

定义模块

// define([module], callback)
define(['myLib'], () =>{
    function foo(){
        console.log('mylib')
    }
    return {
        foo : foo
 }
})
require.config({
  baseUrl: "js/lib",
  paths: {
    "jquery": "jquery.min",                 //实际路径为js/lib/jquery.min.js
    "underscore": "underscore.min",
  }
});

使用模块

// require([module], callback)
require(['myLib'], mod => {
  mod.foo()
})

// myLib

为什么要使用AMD规范呢?

因为AMD是专门为浏览器中js环境设计的规范。它吸取了CommonJS的一些优点,但是没有全部都照搬过来。也是非常容易上手。

CMD

CMD在很多地方和AMD有相似之处,在这里我只说两者的不同点。首先,CMD规范和CommonJS规范是兼容的,相比AMD,它简单很多。遵循CMD规范的模块,可以在Node.js中运行。SeaJS是推荐是用CMD的写法,那么就使用SeaJS来编写一个简单的例子:

// AMD写法  AMD的依赖需要前置书写
define(["a", "b", "c", "d", "e", "f"], function(a, b, c, d, e, f) { 
  // 等于在最前面声明并初始化了要用到的所有模块
  a.doSomething()
  if (false) {
    // 即便没用到某个模块 b,但 b 还是提前执行了
    b.doSomething()
  }
})

// CMD写法 CMD的依赖就近书写即可,不需要提前声明
define(function(require, exports, module) {
  var a = require('./a')  //在需要时申明  同步
  a.doSomething()
  if (false) {
    var b = require('./b')
    b.doSomething()
    }
    require.async('a', math =>{  //异步
    a.add(1, 2);
  })
})

/** sea.js **/
// 定义模块 math.js
define(function(require, exports, module) {
  var $ = require('jquery.js')
  var add = function(a,b){
    return a+b
  }
  exports.add = add
})
// 加载模块
seajs.use(['math.js'], function(math){
  var sum = math.add(1+2)
})

CMD规范我们可以发现其API职责专一,例如同步加载和异步加载的API都分为require和require.async,而AMD的API比较多功能。

UMD

UMD是AMD和CommonJS的糅合

AMD模块以浏览器第一的原则发展,异步加载模块。
CommonJS模块以服务器第一原则发展,选择同步加载,它的模块无需包装(unwrapped modules)。
这迫使人们又想出另一个更通用的模式UMD (Universal Module Definition)。希望解决跨平台的解决方案。

UMD先判断是否支持Node.js的模块(exports)是否存在,存在则使用Node.js模块模式。
在判断是否支持AMD(define是否存在),存在则使用AMD方式加载模块。

(function (window, factory) {
  if (typeof exports === 'object') {
    module.exports = factory()
  } else if (typeof define === 'function' && define.amd) {
    define(factory)
  } else {
    window.eventUtil = factory()
  }
})(this, function () {
  //module ...
})

ESModule

历史上,JavaScript一直没有模块(module)体系,无法将一个大程序拆分成互相依赖的小文件,再用简单的方法拼装起来。其他语言都有这项功能,比如Ruby的 require 、Python的 import ,甚至就连CSS都有 @import ,但是JavaScript任何这方面的支持都没有,这对开发大型的、复杂的项目形成了巨大障碍。

ES6模块的设计思想,是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。CommonJS和AMD模块,都只能在运行时确定这些东西。比如,CommonJS模块就是对象,输入时必须查找对象属性。

模块功能主要由两个命令构成: export 和 import。export 命令用于规定模块的对外接口,import 命令用于输入其他模块提供的功能

// a.js
var num1 = 1
var num2 = 2

export { num1, num2 }
// b.js
import { num1, num2 } from './a.js'

function add(num1, num2) {
  return num1 + num2
}

console.log(add(num1, num2))

如果想为输入的变量重新取一个名字,import命令要使用 as 关键字,将输入的变量重命名。

import { num1 as snum } from './a'

mport 命令具有提升效果,会提升到整个模块的头部

add();
import { add} from './tools'

如果在一个模块之中,先输入后输出同一个模块, import 语句可以与 export 语句写在一起。

export { es6 as default } from './a'

// 等同于
import { es6 } from './a'
export default es6

还可以使用整体加载,即用星号( * )指定一个对象,所有输出值都加载在这个对象上面, 但不包括default

import * as tools from './a'
import multy from './a
tools.add(2, 1)         //3
tools.reduce(2, 1)  //1
multy(2,1)                  //2

ES6模块加载的实质

ES6模块加载的机制,与CommonJS模块完全不同。CommonJS模块输出的是一个值的拷贝,而ES6模块输出的是值的引用。

CommonJS模块输出的是被输出值的拷贝,也就是说,一旦输出一个值,模块内部的变化就影响不到这个值

// lib.js
var counter = 3
function incCounter() {
  counter++;
}
module.exports = {
  counter: counter,
  incCounter: incCounter,
}
// main.js
var mod = require('./lib')
console.log(mod.counter) // 3
mod.incCounter()
console.log(mod.counter) // 3

lib.js 模块加载以后,它的内部变化就影响不到输出的 mod.counter 了。这是因为 mod.counter 是一个原始类型的值,会被缓存。除非写成一个函数,才能得到内部变动后的值

// lib.js
var counter = 3
function incCounter() {
  counter++
}
module.exports = {
  get counter() {
    return counter
  },
  incCounter: incCounter,
}
// main.js
var mod = require('./lib')
console.log(mod.counter) // 3
mod.incCounter()
console.log(mod.counter) // 4

ES6模块的运行机制与CommonJS不一样,它遇到模块加载命令 import 时,不会去执行模块,而是只生成一个动态的只读引用。等到真的需要用到时,再到模块里面去取值,换句话说,ES6的输入有点像Unix系统的”符号连接“,原始值变了,import 输入的值也会跟着变。因此,ES6模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,699评论 6 513
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 94,124评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 167,127评论 0 358
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,342评论 1 294
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,356评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 52,057评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,654评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,572评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,095评论 1 318
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,205评论 3 339
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,343评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 36,015评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,704评论 3 332
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,196评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,320评论 1 271
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,690评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,348评论 2 358

推荐阅读更多精彩内容

  • XSS攻击是什么 常见的 XSS 输入XSS 输入通常包含 JavaScript 脚本,如弹出恶意警告框: ale...
    followyounger1阅读 341评论 0 2
  • 今天早上我们进行了五年级的心育课,主题是要勇敢说不No,No,No。上完这节课,感触很深,遇到困难,挫折要勇敢一点...
    沒有沒有沒有沒有阅读 197评论 0 0
  • [美] 贝塔·哈德 / [美] 埃尔默·哈德 /著 火棘果子/译tags: 季节 四季活动设计:南方过冬的鸟,换...
    ClassicMin阅读 138评论 0 0
  • 焦虑 2017年的7月1日是雯雯小升初的日子,从小对大大小小的考试都会焦虑的我来说,这无疑是最大的焦虑。 进入六月...
    心怡雨文阅读 364评论 3 0