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
功能,怎麼才能使用這個功能?