因为 Js 起初定位的缘由(刚开始没想到会应用在过于复杂的场景),因此它自己并无提供模块系统,随着应用的复杂化,模块化成为了一个必须解决的问题。本着菲麦深刻原理的原则,颇有必要来揭开模块化的面纱javascript
要对一个东西进行深刻的剖析,有必要带着目的去看。模块化所要解决的问题能够用一句话归纳html
在没有全局污染的状况下,更好的组织项目代码前端
举一个简单的栗子,咱们如今有以下的代码:java
function doSomething () { const a = 10; const b = 11; const add = function (a, b) { return a + b } add (a + b) }复制代码
在现实的应用场景中,doSomething 可能须要作不少不少的事情,add 函数可能也更为复杂,而且能够复用,那么咱们但愿能够将 add 函数独立到一个单独的文件中,因而:node
// doSomething.js 文件 const add = require('add.js'); const a = 10; const b = 11; add(a+ b);复制代码
// add.js 文件 function add (a, b) { return a + b; } module.exports = add;复制代码
这样作的目的显而易见,更好的组织项目代码,注意到两个文件中的 require
和 module.exports
,从如今的上帝视角来看,这出自 CommonJS 规范(后文会有一个章节来专门讲规范)中的关键字,分别表明导入和导出,抛开规范而言,这实际上是咱们模块化之路上须要解决的问题。另外,虽然 add 模块须要获得复用,可是咱们并不但愿在引入 add 的时候形成全局污染c++
在上述的例子中,咱们已经将代码拆分到了两个模块文件当中,在不形成全局污染的状况下,如何实现 require
,才能使得例子中的代码作到正常运行呢?ajax
先不考虑模块文件代码的载入过程,假设 require
已经能够从模块文件中读取到代码字符串,那么 require
能够这样实现后端
function require (path) { // lode 方法读取 path 对应的文件模块的代码字符串 // let code = load(path); // 不考虑 load 的过程,直接得到模块 add 代码字符串 let code = 'function add(a, b) {return a+b}; module.exports = add'; // 封装成闭包 code = `(function(module) {${code}})(context)` // 至关于 exports,用于导出对象 let context = {}; // 运行代码,使得结果影响到 context const run = new Function('context', code); run(context); //返回导出的结果 return context.exports; }复制代码
这有几个要点:
1) 为了避免形成全局污染,须要将代码字符串封装成闭包的形式,而且导出关键字 module.exports
,module 是与外界联系的惟一载体,须要做为闭包匿名函数的入参,与引用方传入的上下文 context
进行关联
2) 使用 new Function
来执行代码字符串,估计大部分同窗对 new Function
是不熟悉的,由于通常状况下定义一个函数无需如此,要知道,用 Function 类能够直接建立函数,语法以下:数组
var function_name = new function(arg1, arg2, ..., argN, function_body)复制代码
在上面的形式中,每一个 arg 都是一个参数,最后一个参数是函数主体(要执行的代码)。这些参数必须是字符串。也就是说,可使用它来执行字符串代码,相似于 eval
,而且相比 eval
, 还能够经过参数的形式传入字符串代码中的某些变量的值
3)若是曾经你有疑惑过为何规范的导出关键字只有 exports
而咱们实际使用过程当中却要使用module.exports
(写过 Node 代码的应该不会陌生),那在这段代码中就能够找到答案了,若是只用 exports
来接收 context
,那么对 exports 的从新赋值对 context
不会有任何影响(参数的地址传递),不信将代码改为以下形式再跑一跑:浏览器
解决了代码的运行问题,还须要解决模块文件代码的载入问题,根据上述实例,咱们的目标是将模块文件代码以字符串的形式载入
在 Node 容器,全部的模块文件都在本地,只须要从本地磁盘读取模块文件载入字符串代码,再走上述的流程就能够了。事实证实,Node 非内建、核心、c++ 模块的载入执行方式大致如此(虽然使用的不是 new Function,但也是一个相似的方法)
在 RN/Weex 容器,要载入一个远程 bundle.js,能够经过 Native 的能力请求一个远程的 js 文件,再读取成字符串代码载入便可(按照这个逻辑,Node 读取一个远程的 js 模块好像也无不可,虽然大多数状况下咱们不须要这么作)
在浏览器环境,全部的 Js 模块都须要远程读取,尴尬的是,受限于浏览器提供的能力,并不能经过 ajax 以文件流的形式将远程的 js 文件直接读取为字符串代码。前提条件没法达成,上述运行策略便行不通,只能另辟蹊径
这就是为何有了 CommonJs 规范了,为何还会出现 AMD/CMD 规范的缘由
那么浏览器上是怎么作的呢?在浏览器中经过 Js 控制动态的载入一个远程的 Js 模块文件,须要动态的插入一个 <script>
节点:
// 摘抄自 require.js 的一段代码 var node = config.xhtml ? document.createElementNS('http://www.w3.org/1999/xhtml', 'html:script') : document.createElement('script'); node.type = config.scriptType || 'text/javascript'; node.charset = 'utf-8'; node.async = true; node.setAttribute('data-requirecontext', context.contextName); node.setAttribute('data-requiremodule', moduleName); node.addEventListener('load', context.onScriptLoad, false); node.addEventListener('error', context.onScriptError, false);复制代码
要知道,设置了 <script>
标签的 src 以后,代码一旦下载完成,就会当即执行,根本由不得你再封装成闭包,因此文件模块须要在定义之初就要作文章,这就是咱们说熟知的 AMD/CMD 规范中的 define
,开篇的 add.js 须要从新改写一下
// add.js 文件 define ('add',function () { function add (a, b) { return a + b; } return add; })复制代码
而对于 define 的实现,最重要的就是将 callback 的执行结果注册到 context 的一个模块数组中:
context.modules = {} function define(name, callback) { context.modules[name] = callback && callback() }复制代码
因而 require 就能够从 context.modules 中根据模块名载入模块了,是否是有了一种本身去写一个 “requirejs” 的冲动感
具体的 AMD 实现固然还会复杂不少,还须要控制模块载入时序、模块依赖等等,可是了解了这其中的灵魂,想必去精读 require.js 的源码也不是一件困难的事情
Webpack 也能够配置异步模块,当配置为异步模块的时候,在浏览器环境一样的是基于动态插入 <script>
的方式载入远程模块。在大多数状况下,模块的载入方式都是相似于 Node 的本地磁盘同步载入的方式
嫑忘记,Webpack 除了有模块化的能力,仍是一个在辅助完善开发工做流的工具,也就是说,Webpack 的模块化是在开发阶段的完成的,使用 Webpack 构筑的工做环境,在开发阶段虽然是独立的模块文件,可是在运行时,倒是一个合并好的文件
因此 Webpack 是一种在非运行时的模块化方案(基于 CommonJs),只有在配置了异步模块的时候对异步模块的加载才是运行时的(基于 AMD)
通用的问题在解决的过程当中总会造成规范,上文已经屡次提到 CommonJs、AMD、CMD,有必要花点篇幅来说一讲规范
Js 的模块化规范的萌发于将 Js 扩展到后端的想法,要使得 Js 具有相似于 Python、Ruby 和 Java 那样具有开发大型应用的基础能力,模块化规范是必不可少的。CommonJS 规范的提出,为Js 制定了一个美好愿景,但愿 Js 能在任何地方运行,包括但不限于:
CommonJS 对模块的定义并不复杂,主要分为模块引用、模块定义和模块标识
CommonJs 规范在 Node 中大放异彩而且相互促进,可是在浏览器端,鉴于网络的缘由,同步的方式加载模块显然不太实用,在通过一段争执以后,AMD 规范最终在前端场景中胜出(全称 Asynchronous Module Definition,即“异步模块定义”)
什么是 AMD,为何须要 AMD ?在前述模块化实现的推演过程当中,你应该可以找到答案
除此以外还有国内玉伯提出的 CMD 规范,AMD 和 CMD 的差别主要是,前者须要在定义之初声明全部的依赖,后者能够在任意时机动态引入模块。CMD 更接近于 CommonJS
两种规范都须要从远程网络中载入模块,不一样之处在于,前者是预加载,后者是延迟加载
若是有心,能够参照本文的推演,来实现一个 “yourRequireJs”,没有什么比重复造轮子更能让知识沉淀~~
菲麦前端 是一个让知识深刻原理的知识社群,咱们有 知识星球、公众号以及群,欢迎加微勾搭:facemagic2014