此文章原文地址在掘金,为了查看方便转载至个人博客中,也是为了敲一遍让自己印象更深一些。
原文地址: 《import、require、export、module.exports 混合使用详解》
感谢大佬的分享~
前言
自从使用了 es6 的模块系统后,各种地方愉快地使用 import
export default
,但也会在老项目中看到使用CommonJS 规范的 require
module.exports
。甚至有时候也会常常看到两者互用的场景。使用没有问题,但其中的关联与区别不得其解,使用起来也糊里糊涂。比如:
- 为何有的地方使用
require
去引用一个模块时需要加上default
?require('xx').default
- 经常在各大UI组件引用的文档上会看到说明
import { button } from 'xx-ui'
这样会引入所有组件内容,需要添加额外的 babel 配置,比如babel-plugin-component
? - 为什么可以使用 es6 的 import 去引用 commonjs 规范定义的模块,或者反过来也可以又是为什么?
- 我们在浏览一些 npm 下载下来的 UI 组件模块时(比如说 element-ui 的 lib 文件下),看到的都是 webpack 编译好的 js 文件,可以使用 import 或 require 再去引用。但是我们平时编译好的 js 是无法再被其他模块 import 的,这是为什么?
- babel 在模块化的场景中充当了什么角色?以及 webpack ?哪个启到了关键作用?
- 听说 es6 还有
tree-shaking
功能,怎么才能使用这个功能?
如果你对这些问题都了然于心,那么可以关掉本文了,如果有疑问,这篇文章就是为你准备的!
webpack与babel在模块化中的作用
webpack模块化的原理
webpack
本身维护了一套模块系统。这套模块系统兼容了所有前端历史进程下的模块规范,包括 AMD、CommonJS、ES6 等。本文主要针对 CommonJS、ES6 规范进行说明。模块化的实现其实就在最后编译的文件内
我编写了一个demo
更好的展示效果
1 | // webpack |
1 |
|
1 |
|
1 | (function(modules) { |
上面这段 js 就是使用 webpack 编译后的精简版代码,其中就包含了 webpack 的运行时代码,其中就是关于模块的实现
我们再继续精简代码,就会发现这是个自执行函数
1 | (function(modules){ |
自执行函数传入参数是个数组,此数组包含了所有的模块,包裹在函数中。
自执行函数提的逻辑就是处理模块的逻辑,关键在于__webpack_require__
函数,这个函数就是require
或import
的替代,我们可以看到函数体内先定义了这个函数,然后调用了他。
这里会传入一个moduleId
,此例中是 0 ,也就是我们的入口模块a.js
的内容
我们再看__webpack_require__
内执行了
1 | modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); |
即,从入参的modules
数组中取出第一个函数进行调用,并入参
module
module.exports
webpack_require
我们再看第一个函数(入口模块)的逻辑(精简)
1 | function (module, __webpack_exports__, __webpack_require__) { |
我们可以看到入口模块又调用了__webpack_require__(1)
去引用参数数组里的第二个参数
然后会将入参的__webpack_exports__
对象添加default
属性,并赋值。
这里我们就能看到模块化的实现原理。这里的__webpack_exports__
就是这个模块的module.exports
通过对象的引用传参,间接的给module.exports
添加属性
最后会将module.exports
return 出来,就完成了__webpack_require__
函数的使命。
比如在入口模块中又调用了__webpack_require__(1)
,就会得到这个模块返回的module.exports
。
但这个自动执行函数的地步,webpack
会将入口模块的输出也进行返回
1 | return __webpack_require__(0); |
目前这种编译后的 js ,将入口模块的输出(module.exports
)进行输出没有任何作用,只会作用于当前作用域。这个 js 并不能被其他模块继续以require
或import
的方式引用。
babel的作用
我们在平时的学习中也知道babel
主要用来将 ES6 的代码处理为 ES5 可识别的语法。当然也包括 ES6 的模块语法的转换。而剩下的则可以放心地交给webpack
,毕竟她的模块化方案已经可以很好的将 ES6 模块化转换成webpack
的模块化了。
其实两者的转换思路差不多,区别在于 webpack 的原生转换 可以多做一步静态分析,使用tree-shaking 技术(下面会讲到)
babel 能提前将 es6 的 import 等模块关键字转换成 commonjs 的规范。这样 webpack 就无需再做处理,直接使用 webpack 运行时定义的
__webpack_require__
处理。
这也就解释了 问题5
babel在模块化的场景中充当了什么角色?以及webpack?哪个起到了关键作用?
那么babel
是如何转换 es6 的模块语法呢?
导出模块
ES6 的导出模块写法有
1 | export default 123; |
babel
会将这些统统转换成CommonJS 的exports
1 | exports.default = 123; |
babel
转换 es6 的模块输出逻辑非常简单,即,将所有输出都付给exports
,并带上一个标志__esModule
表明这是个由 es6 转换来的 CommonJS 输出
babel
将模块的导出转换为 CommonJS 规范后,也会将引入import
叶转换为 CommonJS 规范。即采用require
去引用模块,再加以一定的处理,符合 es6 的使用意图
引入default
对于最常见的
1 | import a from './a.js' |
在 es6 中的import a from './a.js'
的本意是像去引入一个 es6 模块中的default
输出。
通过babel
转换后得到var a = require(./a.js)
得到的对象却是整个对象,肯定不是 es6 语句的本意,所以需要对a
做些改变。
我们在导出提到,default
输出会赋值给导出对象的default
属性。
1 | exports.default = 123; |
所以 babel 加了个 help _interopRequireDefault
函数
1 | function _interopRequireDefault(obj) { |
所以这里最后的a
变量就是require
值的default
属性。若原本就是 CommonJS 规范的模块,那么就是那个模块的导出对象
引入*通配符
我们使用import * as a from './a.js'
es6 语法的本意是想将 es6 模块的所有命名输出以及default
输出打包成一个对象赋值给a
变量
已知以 CommonJS 规范导出
1 | exports.default = 123; |
那么对于 es6 转换来的输出通过var a = require('./a.js')
导入这个对象就已经符合意图。
所以直接返回这个对象
1 | if (obj && obj.__esModule) { |
- 若本就是 CommonJS 规范的模块,导出时无
default
属性,需要添加一个default
属性,并把整个模块对象再次赋值给default
属性。
1 | function _interopRequireWildcard(obj) { |
import {a} from ‘./a.js’
直接转换成require ('./a.js').a
即可
总结
经过上面的转换分析,我们得知即使我们使用了 es6 的模块系统,如果借助babel
的转换,es6 的模块系统最终还是会转换成 CommonJS 的规范。
所以我们如果是使用babel
转换 es6 模块,混合使用 es6 模块和 CommonJS 的规范是没有问题的,因为最终都会转换成 CommonJS。
- 这里就解释了问题3
为什么可以使用 es6 的 import 去引用 commonjs 规范定义的模块,或者反过来也可以又是为什么?
babel5 && babel6
我们在上文babel
对导出模块的转换提到, es6 的exports default
都会被转换成exports.default
,即使这个模块只有一个输出
这也就解释了问题1:
为何有的地方使用
require
去引用一个模块时需要加上default
?require('xx').default
我们经常会使用 es6 的export default
来输出模块,而且这个输出是这个模块的唯一输出,我们会误以为这种写法输出的是模块的默认输出
1 | // a.js |
1 | // b.js 错误 |
在使用require
进行引用时,我们也会误以为引入的是a
文件的默认输出
结果这里需改成var foo = require('./a.js').default
这个场景写在webpack
代码分割逻辑时经常会遇到
1 | require.ensure([], (require) => { |
这是babel6
的变更,在babel5
的时候可不是这样的
在babel5
时代,大部分人再用require
去引用 es6 输出的default
,只是把default
输出看作是一个模块的默认输出,所以babel5
对这个逻辑做了hack
,如果一个 es6 模块只有一个default
输出,那么在转换成 CommonJS 的时候也一起赋值给module.exports
,即整个导出对象被赋值了default
所对应的值。
这样就不需要加default
,require('./a.js')
的值就是想要的default
值
但这样做是不符合 es6 的定义的。在 es6 的定义里,default
只是个名字,没有任何意义。
1 | export default = 123; |
这两者含义是一样的,分别为输出名为default
和a
的变量
还有一个很重要的问题,一旦a.js
文件里又添加了一个具名的输出,那么引入方就会出麻烦
1 | // a.js |
1 | // b.js |
所以babel6
去掉了这个hack
。这是个正确的决定。升级babel6
后产生的不兼容问题 可以通过引入babel-plugin-add-module-exports解决
webpack编译后的js,如何再被其他模块引用
通过webpack
模块化原理章节给出的webpack
配置编译后的 js 是无法被其他模块引用的。webpack
提供了output.libraryTarget
配置指定构建完的 js 的用途
默认var
如果制定了output.library = 'test'
入口模块返回的module.exports
暴露给全局var test = returned_module_exports
CommonJS
如果library: 'spon-ui'
入口模块返回的module.exports
赋值给exports['spon-ui']
CommonJS2
入口模块返回的module.exports
赋值给module.exports
所以element-ui
的构建方式采用 CommonJS2, 导出的组件的 js 最后都会赋值给module.exports
,供其他模块引用。
- 这里解释了问题4
我们在浏览一些 npm 下载下来的 UI 组件模块时(比如说 element-ui 的 lib 文件下),看到的都是 webpack 编译好的 js 文件,可以使用 import 或 require 再去引用。但是我们平时编译好的 js 是无法再被其他模块 import 的,这是为什么?
模块依赖的优化
按需加载的原理
我们在使用各大 UI 组件库时都会被介绍到为了避免引入全部文件,请使用babel-plugin-component
等babel
插件
1 | import { Button, Select } from 'element-ui' |
由前文可知import
会先转为 CommonJS,即
1 | var a = require('element-ui'); |
var a = require('element-ui')
这个过程就会将所有组件都引入进来了
所以babel-plugin-component
就做了一件事:将import {Button, Select} from 'element-ui'
转换成了
1 | import Button from 'element-ui/lib/button' |
即使转换成了 CommonJS 规范,也只是引入自己这个组件的 js,将引入量减少到最低。
所以我们会看到几乎所有的 UI 组件库的目录形式都是
1 | |-lib |
index.common.js
给import element from 'element-ui'
这种形式调用全部组建。
lib
下的各组件用于按需引用。
- 这里解决了问题2:
经常在各大UI组件引用的文档上会看到说明
import { button } from 'xx-ui'
这样会引入所有组件内容,需要添加额外的 babel 配置,比如babel-plugin-component
?
tree-shaking
webpack2
开始引入tree-shaking
技术,通过静态分析 es6 的语法,可以删除没有被使用的模块。他只对 es6 的模块有效,所以一旦babel
将 es6 的模块转换成 CommonJS,webpack2
将无法使用这项优化。所以要使用这项技术,我们只能使用webpack
的模块处理,再加上babel
的 es6 转换能力(需要关闭模块转换)。
最方便的使用方法为修改babel
的配置
1 | use: { |
修改最开始demo
1 | // webpack |
1 |
|
1 |
|
修改的点在于增加了babel
,并关闭其modules
功能。然后在c.js
中增加一个输出export {foo}
,但是a.js
中并不引用它。
最后在编译出的 js 中,c.js
模块如下:
1 |
|
foo
变量被标记为没有使用,在最后压缩时这段会被删除。
需要说明的是,即使在引入模块时使用了 es6,但引入的那个模块确实使用 CommonJS进行输出,这也无法使用tree-shaking
。
而第三方库大多是遵循 CommonJS 规范的,这也造成了引入第三方库无法减少不必要的引入。
所以对于未来来说,第三方库要同时发布 CommonJS 格式和 es6 格式的模块。es6 模块的入口由package.json
的字段module
指定。而 CommonJS 则还是在main
字段指定
- 这里解释了问题6
听说 es6 还有
tree-shaking
功能,怎么才能使用这个功能?