Js模块打包exportsrequireimport的⽤法和区别
⽬录
1、Commonjs之 exports和require⽤法
1.1 CommonJS导出之ports
1.2 CommonJS导⼊之require
2、ES6 Module 之 export 和 import ⽤法
2.1 ES6 Module导出之export
2.2 ES6 Module导出之import
3、CommonJS和ES6 Module的区别
3.1 动态和静态
3.2 值拷贝和动态映射
3.3 循环依赖
4、模块打包原理
1、Commonjs之 exports和require⽤法
Commonejs规定每个⽂件是⼀个模块。将⼀个JavaScript⽂件直接通过script标签引⼊页⾯中,和封装成CommonJS模块最⼤的不同在于:前者的顶层作⽤域是全局作⽤域,在进⾏变量及函数声明时会污染全局环境;⽽后者会形成⼀个属于模块⾃⾝的作⽤域,所有的变量及函数只有⾃⼰能访问,对外是不可见的。
1.1 CommonJS导出之ports
导出是⼀个模块向外暴露⾃⾝的唯⼀⽅式。在CommonJS中,通过ports可以导出模块中的内容,如:
name: 'commonJS_exports.js',
add: function(a, b){
return a + b;
}
}
为了书写⽅便,CommonJS也⽀持另⼀种简化的导出⽅式:直接使⽤exports。效果和上⾯⼀样:
exports.name = 'commonJS_exports.js';
exports.add = function(a, b){
return a + b;
}
注意:导出时不要把ports 与 exports混⽤,下⾯举⼀个错误的⽰例:
exports.add = function(a, b){
return a + b;
}
name: 'commonJS_exports.js'
}
上⾯的代码先通过exports导出add属性,然后将ports重新赋值为另外⼀个对象。这会导致原本拥有的add属性的对象丢失了,最后导出的只有name。因此建议⼀个模块中的导出⽅式要么使⽤ports,要么使⽤exports,不要混着⼀起⽤。
在实际使⽤中,为了提⾼可读性,应该将ports及exports语句放在模块的末尾。
1.2 CommonJS导⼊之require
在CommonJS中使⽤require进⾏模块导⼊。commonJS_exports.js导出代码:
console.log('...hello, 我是commonJS_start..')
//1、第⼀种写法
name: 'commonJS_exports.js',
add: function(a, b){
return a + b;
}
}
PageModule.vue页⾯中导⼊代码:
//1、测试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);
})
2、ES6 Module 之 export 和 import ⽤法
2015年6⽉,发布的ES6才添加了模块这⼀特性。ES6 Module也是将每个⽂件作为⼀个模块,每个模块拥有⾃⾝的作⽤域,不同的是导⼊、导出语句。import和export也作为保留关键字在ES6版本中加⼊了进来(CommonJS中的module并不属于关键字)。
2.1 ES6 Module导出之export
在ES6 Module中使⽤export命令来导出模块。export有两种导出形式:
命名导出
默认导出
2.1.1 命名导出有两种不同的写法:
//第⼀种导出⽅式:命名导出
//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
2.1.2 与命名导出不同,模块的默认导出只能有⼀个。如:
//第⼆种导出⽅式:默认导出
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(){ ... }
2.2 ES6 Module导出之import
ES6 Module中使⽤import语法导⼊模块。
2.2.1 我们看下对于命名导出模块如何导⼊
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>对象中,从⽽减少了对当前作⽤域的影响。
2.2.2 我们再看下对默认导出的导⼊
//第⼆种导出⽅式:默认导出
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必须写在⼤括号前⾯,⽽不能顺序颠倒,否则会引起提⽰语法错误。
2.2.3 复合写法。
在⼯程中,有时需要把某⼀个模块导⼊之后⽴即导出,⽐如专门⽤来集合所有页⾯或组件的⼊⼝⽂件。此时可以采⽤复合形式的写法:
export {name, add} from '../api/module/es6_export.js'
不过,上⾯的复合写法⽬前只⽀持“命名导出”⽅式暴露出来的变量。默认导出则没有对应的复合形式,只能将导⼊和导出拆开写:
import esObj from  '../api/module/es6_export.js'
export default esObj
3、CommonJS和ES6 Module的区别
上⾯我们分别介绍CommonJS和ES6 Module两种形式的模块定义,在实际开发中我们经常会将⼆者混⽤,下⾯对⽐⼀下它们的特性:
3.1 动态和静态
CommonJS和ES6 Module最本质的区别在于前者对模块依赖的解决是“动态的”,⽽后者是“静态的”。这⾥“动态”的含义是, 模块依赖关系的建⽴发⽣在代码运⾏阶段;⽽“静态”则是模块依赖关系的建⽴发⽣在代码编译阶段。
我们先看⼀个CommonJS的例⼦:
// commonJS_exports.js
//PageModule.vue
const name = require('../api/module/commonJS_exports').name;
当模块PageModule.vue加载模块commonJS_exports.js时,会执⾏commonJS_exports.js中的代码,并将其ports对象作为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来说具备以下⼏点优势:
冗余代码检测和排除。我们可以⽤静态分析⼯具分析⼯具检测出哪些模块没有被调⽤过。⽐如,在引⼊⼯具类库时,⼯程中往往只⽤到了其中⼀部分组件或接⼝,但有可能会将其代码完整地加载进来。未被调⽤到的模块代码永远不会被执⾏,也就成为了冗余代码。通过静态分析可以在打包时去掉这些未曾使⽤过的模块,以减少打包资源体积。
模块变量类型检查。JavaScript属于动态类型语⾔,不会在代码执⾏前检查类型错误(⽐如对⼀个字符串类型的值进⾏函数调⽤)。ES6 Module的静态模块结构有助于确保模块之间传递的值或接⼝类型是正确的。
编译器优化。在CommonJS等动态模块系统中,⽆论采⽤哪种⽅式,本质上导⼊的都是⼀个对象,⽽ES6 Module⽀持直接导⼊变量,减少了引⽤层级,程序效率更⾼。
3.2 值拷贝和动态映射
在导⼊⼀个模块时,对于CommonJS来说获取的是⼀份导出值的拷贝;⽽在ES6 Module中则是值的动态映射,并且这个映射是只读的。例⼦:
/
/commonJS_exports.js
var count = 0;
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的值也随之变化。
import语句我们不可以对ES6 Module导⼊的变量进⾏更改,可以将这种映射关系理解为⼀⾯镜⼦,从镜⼦⾥我们可以实时观察到原有的事物,但是并不可以操作镜⼦中的影像。
3.3 循环依赖
循环依赖是指模块A依赖于B, 同时模块B依赖于模块A。⼀般来说⼯程中应该尽量避免循环依赖的产⽣,因为从软件设计的⾓度来说,单向的依赖关系更加清晰,⽽循环依赖则会带来⼀定的复杂度。⽽在实际开发中,循环依赖有时会在我们不经意间产⽣,因为当⼯程的复杂度上升到⾜够规模时,就容易出现隐藏的循环依赖关系。
简单来说,A和B两个模块之间是否存在直接的循环依赖关系是很容易被发现的。但实际情况往往是A依赖于B,B依赖于C,C 依赖于D,最后绕了⼀圈,D⼜依赖于A。当中间模块太多时就很难发现A和B之间存在着隐式的循环依赖。
因此,如何处理循环依赖是开发者必须要⾯对的问题。
3.3.1 我们⾸先看下在CommonJS中循环依赖的问题⽰例:
//bar.js
const foo = require('./foo.js');
console.log('value of foo: ', foo);
//foo.js
const bar = require('./bar.js');
console.log('value of bar: ', bar);
//PageModule.vue

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。