Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

探索webpack 机制 #30

Open
libin1991 opened this issue Jan 28, 2018 · 0 comments
Open

探索webpack 机制 #30

libin1991 opened this issue Jan 28, 2018 · 0 comments

Comments

@libin1991
Copy link
Owner

本文从简单的例子入手,从打包文件去分析以下三个问题: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('hello world');

webpack打包后的代码

// dist/bundle.js
 /******/ (function(modules) { // webpackBootstrap
/******/     // The module cache
/******/     var installedModules = {};
/******/
/******/     // The require function
/******/     function __webpack_require__(moduleId) {
/******/
/******/         // Check if module is in cache
/******/         if(installedModules[moduleId]) {
/******/             return installedModules[moduleId].exports;
/******/         }
/******/         // Create a new module (and put it into the cache)
/******/         var module = installedModules[moduleId] = {
/******/             i: moduleId,
/******/             l: false,
/******/             exports: {}
/******/         };
/******/
/******/         // Execute the module function
/******/         modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
/******/
/******/         // Flag the module as loaded
/******/         module.l = true;
/******/
/******/         // Return the exports of the module
/******/         return module.exports;
/******/     }
/******/
/******/
/******/     // expose the modules object (__webpack_modules__)
/******/     __webpack_require__.m = modules;
/******/
/******/     // expose the module cache
/******/     __webpack_require__.c = installedModules;
/******/
/******/     // define getter function for harmony exports
/******/     __webpack_require__.d = function(exports, name, getter) {
/******/         if(!__webpack_require__.o(exports, name)) {
/******/             Object.defineProperty(exports, name, {
/******/                 configurable: false,
/******/                 enumerable: true,
/******/                 get: getter
/******/             });
/******/         }
/******/     };
/******/
/******/     // getDefaultExport function for compatibility with non-harmony modules
/******/     __webpack_require__.n = function(module) {
/******/         var getter = module && module.__esModule ?
/******/             function getDefault() { return module['default']; } :
/******/             function getModuleExports() { return module; };
/******/         __webpack_require__.d(getter, 'a', getter);
/******/         return getter;
/******/     };
/******/
/******/     // Object.prototype.hasOwnProperty.call
/******/     __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
/******/
/******/     // __webpack_public_path__
/******/     __webpack_require__.p = "";
/******/
/******/     // Load entry module and return exports
/******/     return __webpack_require__(__webpack_require__.s = 0);
/******/ })
/************************************************************************/
/******/ ([
/* 0 */
/***/ (function(module, exports) {

console.log('hello world');


/***/ })
/******/ ]);

一看你就会想,我就一行代码,你给我打包那么多???(黑人问号)

我们来分析一下这部分代码,先精简一下,其实整体就是一个自执行函数,然后传入一个模块数组

(function(modules) { 
     //...
 })([function(module, exports) {
     //..
 }])

好了,传入模块数组做了什么(其实注释都很明显了,我只是大概翻译一下)

/******/ (function(modules) { // webpackBootstrap
/******/     // The module cache  缓存已经load过的模块
/******/     var installedModules = {};
/******/
/******/     // The require function  引用的函数
/******/     function __webpack_require__(moduleId) {
/******/
/******/         // Check if module is in cache 假如在缓存里就直接返回
/******/         if(installedModules[moduleId]) {
/******/             return installedModules[moduleId].exports;
/******/         }
/******/         // Create a new module (and put it into the cache) 构造一个模块并放入缓存
/******/         var module = installedModules[moduleId] = {
/******/             i: moduleId, //模块id
/******/             l: false, // 是否已经加载完毕
/******/             exports: {} // 对外暴露的内容
/******/         };
/******/
/******/         // Execute the module function 传入模块参数,并执行模块
/******/         modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
/******/
/******/         // Flag the module as loaded 标记模块已经加载完毕
/******/         module.l = true;
/******/
/******/         // Return the exports of the module 返回模块暴露的内容
/******/         return module.exports;
/******/     }
/******/
/******/
/******/     // expose the modules object (__webpack_modules__) 暴露模块数组
/******/     __webpack_require__.m = modules;
/******/
/******/     // expose the module cache 暴露缓存数组
/******/     __webpack_require__.c = installedModules;
/******/
/******/     // define getter function for harmony exports 为ES6 exports定义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
/******/             });
/******/         }
/******/     };
/******/
/******/     // getDefaultExport function for compatibility with non-harmony modules 解决ES module和Common js module的冲突,ES则返回module['default']
/******/     __webpack_require__.n = function(module) {
/******/         var getter = module && module.__esModule ?
/******/             function getDefault() { return module['default']; } :
/******/             function getModuleExports() { return module; };
/******/         __webpack_require__.d(getter, 'a', getter);
/******/         return getter;
/******/     };
/******/
/******/     // Object.prototype.hasOwnProperty.call
/******/     __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
/******/
/******/     // __webpack_public_path__ webpack配置下的公共路径
/******/     __webpack_require__.p = "";
/******/
/******/     // Load entry module and return exports 最后执行entry模块并且返回它的暴露内容
/******/     return __webpack_require__(__webpack_require__.s = 0);
/******/ })
/************************************************************************/
/******/ ([
/* 0 */
/***/ (function(module, exports) {

console.log('hello world');


/***/ })
/******/ ]);

整体流程是怎样的呢

  • 传入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模块

https://doc.webpack-china.org/concepts/modules#-webpack-
webpack 模块能够以各种方式表达它们的依赖关系,几个例子如下:

  • ES2015 import 语句
  • CommonJS require() 语句
  • AMD define 和 require 语句
  • css/sass/less 文件中的 @import 语句。
  • 样式(url(…))或 HTML 文件()中的图片链接(image url)

强大的webpack模块可以兼容各种模块化方案,并且无侵入性(non-opinionated)

我们可以再编写例子一探究竟

CommonJS

修改src/index.js

var cj = require('./cj.js');

console.log('hello world');
cj();

新增src/cj.js,保持前面例子其他不变

// src/cj.js
function a() {
    console.log("CommonJS");
}
module.exports = a;

再次运行webpack

/******/ (function(modules) { // webpackBootstrap
  //... 省略代码
/******/ })
/************************************************************************/
/******/ ([
/* 0 */
/***/ (function(module, exports, __webpack_require__) {

let cj = __webpack_require__(1);

console.log('hello world');
cj();


/***/ }),
/* 1 */
/***/ (function(module, exports) {

function a() {
    console.log("CommonJS");
}
module.exports = a;


/***/ })
/******/ ]);

我们可以看到模块数组多了个引入的文件,然后index.js模块函数多了个参数__webpack_require__,去引用文件(__webpack_require__在上一节有介绍),整体上就是依赖的模块修改了module.exports,然后主模块执行依赖模块,获取exports即可

ES2015 import

新增src/es.js

// src/es.js
export default function b() {
    console.log('ES Modules');
}

修改src/index.js

// src/index.js
import es from './es.js';

console.log('hello world');
es();

webpack.config.js不变,执行webpack

/******/ (function(modules) { // webpackBootstrap
// ... 省略代码
/******/ })
/************************************************************************/
/******/ ([
/* 0 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {

"use strict";
Object.defineProperty(__webpack_exports__, "__esModule", { value: true });
/* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__es_js__ = __webpack_require__(1);


console.log('hello world');
Object(__WEBPACK_IMPORTED_MODULE_0__es_js__["a" /* default */])();


/***/ }),
/* 1 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {

"use strict";
/* harmony export (immutable) */ __webpack_exports__["a"] = b;
function b() {
    console.log('ES Modules');
}


/***/ })
/******/ ]);

我们可以看到它们都变成了严格模式,webpack自动采用的

表现其实跟CommonJS相似,也是传入export然后修改,在主模块再require进来,

我们可以看到这个

Object.defineProperty(__webpack_exports__, "__esModule", { value: true });

这个干嘛用的?其实就是标记当前的exports是es模块,还记得之前的__webpack_require__.n吗,我们再拿出来看看

/******/     // getDefaultExport function for compatibility with non-harmony modules 解决ES module和Common js module的冲突,ES则返回module['default']
/******/     __webpack_require__.n = function(module) {
/******/         var getter = module && module.__esModule ?
/******/             function getDefault() { return module['default']; } :
/******/             function getModuleExports() { return module; };
/******/         __webpack_require__.d(getter, 'a', getter);
/******/         return getter;
/******/     };

为了避免跟非ES Modules冲突?冲突在哪里呢?
其实这部分如果你看到babel转换ES Modules源码就知道了,为了兼容模块,会把ES Modules直接挂在exports.default上,然后加上__esModule属性,引入的时候判断一次是否是转换模块,是则引入module[‘default’],不是则引入module

我们再多引入几个ES Modules看看效果

// src/es.js
export function es() {
    console.log('ES Modules');
}

export function esTwo() {
    console.log('ES Modules Two');
}

export function esThree() {
    console.log('ES Modules Three');
}

export function esFour() {
    console.log('ES Modules Four');
}

我们多引入esTwo和esFour,但是不使用esFour

// src/index.js
import { es, esTwo, esFour} from './es.js';

console.log('hello world');
es();
esTwo();

得出

/******/ (function(modules) { // webpackBootstrap
// ...
/******/ })
/************************************************************************/
/******/ ([
/* 0 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {

"use strict";
Object.defineProperty(__webpack_exports__, "__esModule", { value: true });
/* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__es_js__ = __webpack_require__(1);


console.log('hello world');
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__) {

"use strict";
/* harmony export (immutable) */ __webpack_exports__["a"] = es;
/* harmony export (immutable) */ __webpack_exports__["b"] = esTwo;
/* unused harmony export esThree */
/* unused harmony export esFour */
function es() {
    console.log('ES Modules');
}

function esTwo() {
    console.log('ES Modules Two');
}

function esThree() {
    console.log('ES Modules Three');
}

function esFour() {
    console.log('ES Modules Four');
}



/***/ })
/******/ ]);

嗯嗯其实跟前面是一样的,举出这个例子重点在哪里呢,有没有注意到注释中

/* unused harmony export esThree */
/* unused harmony export esFour */

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__)

    );
})

简单来讲收集define Array然后置入返回函数,根据参数获取依赖

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的模块,这是令人震惊的,这就不能通过普通的hack commonjs或者函数调用简单去调用了,这就是anything to JS,它就需要借助webpack loader去实现了

像css就是转换成一段js代码,通过处理,调用时就是可以用js将这段css插入到style中,image也类似,这部分就不详细阐述了,有兴趣的读者可以深入去研究

webpack3新特性

我们可以再顺便看下webpack3新特性的表现
具体可以看这里https://medium.com/webpack/webpack-3-official-release-15fd2dd8f07b

Scope Hoisting

我们可以发现模块数组是一个一个独立的函数然后闭包引用webpack主函数的相应内容,每个模块都是独立的,然后带来的结果是在浏览器中执行速度变慢,然后webpack3学习了Closure Compiler和RollupJS这两个工具,连接所有闭包到一个闭包里,放入一个函数,让执行速度更快,并且整体代码体积也会有所缩小

我们可以实际看一下效果(要注意的是这个特性只支持ES Modules,是不支持CommonJs和AMD的)
使用上面的例子,配置webpack.config.js,增加new webpack.optimize.ModuleConcatenationPlugin()

const path = require('path');
const webpack = require('webpack');

module.exports = {
  entry: './src/index.js',
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist')
  },
  module: {
  },
  plugins: [
    new webpack.optimize.ModuleConcatenationPlugin(),
  ]
};

打包

/******/ (function(modules) { // webpackBootstrap
// ... 省略代码
/******/ })
/************************************************************************/
/******/ ([
/* 0 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {

"use strict";
Object.defineProperty(__webpack_exports__, "__esModule", { value: true });

// CONCATENATED MODULE: ./src/es.js
function es() {
    console.log('ES Modules');
}

function esTwo() {
    console.log('ES Modules Two');
}

function esThree() {
    console.log('ES Modules Three');
}

function esFour() {
    console.log('ES Modules Four');
}


// CONCATENATED MODULE: ./src/index.js
// src/index.js


console.log('hello world');
es();


/***/ })
/******/ ]);

我们可以惊喜的发现没有什么require了,它们拼接成了一个函数,good!😃

Magic Comments

code splitting是webpack一个重点特性之一,涉及到要动态引入的时候,webpack可以使用 require.ensure去实现,后来webpack2支持使用了符合 ECMAScript 提案 的 import() 语法,但是它有个不足之处,无法指定chunk的名称chunkName,为了解决这个问题,出现了Magic Comments,支持用注释的方式去指定,如下

import(/* webpackChunkName: "my-chunk-name" */ 'module');

小结

webpack是一个强大的模块打包工具,在处理依赖、模块上都很优秀,本文从bundle.js文件分析出发去探索了不同模块方案的加载机制,初步去理解webpack,并且对webpack3特性进行阐述,当然,webpack还有很多地方需要去探索深究,敬请期待以后的文章吧~

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant