探索webpack模块及webpack3新特性
本文从简单的例子入手,从打包文件去分析以下三个问题:webpack打包文件是怎样的?如何做到兼容各大模块化方案的?webpack3带来的新特性又是什么?
一个简单的例子
webpack配置
//webpack.config.js module.exports={ entry:'./src/index.js', output:{ filename:'bundle.js', path:path.resolve(__dirname,'dist') }, };
简单的js文件
//src/index.js console.log('helloworld');
webpack打包后的代码
一看你就会想,我就一行代码,你给我打包那么多???(黑人问号)
//dist/bundle.js /******/(function(modules){//webpackBootstrap /******///Themodulecache /******/varinstalledModules={}; /******/ /******///Therequirefunction /******/function__webpack_require__(moduleId){ /******/ /******///Checkifmoduleisincache /******/if(installedModules[moduleId]){ /******/returninstalledModules[moduleId].exports; /******/} /******///Createanewmodule(andputitintothecache) /******/varmodule=installedModules[moduleId]={ /******/i:moduleId, /******/l:false, /******/exports:{} /******/}; /******/ /******///Executethemodulefunction /******/modules[moduleId].call(module.exports,module,module.exports,__webpack_require__); /******/ /******///Flagthemoduleasloaded /******/module.l=true; /******/ /******///Returntheexportsofthemodule /******/returnmodule.exports; /******/} /******/ /******/ /******///exposethemodulesobject(__webpack_modules__) /******/__webpack_require__.m=modules; /******/ /******///exposethemodulecache /******/__webpack_require__.c=installedModules; /******/ /******///definegetterfunctionforharmonyexports /******/__webpack_require__.d=function(exports,name,getter){ /******/if(!__webpack_require__.o(exports,name)){ /******/Object.defineProperty(exports,name,{ /******/configurable:false, /******/enumerable:true, /******/get:getter /******/}); /******/} /******/}; /******/ /******///getDefaultExportfunctionforcompatibilitywithnon-harmonymodules /******/__webpack_require__.n=function(module){ /******/vargetter=module&&module.__esModule? /******/functiongetDefault(){returnmodule['default'];}: /******/functiongetModuleExports(){returnmodule;}; /******/__webpack_require__.d(getter,'a',getter); /******/returngetter; /******/}; /******/ /******///Object.prototype.hasOwnProperty.call /******/__webpack_require__.o=function(object,property){returnObject.prototype.hasOwnProperty.call(object,property);}; /******/ /******///__webpack_public_path__ /******/__webpack_require__.p=""; /******/ /******///Loadentrymoduleandreturnexports /******/return__webpack_require__(__webpack_require__.s=0); /******/}) /************************************************************************/ /******/([ /*0*/ /***/(function(module,exports){ console.log('helloworld'); /***/}) /******/]);
我们来分析一下这部分代码,先精简一下,其实整体就是一个自执行函数,然后传入一个模块数组
(function(modules){ //... })([function(module,exports){ //.. }])
好了,传入模块数组做了什么(其实注释都很明显了,我只是大概翻译一下)
/******/(function(modules){//webpackBootstrap /******///Themodulecache缓存已经load过的模块 /******/varinstalledModules={}; /******/ /******///Therequirefunction引用的函数 /******/function__webpack_require__(moduleId){ /******/ /******///Checkifmoduleisincache假如在缓存里就直接返回 /******/if(installedModules[moduleId]){ /******/returninstalledModules[moduleId].exports; /******/} /******///Createanewmodule(andputitintothecache)构造一个模块并放入缓存 /******/varmodule=installedModules[moduleId]={ /******/i:moduleId,//模块id /******/l:false,//是否已经加载完毕 /******/exports:{}//对外暴露的内容 /******/}; /******/ /******///Executethemodulefunction传入模块参数,并执行模块 /******/modules[moduleId].call(module.exports,module,module.exports,__webpack_require__); /******/ /******///Flagthemoduleasloaded标记模块已经加载完毕 /******/module.l=true; /******/ /******///Returntheexportsofthemodule返回模块暴露的内容 /******/returnmodule.exports; /******/} /******/ /******/ /******///exposethemodulesobject(__webpack_modules__)暴露模块数组 /******/__webpack_require__.m=modules; /******/ /******///exposethemodulecache暴露缓存数组 /******/__webpack_require__.c=installedModules; /******/ /******///definegetterfunctionforharmonyexports为ES6exports定义getter /******/__webpack_require__.d=function(exports,name,getter){ /******/if(!__webpack_require__.o(exports,name)){//假如exports本身不含有name这个属性 /******/Object.defineProperty(exports,name,{ /******/configurable:false, /******/enumerable:true, /******/get:getter /******/}); /******/} /******/}; /******/ /******///getDefaultExportfunctionforcompatibilitywithnon-harmonymodules解决ESmodule和Commonjsmodule的冲突,ES则返回module['default'] /******/__webpack_require__.n=function(module){ /******/vargetter=module&&module.__esModule? /******/functiongetDefault(){returnmodule['default'];}: /******/functiongetModuleExports(){returnmodule;}; /******/__webpack_require__.d(getter,'a',getter); /******/returngetter; /******/}; /******/ /******///Object.prototype.hasOwnProperty.call /******/__webpack_require__.o=function(object,property){returnObject.prototype.hasOwnProperty.call(object,property);}; /******/ /******///__webpack_public_path__webpack配置下的公共路径 /******/__webpack_require__.p=""; /******/ /******///Loadentrymoduleandreturnexports最后执行entry模块并且返回它的暴露内容 /******/return__webpack_require__(__webpack_require__.s=0); /******/}) /************************************************************************/ /******/([ /*0*/ /***/(function(module,exports){ console.log('helloworld'); /***/}) /******/]);
整体流程是怎样的呢
- 传入module数组
- 调用__webpack_require__(__webpack_require__.s=0)
构造module对象,放入缓存
调用module,传入相应参数modules[moduleId].call(module.exports,module,module.exports,__webpack_require__);(这里exports会被函数内部的东西修改)
标记module对象已经加载完毕
返回模块暴露的内容(注意到上面函数传入了module.exports,可以对引用进行修改)
- 模块函数中传入module,module.exports,__webpack_require__
- 执行过程中通过对上面三者的引用修改,完成变量暴露和引用
webpack模块机制是怎样的
我们可以去官网看下webpack模块
doc.webpack-china.org/concepts/mo…
webpack模块能够以各种方式表达它们的依赖关系,几个例子如下:
- ES2015import语句
- CommonJSrequire()语句
- AMDdefine和require语句
- css/sass/less文件中的@import语句。
- 样式(url(...))或HTML文件()中的图片链接(imageurl)
强大的webpack模块可以兼容各种模块化方案,并且无侵入性(non-opinionated)
我们可以再编写例子一探究竟
CommonJS
修改src/index.js
varcj=require('./cj.js'); console.log('helloworld'); cj();
新增src/cj.js,保持前面例子其他不变
//src/cj.js functiona(){ console.log("CommonJS"); } module.exports=a;
再次运行webpack
/******/(function(modules){//webpackBootstrap //...省略代码 /******/}) /************************************************************************/ /******/([ /*0*/ /***/(function(module,exports,__webpack_require__){ letcj=__webpack_require__(1); console.log('helloworld'); cj(); /***/}), /*1*/ /***/(function(module,exports){ functiona(){ console.log("CommonJS"); } module.exports=a; /***/}) /******/]);
我们可以看到模块数组多了个引入的文件,然后index.js模块函数多了个参数__webpack_require__,去引用文件(__webpack_require__在上一节有介绍),整体上就是依赖的模块修改了module.exports,然后主模块执行依赖模块,获取exports即可
ES2015import
新增src/es.js
//src/es.js exportdefaultfunctionb(){ console.log('ESModules'); }
修改src/index.js
//src/index.js importesfrom'./es.js'; console.log('helloworld'); es(); webpack.config.js不变,执行webpack /******/(function(modules){//webpackBootstrap //...省略代码 /******/}) /************************************************************************/ /******/([ /*0*/ /***/(function(module,__webpack_exports__,__webpack_require__){ "usestrict"; Object.defineProperty(__webpack_exports__,"__esModule",{value:true}); /*harmonyimport*/var__WEBPACK_IMPORTED_MODULE_0__es_js__=__webpack_require__(1); console.log('helloworld'); Object(__WEBPACK_IMPORTED_MODULE_0__es_js__["a"/*default*/])(); /***/}), /*1*/ /***/(function(module,__webpack_exports__,__webpack_require__){ "usestrict"; /*harmonyexport(immutable)*/__webpack_exports__["a"]=b; functionb(){ console.log('ESModules'); } /***/}) /******/]);
我们可以看到它们都变成了严格模式,webpack自动采用的
表现其实跟CommonJS相似,也是传入export然后修改,在主模块再require进来,
我们可以看到这个
Object.defineProperty(__webpack_exports__,"__esModule",{value:true});
这个干嘛用的?其实就是标记当前的exports是es模块,还记得之前的__webpack_require__.n吗,我们再拿出来看看
/******///getDefaultExportfunctionforcompatibilitywithnon-harmonymodules解决ESmodule和Commonjsmodule的冲突,ES则返回module['default'] /******/__webpack_require__.n=function(module){ /******/vargetter=module&&module.__esModule? /******/functiongetDefault(){returnmodule['default'];}: /******/functiongetModuleExports(){returnmodule;}; /******/__webpack_require__.d(getter,'a',getter); /******/returngetter; /******/};
为了避免跟非ESModules冲突?冲突在哪里呢?
其实这部分如果你看到babel转换ESModules源码就知道了,为了兼容模块,会把ESModules直接挂在exports.default上,然后加上__esModule属性,引入的时候判断一次是否是转换模块,是则引入module['default'],不是则引入module
我们再多引入几个ESModules看看效果
//src/es.js exportfunctiones(){ console.log('ESModules'); } exportfunctionesTwo(){ console.log('ESModulesTwo'); } exportfunctionesThree(){ console.log('ESModulesThree'); } exportfunctionesFour(){ console.log('ESModulesFour'); }
我们多引入esTwo和esFour,但是不使用esFour
//src/index.js import{es,esTwo,esFour}from'./es.js'; console.log('helloworld'); es(); esTwo();
得出
/******/(function(modules){//webpackBootstrap //... /******/}) /************************************************************************/ /******/([ /*0*/ /***/(function(module,__webpack_exports__,__webpack_require__){ "usestrict"; Object.defineProperty(__webpack_exports__,"__esModule",{value:true}); /*harmonyimport*/var__WEBPACK_IMPORTED_MODULE_0__es_js__=__webpack_require__(1); console.log('helloworld'); Object(__WEBPACK_IMPORTED_MODULE_0__es_js__["a"/*es*/])(); Object(__WEBPACK_IMPORTED_MODULE_0__es_js__["b"/*esTwo*/])(); /***/}), /*1*/ /***/(function(module,__webpack_exports__,__webpack_require__){ "usestrict"; /*harmonyexport(immutable)*/__webpack_exports__["a"]=es; /*harmonyexport(immutable)*/__webpack_exports__["b"]=esTwo; /*unusedharmonyexportesThree*/ /*unusedharmonyexportesFour*/ functiones(){ console.log('ESModules'); } functionesTwo(){ console.log('ESModulesTwo'); } functionesThree(){ console.log('ESModulesThree'); } functionesFour(){ console.log('ESModulesFour'); } /***/}) /******/]);
嗯嗯其实跟前面是一样的,举出这个例子重点在哪里呢,有没有注意到注释中
/*unusedharmonyexportesThree*/ /*unusedharmonyexportesFour*/
esThree是我们没有引入的模块,esFour是我们引用但是没有使用的模块,webpack均对它们做了unused的标记,其实这个如果你使用了webpack插件uglify,通过标记,就会把esThree和esFour这两个未使用的代码消除(其实它就是tree-shaking)
AMD
我们再来看看webpack怎么支持AMD
新增src/amd.js
//src/amd.js define([ ],function(){ return{ amd:function(){ console.log('AMD'); } }; });
修改index.js
//src/index.js define([ './amd.js' ],function(amdModule){ amdModule.amd(); });
得到
/******/(function(modules){//webpackBootstrap //...省略代码 /******/}) /************************************************************************/ /******/([ /*0*/ /***/(function(module,exports,__webpack_require__){ var__WEBPACK_AMD_DEFINE_ARRAY__,__WEBPACK_AMD_DEFINE_RESULT__;!(__WEBPACK_AMD_DEFINE_ARRAY__=[ __webpack_require__(1) ],__WEBPACK_AMD_DEFINE_RESULT__=function(amdModule){ amdModule.amd(); }.apply(exports,__WEBPACK_AMD_DEFINE_ARRAY__), __WEBPACK_AMD_DEFINE_RESULT__!==undefined&&(module.exports=__WEBPACK_AMD_DEFINE_RESULT__)); /***/}), /*1*/ /***/(function(module,exports,__webpack_require__){ var__WEBPACK_AMD_DEFINE_ARRAY__,__WEBPACK_AMD_DEFINE_RESULT__;!(__WEBPACK_AMD_DEFINE_ARRAY__=[ ],__WEBPACK_AMD_DEFINE_RESULT__=function(){ return{ amd:function(){ console.log('AMD'); } }; }.apply(exports,__WEBPACK_AMD_DEFINE_ARRAY__), __WEBPACK_AMD_DEFINE_RESULT__!==undefined&&(module.exports=__WEBPACK_AMD_DEFINE_RESULT__)); /***/}) /******/]);
先看amd.js整理一下代码
function(module,exports,__webpack_require__){ var__WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__; !( __WEBPACK_AMD_DEFINE_ARRAY__=[], __WEBPACK_AMD_DEFINE_RESULT__=function(){ return{ amd:function(){ console.log('AMD'); } }; }.apply(exports,__WEBPACK_AMD_DEFINE_ARRAY__), __WEBPACK_AMD_DEFINE_RESULT__!==undefined&& (module.exports=__WEBPACK_AMD_DEFINE_RESULT__) ); })
简单来讲收集defineArray然后置入返回函数,根据参数获取依赖
apply对数组拆解成一个一个参数
再看index.js模块部分
function(module,exports,__webpack_require__){ var__WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__; !( __WEBPACK_AMD_DEFINE_ARRAY__=[__webpack_require__(1)], __WEBPACK_AMD_DEFINE_RESULT__=function(amdModule){ amdModule.amd(); }.apply(exports,__WEBPACK_AMD_DEFINE_ARRAY__), __WEBPACK_AMD_DEFINE_RESULT__!==undefined&& (module.exports=__WEBPACK_AMD_DEFINE_RESULT__) ); }
其实就是引入了amd.js暴露的{amd:[Function:amd]}
css/image?
css和image也可以成为webpack的模块,这是令人震惊的,这就不能通过普通的hackcommonjs或者函数调用简单去调用了,这就是anythingtoJS,它就需要借助webpackloader去实现了
像css就是转换成一段js代码,通过处理,调用时就是可以用js将这段css插入到style中,image也类似,这部分就不详细阐述了,有兴趣的读者可以深入去研究
webpack3新特性
我们可以再顺便看下webpack3新特性的表现
具体可以看这里medium.com/webpack/web…
ScopeHoisting
我们可以发现模块数组是一个一个独立的函数然后闭包引用webpack主函数的相应内容,每个模块都是独立的,然后带来的结果是在浏览器中执行速度变慢,然后webpack3学习了ClosureCompiler和RollupJS这两个工具,连接所有闭包到一个闭包里,放入一个函数,让执行速度更快,并且整体代码体积也会有所缩小
我们可以实际看一下效果(要注意的是这个特性只支持ESModules,是不支持CommonJs和AMD的)
使用上面的例子,配置webpack.config.js,增加newwebpack.optimize.ModuleConcatenationPlugin()
constpath=require('path'); constwebpack=require('webpack'); module.exports={ entry:'./src/index.js', output:{ filename:'bundle.js', path:path.resolve(__dirname,'dist') }, module:{ }, plugins:[ newwebpack.optimize.ModuleConcatenationPlugin(), ] };
打包
/******/(function(modules){//webpackBootstrap //...省略代码 /******/}) /************************************************************************/ /******/([ /*0*/ /***/(function(module,__webpack_exports__,__webpack_require__){ "usestrict"; Object.defineProperty(__webpack_exports__,"__esModule",{value:true}); //CONCATENATEDMODULE:./src/es.js functiones(){ console.log('ESModules'); } functionesTwo(){ console.log('ESModulesTwo'); } functionesThree(){ console.log('ESModulesThree'); } functionesFour(){ console.log('ESModulesFour'); } //CONCATENATEDMODULE:./src/index.js //src/index.js console.log('helloworld'); es(); /***/}) /******/]);
我们可以惊喜的发现没有什么require了,它们拼接成了一个函数,good!