前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >带你秒懂 Webpack 原理

带你秒懂 Webpack 原理

作者头像
前端bubucuo
发布2022-09-16 17:42:54
4550
发布2022-09-16 17:42:54
举报
文章被收录于专栏:bubucuo

以下文章来源于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\";\n\nvar _a = require(\"./a.js\");\n\nconsole.log('Hello bundle ', _a.a);"},"./src/a.js":{"dependencies":{"./b.js":"./src/b.js"},"code":"\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n  value: true\n});\nexports.a = void 0;\n\nvar _b = require(\"./b.js\");\n\nvar a = 'A' + _b.b;\nexports.a = a;"},"./src/b.js":{"dependencies":{},"code":"\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n  value: true\n});\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. 输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统。

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

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-11-26,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 bubucuo 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档