写一个适应所有环境的JS模块

背景

在ES6以前,JS语言没有模块化,如何让JS不止运行在浏览器,且能更有效的管理代码,于是应运而生CommonJS这种规范,定义了三个全局变量:

require,exports,module

require 用于引入一个模块
exports 对外暴露模块的接口,可以是任何类型
module 是这个模块本身的对象
require引入时获取的是这个模块对外暴露的接口(exports
Node.js 使用了CommonJS规范:

var foo = require('foo');

var out = foo.bar();

module.exports = out;

在浏览器端,不像Node.js内部支持CommonJS,如何进行模块化,
于是出现了 CMD 与 AMD 两种方式,其主要代表是 seajs 和 requirejs,
他们都定义了一个全局函数 define 来创建一个模块:

// CMD
define(function(require, exports, module) {
  var foo = require('foo');
  var out = foo.bar();
  module.exports = out;
})

// AMD
define(['foo'], function(foo) {
  var out = foo.bar();
  return out;
});

可以看出CMD完好的保留了CommonJS的风格,
而AMD用了一种更简洁的依赖注入和函数返回的方式实现模块化。
两者除风格不同外最大区别在于加载依赖模块的方式,
CMD是懒加载,在require时才会加载依赖,
而AMD是预加载,在定义模块时就提前加载好所有依赖。
各有千秋,各有适合的场景,网上有两者详细评测和激烈的讨论。

正题

我们要实现一个模块,让它既能在seajs(CMD)环境里引入,又能在requirejs(AMD)环境中引入,
当然也能在Node.js(CommonJS)中使用,另外还可以在没有模块化的环境中用script标签全局引入,
可谓是对write once,run anywhere的向往,实际上大部分npm的前端组件包也要考虑这个。

  • 首先一个模块看起来应该是这样:
var moduleName = {};
return moduleName;

当然,模块输出的不止可以是对象,还是可以是任何值,包括一个类。

  • 分析CMD和AMD,我们需要提供一个工厂函数传入define来定义模块,所以变成这样:
function factory () {
  var moduleName = {};
  return moduleName;
}
  • 为适应Node.js,可以来判断全局变量,由于require在CMD和ADM中都有定义,所以只判断:
typeof module !== 'undefined' && typeof exports === 'object'

于是变成这样:

function factory () {
  var moduleName = {};
  return moduleName;
}

if (typeof module !== 'undefined' && typeof exports === 'object') {
  module.exports = factory()
}

至此已经能够满足Node.js的需求。

  • 当没有上述全局变量,且有define全局变量时,我们认为是AMD或CMD,可以直接将factory传入define:
function factory () {
  var moduleName = {};
  return moduleName;
}

if (typeof module !== 'undefined' && typeof exports === 'object') {
  module.exports = factory()
} else if (typeof define === 'function' && (define.cmd || define.amd)) {
  define(factory)
}

注意:CMD其实也支持return返回模块接口,所以两者可以通用。

  • 最后是script标签全局引入,我们可以将模块放在window上,

为了模块内部在浏览器和Node.js中都能使用全局对象,我们可以做此判断:

var global = typeof window !== 'undefined' ? window : global;

同时,我们用一个立刻执行的闭包函数将所有代码包含,来避免污染全局空间,
并将global对象传入闭包函数,最终变成这样:

;(function (global) {
  function factory () {
    var moduleName = {};
    return moduleName;
  }
  
  if (typeof module !== 'undefined' && typeof exports === 'object') {
    module.exports = factory()
  } else if (typeof define === 'function' && (define.cmd || define.amd)) {
    define(factory)
  } else {
    global.moduleName = factory();
  }
})(typeof window !== 'undefined' ? window : global);

注意:闭包前加上分号是为了给前一个模块填坑,分号多了没问题,少了则语句可能发生变化。

  • 我们参考一下Vuex的源码:
;(function (global, factory) {
  typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
    typeof define === 'function' && define.amd ? define(factory) :
      (global.Vuex = factory());
}(this, (function () {
  'use strict';
  // ……
  var index = {
    Store: Store,
    install: install,
    version: '2.5.0',
    mapState: mapState,
    mapMutations: mapMutations,
    mapGetters: mapGetters,
    mapActions: mapActions,
    createNamespacedHelpers: createNamespacedHelpers
  };
  return index;
})));

这里有两个变化:
函数factory以匿名函数的方式引入,结构从;()(); 变成 ;(()());
this代替了typeof window !== 'undefined' ? window : globalthis在浏览器是window,在Node中是golbal,很是精妙。

  • 稍微简化一下:
;(function (global) {
  function factory () {
    var index= {};
    return index;
  }
  
  typeof module !== 'undefined' && typeof exports === 'object' ? module.exports = factory() :
  typeof define === 'function' && (define.cmd || define.amd) ? define(factory) :
  (global.moduleName = factory());
})(this);

或者

;(function (flobal, factory) {
  typeof module !== 'undefined' && typeof exports === 'object' ? module.exports = factory() :
  typeof define === 'function' && (define.cmd || define.amd) ? define(factory) :
  (global.moduleName = factory());
}(this, (function () {
    var index = {};
    return index;
  }
)));
  • 于是同一个js文件我们能愉快的在不同环境这样引入:
// Node.js
var myModule = require('moduleName');

// Seajs
define(function (require, exports, module) {
  var myModule = require('moduleName');
})

// Requirejs
define(['moduleName'], function (moduleName) {
})

// Browser global
<script src="moduleName.js"></script>

感谢浏览,欢迎评论指正,转载请标明出处。
参考博文:http://www.cnblogs.com/brandonchen/p/5550470.html

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

推荐阅读更多精彩内容