带你秒懂 Webpack 原理

2022-09-16 17:42:54 浏览数 (1)

以下文章来源于Enjo ,作者Enjo

没理

bundle bundle bundle 重要的事情讲三遍

‍‍‍一个基础的打包编译工具可以做什么‍‍‍

1.解析

a. 词法分析:分词,生成 tokens

b. 语法分析:生成抽象语法树 ast

2.转换

a. ES5 语法转换为 ES5

b. 处理模块,收集依赖

3. 生成code

生成一个可以在浏览器加执行的 js 文件。

webpack 是什么?

它是一个将一切资源(如scripts / images / styles/ assets)都当成模块的模块化打包工具。

webpack 是如何生成 bundle 的?

生成 bundle 文件后,html 页面就可以利用 script 标签的 src 去引入该文件。

实现一个基础版的 webpack

所谓基础版的 webpack 是指不包含 不包含 不包含 loader 和 plugin。

1. 先看一个demo

打包后的文件

2. 实现 webpack

测试文件 bundle.js

测试文件 bundle.js

2.1 定义 Webpack 类

定义 Webpack 类

2.2 实现 lib/webpack.js

第一步:从入口文件出发,读取入口文件内容

实现:定义 readFile 方法,读取文件内容

第二步:文件内容转化为 AST

定义parseContent 方法,使用 @babel/parser 的 parse 方法以模块模式将文件内容转化为抽象语法树 AST。

第三步:收集依赖关系

对 AST 节点进行递归遍历,收集当前模块的依赖关系,保存在 dependencies 里。 通过 @babel/traverse 模块对AST节点进行遍历,找到 type是 ImportDeclaration 的节点,保存当前节点的依赖关系。

第四步:AST 转化为 code

通过 @babel/core 模块的 transformFromAst 方法把 AST 转化为可执行的代码。

第五步:生成当前文件完整的文件关系依赖映射

经过第一~第五步,生成当前modulePath的关系依赖映射:

{ filename: modulePath, // 文件路径,形如 ./src/index.js dependencies, // 当前文件的依赖模块,形如{'./a.js': './src/a.js'} code // 浏览器可执行的代码 }

第六步:生成关系图谱

从入口文件出发,对当前文件的所有依赖都执行第一~第五步的过程,递归遍历当前模块依赖的所有模块,最后生成依赖关系图谱 graph。

最后,生成 bundle

把依赖关系图谱转换为浏览器可执行的 JS 文件。 1. 从入口文件开始执行生成的代码片段 code; 2. 模块是否有依赖:

a. 有依赖,则自定义 require 方法,把模块的相对路径转化为相对于根目录的路径,如 require("./a.js") ---> require("./src/a.js"); b. 无依赖,执行 code 即可 3. 导出的模块都挂载在 exports 上。

生成的代码可以在浏览器直接输出结果,大家可以试下:

代码语言:javascript复制
(function(graph) {
    // webpackBootstrap
    function require(modulePath){
        // 缺失了 require 补齐:require("./a.js") ---> require("./src/a.js")
        function localRequire(relativePath) {
            return require(graph[modulePath].dependencies[relativePath]);
        }
        const exports = {};
        // 自执行函数前的表达式或者方法执行必须加分号,否则自执行函数会被当作表达式或者方法的参数执行
        (function(require, exports, code){
            eval(code);
        })(localRequire, exports, graph[modulePath].code)
        
        return exports;
    }
    require('./src/index.js');
})({"./src/index.js":{"dependencies":{"./a.js":"./src/a.js"},"code":""use strict";nnvar _a = require("./a.js");nnconsole.log('Hello bundle ', _a.a);"},"./src/a.js":{"dependencies":{"./b.js":"./src/b.js"},"code":""use strict";nnObject.defineProperty(exports, "__esModule", {n  value: truen});nexports.a = void 0;nnvar _b = require("./b.js");nnvar a = 'A'   _b.b;nexports.a = a;"},"./src/b.js":{"dependencies":{},"code":""use strict";nnObject.defineProperty(exports, "__esModule", {n  value: truen});nexports.b = void 0;nvar b = 'B';nexports.b = b;"}})
        

完整代码

代码语言:javascript复制
const webpack = require('./lib/webpack-wechat.js')
const config = require('./webpack.config.js')

new webpack(config)

// webpack.js
const fs = require('fs')
const path = require('path')
const parser  = require('@babel/parser')
const traverse = require('@babel/traverse').default
const babel = require('@babel/core')

class Webpack {
    constructor(options) {
        this.entry = options.entry
        this.output = options.output
        this.bundleFile()
    }
    // 1. 读取文件内容
    readFile(modulePath) {
        const content = fs.readFileSync(modulePath, 'utf-8')
        return content
    }
    // 2. 解析文件内容生成 ast
    parseContent(content) {
        const ast = parser.parse(content, {
            sourceType: 'module'
        })
        return ast
    }
    // 3. 从入口文件开始收集模块的依赖,生成依赖关系图
    // 收集单个模块的依赖,生成 code
    transform(modulePath) {
        const content = this.readFile(modulePath)
        const ast = this.parseContent(content)
        const dependencies = {}
        traverse(ast, {
            ImportDeclaration({ node }) {
                const dirname = path.dirname(modulePath)
                dependencies[node.source.value] = './'   path.join(dirname, node.source.value)
            }
        })
        const { code } = babel.transformFromAst(ast, null, {
            presets: ['@babel/preset-env']
        })
        return {
            filename: modulePath,
            dependencies,
            code
        }
    }
    generateDependenciesGraph(entry) {
        const entryModule = this.transform(entry)
        const graphArray = [entryModule]

        for (let i = 0; i < graphArray.length; i   ) {
            const module = graphArray[i]
            const { dependencies } = module
            if (dependencies) {
                for (let key in dependencies) {
                    graphArray.push(this.transform(dependencies[key]))
                }
            }
        }

        let graph = {}
        graphArray.forEach(item => {
            const { filename, dependencies, code } = item
            graph[filename] = {
                dependencies,
                code
            }
        })
        return graph
    }
    // 生成 bundle
    bundleFile() {
        const { entry } = this
        const graph = JSON.stringify(this.generateDependenciesGraph(entry))
        const content =  `
            (function(graph) {
                // webpackBootstrap
                function require(modulePath){
                    // 缺失了 require 补齐:require("./a.js") ---> require("./src/a.js")
                    function localRequire(relativePath) {
                        return require(graph[modulePath].dependencies[relativePath]);
                    }
                    const exports = {};
                    // 自执行函数前的表达式或者方法执行必须加分号,否则自执行函数会被当作表达式或者方法的参数执行
                    (function(require, exports, code){
                        eval(code);
                    })(localRequire, exports, graph[modulePath].code)
                    
                    return exports;
                }
                require('${entry}');
            })(${graph})
        `
        const { path: relativePath, filename } = this.output
        const bundlePath = path.join(relativePath, filename)
        fs.writeFileSync(bundlePath, content, 'utf-8')
    }
}

module.exports = Webpack

最后

本文只是实现了一个简单的 webpack,不包含 loader 和 plugin。让大家对 webpack 打包编译过程有个简单的了解。webpack 引入 loader 和 plugin 的完整流程推荐大家看下这篇文章:一文掌握Webpack编译流程

摘自上文:

1. 初始化参数: 从配置文件和 Shell 语句中读取与合并参数,得出最终的参数。

2. 开始编译: 根据我们的webpack配置注册好对应的插件调用 compile.run 进入编译阶段,在编译的第一阶段是 compilation,他会注册好不同类型的module对应的 factory,不然后面碰到了就不知道如何处理了。

3.编译模块: 进入 make 阶段,会从 entry 开始进行两步操作:第一步是调用 loaders 对模块的原始代码进行编译,转换成标准的JS代码, 第二步是调用 acorn 对JS代码进行语法分析,然后收集其中的依赖关系。每个模块都会记录自己的依赖关系,从而形成一颗关系树。

4. 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会。

5. 输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统。

文中如有不同见解,大家可以评论区讨论,欢迎大家提出宝贵的意见和建议。

0 人点赞