美文网首页
webpack打包bundle原理

webpack打包bundle原理

作者: 0月 | 来源:发表于2021-10-03 15:36 被阅读0次

    打包结果

    webpack打包后输出默认是dist/main.js;其中的每个js模块都会进行编译和进行依赖图谱graphMap记录。如下源码

    // src/index.js
    import { a } from './a.js'
    console.log(a)
    //////////////////////////////////////
    // src/a.js
    import { b } from './b.js'
    export const a = 'a' + b
    /////////////////////////////////////
    // src/b.js
    export const b = 'b'
    

    编译生成graphMap大概是这样子

    graphMap: {
      'src/index.js': {
          code: xxx,
          dependencies: {'./a.js':  'src/a.js'}
        },
      'src/a.js': {
          code: xxx,
          dependencies: {'./b.js': 'src/b.js'}
        },
      'src/b.js': {
          code: xxx,
          dependencies: {}
        }
    }
    

    现在我们尝试去研究一下整个流程是怎么样的。

    webpack的Compiler如何编译代码

    首先要有一个bundle.js; 它的作用主要是提供模块编译生成依赖图谱的功能。

    // bundle.js
    const fs = require('fs');
    const path = require('path');
    const babelParser = require('@babel/parser');
    const babelTraverse = require('@babel/traverse').default;
    const babel = require('@babel/core');
    
    // 模块依赖分析,传入入口,返回一个数组表示各个模块的相对路径和绝对路径
    const moduleAnalyser = fileName => {
        // 拿到源码内容
        const content = fs.readFileSync(fileName, 'utf-8')
    
        // 生成ast
        const ast = babelParser.parse(content, {
            sourceType: 'module'
        })
    
        // 分析ast 拿到每个依赖的相对路径和绝对路径
        const dependencies = {}
        babelTraverse(ast, {
            ImportDeclaration({ node }) {
                const dirname = path.dirname(fileName)
                const absolutePath = path.join('./', dirname, node.source.value) // 绝对路径
                dependencies[node.source.value] = absolutePath
            }
        })
    
        // 对ast进行转换为es5代码
        const { code } = babel.transformFromAst(ast, null, {
            "presets": [
                "@babel/preset-env"
            ]
        })
    
        // 返回入口路径、依赖关系、翻译好的es5源码
        return {
            fileName,
            dependencies,
            code
        }
    }
    
    // 从入口开始寻找依赖,生成依赖图谱
    const makeModuleGraph = entry => {
        const graph = {}
        const analyser = entry => {
            const { dependencies, code, fileName } = moduleAnalyser(entry)
            graph[fileName] = {
                code,
                dependencies
            }
    
            if (Object.getOwnPropertyNames(dependencies).length) {
                for (let i in dependencies) {
                    // 防止循环引用!
                    if (!graph[dependencies[i]]) {
                        analyser(dependencies[i])
                    }
                }
            }
        }
    
        analyser(entry)
    
        return graph
    }
    module.exports = makeModuleGraph;
    
    • 模块编译分三个步骤
      1. 通过babelParser将源码转换成ast
      2. 通过 babelTraverse遍历ast, 把import的相对路径提取出来,并且生成基于src目录的绝对路径
      3. 再通过babel.transformFromAst把ast转换为es5源码 code

    模块编译得到该模块的3个信息 fileName, dependencies, code

    • 生成依赖图谱分为两个步骤
      1. 准备一个空对象graph,从入口文件开始进行分析模块并把模块分析后的3个信息记录到graph中: fileName: { dependencies, code}
      2. 遇到有dependencies的,递归解析模块

    然后再提供一个编译类Compiler去进行编译整合工作

    // Compiler.js
    const makeModuleGraph = require('./bundle.js')
    
    class Compiler{
        constructor(config) {
            this.entry = config.entry // 入口
            this.output = config.output // 出口
        }
    
        // 生成最终打包输出的代码
        generateCode (entry) {
            const graph = makeModuleGraph(entry)
            const stringifyGraph = JSON.stringify(graph)
            const code = `
                ;(function(graph){
                    function require(relativePath) {
    
                        function getFullPath (path) {
                            return require(graph[relativePath].dependencies[path]);
                        }
    
                        var exports = {};
                        (function(require, code) {
                            eval(code);
                        })(getFullPath, graph[relativePath].code)
    
                        return exports;
                    }
                    
                    require('${entry}');
                })(${stringifyGraph});
            `
            return code
        }
    
        run() {
           const code = this.generateCode(this.entry)
           console.log(code)
        }
    }
    
    module.exports = Compiler
    

    其中最烧脑的部分就是generateCode 的模板字符串的那块逻辑,这得仔细品尝了

    配置与打包

    最后配置一个webpack.config.js

    const path = require('path');
    module.exports = {
        entry: './src/index.js',
        output: {
            path: path.resolve(__dirname, 'dist'),
            filename: '[name].js'
        }
    }
    

    与一个index.js来运行打包逻辑

    const Compiler = require('./Compiler.js')
    const config = require('./webpack.config.js')
    const compiler= new Compiler(config)
    compiler.run();
    

    目录结构如下,及打包命令 "build": "node ./index.js"

    image.png
    把打包输出的code复制粘贴到控制台进行验证,根据我们的预期应该是输出'ab'字符串的 npm run build.png

    符合预期!

    相关文章

      网友评论

          本文标题:webpack打包bundle原理

          本文链接:https://www.haomeiwen.com/subject/tnvvnltx.html