19年目标:消灭英语!我新开了一个公众号记录一个程序员学英语的历程javascript
有提高英语诉求的小伙伴能够关注公众号:csenglish 程序员学英语,天天花10分钟交做业,跟我一块儿学英语吧前端
自从使用了 es6 的模块系统后,各类地方愉快地使用 import
export default
,但也会在老项目中看到使用commonjs规范的 require
module.exports
。甚至有时候也会经常看到二者互用的场景。使用没有问题,但其中的关联与区别不得其解,使用起来也糊里糊涂。好比:java
require
去引用一个模块时须要加上 default
? require('xx').default
import { button } from 'xx-ui'
这样会引入全部组件内容,须要添加额外的 babel 配置,好比 babel-plugin-component
?tree-shaking
功能,怎么才能使用这个功能?若是你对这些问题都了然于心,那么能够关掉本文了,若是有疑问,这篇文章就是为你准备的!node
webpack 自己维护了一套模块系统,这套模块系统兼容了全部前端历史进程下的模块规范,包括 amd
commonjs
es6
等,本文主要针对 commonjs es6
规范进行说明。模块化的实现其实就在最后编译的文件内。webpack
我编写了一个 demo 更好的展现效果。程序员
// webpack const path = require('path'); module.exports = { entry: './a.js', output: { path: path.resolve(__dirname, 'dist'), filename: 'bundle.js', } };
// a.js import a from './c'; export default 'a.js'; console.log(a);
// c.js export default 333;
(function(modules) { function __webpack_require__(moduleId) { var module = { i: moduleId, l: false, exports: {} }; modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); return module.exports; } return __webpack_require__(0); })([ (function (module, __webpack_exports__, __webpack_require__) { // 引用 模块 1 "use strict"; Object.defineProperty(__webpack_exports__, "__esModule", { value: true }); /* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__c__ = __webpack_require__(1); /* harmony default export */ __webpack_exports__["default"] = ('a.js'); console.log(__WEBPACK_IMPORTED_MODULE_0__c__["a" /* default */]); }), (function (module, __webpack_exports__, __webpack_require__) { // 输出本模块的数据 "use strict"; /* harmony default export */ __webpack_exports__["a"] = (333); }) ]);
上面这段 js 就是使用 webpack 编译后的代码(通过精简),其中就包含了 webpack的运行时代码,其中就是关于模块的实现。es6
咱们再精简下代码,会发现这是个自执行函数。web
(function(modules) { })([]);
自执行函数的入参是个数组,这个数组包含了全部的模块,包裹在函数中。npm
自执行函数体里的逻辑就是处理模块的逻辑。关键在于 __webpack_require__
函数,这个函数就是 require
或者是 import
的替代,咱们能够看到在函数体内先定义了这个函数,而后调用了他。这里会传入一个 moduleId
,这个例子中是0,也就是咱们的入口模块 a.js
的内容。element-ui
咱们再看 __webpack_require__
内执行了
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); return module.exports;
即从入参的 modules 数组中取第一个函数进行调用,并入参
咱们再看第一个函数(即入口模块)的逻辑(精简):
function (module, __webpack_exports__, __webpack_require__) { /* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__c__ = __webpack_require__(1); /* harmony default export */ __webpack_exports__["default"] = ('a.js'); console.log(__WEBPACK_IMPORTED_MODULE_0__c__["a" /* default */]); }
咱们能够看到入口模块又调用了 __webpack_require__(1)
去引用入参数组里的第2个函数。
而后会将入参的 __webpack_exports__
对象添加 default
属性,并赋值。
这里咱们就能看到模块化的实现原理,这里的 __webpack_exports__
就是这个模块的 module.exports
经过对象的引用传参,间接的给 module.exports 添加属性。
最后会将 module.exports return 出来。就完成了 __webpack_require__
函数的使命。
好比在入口模块中又调用了 __webpack_require__(1)
,就会获得这个模块返回的 module.exports
。
但在这个自执行函数的底部,webpack
会将入口模块的输出也进行返回
return __webpack_require__(0);
目前这种编译后的js,将入口模块的输出(即 module.exports
) 进行输出没有任何做用,只会做用于当前做用域。这个js并不能被其余模块继续以 require
或 import
的方式引用。
按理说 webpack 的模块化方案已经很好的将es6 模块化转换成 webpack 的模块化,可是其他的 es6 语法还须要作兼容性处理。babel 专门用于处理 es6 转换 es5。固然这也包括 es6 的模块语法的转换。
其实二者的转换思路差很少,区别在于 webpack 的原生转换 能够多作一步静态分析,使用tree-shaking 技术(下面会讲到)
babel 能提早将 es6 的 import 等模块关键字转换成 commonjs 的规范。这样 webpack 就无需再作处理,直接使用 webpack 运行时定义的
__webpack_require__
处理。
这里就解释了 问题5。
babel 在模块化的场景中充当了什么角色?以及 webpack ?哪一个启到了关键做用?
那么 babel 是如何转换 es6 的模块语法呢?
es6 的导出模块写法有
export default 123; export const a = 123; const b = 3; const c = 4; export { b, c };
babel 会将这些通通转换成 commonjs 的 exports。
exports.default = 123; exports.a = 123; exports.b = 3; exports.c = 4; exports.__esModule = true;
babel 转换 es6 的模块输出逻辑很是简单,即将全部输出都赋值给 exports,并带上一个标志 __esModule
代表这是个由 es6 转换来的 commonjs 输出。
babel将模块的导出转换为commonjs规范后,也会将引入 import 也转换为 commonjs 规范。即采用 require 去引用模块,再加以必定的处理,符合es6的使用意图。
对于最多见的
import a from './a.js';
在es6中 import a from './a.js' 的本意是想去引入一个 es6 模块中的 default 输出。
经过babel转换后获得 var a = require(./a.js)
获得的对象倒是整个对象,确定不是 es6 语句的本意,因此须要对 a 作些改变。
咱们在导出提到,default 输出会赋值给导出对象的default属性。
exports.default = 123;
因此 babel 加了个 help _interopRequireDefault
函数。
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } var _a = require('assert'); var _a2 = _interopRequireDefault(_a); var a = _a2['default'];
因此这里最后的 a 变量就是 require 的值的 default 属性。若是原先就是commonjs规范的模块,那么就是那个模块的导出对象。
咱们使用 import * as a from './a.js'
es6语法的本意是想将 es6 模块的全部命名输出以及defalut输出打包成一个对象赋值给a变量。
已知以 commonjs 规范导出:
exports.default = 123; exports.a = 123; exports.b = 3; exports.__esModule = true;
那么对于 es6 转换来的输出经过 var a = require('./a.js')
导入这个对象就已经符合意图。
因此直接返回这个对象。
if (obj && obj.__esModule) { return obj; }
若是原本就是 commonjs 规范的模块,导出时没有default属性,须要添加一个default属性,并把整个模块对象再次赋值给default属性。
function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; // (A) if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } }
直接转换成 require('./a.js').a
便可。
通过上面的转换分析,咱们得知即便咱们使用了 es6 的模块系统,若是借助 babel 的转换,es6 的模块系统最终仍是会转换成 commonjs 的规范。因此咱们若是是使用 babel 转换 es6 模块,混合使用 es6 的模块和 commonjs 的规范是没有问题的,由于最终都会转换成 commonjs。
这里解释了问题3
为何可使用 es6 的 import 去引用 commonjs 规范定义的模块,或者反过来也能够又是为何?
咱们在上文 babel 对导出模块的转换提到,es6 的 export default
都会被转换成 exports.default
,即便这个模块只有这一个输出。
这也解释了问题1
为什么有的地方使用require
去引用一个模块时须要加上default
?require('xx').default
咱们常常会使用 es6 的 export default 来输出模块,并且这个输出是这个模块的惟一输出,咱们会误觉得这种写法输出的是模块的默认输出。
// a.js export default 123;
// b.js 错误 var foo = require('./a.js')
在使用 require
进行引用时,咱们也会误觉得引入的是a文件的默认输出。
结果这里须要改为 var foo = require('./a.js').default
这个场景在写 webpack 代码分割逻辑时常常会遇到。
require.ensure([], (require) => { callback(null, [ require('./src/pages/profitList').default, ]); });
这是 babel6 的变动,在 babel5 的时候可不是这样的。
http://babeljs.io/docs/plugin...
在 babel5 时代,大部分人在用 require 去引用 es6 输出的 default,只是把 default 输出看做是一个模块的默认输出,因此 babel5 对这个逻辑作了 hack,若是一个 es6 模块只有一个 default 输出,那么在转换成 commonjs 的时候也一块儿赋值给 module.exports
,即整个导出对象被赋值了 default 所对应的值。
这样就不须要加 default,require('./a.js')
的值就是想要的 default值。
但这样作是不符合 es6 的定义的,在es6 的定义里,default 只是个名字,没有任何意义。
export default = 123; export const a = 123;
这二者含义是同样的,分别为输出名为 default 和 a 的变量。
还有一个很重要的问题,一旦 a.js 文件里又添加了一个具名的输出,那么引入方就会出麻烦。
// a.js export default 123; export const a = 123; // 新增
// b.js var foo = require('./a.js'); // 由以前的 输出 123 // 变成 { default: 123, a: 123 }
因此 babel6 去掉了这个hack,这是个正确的决定,升级 babel6 后产生的不兼容问题 能够经过引入 babel-plugin-add-module-exports 解决。
经过 webpack 模块化原理章节给出的 webpack 配置编译后的 js 是没法被其余模块引用的,webpack 提供了 output.libraryTarget
配置指定构建完的 js 的用途。
若是指定了 output.library = 'test'
入口模块返回的 module.exports 暴露给全局 var test = returned_module_exports
若是library: 'spon-ui' 入口模块返回的 module.exports 赋值给 exports['spon-ui']
入口模块返回的 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 插件。
import { Button, Select } from 'element-ui'
由前文可知 import 会先转换为 commonjs, 即
var a = require('element-ui'); var Button = a.Button; var Select = a.Select;
var a = require('element-ui');
这个过程就会将全部组件都引入进来了。
因此 babel-plugin-component
就作了一件事,将 import { Button, Select } from 'element-ui'
转换成了
import Button from 'element-ui/lib/button' import Select from 'element-ui/lib/select'
即便转换成了 commonjs 规范,也只是引入本身这个组件的js,将引入量减小到最低。
因此咱们会看到几乎全部的UI组件库的目录形式都是
|-lib ||--component1 ||--component2 ||--component3 |-index.common.js
index.common.js
给 import element from 'element-ui'
这种形式调用所有组件。
lib 下的各组件用于按需引用。
这里解释了问题2
常常在各大UI组件引用的文档上会看到说明import { button } from 'xx-ui'
这样会引入全部组件内容,须要添加额外的 babel 配置,好比babel-plugin-component
?
webpack2 开始引入 tree-shaking 技术,经过静态分析 es6 的语法,能够删除没有被使用的模块。他只对 es6 的模块有效,因此一旦 babel 将 es6 的模块转换成 commonjs,webpack2 将没法使用这项优化。因此要使用这项技术,咱们只能使用 webpack 的模块处理,加上 babel 的es6转换能力(须要关闭模块转换)。
最方便的使用方法为修改babel的配置。
use: { loader: 'babel-loader', options: { presets: [['babel-preset-es2015', {modules: false}]], } }
修改最开始demo
// webpack const path = require('path'); module.exports = { entry: './a.js', output: { path: path.resolve(__dirname, 'dist'), filename: 'bundle.js', }, module: { rules: [ { test: /\.js$/, exclude: /(node_modules|bower_components)/, use: { loader: 'babel-loader', options: { presets: [['babel-preset-es2015', {modules: false}]], } } } ] } };
// a.js import a from './c'; export default 'a.js'; console.log(a);
// c.js export default 333; const foo = 123; export { foo };
修改的点在于增长了babel,并关闭其modules功能。而后在 c.js 中增长一个输出 export { foo }
,可是 a.js 中并不引用它。
最后在编译出的 js 中,c.js 模块以下:
"use strict"; /* unused harmony export foo */ /* harmony default export */ __webpack_exports__["a"] = (333); var foo = 123;
foo 变量被标记为没有使用,在最后压缩时这段会被删除。
须要说明的是,即便在 引入模块时使用了 es6 ,可是引入的那个模块倒是使用 commonjs 进行输出,这也没法使用tree-shaking。
而第三方库大可能是遵循 commonjs 规范的,这也形成了引入第三方库没法减小没必要要的引入。
因此对于将来来讲第三方库要同时发布 commonjs 格式和 es6 格式的模块。es6 模块的入口由 package.json 的字段 module 指定。而 commonjs 则仍是在 main 字段指定。
这里解释了问题6
据说 es6 还有
tree-shaking
功能,怎么才能使用这个功能?