# webpack源码(一)

# loader

手写一个简单loader

loader/index.js

// webpack自带包
const loaderUtils = require('loader-utils');

module.exports = function(content) {
    // 获取配置文件
    const options = loaderUtils.getOptions(this);
    console.log(options.data);

    return content + 'console.log(1);';
}
1
2
3
4
5
6
7
8
9
10

安装一下 loader-utils,用来获取loader参数

yarn add loader-utils --dev
1

简单的写一个webpack文件,引入一下刚写的loader

const path = require('path');

module.exports = {
    module: {
        rules: [
            {
                test: /\.m?js$/,
                exclude: /(node_modules|bower_components)/,
                use: {
                    loader: path.resolve('./loader/index.js'),
                    options: {
                        data: '这是一个options'
                    }
                }
            }
        ]
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

运行一下

webpack

看到控制台输出了我们的options.data,说明我们通过 loaderUtils.getOptions(this); 拿到了loader中的参数

再看一下通过 Webpack 打包出来的文件,在最下面找到

eval("console.log('京程一灯');console.log(1);\n\n//# sourceURL=webpack:///./src/index.js?");
1

我们在 loader 中最后返回的钩子中加入的 return content + 'console.log(1);';挂在了原本内容的最后

再添加一个运行前钩子 pitch


// webpack自带包
const loaderUtils = require('loader-utils');

module.exports = function(content) {
    // 获取配置文件
    const options = loaderUtils.getOptions(this);
    console.log(this.data.value);
    console.log(options.data);

    return content + 'console.log(1);';
}

module.exports.pitch = function(r, pre, data) {
    data.value = 'pitch';
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

webpack

可以看到也可以在运行loader的时候拿到

# 单文件打包

接下来来分析一下单文件打包

我们把loader什么的都去掉,单纯的打包我们的index.js文件

console.log('hello webpack');
1

然后看一下打包后的文件

一个清理干净的main.js

(function (modules) { // webpackBootstrap
  // 缓存
  var installedModules = {};

  // The require function
  function __webpack_require__(moduleId) {

    if (installedModules[moduleId]) {
      return installedModules[moduleId].exports;
    }
    // Create a new module (and put it into the cache)
    var module = installedModules[moduleId] = {
      exports: {}
    };

    // Execute the module function
    modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);

    // Return the exports of the module
    return module.exports;
  }
  // Load entry module and return exports
  return __webpack_require__("./src/index.js");
})
  ({
    "./src/index.js":
      (function (module, exports) {
        eval("console.log('hello webpack');");
      })
  });
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
  • 先把没用的注释都删掉,可以看到整个是一个大的闭包
  • installedModules 是个缓存
  • function __webpack_require__(){} 是webpack自己实现的一套引入规则
  • 然后我们把下面一些什么 .m .n .r 巴拉巴拉的东西都先删掉,后面再说
  • return __webpack_require__(__webpack_require__.s = "./src/index.js"); 相当于直接传了一个入口函数 return __webpack_require__("./src/index.js");

接下来我们分析一下webpack自己实现的 __webpack_require__

  • 首先 __webpack_require__ 的参数其实就是入口文件的路径 "./src/index.js"
  • 进入后定义一个缓存,先判断是否有缓存,当然第一次肯定是没有的┓( ´∀` )┏
  • 没有缓存那我们就把参数也就是入口地址存到缓存中去,相当于定义了 module.exports = {}
  • 接下来 modules 也就是我们通过闭包传入的 入口地址:执行函数 的键值对
  • 通过 modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); 来执行
  • 将前面定义的 module.exports 绑定,然后传入 modulemodule.exports 做参数
  • 我们把这段代码直接放到浏览器执行可以直接运行

webpack

# 同步文件加载

在同级目录下导出一个 sync.js

const data = 'sync';
export default data;
1
2

index.js

import sync from './sync';
console.log(sync);
console.log('hello webpack');
1
2
3

打包一下看看干净的main.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] = {
      exports: {}
    };

    // Execute the module function
    modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
    // Return the exports of the module
    return module.exports;
  }
  // Load entry module and return exports
  return __webpack_require__("./src/index.js");
})
  ({
    "./src/index.js":
      (function (module, __webpack_exports__, __webpack_require__) {
        "use strict";
        /* harmony import */
        var _sync__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./sync */ "./src/sync.js");
        console.log(_sync__WEBPACK_IMPORTED_MODULE_0__["default"]);
        console.log('hello webpack');
      }),
    "./src/sync.js":
      (function (module, __webpack_exports__, __webpack_require__) {
        "use strict";
        const data = 'sync';
        /* harmony default export */ 
        __webpack_exports__["default"] = (data);
      })
  });
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
  • 同样的我们删除了没用的代码
  • 顺着代码,通过 __webpack_require__ 调用了入口文件
  • 进入 index.js 中后往下走,/* harmony import */ 和谐的导入,也就是webpack自己的导入
  • 又通过 __webpack_require__ 调用了 sync.js
  • 进入sync中后,定了变量,然后用传入的 __webpack_exports__ 通过 ["default"] 把内容导出
  • 又回到index 中,console 了刚刚导进来的 ["default"]
  • 最后console了 "hello webpack"

# 异步文件导入

async.js

const data = 'async';
export default data;
1
2

index.js

import('./async').then(_ => {
    console.log(_);
})
console.log('hello webpack');
1
2
3
4

打包后会生成两个文件:0.js 和 main.js

0.js

(window["webpackJsonp"] = []).push([
  [0],
  {
    "./src/async.js":
      /*! exports provided: default */
      (function (module, __webpack_exports__, __webpack_require__) {
        "use strict";
        const data = 'async';
        /* harmony default export */
        __webpack_exports__["default"] = (data);

      })
  }
]);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
  • 可以看到清理干净后的0.js实际上是在一个数组中push了一个二维数组
  • [[0], {"key": (function(){})}]
  • 里面的内容就是 async 中的内容

main.js

(function (modules) { // webpackBootstrap
  // install a JSONP callback for chunk loading
  function webpackJsonpCallback(data) {
    var chunkIds = data[0];
    var moreModules = data[1];

    var moduleId, chunkId, i = 0, resolves = [];
    for (; i < chunkIds.length; i++) {
      chunkId = chunkIds[i];
      if (Object.prototype.hasOwnProperty.call(installedChunks, chunkId) && installedChunks[chunkId]) {
        resolves.push(installedChunks[chunkId][0]);
      }
      installedChunks[chunkId] = 0;
    }
    for (moduleId in moreModules) {
      if (Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
        modules[moduleId] = moreModules[moduleId];
      }
    }
    if (parentJsonpFunction) parentJsonpFunction(data);

    while (resolves.length) {
      resolves.shift()();
    }

  };
  // The module cache
  var installedModules = {};

  var installedChunks = {
    "main": 0
  };

  // script path function
  function jsonpScriptSrc(chunkId) {
    return __webpack_require__.p + "" + ({}[chunkId] || chunkId) + ".js"
  }

  // 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;
  }

  __webpack_require__.e = function requireEnsure(chunkId) {
    var promises = [];


    // JSONP chunk loading for javascript

    var installedChunkData = installedChunks[chunkId];
    if (installedChunkData !== 0) { // 0 means "already installed".

      // a Promise means "currently loading".
      if (installedChunkData) {
        promises.push(installedChunkData[2]);
      } else {
        // setup Promise in chunk cache
        var promise = new Promise(function (resolve, reject) {
          installedChunkData = installedChunks[chunkId] = [resolve, reject];
        });
        promises.push(installedChunkData[2] = promise);

        // start chunk loading
        var script = document.createElement('script');
        var onScriptComplete;
        script.charset = 'utf-8';
        script.timeout = 120;
        script.src = jsonpScriptSrc(chunkId);

        document.head.appendChild(script);
      }
    }
    return Promise.all(promises);
  };


  // __webpack_public_path__
  __webpack_require__.p = "";

  // on error function for async loading
  __webpack_require__.oe = function (err) { console.error(err); throw err; };

  var jsonpArray = window["webpackJsonp"] = window["webpackJsonp"] || [];
  var oldJsonpFunction = jsonpArray.push.bind(jsonpArray);
  jsonpArray.push = webpackJsonpCallback;
  jsonpArray = jsonpArray.slice();
  for (var i = 0; i < jsonpArray.length; i++) webpackJsonpCallback(jsonpArray[i]);
  var parentJsonpFunction = oldJsonpFunction;


  // Load entry module and return exports
  return __webpack_require__("./src/index.js");
})

  ({

    "./src/index.js":

      (function (module, exports, __webpack_require__) {

        __webpack_require__.e(/*! import() */ 0)
          .then(__webpack_require__.bind(null, /*! ./async */ "./src/async.js"))
          .then(_ => { console.log(_); })
        console.log('hello webpack');

      })

  });
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127

main.js 的内容明显要复杂很多 先看 webpackJsonpCallback 方法

function webpackJsonpCallback(data) {
    var chunkIds = data[0];
    var moreModules = data[1];

    var moduleId, chunkId, i = 0, resolves = [];
    for (; i < chunkIds.length; i++) {
      chunkId = chunkIds[i];
      if (Object.prototype.hasOwnProperty.call(installedChunks, chunkId) && installedChunks[chunkId]) {
        resolves.push(installedChunks[chunkId][0]);
      }
      installedChunks[chunkId] = 0;
    }
    for (moduleId in moreModules) {
      if (Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
        modules[moduleId] = moreModules[moduleId];
      }
    }
    if (parentJsonpFunction) parentJsonpFunction(data);

    while (resolves.length) {
      resolves.shift()();
    }

  };
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
  • 从下面的代码中可以看出,webpackJsonpCallback 传入的是从 0.js 中push进去的数组
  • chunkIds 就是 [0]moreModules 是存入的函数
  • 接下来做了一个循环把chunk都加进来
  • modules[moduleId] = moreModules[moduleId]; 把函数绑定

接下来就 __webpack_require__ 这个东西跟之前的都一样

然后就是 webpack_require.e

__webpack_require__.e = function requireEnsure(chunkId) {
    var promises = [];
    // JSONP chunk loading for javascript
    var installedChunkData = installedChunks[chunkId];
    if (installedChunkData !== 0) { // 0 means "already installed".

      // a Promise means "currently loading".
      if (installedChunkData) {
        promises.push(installedChunkData[2]);
      } else {
        // setup Promise in chunk cache
        var promise = new Promise(function (resolve, reject) {
          installedChunkData = installedChunks[chunkId] = [resolve, reject];
        });
        promises.push(installedChunkData[2] = promise);

        // start chunk loading
        var script = document.createElement('script');
        var onScriptComplete;

        script.charset = 'utf-8';
        script.timeout = 120;
        script.src = jsonpScriptSrc(chunkId);
        document.head.appendChild(script);
      }
    }
    return Promise.all(promises);
  };
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
  • 从下面的入口文件可以看出,webpack_require.e 是一个关键的东西,传入 0 的chunk值
  • 进入分支后创建promise往里面push东西
  • 往script中加了一设定

然后就是最后一段

__webpack_require__.p = "";
	var jsonpArray = window["webpackJsonp"] = [];
  var oldJsonpFunction = jsonpArray.push.bind(jsonpArray);
  // 文件绑定modules
	jsonpArray.push = webpackJsonpCallback;
	jsonpArray = jsonpArray.slice();
	for(var i = 0; i < jsonpArray.length; i++) webpackJsonpCallback(jsonpArray[i]);
	var parentJsonpFunction = oldJsonpFunction;
	// Load entry module and return exports
	return __webpack_require__(__webpack_require__.s = "./src/index.js");
1
2
3
4
5
6
7
8
9
10
  • 其实就是把0.js中的东西引过来,然后调用

# AST语法树

为什么说webpack慢,因为loader慢,为什么loader慢,因为每个loader都要先把代码转化为AST然后再把AST转化成代码,所以就会很长时间

安装几个包

yarn add acorn acorn-walk magic-string --dev
1

src/index.js

const data = '这是一个data';
console.log('hello webpack');
1
2

loader/index.js

// webpack自带包
const loaderUtils = require('loader-utils');
const acorn = require('acorn');
const acornWalk = require('acorn-walk');
const magicString = require('magic-string');


module.exports = function(content) {
    // 获取配置文件
    const options = loaderUtils.getOptions(this);
    const ast = acorn.parse(content);
    console.log('ast', ast);

    return content + 'console.log(1);';
}

module.exports.pitch = function(r, pre, data) {
    data.value = 'pitch';
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

运行一下看看ast拿到了什么东西

webpack

接下来要遍历语法树,去找到我们要替换的东西

 const ast = acorn.parse(content);
    // 遍历语法树
    acornWalk.simple(ast, {
        VariableDeclaration(node) {
            console.log('node', node);
        }
    })
1
2
3
4
5
6
7
  • acornWalk.simple() 方法遍历语法树
  • 找到我们刚才从ast中看到的 VariableDeclaration 类型看看能输出什么

webpack

接下来就要开始替换 const 了

const ast = acorn.parse(content);
    // 把ast转成string
    const code = new magicString(content);
    // 遍历语法树
    acornWalk.simple(ast, {
        VariableDeclaration(node) {
            // 拿到node中的start
            const {start} = node;
            code.overwrite(start, start + 5, 'var');
        }
    })

    return code.toString();
1
2
3
4
5
6
7
8
9
10
11
12
13
  • 遍历语法树
  • VariableDeclaration 类型拿到节点的 start 开始位置
  • code.overwrite 重写这段代码
  • code.overwrite 三个参数,起始位置,终止位置,和要替换成的字符串
  • 最后再把code转成字符串

看看打包后变成了什么,把const 换成了 var ┓( ´∀` )┏

eval("var data = '这是一个data';\nconsole.log('hello webpack');\n\n//# sourceURL=webpack:///./src/index.js?");
1

# 手写一个简单webpack

准备好打包的文件 src/index.js

import sync from './sync';
console.log(sync);
console.log('hello webpack');
1
2
3

换一种新的ast解析的包

yarn add babylon @babel/traverse
1

创建 my-webpack.js

// 解析ast
const babylon = require('babylon');
// 遍历ast
const traverse = require('@babel/traverse').default;
const magicString = require('magic-string');
const entry = './src/index.js';
const fs = require('fs');

function parse(filename) {
    const content = fs.readFileSync(filename, 'utf-8');
    const ast = babylon.parse(content, {
        sourceType:'module'
    })
    traverse(ast, {
        ImportDeclaration({node}) {
            console.log(node);
        }
    })
}
parse(entry);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

输出看一下找到的 ImportDeclaration 下的信息

webpack

把内容中的 import 重写掉

const content = fs.readFileSync(filename, 'utf-8');
    const ast = babylon.parse(content, {
        sourceType: 'module'
    })
    const code = new magicString(content);
    traverse(ast, {
        ImportDeclaration({ node }) {
            const { start, end, specifiers, source } = node;
            const newfile = path.join('./src/', `${source.value}.js`)
            code.overwrite(start, end, `var ${specifiers[0].local.name} = __webpack_require__('${newfile}');`);
            const _code = code.toString();
            console.log(_code);
        }
    })
1
2
3
4
5
6
7
8
9
10
11
12
13
14

webpack

继续

const arrGroup = [];
const dep = [];

function parse(filename) {
    const content = fs.readFileSync(filename, 'utf-8');
    const ast = babylon.parse(content, {
        sourceType: 'module'
    })
    const code = new magicString(content);
    traverse(ast, {
        ImportDeclaration({ node }) {
            const { start, end, specifiers, source } = node;
            const newfile = path.join('./src/',`${source.value}.js`);
            code.overwrite(start, end, `var ${specifiers[0].local.name} = __webpack_require__('${newfile}');`);
            arrGroup.push(newfile);
        }
    })
    const _code = code.toString();
    dep.push({
        filename,
        _code
    })
    return arrGroup;
}

const arrfile = parse(entry);
console.log('arrfile',arrfile);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
  • 定义了两个数组 deparrGrouparrGroup用来存放文件
  • ImportDeclaration 中就完成了 import 的转换,在外面转成字符串传到dep中

继续

const babylon = require('babylon');
const traverse = require('@babel/traverse').default;
const magicString = require('magic-string');
const entry = './src/index.js';
const fs = require('fs');
const path = require('path');

const dep = [];

function parse(filename) {
    const content = fs.readFileSync(filename, 'utf-8');
    const arrGroup = [];
    const ast = babylon.parse(content, {
        sourceType: 'module'
    })
    const code = new magicString(content);
    traverse(ast, {
        ExportDeclaration({node}) {
            const {start, end, declaration} = node;
            code.overwrite(start,end,`__webpack_exports__["default"] = ${declaration.name};`)
        },
        ImportDeclaration({ node }) {
            const { start, end, specifiers, source } = node;
            const newfile = path.join('./src/',`${source.value}.js`);
            code.overwrite(start, end, `var ${specifiers[0].local.name} = __webpack_require__('${newfile}');`)
            arrGroup.push(newfile)

        }
    })
    const _code = code.toString();
    dep.push({
        filename,
        _code
    })
    return arrGroup;
}

const arrfile = parse(entry);


for (let item in arrfile) {
    parse(arrfile[item]);
}
console.log(dep);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
  • 第一轮执行了 parse(entry) 之后,把入口文件的代码替换完了
  • 将需要导入的文件放在数组中进行遍历,又进入ast中遍历,这次要把 exports 替换掉

接下来就要用模板替换,安装

const babylon = require('babylon');
const traverse = require('@babel/traverse').default;
const magicString = require('magic-string');
const entry = './src/index.js';
const fs = require('fs');
const path = require('path');
const ejs = require('ejs');
const dep = [];

function parse(filename) {
    const content = fs.readFileSync(filename, 'utf-8');
    const arrGroup = [];
    const ast = babylon.parse(content, {
        sourceType: 'module'
    })
    const code = new magicString(content);
    traverse(ast, {
        ExportDeclaration({node}) {
            const {start, end, declaration} = node;
            code.overwrite(start,end,`__webpack_exports__["default"] = ${declaration.name};`)
        },
        ImportDeclaration({ node }) {
            const { start, end, specifiers, source } = node;
            const newfile = path.join('./src/',`${source.value}.js`);
            code.overwrite(start, end, `var ${specifiers[0].local.name} = __webpack_require__('${newfile}');`)
            arrGroup.push(newfile)

        }
    })
    const _code = code.toString();
    dep.push({
        filename,
        _code
    })
    return arrGroup;
}

const arrfile = parse(entry);


for (let item in arrfile) {
    parse(arrfile[item]);
}

const template = `
(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] = {
			exports: {}
		};
		// Execute the module function
		modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
		
		// Return the exports of the module
		return module.exports;
	}
	return __webpack_require__("${entry}");
})
({
    <% for(let i = 0; i < dep.length; i++){ %>
    "<%- dep[i]['filename'] %>":(function(module, __webpack_exports__, __webpack_require__) {
    "use strict";
        <%- dep[i]['_code'] %>
    }),
    <% } %>
});
`
const result = ejs.render(template, {
    dep
})
console.log(result);

fs.writeFileSync('./dist/yd-main.js', result);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
  • 引入ejs模板引擎,将webpack中的那部分不动的代码写进去,将我们刚写的东西替换掉
  • 写入到 yd-main.js 文件中