webpack原理分析

webpack原理分析

写在前面:
这篇文章,我是学习了webpack打包原理 ? 看完这篇你就懂了 !后写的, 照着这篇文章的内容,自己手动实现了一遍,收益颇丰。在这里对原作者表示感谢,侵删。文章非绝对原创,特此声明。

什么是webpack

本质上,webpack是一个现代JavaScript应用程序的静态模块打包器(module bundler)。当webpack处理应用程序是,它会递归的构建一个依赖关系图(dependency graph), 其中包含应用程序需要的每个模块,然后将所有这些模块打包成一个或者多个bundle。

webpack就像一条生产线,要经过一系列处理流程后,才能将源文件转换成输出结果。这条生产线上的每个处理流程的智者都是单一的,多个流程之间存在依赖关系,只要当完成当前处理后,才能交给下一个流程去处理。插件就是一个插入到生产线中的功能,在特定时间,对生产线上的资源做处理。

Webpack 通过Tapable来组织这条复杂的生产线。webpack在运营过程中会广播事件,插件只需要监听它所关心的事件,就能加入到这条生产线中,去改变生产线的运作。webpack的事件流机制,保证了插件的有序性。使得整个扩展性很好。—深入浅出webpack吴浩麟

Tapable 是一个小型的库,允许你对一个 javascript 模块添加和应用插件。它可以被继承或混入到其他模块中。类似于 NodeJS 的 EventEmitter 类,专注于自定义事件的触发和处理。除此之外,Tapable 还允许你通过回调函数的参数,访问事件的“触发者(emittee)”或“提供者(producer)”。

其中下面的这段代码,最难理解,所有的import的代码会用一个require来加载,当把入口require后,原有的require方法就会在自执行匿名函数的作用域中,被重写。从而实现对相对路径资源的引用。

Webpack的核心概念

Entry

入口起点(enter point)指示webpack应该使用哪个模块,来作为构建其内部依赖图的开始。相当于树的根节点。

进入入口起点后,webpack会将代码解析成AST语法树,然后webpack会根据语法树找出有哪些模块和库是入口起点(直接和间接)依赖的。

每个依赖项随即被处理,最后输出到bundle的文件中。

Output

Output属性告诉webpack在哪里输出它所创建的bundles,以及如何命名这些文件,默认值为./dist。如果不做特殊指定的话,例如external,代码都会被编译到你指定的输出文件中。

Module

模块,在webpack里一切皆模块,一个模块对应着一个文件,比如js是一个模块,scss也是一个模块。webpack会从配置的Entry开始递归找出所有依赖的模块。

Chunk

这是 webpack 特定的术语被用在内部来管理 building 过程。bundle 由 chunk 组成,其中有几种类型(例如,入口 chunk(entry chunk) 和子 chunk(child chunk))。通常 chunk 会直接对应所输出的 bundle,但是有一些配置并不会产生一对一的关系。

简单来讲,写的代码是module,编译中的代码叫chunk,输出的代码叫bundle.

Loader

loader让webpack能够去处理那些非JavaScript文件(webpack 自身职能理解JavaScript).

loader可以将所有类型的文件,转换成为webpack能处理的有效模块,然后你就可以利用webpack的打包能力,对他们进行处理。

本质上,webpack loader将所有类型的文件,转换为应用程序的依赖图(和最终的bundle)可直接引用的模块。

Plugin

Loader被用于转换某些类型的模块,而插件则可以用于执行更广泛的任务。

插件的范围包括,从打包优化和压缩,一直到重新定义环境中的变量。插件接口功能极其强大,可以用来处理各种各样的任务。

Webpack构建流程

webpack的运行流程是一个串行的过程,从启动到结束会一次执行以下流程。

  1. 初始化参数:从配置文件和shell渔具中读取、合并参数,并得出最终的webpack配置。
  2. 开始编译:根据上一步得到的参数初始化Compiler对象,加载所有配置的插件,执行对象的run方法开始执行编译。
  3. 确定入口:根据配置文件中的entry找出所有的入口文件。
  4. 编译模块:从入口文件触发,调用所有配置的Loader对模块进行翻译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理。
  5. 完成模块编译:在经过第4步使用loader翻译完所有模块后,得到每个模块被翻译后的最终内容,以及他们之间的依赖关系。
  6. 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的Chunk,再把每个Chunk转换成一个单独的文件,加入到输出列表,这步是可以修改输出内容的最后机会。
  7. 输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件写入到文件系统。

在以上过程中,Webpack会在特定的时间点广播出特定的事件,插件在监听到感兴趣的事件后,会执行响应的逻辑,并且插件可以调用Webpack提供的API改变Webpack的运行结果。

实践加深理解,写一个webpackDemo

1. 定义一个compiler类

​ 这个类相当于webpack的核心框架。

class Compiler {
  constructor(options) {
    // webpack 配置
    const { entry, output } = options
    // 入口
    this.entry = entry
    // 出口
    this.output = output
    // 模块
    this.modules = []
  }
  // 构建启动
  run() {}
  // 重写 require函数,输出bundle
  generate() {}
}
2.解析入口文件,获取AST语法树

创建webpack配置文件:

// webpack.config.js

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

使用@babel/parser来帮助我们分析代码内部的语法,返回一个AST抽象语法树。

const fs = require('fs')
const parser = require('@babel/parser')
const options = require('./webpack.config')

const Parser = {
  getAst: path => {
    // 读取入口文件
    const content = fs.readFileSync(path, 'utf-8')
    // 将文件内容转为AST抽象语法树
    return parser.parse(content, {
      sourceType: 'module'
    })
  }
}

class Compiler {
  constructor(options) {
    // webpack 配置
    const { entry, output } = options
    // 入口
    this.entry = entry
    // 出口
    this.output = output
    // 模块
    this.modules = []
  }
  // 构建启动
  run() {
    const ast = Parser.getAst(this.entry)
  }
  // 重写 require函数,输出bundle
  generate() {}
}

new Compiler(options).run()

创建我们的业务代码

// index.js
import welcome from './utils.js';
console.log('Hello biubiu!')
welcome();

// ./utils.js
export default function ppt() {
    console.log('Welcome to webpack!');
}
3. 找出所有依赖模块

Babel提供了@babel/traverse(遍历)方法来维护这个AST预发树的整体状态,我们在这里使用它来找出依赖模块。

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

const Parser = {
  getAst: path => {
    // 读取入口文件
    const content = fs.readFileSync(path, 'utf-8')
    // 将文件内容转为AST抽象语法树
    return parser.parse(content, {
      sourceType: 'module'
    })
  },
  getDependecies: (ast, filename) => {
    console.log('ast', ast);
    const dependecies = {}
    // 遍历所有的 import 模块,存入dependecies
    traverse(ast, {
      // 类型为 ImportDeclaration 的 AST 节点 (即为import 语句)
      /**
     * ImportDeclaration({ node }) {}的写法,等同于下面的写法
     * ImportDeclaration: function ImportDeclaration({ node }) {
        const dirname = path.dirname(filename)
        // 保存依赖模块路径,之后生成依赖关系图需要用到
        const filepath = './' + path.join(dirname, node.source.value)
        dependecies[node.source.value] = filepath
      }
     */
      ImportDeclaration({ node }) {
        const dirname = path.dirname(filename)
        // 保存依赖模块路径,之后生成依赖关系图需要用到
        const filepath = './' + path.join(dirname, node.source.value)
        dependecies[node.source.value] = filepath
      }
    })
    return dependecies
  }
}

class Compiler {
  constructor(options) {
    // webpack 配置
    const { entry, output } = options
    // 入口
    this.entry = entry
    // 出口
    this.output = output
    // 模块
    this.modules = []
  }
  // 构建启动
  run() {
    const { getAst, getDependecies } = Parser
    const ast = getAst(this.entry)
    const dependecies = getDependecies(ast, this.entry)
  }
  // 重写 require函数,输出bundle
  generate() {}
}

new Compiler(options).run()
4.将AST转化为Code

将AST 转化为浏览器可执行的代码,我们主要使用的是@babel/core和@babel/preset-env.

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

const options = require('./webpack.config.js');

const Parser = {
    getAst: path => {
        const content = fs.readFileSync(path, 'utf-8');

        return parser.parse(content, {
            sourceType: 'module'
        })
    },
    getDependecies: (ast, fileName) => {
        const dependecies = {};
        // 类型为importDeclaration的AST 节点
        console.log(ast);
        traverse(ast, {
            ImportDeclaration: function ImportDeclaration({node}) {
                // console.log(node);
                const dirname = path.dirname(fileName);
                // 保存依赖模块路径,之后生成依赖关系图的时候用到
                const filepath = './' + path.join(dirname, node.source.value);
                // console.log("ImportDeclaration", node, fileName, filepath);
                dependecies[node.source.value] = filepath;
            }
        })s
        return dependecies;
    },
    getCode: (ast) => {
        // ast 转为code
        const { code } = transformFromAst(ast, null, {
            presets: ['@babel/preset-env']
        });
        return code;
    }
};

class Compiler {
    constructor(options) {
        const { entry, output } = options;

        this.entry = entry;

        this.output = output;
        // 模块
        this.modules = [];
    }

    // 构建启动
    run() {
        const ast = Parser.getAst(this.entry);
        // console.log(ast);
        const dependecies = Parser.getDependecies(ast,this.entry);
        // console.log(dependecies);

        const code = Parser.getCode(ast);
        // console.log(code);
    }
}

new Compiler(options).run();
5. 递归解析所有依赖项,生成依赖关系图
const fs = require('fs');
const path = require('path');
const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const { transformFromAst } = require('@babel/core');

const options = require('./webpack.config.js');

const Parser = {
    getAst: path => {
        const content = fs.readFileSync(path, 'utf-8');

        return parser.parse(content, {
            sourceType: 'module'
        })
    },
    getDependecies: (ast, fileName) => {
        const dependecies = {};
        // 类型为importDeclaration的AST 节点
        console.log(ast);
        traverse(ast, {
            ImportDeclaration: function ImportDeclaration({node}) {
                // console.log(node);
                const dirname = path.dirname(fileName);
                // 保存依赖模块路径,之后生成依赖关系图的时候用到
                const filepath = './' + path.join(dirname, node.source.value);
                // console.log("ImportDeclaration", node, fileName, filepath);
                dependecies[node.source.value] = filepath;
            }
        })

        return dependecies;
    },
    getCode: (ast) => {
        // ast 转为code
        const { code } = transformFromAst(ast, null, {
            presets: ['@babel/preset-env']
        });
        return code;
    }
};

class Compiler {
    constructor(options) {
        const { entry, output } = options;

        this.entry = entry;

        this.output = output;
        // 模块
        this.modules = [];
    }

    build(fileName) {
        const { getAst, getDependecies, getCode } = Parser;
        const ast = getAst(fileName);
        
        const dependecies = getDependecies(ast, fileName);

        const code = getCode(ast);

        return {
            // 文件路径,可以作为每个模块的唯一标识符
            fileName,
            // 依赖对象,保存着依赖模块路径
            dependecies,
            // 文件内容
            code

        }

    }

    run() {
        // 解析入口文件
        const info = this.build(this.entry);

        this.modules.push(info);
        this.modules.forEach(({dependecies}) => {
            // 判断有一栏对象,递归解析所有的依赖项
            if(dependecies) {
                for(const dependecy in dependecies) {
                    this.modules.push(this.build(dependecies[dependecy]));
                }
            }
        });
        // 生成依赖关系图
        const dependecyGraph = this.modules.reduce((graph, item) => ({
            ...graph,
            //使用文件路径作为每个模块唯一标识符,保存对应模块的依赖对象和文件内容
            [item.fileName]: {
                dependecies: item.dependecies,
                code: item.code
            }
        }), {});

        // console.log(dependecyGraph);
    }
   
}

new Compiler(options).run();
6. 重写require函数,生成代码,输出bundle
const fs = require('fs');
const path = require('path');
const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const { transformFromAst } = require('@babel/core');

const options = require('./webpack.config.js');

const Parser = {
    getAst: path => {
        const content = fs.readFileSync(path, 'utf-8');

        return parser.parse(content, {
            sourceType: 'module'
        })
    },
    getDependecies: (ast, fileName) => {
        const dependecies = {};
        // 类型为importDeclaration的AST 节点
        console.log(ast);
        traverse(ast, {
            ImportDeclaration: function ImportDeclaration({node}) {
                // console.log(node);
                const dirname = path.dirname(fileName);
                // 保存依赖模块路径,之后生成依赖关系图的时候用到
                const filepath = './' + path.join(dirname, node.source.value);
                // console.log("ImportDeclaration", node, fileName, filepath);
                dependecies[node.source.value] = filepath;
            }
        })

        return dependecies;
    },
    getCode: (ast) => {
        // ast 转为code
        const { code } = transformFromAst(ast, null, {
            presets: ['@babel/preset-env']
        });
        return code;
    }
};

class Compiler {
    constructor(options) {
        const { entry, output } = options;

        this.entry = entry;

        this.output = output;
        // 模块
        this.modules = [];
    }

    build(fileName) {
        const { getAst, getDependecies, getCode } = Parser;
        const ast = getAst(fileName);
        
        const dependecies = getDependecies(ast, fileName);

        const code = getCode(ast);

        return {
            // 文件路径,可以作为每个模块的唯一标识符
            fileName,
            // 依赖对象,保存着依赖模块路径
            dependecies,
            // 文件内容
            code
        }
    }

    run() {
        // 解析入口文件
        const info = this.build(this.entry);

        this.modules.push(info);
        this.modules.forEach(({dependecies}) => {
            // 判断有一栏对象,递归解析所有的依赖项
            if(dependecies) {
                for(const dependecy in dependecies) {
                    this.modules.push(this.build(dependecies[dependecy]));
                }
            }
        });
        // 生成依赖关系图
        const dependecyGraph = this.modules.reduce((graph, item) => ({
            ...graph,
            //使用文件路径作为每个模块唯一标识符,保存对应模块的依赖对象和文件内容
            [item.fileName]: {
                dependecies: item.dependecies,
                code: item.code
            }
        }), {});

        // console.log(dependecyGraph);

        this.generate(dependecyGraph);
    }

    // 重写require函数(浏览器不能识别common.js语法),输出bundle
    generate(code) {
        const filepath = path.join(this.output.path, this.output.fileName);
        const bundle = `(function (graph) {
            ;
        
            function require(moduleId) {
                const exports = {}
                ;function localRequire(relativePath) {
                    return require(graph[moduleId].dependecies[relativePath]);
                }
        
                ;(function(require, exports, code){
                    eval(code);
                }(localRequire, exports, graph[moduleId].code))
        
                return exports;
            }
            require('${this.entry}')
        })(${JSON.stringify(code)})`;
        fs.writeFileSync(filepath, bundle, 'utf-8');
    }
}

new Compiler(options).run();
7. 深入剖析require的逻辑

这里有一个前提是,如果把代码想成一个树的话,那么entry就是根节点所在,正因为有了根节点的存在,我们才能通过重写require的方法,来实现这个逻辑。

7.1 先定义一个require

先来个简单点的:

generate(code) {
        const filepath = path.join(this.output.path, this.output.fileName);
        const bundle = `(function (graph) {
            ;
        
            function require(moduleId) {
                console.log("moduleId", moduleId);
            }
            require('${this.entry}')
        })(${JSON.stringify(code)})`;
        fs.writeFileSync(filepath, bundle, 'utf-8');
    }

输出的结果是:

(function (graph) {
    ;

    function require(moduleId) {
        console.log(moduleId);
    }
    require('./src/index.js')
})({
    "./src/index.js": {
        "dependecies": {
            "./utils.js": "./src/utils.js"
        },
        "code": "\"use strict\";\n\nvar _utils = _interopRequireDefault(require(\"./utils.js\"));\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { \"default\": obj }; }\n\nconsole.log('Hello biubiu!');\n(0, _utils[\"default\"])();"
    },
    "./src/utils.js": {
        "dependecies": {},
        "code": "\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n  value: true\n});\nexports[\"default\"] = ppt;\n\nfunction ppt() {\n  console.log('Welcome to webpack!');\n}"
    }
})
7.2 获取可执行代码

通过moduleId取出graph中的代码,然后执行。

generate(code) {
        const filepath = path.join(this.output.path, this.output.fileName);
        const bundle = `(function (graph) {
            ;
        
            function require(code) {
                console.log(code);
                eval(code);
            }(graph[moduleId].code)
            require('${this.entry}')
        })(${JSON.stringify(code)})`;
        fs.writeFileSync(filepath, bundle, 'utf-8');
    }

输出结果:

(function (graph) {
    ;

    function require(code) {
        console.log(code);
        eval(code);
    }(graph[moduleId].code)
    require('./src/index.js')
})({
    "./src/index.js": {
        "dependecies": {
            "./utils.js": "./src/utils.js"
        },
        "code": "\"use strict\";\n\nvar _utils = _interopRequireDefault(require(\"./utils.js\"));\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { \"default\": obj }; }\n\nconsole.log('Hello biubiu!');\n(0, _utils[\"default\"])();"
    },
    "./src/utils.js": {
        "dependecies": {},
        "code": "\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n  value: true\n});\nexports[\"default\"] = ppt;\n\nfunction ppt() {\n  console.log('Welcome to webpack!');\n}"
    }
})

执行后发现会报错,是因为./src/utils.js 模块没有被执行。

7.3 依赖对象寻址映射,获取 exports 对象
generate(code) {
        const filepath = path.join(this.output.path, this.output.fileName);
        const bundle = `(function (graph) {
            ;
        
            // 重写require函数
            function require(moduleId) {
              // 找到对应moduleId的依赖对象,调用require函数,eval执行,拿到exports对象
              function localRequire(relativePath) {
                return require(graph[moduleId].dependecies[relativePath]) // {__esModule: true, say: ƒ say(name)}
              }
              // 定义exports对象
              var exports = {}
              ;(function(require, exports, code) {
                // commonjs语法使用module.exports暴露实现,我们传入的exports对象会捕获依赖对象(hello.js)暴露的实现(exports.say = say)并写入
                eval(code)
              })(localRequire, exports, graph[moduleId].code)
              // 暴露exports对象,即暴露依赖对象对应的实现
              return exports
            }
            // 从入口文件开始执行
            require('${this.entry}')
        })(${JSON.stringify(code)})`;
        fs.writeFileSync(filepath, bundle, 'utf-8');
    }

参考文档:

Webpack概念术语

webpack打包原理 ? 看完这篇你就懂了 !

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值