Webpack如何实现打包功能?最近看文章刷到这个官方教学视频,666啊,建议先进去看一看!

1. 打包文件分析

首先webpack构建出的bundle.js长这样。整个代码是一个自执行函数,函数接收的是一个数组,数组的每一项是一个模块,它们也被一个函数包裹起来。

1
2
3
4
5
6
7
(function(modules) {
// 模拟require语句
function _webpack_require__() {
}
// 执行存放所有模块数组中的第0个模块
_webpack_require__[0]
})([/* 存放所有模块的数组 */module0, module1])

webpack如何处理importrequire?
bundle.js能直接运行在浏览器中的原因在于输出的文件中定义了一个_webpack_require__函数,来模拟Node.js.中的require语句

2. 实现一个简单的webpack

Webpack通过入口文件逐层遍历到模块依赖,进行代码分析,转换,最终生成可在浏览器运行的打包后的代码。

  • 解析入口文件,获取AST
  • 从AST中找到import生成,找出所有依赖模块
  • AST转换为可执行的code
  • 递归生成所有依赖项,生成依赖关系图
  • 重写require函数,输出bundle(一个立即执行函数,一个require函数,所有module)
    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
    const fs = require('fs')
    const path = require('path')
    const babylon = require('babylon')
    const traverse = require('babel-traverse').default
    const babel = require('babel-core')

    let ID = 0

    /*
    * 解析文件,生成这个模块的code, 提取依赖
    * accept a path to a file, read its contents, and extract its dependencies
    */
    function createAsset (filePath) {
    // 读取文件,生成ast
    const content = fs.readFileSync(filePath, 'utf-8')
    const ast = babylon.parse(content, {
    sourceType: 'module'
    })

    // 读取代码中的import声明,获取这个模块的依赖模块
    const dependencies = []
    traverse(ast, {
    ImportDeclaration: ({ node }) => {
    dependencies.push(node.source.value)
    }
    })

    // 将模块代码用babel-preset-env转一下
    const id = ID++
    const { code } = babel.transformFromAst(ast, null, {
    presets: ['env']
    })

    return {
    id, // moduleId
    filePath, // filePath
    dependencies, // 模块依赖的模块
    code // 模块代码
    }
    }


    /*
    * 从入口文件,递归生成所有依赖
    * Now that we can extract the dependencies of a single module, we are going to
    * start by extracting the dependencies of the entry file.
    */
    function createGraph (entry) {
    // entry: the main file of our application
    const mainAsset = createAsset(entry)
    const queue = [mainAsset]

    for (const asset of queue) {
    const dirname = path.dirname(asset.filePath)

    asset.mapping = {}
    asset.dependencies.forEach(relativePath => {
    const absolutePath = path.join(dirname, relativePath)
    const child = createAsset(absolutePath)
    asset.mapping[relativePath] = child.id
    queue.push(child)
    })
    }
    return queue
    }


    /*
    * 生成bundle.js的code
    */
    function bundle (graph) {
    let modules = ''

    // 传送module,使用了CommonJS的模块标准,他们需要require, module, exports这3个可用的方法
    // 因为浏览器不支持,所以我们会自己实现它

    // the code of each module wrapped with a function
    // This is because modules should be scoped
    // Defining a variable in on module shouldn't affect others of the global scope

    // When we transpile our modules, use the CommonJS module system
    // They expect a `require`, a `module` and an `exports` objects to be available
    // Those are not normally available in the browser
    // so we will implement them and inject them into our function wrappers

    // For the second value, we stringify the mapping between a module and its dependencies.
    // This is an object that looks like this:
    // { './relative/path': 1 }
    graph.forEach(mod => {
    modules += `${mod.id}: [
    function (require, module, exports) {
    ${mod.code}
    },
    ${JSON.stringify(mod.mapping)}
    ],`
    })


    // 立即执行函数
    // 定义`require()`
    const result = `
    (function (modules) {
    function require(id) {
    const [fn, mapping] = modules[id]

    function localRequire (relativePath) {
    return require(mapping[relativePath])
    }

    const module = { exports: {} }

    fn(localRequire, module, module.exports)

    return module.exports
    }
    require(0)
    })({${modules}})
    `
    return result
    }


    const graph = createGraph('./entry.js')
    const result = bundle(graph)
    console.log(result)

我们写一个demo,执行node bundle.js

1
2
3
4
5
6
7
8
9
10
// entry.js
import message from './message.js'
console.log(message)

// message.js
import { name } from './name.js'
export default `hello ${name}`

// name.js
export const name = '123'

输出的result,把代码放到浏览器控制台,执行后输出hello 123

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
(function (modules) {
function require(id) {
const [fn, mapping] = modules[id]

// lcoalRequire(relativePath) = require(moduleId)
function localRequire (relativePath) {
return require(mapping[relativePath])
}

// 定义module, moduleExport,执行模块内的代码,返回module.exports
// 因此 require(id) 返回值是模块的module.exports
const module = { exports: {} }
fn(localRequire, module, module.exports)
return module.exports
}
require(0)
})(
{
0: [
function (require, module, exports) {
"use strict";
var _message = require("./message.js");
var _message2 = _interopRequireDefault(_message);
function _interopRequireDefault(obj) {
return obj && obj.__esModule ? obj : { default: obj }
}
console.log(_message2.default);
},
{"./message.js":1}
],
1: [
function (require, module, exports) {
"use strict";

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

var _name = require("./name.js");
exports.default = "hello " + _name.name;
},
{"./name.js":2}
],
2: [
function (require, module, exports) {
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
var name = exports.name = '111';
},
{}
],
})

参考资料