1

为什么需要打包

原始的<script>标签加载JS的方式的弊端:

1.容易污染全局变量

2.模块加载的顺序需要事先定义好

3.模块之间的管理要主观了解清楚

4.模块加载过多会导致请求过多;全部打包在一起影响项目的初始化

了解完为什么需要打包之后,那我们很容易就能得出我们打包的目的:处理模块之间的依赖关系,从而更好地加载资源

打包器的基本流程

1.处理输入文件,分析所有依赖项

2.解析依赖项,生成依赖关系图

3.根据依赖关系图,生成一个在浏览器中可执行的代码,输出bundle.js

打包器的具体实现

首先来生成一个代码结果

- src
-- hello.js
-- world.js
-- entry.js
- webpack.js
- webpack.config.js
- package.json
entry.js
import hello from './hello.js'
import {world} from './world.js'

hello(world);
console.log('hello, webpack');
hello.js
export default function hello(name) {
  console.log(`hello ${name}!`)
}
world.js
export const world = 'world';
webpack.config.js
const path = require("path");

module.exports = {
    entry: "./src/entry.js",
    output: {
        path: path.resolve(__dirname, "./dist"),
        filename:"bundle.js"
    }
}

我们主要是使用三个库来实现我们的打包

@babel/parser: babel解析器,将代码转化为抽象语法树AST

@babel/traverse: 配合babel解析器,遍历及更新AST每一个子节点

@babel/core: 获取模块中的可执行内容

下面我们来一步一步实现webpack的主要代码,先看一个基础架构

基础框架

const fs = require("fs")
const parser = require("@babel/parser")
const traverse = require("@babel/traverse").default
const path = require("path")
const {
    transformFromAst
} = require("@babel/core")

module.exports = class Webpack {
    constructor(options) {
        const {
            entry,
            output
        } = options
        this.entry = entry;
        this.output = output;
    }
    // 主逻辑
    run() {}
    // 处理逻辑依赖
    handleDependence() {}
    // 生成bundle代码
      generateBundle() {}
    // 输出打包文件
    outputBundleFile() {}
}

handleDependence

首先来看下handleDependence

        // 递归处理每个文件的逻辑依赖,以及每个文件的内部的相对地址和绝对地址的对应关系
    handleDependence(entry) {
        // 生成模块依赖图
        const dependenceGraph = {
            [entry]: this.createAsset(entry),
        }

        // 递归遍历
        const recursionDep = (filename, assert) => {
            // 维护相对路径和绝对路径的对应关系
            assert.mapping = {};
            // 返回路径的目录名
            const dirname = path.dirname(filename);
            assert.dependencies.forEach(relativePath => {
                const absolutePath = path.join(dirname, relativePath);
                // 设置相对路径和绝对路径的对应关系
                assert.mapping[relativePath] = absolutePath;
                // 如果没有该绝对路径的依赖关系
                if (!dependenceGraph[absolutePath]) {
                    const child = this.createAsset(absolutePath);
                    dependenceGraph[absolutePath] = this.createAsset(absolutePath);
                    if (child.dependencies.length > 0) {
                        // 递归
                        recursionDep(absolutePath, child);
                    }
                }
            })
        }

        for (let filename in dependenceGraph) {
            let assert = dependenceGraph[filename]
            recursionDep(filename, assert);
        }
        return dependenceGraph;
    }

其中createAsset为

// 获取每个文件的可执行代码和依赖关系
createAsset(entry) {
        // 读取文件内容
        const entryContent = fs.readFileSync(entry, 'utf-8')
        // 使用 @babel/parser(JavaScript解析器)解析代码,生成 ast(抽象语法树)
        const ast = babelParser.parse(entryContent, {
            sourceType: "module"
        })

        // 从 ast 中获取所有依赖模块(import),并放入 dependencies 中
        const dependencies = []
        traverse(ast, {
            // 遍历所有的 import 模块,并将相对路径放入 dependencies
            ImportDeclaration: ({
                node
            }) => {
                dependencies.push(node.source.value)
            }
        })

        // 获取文件内容
        const {
            code
        } = transformFromAst(ast, null, {
            presets: ['@babel/preset-env'],
        });

        return {
            code,
            dependencies,
        }
}

handleDependence 返回的结果格式如下所示:

{ 
  './src/entry.js': 
   { 
     code: '"use strict";\n\nvar _hello = _interopRequireDefault(require("./hello.js"));\n\nvar _world = require("./world.js");\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }\n\n(0, _hello["default"])(_world.world);\nconsole.log(\'hello, webpack\');',
     dependencies: [ './hello.js', './world.js' ],
     mapping: { './hello.js': 'src/hello.js', './world.js': 'src/world.js' } 
   },
  'src/hello.js': 
   { 
     code: '"use strict";\n\nObject.defineProperty(exports, "__esModule", {\n  value: true\n});\nexports["default"] = hello;\n\nfunction hello(name) {\n  console.log("hello ".concat(name, "!"));\n}',
     dependencies: [] 
   },
  'src/world.js': 
   { 
     code: '"use strict";\n\nObject.defineProperty(exports, "__esModule", {\n  value: true\n});\nexports.world = void 0;\nvar world = \'world\';\nexports.world = world;',
     dependencies: [] 
   } 
}

我们下一步的目的就是要根据这个映射表来生成出来一个可执行的代码。

我们最终生成的是一个文件,那我们根据生成的对应结构,拼接成字符串,最终输入到文件中,再执行即可。

为了不污染全局环境,我们采用立即执行的方法。

综上,我们的立即执行方法可以描述为

const result = `
  (function() {
  })()
`

同时,我们再来看下处理后的代码,以./src/entry.js文件为例子

                        "use strict";
                        Object.defineProperty(exports, "__esModule", {
                value: true
            });
            exports["default"] = message;

            var _hello = require("./hello.js");

            var _name = require("./name.js");

            function message() {
                console.log("".concat(_hello.hello, " ").concat(_name.name, "!"));
            }

使用的是 CommonJS 规范,而浏览器不支持 commonJS(浏览器没有 module 、exports、require、global);所以这里我们需要实现它们,并注入到包装器函数内。

let modules = ''
for (let filename in Dependence) {
  let mod = graph[filename]
  modules += `'${filename}': {
    code: function(require, module, exports) {
      ${mod.code}
    },
    mapping: ${JSON.stringify(mod.mapping)},
    },`
}

我们再扩展下result的生成,将代码中的相对地址替换成绝对地址。

const result = `
    (function(modules) {
      function require(moduleId) {
        const {fn, mapping} = modules[moduleId]
        function localRequire(name) {
          return require(mapping[name])
        }
        const module = {exports: {}}
        fn(localRequire, module, module.exports)
        return module.exports
      }
      require('${this.entry}')
    })({${modules}})
  `

entry是入口地址, modules是处理后的dependence;

generateBundle

那generateBundle为

generateBundle(dependenceMap) {
        let modules = '';
        for (let filename in dependenceMap) {
            let mod = dependenceMap[filename]
            modules += `'${filename}': {
                code: function(require, module, exports) {
                    ${mod.code}
                },
                mapping:  ${JSON.stringify(mod.mapping)},
            },`
        }
        const result = `
            (function(modules) {
                function require(moduleId) {
                    const {code, mapping} = modules[moduleId]
                    function localRequire(name) {
                        return require(mapping[name])
                    }
                    const module = {exports: {}}
                    code(localRequire, module, module.exports)
                    return module.exports
                }
                require('${this.entry}')
            })({${modules}})
        `;
        return result;
    }

outputBundle

最后的outputBundle方法

// 输出打包文件
    outputBundleFile(bundle) {
        const filePath = path.join(this.output.path, this.output.filename)
        fs.access(filePath, (err) => {
            if (!err) {
                fs.writeFileSync(filePath, bundle, 'utf-8');
            } else {
                console.log(err)
                fs.mkdir(this.output.path, {
                    recursive: true
                }, (err) => {
                    if (err) throw err;
                    fs.writeFileSync(filePath, bundle, 'utf-8');
                });
            }
        });
    }

run

run() {
    this.outputBundleFile(this.generateBundle(this.handleDependence(this.entry)));
}

bundle.js

(function (modules) {
    function require(moduleId) {
        const {
            code,
            mapping
        } = modules[moduleId]

        function localRequire(name) {
            return require(mapping[name])
        }
        const module = {
            exports: {}
        }
        code(localRequire, module, module.exports)
        return module.exports
    }
    require('./src/entry.js')
})({
    './src/entry.js': {
        code: function (require, module, exports) {
            "use strict";

            var _hello = _interopRequireDefault(require("./hello.js"));

            var _world = require("./world.js");

            function _interopRequireDefault(obj) {
                return obj && obj.__esModule ? obj : {
                    "default": obj
                };
            }

            (0, _hello["default"])(_world.world);
            console.log('hello, webpack');
        },
        mapping: {
            "./hello.js": "src/hello.js",
            "./world.js": "src/world.js"
        },
    },
    'src/hello.js': {
        code: function (require, module, exports) {
            "use strict";

            Object.defineProperty(exports, "__esModule", {
                value: true
            });
            exports["default"] = hello;

            function hello(name) {
                console.log("hello ".concat(name, "!"));
            }
        },
        mapping: undefined,
    },
    'src/world.js': {
        code: function (require, module, exports) {
            "use strict";

            Object.defineProperty(exports, "__esModule", {
                value: true
            });
            exports.world = void 0;
            var world = 'world';
            exports.world = world;
        },
        mapping: undefined,
    },
})

放到浏览器中,可以完美执行。

hello world!
hello, webpack

完整代码

const fs = require("fs")
const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default
const path = require("path")
const options = require("./webpack.config.js");
const {
    transformFromAst
} = require("@babel/core")

class Webpack {
    constructor(options) {
        const {
            entry,
            output
        } = options
        this.entry = entry
        this.output = output
        this.modulesArr = []
    }
    // 主逻辑
    run() {
        this.outputBundleFile(this.generateBundle(this.handleDependence(this.entry)));
    }
    // 处理逻辑依赖
    handleDependence(entry) {
        // 生成模块依赖图
        const dependenceGraph = {
            [entry]: this.createAsset(entry),
        }

        // 递归遍历
        const recursionDep = (filename, assert) => {
            // 维护相对路径和绝对路径的对应关系
            assert.mapping = {};
            // 返回路径的目录名
            const dirname = path.dirname(filename);
            assert.dependencies.forEach(relativePath => {
                const absolutePath = path.join(dirname, relativePath);
                // 设置相对路径和绝对路径的对应关系
                assert.mapping[relativePath] = absolutePath;
                // 如果没有该绝对路径的依赖关系
                if (!dependenceGraph[absolutePath]) {
                    const child = this.createAsset(absolutePath);
                    dependenceGraph[absolutePath] = this.createAsset(absolutePath);
                    if (child.dependencies.length > 0) {
                        // 递归
                        recursionDep(absolutePath, child);
                    }

                }
            })
        }

        for (let filename in dependenceGraph) {
            let assert = dependenceGraph[filename]
            recursionDep(filename, assert);
        }
        return dependenceGraph;
    }
    createAsset(entry) {
        // 读取文件内容
        const entryContent = fs.readFileSync(entry, 'utf-8')
        // 使用 @babel/parser(JavaScript解析器)解析代码,生成 ast(抽象语法树)
        const ast = parser.parse(entryContent, {
            sourceType: "module"
        })

        // 从 ast 中获取所有依赖模块(import),并放入 dependencies 中
        const dependencies = []
        traverse(ast, {
            // 遍历所有的 import 模块,并将相对路径放入 dependencies
            ImportDeclaration: ({
                node
            }) => {
                dependencies.push(node.source.value)
            }
        })

        // 获取文件内容
        const {
            code
        } = transformFromAst(ast, null, {
            presets: ['@babel/preset-env'],
        });

        return {
            code,
            dependencies,
        }
    }
    generateBundle(dependenceMap) {
        let modules = '';
        for (let filename in dependenceMap) {
            let mod = dependenceMap[filename]
            modules += `'${filename}': {
                code: function(require, module, exports) {
                    ${mod.code}
                },
                mapping:  ${JSON.stringify(mod.mapping)},
            },`
        }
        const result = `
            (function(modules) {
                function require(moduleId) {
                    const {code, mapping} = modules[moduleId]
                    function localRequire(name) {
                        return require(mapping[name])
                    }
                    const module = {exports: {}}
                    code(localRequire, module, module.exports)
                    return module.exports
                }
                require('${this.entry}')
            })({${modules}})
        `;
        return result;
    }
    // 输出打包文件
    outputBundleFile(bundle) {
        const filePath = path.join(this.output.path, this.output.filename)
        fs.access(filePath, (err) => {
            if (!err) {
                fs.writeFileSync(filePath, bundle, 'utf-8');
            } else {
                console.log(err)
                fs.mkdir(this.output.path, {
                    recursive: true
                }, (err) => {
                    if (err) throw err;
                    fs.writeFileSync(filePath, bundle, 'utf-8');
                });
            }
        });
    }

}

(new Webpack(options)).run();

后续扩展

1.支持多文件

2.输出公共bundle

3.多线程打包

4.插件机制设计

等等

参考资料

1.https://juejin.im/post/5e0d52716fb9a047f0002407?utm_source=gold_browser_extension

2.https://mp.weixin.qq.com/s/uTAJZoqFFDn5cfkwcYr11Q


儿独
729 声望25 粉丝