我曾经作过js讲师,在个人任教过程当中,模块系统一直是学生们的薄弱点。有一个充分的理由能够解释这个问题:模块在javascript中有一段奇怪且不稳定的历史。这篇文章咱们将讨论这段历史,而且,你讲了解过去的模块的相关知识,以更好的理解当前模块的工做原理。
在学习如何在js中建立模块以前,首先须要明白,模块是什么以及为何会存在模块。环顾你的周边,你会发现,不少复杂的东西都是有一个个分离的部件组合在一块儿构成,进而造成一个完整的东西。javascript
以一只手表为例:html
能够看到,一只手表由成百上千的内部部件组成,每个内部部件都有特定的功能和清晰地边界以方便与其余部件协做。把这些部件组合在一块儿,就组成了这只完整的手表。我不是一个手表制造业的专家,可是我由于这种方法的优势是很是直观的。java
若是你仔细的观察一些上图中的构造,你会发现有不少部件都是重复的。因为这种模块化为中心的设计,手表中的不一样功能也能够用到相同的部件。这种可复用部件的能力简化的工做制造流程,而且提升的利润。node
这种设计是可组合性的很是直观的案例。经过制定每一个部件清晰地边界,可以很好地组合每个部件,以创造一个功能齐全的手表。react
设想一下制造过程,公司不会制造手表,他们只是将这些部件拼接起来以产出一只完整的手表。他们能够本身制做这些部件,也能够将这些部件外包给其余工厂,这不重要,重要的是这些部件组合在一块儿就是一只完整的手表,而这些部件来自于哪是可有可无的。jquery
明白手表的整个系统是很困难的,由于它由不少小而复杂的,功能专注的部件组成,每一个部件均可以单独考虑,构造和修复。这种隔离性容许人们单独工做,不会成为彼此的负担。而且,若是一个部件循环,仅仅须要更换这个部件看,而不是更换这只表。webpack
组织是每一个独立的拥有清晰边界的部件为了与其余部件组合的副产品,伴随着模块化,天然就会出现这种状况。git
随着手表这样的结构不断产出,咱们能够愈来愈清晰地认识到模块化的好处,那么,若是咱们换成软件领域呢?实际上是同样的。就像手表的设计同样,软件也应该被设计,分割成不一样的具备特定功能的部件,而且具备为了与其余部件组合的清晰边界。不过,在软件中,这种部件被叫作模模块。到如今为止,模块给咱们的感受可能与react组件和函数截然不同。那模块到底包含什么呢?angularjs
每个模块都具备三部分:依赖,代码内容还有导出。es6
当一个模块须要其余模块的功能,它能够import
这个模块做为依赖,例如,不管什么什么时候,你想建立一个react组件,你只须要import react
模块,若是你想使用lodash
,你也只须要impiort lodash
模块。
肯定好你的模块须要的依赖以后,你就能够开始编写这个模块
exports
是当前模块的接口
,引入这个模块的开发者可使用你导出的一切功能。
说了这么多概念,下面让咱们来点实际的代码。
先来看一个react router的例子,方便起见,能够看一下react提供的模块目录,在react router中合理的利用模块,事实证实,在大多数状况下,他们直接映射react组件到模块,在react项目中分离组件是颇有意义的,从新审查上面的手表结构,将部件换成组件一样有意义。
来看一下MemoryRouter
模块的代码,如今不要关心代码的含义,只须要集中在代码的结构上。
// imports import React from 'react'; import { createMemoryHistory } from 'history'; import Router from './Router'; // code class MemoryRouter extends React.Component { history = createMemoryHistory(this.props); render() { return ( <Router history={this.history} children={this.props.children} />; ) } } //exports export default MemoryRouter;
你能够注意到这个模块的顶部定义了依赖,和一些使当前模块正产工做的必需的模块。接下来,能够看到一些代码。在这个例子中,建立了一个叫作MemoryRouter的新的react组件,最后,在底部定义了对外导出:MemoryRouter,也就是说,任何导入该模块的模块都会获得MemoryRouter这个组件。
如今,咱们对软件中的模块有了一个浅显的认识,让咱们回顾一些手表设计带来的好处,在相同设计的软件中有哪些能够能够直接应用。
由于模块能够在任何须要它的地方import
,因此模块的复用性很强,若是模块在程序中用处不少,你能够单首创建一个包。这个包能够包含一个或多个其余模块,而且上传到npm
开源。reacrt
、lodash
还有jquery
都是能够从npm上下载的npm包。
因为模块定义了导入和导出,因此很容易组合起来,不只如此。一个软件好的设计应该是低耦合,模块增长了代码的灵活性。
npm上有世界上数量最多的免费模块,超过七十万个,若是你须要某个功能的包,就去npm上找吧。
这里使用手表的描述也是合适的。不在赘述。
模块最大的好处也许是组织化了,模块带来的分离,正如你所见的,帮助你避免污染全局命名空间,减小命名冲突。
如今你大概了解了模块的结构和优势。是时候正式构建模块了。对此咱们的方法是很是有条理的。缘由是以前提到的,javascript中的模块有很是奇怪的历史,即便有更新的方法在javascript中建立模块,你也会时不时的看到一些老的建立方式。若是模块从2018年开始,这个可能没有一点用处,也就是说,咱们会回到2010年的模块
时代。那时,angularjs刚刚发布,jquery还在大范围使用。大部分公司使用javascript去构建复杂的web应用,而管理这些复杂的工具就是--模块。
建立模块的第一个想法可能就是用文件分离代码。
// users.js var users = ['Tyler', 'Sarah', 'Dan']; function getUsers() { return users; } // dom.js function addUserToDom(name) { var node = document.createElement('li'); var text = document.createTextNode(name); node.appendChild(text); document.getElementById('users').appendChild(node); } document.getElementById('submit') .addEventListener('click', function() { var input = document.getElementById('input'); addUserToDom(input.value); input.value = ''; }); var users = window.getUsers(); for (var i = 0; i < users.length; i++) { addUserToDom(users[i]); }
<!-- index.html --> <html> <head> <title>Users</title> </head> <body> <h1>Users</h1> <ul id="users"></ul> <input id="input" type="text" placeholder="New User"> </input> <button id="submit">Submit</button> <script src="users.js"></script> <script src="dom.js"></script> </body> </html>
这里查看所有源代码
ok,咱们成功的将app分离成不一样的功能文件,是否是意味着咱们已经实现了模块?不,绝对没有。咱们作的只不过是分离代码所在的位置。在js中,只有建立函数才能生成新的做用域。咱们未在函数中生命的变量,全都在全局对象上。也就是说,你能够访问他们经过window
对象。你会注意到咱们能够访问到,这是糟糕的。由于当咱们更改一些方法时,其实就是在改变咱们整个app。咱们没有分离咱们的代码到模块,只是在物理位置上分离了代码。若是刚开始学习javascript,这个结果可能令你惊讶,不过,这多是你能想到在js中如何实现模块化的第一个想法。
那么,若是分享分离没有给咱们提供模块的功能,那咱们要怎么作呢?重复强调一下模块的优势:复用性、组合型、利用性、隔离性还有可组织性。js有没有原始的特性以供咱们创造模块,以达到上面说的优势?常规函数?当你思考函数的特色,它的特色和模块优势类似。因此,接下来该怎么作呢?若是咱们暴露一个对象来替代直接把整个app暴露在全局对象下,而且命名这个对象为app
,咱们能够吧全部咱们app须要用到的方法,挂在在这个app
对象下。这样会防止咱们污染全局变量。咱们能够在里面放置任何东西,这样对于其余应用来讲依然是不可见得。
// users.js function usersWrapper () { var users = ["Tyler", "Sarah", "Dan"] function getUsers() { return users } APP.getUsers = getUsers } usersWrapper() // dom.js function domWrapper() { function addUserToDOM(name) { const node = document.createElement("li") const text = document.createTextNode(name) node.appendChild(text) document.getElementById("users") .appendChild(node) } document.getElementById("submit") .addEventListener("click", function() { var input = document.getElementById("input") addUserToDOM(input.value) input.value = "" }) var users = APP.getUsers() for (var i = 0; i < users.length; i++) { addUserToDOM(users[i]) } } domWrapper()
<!-- index.html --> <!DOCTYPE html> <html> <head> <title>Users</title> </head> <body> <h1>Users</h1> <ul id="users"></ul> <input id="input" type="text" placeholder="New User"> </input> <button id="submit">Submit</button> <script src="app.js"></script> <script src="users.js"></script> <script src="dom.js"></script> </body> </html>
这里查看所有源代码
如今你查看window对象,相比于,只有咱们的app
对象,和咱们的包裹函数:userWrapper
、domWrapper
。更重要的是,app中很是重要的代码(好比users
)变得不可更改了。由于它不在在全局环境下了。
让咱们更进一步。有没有办法能够丢弃包裹函数?咱们只是定义了它们,而后当即调用。给他们一个全局命名的惟一缘由就是咱们以后能够当即调用它们。若是咱们没有给他们全局命名,有没有办法直接直接调用没有名字(匿名)的函数。不卖关子了,固然有了,就是Immediately Invoked Function Expression
,简写为IIFE
。
它看起来像下面这样:
(function() { console.log('Pronounced IF-EE'); })()
注意,这仅仅是一个被小括号()
包起来的匿名函数。
(function() { console.log('Pronounced IF-EE'); })
而后,就像其余函数同样,为了调用函数,咱们增长了一对小括号在函数而最后。
(function() { console.log('Pronounced IF-EE'); })()
如今,为了放弃丑陋的包裹函数和干净的全局命名空间让咱们来使用IIFE
来更新一下代码。
// users.js (function () { var users = ["Tyler", "Sarah", "Dan"] function getUsers() { return users } APP.getUsers = getUsers })() // dom.js (function () { function addUserToDOM(name) { const node = document.createElement("li") const text = document.createTextNode(name) node.appendChild(text) document.getElementById("users") .appendChild(node) } document.getElementById("submit") .addEventListener("click", function() { var input = document.getElementById("input") addUserToDOM(input.value) input.value = "" }) var users = APP.getUsers() for (var i = 0; i < users.length; i++) { addUserToDOM(users[i]) } })()
这里查看所有源代码
么么哒。如今你在查看window对象,你会发现,咱们仅仅挂在了一个app对象在上面,他将做为全局方法的命名空间。
这就是IIFE模块模式。
IIFE模块模式有什么优势呢?首先,最重要的一点是,咱们没有污染全局命名空间,这避免了变量冲突,而且提供代码私有性。有利就有弊,咱们仍然有一个全局app变量,若是其余框架使用了相同的代码,咱们就有麻烦了。第二点,你可能主要到了html文件中的script的顺序,若是顺序不对,那么app直接会挂掉。
不过,就算这不是最完美的。咱们依然进步了一大块。咱们知道了IIFE模块模式的优势和缺点。若是咱们用咱们的标准建立并管理模块,它有哪些特性呢?
早些时候,咱们对模块分离的第一感受每一个文件都是一个新的模块。就算这种想法在js中不是开箱就用的。我认为对模块来讲这是一个很是显著的分离。每一个文件就是一个单独的模块,而后咱们须要一个特性是每一个文件(模块)都能定义本身的导入和导出。并可在其余文件(模块)中导入。
如今,咱们明确了咱们想要的标准,让咱们开始开发api。咱们须要定义的看起来像是imports
和exports
,从exports开始。为了保证更好理解,任何和module相关的咱们都称之为module
对象。而后,咱们想从模块导出的内容都放在module.exports
上,就像下面这样:
var users = ["Tyler", "Sarah", "Dan"] function getUsers() { return users } module.exports.getUsers = getUsers
也能够这样:
var users = ["Tyler", "Sarah", "Dan"] function getUsers() { return users } module.exports = { getUsers: getUsers }
无论有多少个方法,咱们均可以添加到exports
对象上:
// users.js var users = ["Tyler", "Sarah", "Dan"] module.exports = { getUsers: function () { return users }, sortUsers: function () { return users.sort() }, firstUser: function () { return users[0] } }
好了,咱们解决了如何从模块导出,接下来咱们须要解决如何导入。一样一切从简,首先假设咱们有一个叫作require
的函数,它接受一个字符串路径做为第一个参数,而后返回从这个路径下导出的全部内容。接着上面的user.js文件,引入的方式像这样:
var users = require('./users') users.getUsers() // ["Tyler", "Sarah", "Dan"] users.sortUsers() // ["Dan", "Sarah", "Tyler"] users.firstUser() // ["Tyler"]
哦耶~ 利用假象的module.exports
和require
语法,咱们不只保留了模块的全部优势,还摆脱了IIFE模块模式的缺点。舒服。
看完这个标准,有没有灵光一现?这tm不就是commonjs吗?
commonjs小组定义了模块模式去解决js做用域问题,以确保每一个模块在他们本身的命名空间执行。经过模块明确导出那些变量来实现,经过其余模块定义的require来正确工做。
若是你以前使用过node,conmonjs你会很熟悉。使用node,你能够开箱即用的使用require和module.exports语法,不过,浏览器并未支持。事实上,就算浏览器支持,浏览器也不会使用commonjs,由于它不是异步加载模块。众所周知,浏览器是单线程。异步才是王道。
简单总结一下,commonjs有两个问题,首先浏览器不支持,第二,浏览器就算支持了也会由于commonjs的同步加载形成很糟糕的用户体验。若是咱们能修复这两个问题,这也许是一个好的方案。不过,花费不少的时间去考虑研究commonjs是否对浏览器足够友好有没有意义呢?无论怎么样,这有一个新的解决方案,它叫作模块打包器。
模块打包器的做用是检查你的代码库。寻找全部的imports和exports,而后解析打包成浏览器能够明白的代码到一个单独的新文件。并且你再也不用当心翼翼的引入全部script,你应该直接引入打包好的那个文件。
app.js ---> |
users.js -> | Bundler | -> bundle.js
dom.js ---> |
因此,模块打包器到底作了什么捏?这个问题很大,我也不能所有解释清楚,不过,这有一个经过webpack打包以后的输出,你能够本身领悟领悟,哈哈。
这里查看全部源代码,你也能够下载下来,执行 npm install,而后执行webpack
(function(modules) { // webpackBootstrap // 模块缓存 var installedModules = {}; // require函数 function __webpack_require__(moduleId) { // 检查module是否有缓存 if(installedModules[moduleId]) { return installedModules[moduleId].exports; } // 建立一个module并缓存 var module = installedModules[moduleId] = { i: moduleId, l: false, exports: {} }; // 执行module modules[moduleId].call( module.exports, module, module.exports, __webpack_require__ ); // 设置module为已load module.l = true; // 返回模块的导出 return module.exports; } // 暴露模块对象 __webpack_require__.m = modules; // 暴露模块缓存 __webpack_require__.c = installedModules; // 定义getter函数 __webpack_require__.d = function(exports, name, getter) { if(!__webpack_require__.o(exports, name)) { Object.defineProperty( exports, name, { enumerable: true, get: getter } ); } }; // 在导出中定义__esModule __webpack_require__.r = function(exports) { if(typeof Symbol !== 'undefined' && Symbol.toStringTag) { Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); } Object.defineProperty(exports, '__esModule', { value: true }); }; // 建立假的命名空间对象 // mode & 1: value是模块id,经过它引入 // mode & 2: 合并全部属性到ns对象上 // mode & 4: ns已经存在时,直接返回 // mode & 8|1: 行为和require同样 __webpack_require__.t = function(value, mode) { if(mode & 1) value = __webpack_require__(value); if(mode & 8) return value; if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value; var ns = Object.create(null); __webpack_require__.r(ns); Object.defineProperty(ns, 'default', { enumerable: true, value: value }); if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key)); return ns; }; // getDefaultExport function for compatibility with non-harmony modules __webpack_require__.n = function(module) { var getter = module && module.__esModule ? function getDefault() { return module['default']; } : function getModuleExports() { return module; }; __webpack_require__.d(getter, 'a', getter); return getter; }; // Object.prototype.hasOwnProperty.call __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; // __webpack_public_path__ __webpack_require__.p = ""; // Load entry module and return exports return __webpack_require__(__webpack_require__.s = "./dom.js"); }) /************************************************************************/ ({ /***/ "./dom.js": /*!****************!*\ !*** ./dom.js ***! \****************/ /*! no static exports found */ /***/ (function(module, exports, __webpack_require__) { eval(` var getUsers = __webpack_require__(/*! ./users */ \"./users.js\").getUsers\n\n function addUserToDOM(name) {\n const node = document.createElement(\"li\")\n const text = document.createTextNode(name)\n node.appendChild(text)\n\n document.getElementById(\"users\")\n .appendChild(node)\n}\n\n document.getElementById(\"submit\")\n .addEventListener(\"click\", function() {\n var input = document.getElementById(\"input\")\n addUserToDOM(input.value)\n\n input.value = \"\"\n})\n\n var users = getUsers()\n for (var i = 0; i < users.length; i++) {\n addUserToDOM(users[i])\n }\n\n\n//# sourceURL=webpack:///./dom.js?` );}), /***/ "./users.js": /*!******************!*\ !*** ./users.js ***! \******************/ /*! no static exports found */ /***/ (function(module, exports) { eval(` var users = [\"Tyler\", \"Sarah\", \"Dan\"]\n\n function getUsers() {\n return users\n}\n\nmodule.exports = {\n getUsers: getUsers\n }\n\n//# sourceURL=webpack:///./users.js?`);}) });
你能够注意到有不少奇奇怪怪的代码,你能够阅读注释来简单了解一下到底发生了什么。可是,一个颇有趣的事是,打包后的代码用一个IIFE包裹起来了。也就是说,他们使用了IIFE模块模式获得了一个相对来讲最完美的方案。
javascript的将来是一个活生生的,丰满的语言。TC-31标准委员会,一年内屡次讨论如何潜在改善提升javascript语言。换言之,模块是编写可伸缩性、可维护的js代码的关键特性。在2013年甚至更早以前,这种说法很显然是不存在的。js须要一种模块的标准。一种内建的可处理模块的解决方法,这也拉开了实现js模块化的序幕。
如你如今所知道的。若是你以前接受过建立js系统模块的任务,这个模块最终看起来将是什么样的?commonjs?每一个文件以一种很清晰的方式定义导入和导出,很显然,这个是重中之重。可是有个问题,commonjs加载模块是同步的。虽然这对服务端没有压力,可是对浏览器不是很友好。一个改变是让commonjs支持异步加载,另外一种咱们使用语言本身的模块化,也就是import
和export
。
此次,咱们不须要再假想这种实现了,TC-39标准委员会提出了精确的设计和描述,也就是"ES Modules"。下面让咱们以这种标准化的模块建立javascript模块。
正如上面所说的,为了指定你要导出的模块,你须要使用export
关键字。
// utils.js // Not exported function once(fn, context) { var result return function() { if(fn) { result = fn.apply(context || this, arguments) fn = null } return result } } // Exported export function first (arr) { return arr[0] } // Exported export function last (arr) { return arr[arr.length - 1] }
有几种方式能够导入first
和last
方法,一种是导入全部从urils.js
导出的。
import * as utils from './utils' utils.first([1,2,3]) // 1 utils.last([1,2,3]) // 3
若是咱们不想导入所有导出呢?在这个例子中,若是你只想引入first
方法,你能使用一种叫作命名导入的办法(看起来很想解构,但其实不是哈)。
import { first } from './utils' first([1,2,3]) // 1
还有呢,不只仅能够指定多个导出,你还能够指定一个default
导出。
// leftpad.js export default function leftpad (str, len, ch) { var pad = ''; while (true) { if (len & 1) pad += ch; len >>= 1; else break; } return pad + str; }
当你使用default
导出这种方式,你的导入方式也会发生变化,代替使用*或者使用命名导入,你可使用import name from './patn'
import leftpad from './leftpad'
如今,若是你有默认导出,也有其余格式的导出怎么办呢?这不是问题,按照正确的语法写就能够了,ES Module没有这种限制。
// utils.js function once(fn, context) { var result return function() { if(fn) { result = fn.apply(context || this, arguments) fn = null } return result } } // regular export export function first (arr) { return arr[0] } // regular export export function last (arr) { return arr[arr.length - 1] } // default export export default function leftpad (str, len, ch) { var pad = ''; while (true) { if (len & 1) pad += ch; len >>= 1; else break; } return pad + str; }
那导入语法看起来是什么样的?我以为你能够想象获得。
import leftpad, { first, last } from './utils'
仍是挺爽的是吧?leftpad
是默认导出,first
和last
是常规导出。
ES Modules的关键点在于,它是js语言的一部分,而且现代浏览器已经支持这种写法了。如今,让咱们回到一开始的app,不过此次咱们使用ES Modules来改写一遍。
这里查看全部源代码
// users.js var users = ["Tyler", "Sarah", "Dan"] export default function getUsers() { return users } // dom.js import getUsers from './users.js' function addUserToDOM(name) { const node = document.createElement("li") const text = document.createTextNode(name) node.appendChild(text) document.getElementById("users") .appendChild(node) } document.getElementById("submit") .addEventListener("click", function() { var input = document.getElementById("input") addUserToDOM(input.value) input.value = "" }) var users = getUsers() for (var i = 0; i < users.length; i++) { addUserToDOM(users[i]) }
使用IIFE模式,咱们须要使用script引入每一个js文件。使用commonjs,咱们须要使用webpack等打包器处理咱们的代码,而后引入打包后的文件。而ES Modules中,在一些如今浏览器中,咱们仅仅须要使用script标签引入咱们的未被处理过的入口文件,而后为script标签增长属性:typr='module'
。
<!DOCTYPE html> <html> <head> <title>Users</title> </head> <body> <h1>Users</h1> <ul id="users"> </ul> <input id="input" type="text" placeholder="New User"></input> <button id="submit">Submit</button> <script type=module src='dom.js'></script> </body> </html>
到这里,还有一个commonjs与ES Modules的不一样没有介绍。
commonjs中,你能够在任何地方引入模块,甚至经过判断。
if (pastTheFold === true) { require('./parallax') }
ES Modules须要静态解析(参考js词法解析,也会有提高的效果)的,import语句必须在模块顶部,也就是说,他不能再判断语句中或者其余相似的语句中使用。
if (pastTheFold === true) { import './parallax' // "import' and 'export' may only appear at the top level" }
这是由于加载器会进行模块树的静态解析。找到那些真正被用到的,丢弃那些未被使用到的。这是一个很大的话题。换句话说,这也是为何ES Modules但愿你声明import语句在模块顶部,这样打包器会更快的解析的你依赖树,解析完毕,他才会去真正的工做。
对了,其实你可使用import()
来动态导入。请自行查找。
但愿经过这篇文章能够帮到你。
type='module'
的加载模式都是defer
。标准 | 变量问题 | 依赖 | 动态/懒 加载 | 静态分析 |
---|---|---|---|---|
IIFE | ✔ | × | × | × |
AMD | ✔ | ✔ | ✔ | × |
CMD | ✔ | ✔ | ✔ | × |
commonjs | ✔ | ✔ | ✔ | × |
es6 | ✔ | ✔ | ✔ | ✔ |
再强调一点:es6的模块是值的引用,commonjs是值的拷贝。参考文章