菜单快捷导航:vue
CommoneJS规定每一个文件是一个模块。将一个JavaScript文件直接经过script标签引入页面中,和封装成CommonJS模块最大的不一样在于:前者的顶层做用域是全局做用域,在进行变量及函数声明时会污染全局环境;然后者会造成一个属于模块自身的做用域,全部的变量及函数只有本身能访问,对外是不可见的。react
导出是一个模块向外暴露自身的惟一方式。在CommonJS中,经过module.exports能够导出模块中的内容,如:webpack
module.exports = { name: 'commonJS_exports.js', add: function(a, b){ return a + b; } }
为了书写方便,CommonJS也支持另外一种简化的导出方式:直接使用exports。效果和上面同样:git
exports.name = 'commonJS_exports.js'; exports.add = function(a, b){ return a + b; }
注意:导出时不要把module.exports 与 exports混用,下面举一个错误的示例:es6
exports.add = function(a, b){ return a + b; } module.exports = { name: 'commonJS_exports.js' }
上面的代码先经过exports导出add属性,而后将module.exports从新赋值为另一个对象。这会致使本来拥有的add属性的对象丢失了,最后导出的只有name。所以建议一个模块中的导出方式要么使用module.exports,要么使用exports,不要混着一块儿用。github
在实际使用中,为了提升可读性,应该将module.exports及exports语句放在模块的末尾。web
在CommonJS中使用require进行模块导入。如:npm
commonJS_exports.js导出代码:api
console.log('...hello, 我是commonJS_exports.js....start..') //一、第一种写法 module.exports = { name: 'commonJS_exports.js', add: function(a, b){ return a + b; } }
PageModule.vue页面中导入代码:浏览器
//一、测试CommonJS的exports和require var comObj = require('../api/module/commonJS_exports'); console.log('...name: ', comObj.name); try{ console.log('8 + 9 = ', comObj.add(8, 9)); }catch(e){ console.log(e); }
另外,若是在页面中对同一模块进行屡次导入,则该模块只会在第一次导入时执行,后面的导入不会执行,而是直接导出上次执行后获得的结果。示例以下:
var comObj = require('../api/module/commonJS_exports'); //再调用一次导入,发现导入模块不会再次执行,而是直接导出上次执行后获得的结果 require('../api/module/commonJS_exports'); console.log('...name: ', comObj.name); try{ console.log('8 + 9 = ', comObj.add(8, 9)); }catch(e){ console.log(e); }
咱们看到控制台打印结果以下,导入模块果真只执行了一次:
....test CommonJS 的导入...
...name: commonJS_exports.js
8 + 9 = 17
在module对象中有一个属性loaded用于记录该模块是否被加载过,它的默认值为false,当模块第一次被加载和执行事后会设置为true,后面再次加载时检查到module.loaded为true, 则不会再次执行模块代码。
require函数能够接收表达式,借助这个特性咱们能够动态地指定模块加载路径
const moduleNames = ['foo.js', 'bar.js']; moduleNames.forEach(name=>{ require('./' + name); })
2015年6月,发布的ES6才添加了模块这一特性。ES6 Module也是将每一个文件做为一个模块,每一个模块拥有自身的做用域,不一样的是导入、导出语句。import和export也做为保留关键字在ES6版本中加入了进来(CommonJS中的module并不属于关键字)。
在ES6 Module中使用export命令来导出模块。export有两种导出形式:
//第一种导出方式:命名导出 //1.1 命名导出第一种写法 export const name = 'es6_export.js'; export const add = function(a, b) { return a + b; } // //1.2 命名导出第二种写法 // const name = 'es6_export.js' // const add = function(a, b){ return a + b; } // export { name, add };
第一种写法是将变量的声明和导出写在一行;第二种写法则是先进行变量声明,而后再用同一个export语句导出。两种写法的效果是同样的。在使用命名导出时,还能够经过as关键字对变量重命名。如:
const name = 'es6_export.js' const add = function(a, b){ return a + b; } export { name, add as getSum }; //在导入时即为name和getSum
//第二种导出方式:默认导出 export default{ name: 'es6_export', add: function(a, b){ return a + b; } }
咱们能够将export default理解为对外输出了一个名为default的变量,所以不须要像“命名导出”同样进行变量声明,直接导出便可。
//导出字符串 export default 'this is es6_export.js file ' //导出class export default class {...} //导出匿名函数 export default function(){ ... }
ES6 Module中使用import语法导入模块。
const name = 'es6_export.js' const add = function(a, b){ return a + b; } export { name, add };
// import {name, add } from '../api/module/es6_export.js'; //命名导出第一种导入方式 // import * as esObj from '../api/module/es6_export.js'; //命名导出第二种别名总体导入方式 import {name, add as getSum } from '../api/module/es6_export.js'; //命名导出第三种别名导入方式 // //命名导出第一种导入方式 // console.log('name: ', name); // console.log('12 + 21: ', add(12, 21)); // //命名导出第二种别名导入方式 // console.log('name: ', esObj.name); // console.log('12 + 21: ', esObj.add(12, 21)); //命名导出第三种别名导入方式 console.log('name: ', name); console.log('12 + 21: ', getSum(12, 21));
加载带有命名导出的模块时,import后面要跟一对大括号来将导入的变量名包裹起来,而且这些变量须要与导出的变量名彻底一致。导入变量的效果至关于在当前做用域下声明了这些变量(name和add),而且不可对其进行更改,也就是全部导入的变量都是只读的。
另外和命名导出相似,咱们能够经过as关键字对到导入的变量重命名。在导入多个变量时,咱们还能够采用总体导入的方式,这种import * as <myModule>导入方式能够把全部导入的变量做为属性添加到<myModule>对象中,从而减小了对当前做用域的影响。
//第二种导出方式:默认导出 export default{ name: 'es6_export.js', add: function(a, b){ return a + b; } }
import esObj from '../api/module/es6_export.js'; //默认命名导出的导入测试 console.log('name: ', esObj.name); console.log('12 + 21: ', esObj.add(12, 21));
对于默认导出来讲,import后面直接跟变量名,而且这个名字能够自由指定(好比这里时esObj), 它指代了es6_export.js中默认导出的值。从原理上能够这样去理解:
import { default as esObj } from '../api/module/es6_export';
注意:默认导出自定义变量名和 命名导出总体起别名有点像,可是命名导出总体起别名必须是在import 后面是 * as 别名,而默认导出是import后面直接跟自定义变量名。
最后咱们看一下两种导入方式混合起来的例子:
import React, {Component} from 'react'
这里的React对应的是该模块的默认导出,而Component则是其命名导出中的一个变量。注意:这里的React必须写在大括号前面,而不能顺序颠倒,不然会引发提示语法错误。
在工程中,有时须要把某一个模块导入以后当即导出,好比专门用来集合全部页面或组件的入口文件。此时能够采用复合形式的写法:
export {name, add} from '../api/module/es6_export.js'
不过,上面的复合写法目前只支持“命名导出”方式暴露出来的变量。
默认导出则没有对应的复合形式,只能将导入和导出拆开写:
import esObj from '../api/module/es6_export.js' export default esObj
上面咱们分别介绍CommonJS和ES6 Module两种形式的模块定义,在实际开发中咱们常常会将两者混用,下面对比一下它们的特性:
CommonJS和ES6 Module最本质的区别在于前者对模块依赖的解决是“动态的”,然后者是“静态的”。这里“动态”的含义是, 模块依赖关系的创建发生在代码运行阶段;而“静态”则是模块依赖关系的创建发生在代码编译阶段。
咱们先看一个CommonJS的例子:
// commonJS_exports.js module.exports = { name: 'commonJS_exports' }
//PageModule.vue const name = require('../api/module/commonJS_exports').name;
当模块PageModule.vue加载模块commonJS_exports.js时,会执行commonJS_exports.js中的代码,并将其module.exports对象做为require函数的返回值返回。而且require的模块路径能够动态指定,支持传入一个表达式,咱们甚至能够经过if语句判断是否加载某个模块。所以,在CommonJS模块被执行前,并无办法肯定明确的依赖关系,模块的导入、导出发生在代码的运行阶段。
一样的例子,咱们再对比看下ES6 Module的写法:
//es6_export.js export const name = 'es6_export.js';
//PageModule.vue import { name } from '../api/module/es6_export.js'
ES6 Module的导入、导出语句都是声明式,它不支持导入的路径是一个表达式,而且导入、导出语句必须位于模块的顶层做用域(好比不能放在if语句中)。
所以咱们说,ES6 Module是一种静态的模块结构,在ES6代码的编译阶段就能够分析出模块的依赖关系。它相比于CommonJS来讲具有如下几点优点:
在导入一个模块时,对于CommonJS来讲获取的是一份导出值的拷贝;而在ES6 Module中则是值的动态映射,而且这个映射是只读的。例子:
//commonJS_exports.js var count = 0; module.exports = { count: count, add: function(a, b){ count+=1; return a + b; } }
//PageModule.vue var count = require('../api/module/commonJS_exports.js').count; var add = require('../api/module/commonJS_exports.js').add; console.log(count); //0 这里的count是对commonJS_exports.js中count值的拷贝 add(2, 3); console.log(count); //0 commonJS_exports.js中变量值的改变不会对这里的拷贝值形成影响 count += 1; console.log(count); //1 拷贝的值能够更改
PageModule.vue中的count是对commonJS_exports.js中count的一份值拷贝,所以在调用函数时,虽然更改了本来calculator.js中count的值,可是并不会对PageModule.vue中导入时建立的副本形成影响。另外一方面,在CommonJS中容许对导入的值进行更改。咱们能够在PageModule.vue更改count和add, 将其赋予新值。一样,因为是值的拷贝,这些操做不会影响calculator.js自己。
下面咱们使用ES6 Module将上面的例子进行改写:
//es6_export.js let count = 0; const add = function(a, b){ count += 1; return a + b; } export { count, add }
import {name, add, count } from '../api/module/es6_export'; console.log(count); //0, 对es6_export.js中的count值的映射 add(2, 3); console.log(count); //1 实时反映es6_export.js中count值的变化 // count += 1; //不可更改,会抛出ReferenceError: count is not defined
上面的例子展现了ES6 Module中导入的变量实际上是对原有值的动态映射。PageModule.vue中的count是对calculator.js中的count值的实时反映,当咱们经过调用add函数更改了calculator.js中的count值时,PageModule.vue中count的值也随之变化。
咱们不能够对ES6 Module导入的变量进行更改,能够将这种映射关系理解为一面镜子,从镜子里咱们能够实时观察到原有的事物,可是并不能够操做镜子中的影像。
循环依赖是指模块A依赖于B, 同时模块B依赖于模块A。通常来讲工程中应该尽可能避免循环依赖的产生,由于从软件设计的角度来讲,单向的依赖关系更加清晰,而循环依赖则会带来必定的复杂度。而在实际开发中,循环依赖有时会在咱们不经意间产生,由于当工程的复杂度上升到足够规模时,就容易出现隐藏的循环依赖关系。
简单来讲,A和B两个模块之间是否存在直接的循环依赖关系是很容易被发现的。但实际状况每每是A依赖于B,B依赖于C,C依赖于D,最后绕了一圈,D又依赖于A。当中间模块太多时就很难发现A和B之间存在着隐式的循环依赖。
所以,如何处理循环依赖是开发者必需要面对的问题。
//bar.js const foo = require('./foo.js'); console.log('value of foo: ', foo); module.exports = 'This is bar.js';
//foo.js const bar = require('./bar.js'); console.log('value of bar: ', bar); module.exports = 'This is foo.js';
//PageModule.vue require('../api/module/foo.js'); /* 打印结果: value of foo: {} value of bar: This is bar.js * */
为何foo的值是一个空对象呢?让咱们从头梳理一下代码的实际执行顺寻:
由上面能够看出,尽管循环依赖的模块均被执行了,但模块导入的值并非咱们想要的。咱们再从Webpack的实现角度来看,将上面例子打包后,bundle中有这样一段代码很是重要:
//The require function function __webpack_require__(moduleId){ if(installedModules[moduleId]){ return installedModules[moduleId].exports; } //Create a new module (and put it into the cache) var module = installedModules[moduleId] = { i: moduleId, l: false, exports: {} } //... }
当PageModule.vue引用了foo.js以后,至关于执行了这个__webpack_require__函数,初始化了一个module对象并放入installedModules中。当bar.js再次引用foo.js时,又执行了该函数,但此次是直接从installedModules里面取值,此时它的module.exports是一个空对象。这就解释了上面再第3步看到的现象。
//bar_es6.js import foo from './foo_es6.js'; console.log('value of foo: ', foo); export default 'This is bar_es6.js';
//foo_es6.js import bar from './bar_es6.js'; console.log('value of bar: ', bar); export default 'This is foo_es6.js';
//PageModule.vue import foo_es6 from '../api/module/foo_es6.js';
/* 打印结果: value of foo: undefined value of bar: This is bar_es6.js * */
很遗憾,在bar_es6.js中一样没法获得foo_es6.js正确的导出值,只不过和CommonJS默认导出一个空对象不一样,这里获取到的是undefined。
上面咱们谈到,在导入一个模块时,CommonJS获取到的时值的拷贝,ES6 Module则是动态映射,
//bar_es6_2.js import foo from './foo_es6_2.js'; let invoked = false; function bar(invoker){ if (!invoked){ invoked = true; console.log(invoker + ' invokes bar_es6_2.js'); foo('bar_es6_2.js'); } } export default bar;
//foo_es6_2.js import bar from './bar_es6_2.js' function foo(invoker){ console.log(invoker + ' invokes foo_es6_2.js'); bar('foo_es6_2.js'); } export default foo;
import foo_es6_2 from '../api/module/foo_es6_2.js' foo_es6_2('PageModule.vue'); /* 打印结果: PageModule.vue invokes foo_es6_2.js foo_es6_2.js invokes bar_es6_2.js bar_es6_2.js invokes foo_es6_2.js * */
能够看到,foo_es6_2.js和bar_es6_2.js这一对循环依赖的模块均获取到了正确的导出值。下面咱们分析一下代码的执行过程:
由上面的例子能够看出,ES6 Module的特性使其能够更好的支持循环依赖,只是须要由开发者来保证导入的值被使用时已经设置好正确的导出值。
面对工程中成百上千个模块,webpack究竟时如何将它们有序的组织在一块儿,并按照咱们预想的顺序运行在浏览器上的呢?下面咱们将从原理上进行探究。
仍是用前面的例子:
//commonJS_exports.js module.exports = { add: function(a, b){ return a + b; } }
//PageModule.vue const comObj = require('../api/module/commonJS_exports'); const sum = comObj.add(2, 3); console.log('sum: ', sum);
上面的代码通过Webpack打包后将会成为以下的形式(为了易读性这里只展现代码的答题结构):
//当即执行匿名函数 (function(modules){ //模块缓存 var installedModules = {}; //实现require function __webpack_require__(moduleId){ //... } //执行入口模块的加载 return __webpack_require__(__webpack__require__.s == 0); })({ //modules: 以key-value的形式存储全部被打包的模块 0: function(module, exports, __webpack_require__){ //打包入口 module.exports = __webpack_require__("3qiv"); }, "3qiv": function(module, exports, __webpack_require__){ //PageModule.vue 内容 }, jkzz: function(module, exports){ //commonJS_exports.js 内容 } })
这是一个最简单的Webpack打包结果(bundle),但已经能够清晰地展现出它是如何将具备依赖关系的模块串联在一块儿的。上面的bundle分为如下几个部分:
接下来咱们看看一个bundle是如何在浏览器中执行的:
不难看出,第3步和第4步时一个递归的过程,Webpack为每一个模块创造了一个能够导出和导入模块的环境,但本质上并无修改代码的执行逻辑,所以代码执行的顺序于模块加载的顺序时彻底一致的,这就时Webpack模块打包的奥秘。
本文测试截图:
下载测试DEMO:https://github.com/xiaotanit/tan_vue,若是你以为能够,请顺手点个星^_^。
npm install , npm run serve 后,在浏览器输入测试地址看效果:
http://localhost:8080/pageModule //端口可能会变化
参考书籍:《Webpack实战:入门、进阶与调优》--- 居玉皓