理解webpack原理,手写一个100行的webpack

webpack 是一个现代 JavaScript 应用程序的静态模块打包器(module bundler)

什么是webpack?

它是一个模块打包器,也可以引用官网的一幅图解释,我们可以看到webpack,可以分析各个模块的依赖关系,最终打包成我们常见的静态文件,.js 、 .css 、 .jpg 、.png。今天我们先不弄那么复杂,我们就介绍webpack是怎么分析ES6的模块依赖,怎么把ES6的代码转成ES5的。


image.png

动手前的思考

  1. 为了专注于打包器编写,就不自己设计API了,使用webpack的API就好。

  2. 第一版打包器只实现简单js打包功能,使代码能在浏览器端运行。

  3. 只实现单一入口的打包器

webpack打包js步骤

  1. 根据设置的入口文件,找到对应文件,并分析依赖。

  2. 解析[抽象语法树(AST)。

  3. 获取源码,并做适当修改,使代码能在浏览器端运行。

  4. 将入口文件以及依赖文件,通过模板打包到一个文件中。

分析webpack打包出来的文件

先创建一个简单的项目,执行打包命令npx webpack,得到dist目录下的文件bundle.js如下。


(function(modules) { // webpackBootstrap

// The module cache

var installedModules = {};

// The require function

function __webpack_require__(moduleId) {

// Check if module is in cache

if(installedModules[moduleId]) {

return installedModules[moduleId].exports;

}

// Create a new module (and put it into the cache)

var module = installedModules[moduleId] = {

i: moduleId,

l: false,

exports: {}

};

// Execute the module function

modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);

// Flag the module as loaded

module.l = true;

// Return the exports of the module

return module.exports;

}

// expose the modules object (__webpack_modules__)

__webpack_require__.m = modules;

// expose the module cache

__webpack_require__.c = installedModules;

// define getter function for harmony exports

__webpack_require__.d = function(exports, name, getter) {

if(!__webpack_require__.o(exports, name)) {

Object.defineProperty(exports, name, { enumerable: true, get: getter });

}

};

// define __esModule on exports

__webpack_require__.r = function(exports) {

if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {

Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });

}

Object.defineProperty(exports, '__esModule', { value: true });

};

// create a fake namespace object

// mode & 1: value is a module id, require it

// mode & 2: merge all properties of value into the ns

// mode & 4: return value when already ns object

// mode & 8|1: behave like require

__webpack_require__.t = function(value, mode) {

if(mode & 1) value = __webpack_require__(value);

if(mode & 8) return value;

if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;

var ns = Object.create(null);

__webpack_require__.r(ns);

Object.defineProperty(ns, 'default', { enumerable: true, value: value });

if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));

return ns;

};

// getDefaultExport function for compatibility with non-harmony modules

__webpack_require__.n = function(module) {

var getter = module && module.__esModule ?

function getDefault() { return module['default']; } :

function getModuleExports() { return module; };

__webpack_require__.d(getter, 'a', getter);

return getter;

};

// Object.prototype.hasOwnProperty.call

__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };

// __webpack_public_path__

__webpack_require__.p = "";

// Load entry module and return exports

return __webpack_require__(__webpack_require__.s = "./src/index.js");

})

/************************************************************************/

({

/***/ "./src/index.js":

/*!**********************!*\

  !*** ./src/index.js ***!

  \**********************/

/*! no static exports found */

/***/ (function(module, exports) {

eval("console.log('index.js')\n\n//# sourceURL=webpack:///./src/index.js?");

/***/ })

});

可以看到上面的代码主要分两部分:

  1. webpack_require 是自定义的webpack加载函数
  2. 资源文件列表,键是文件路径, 值是一个函数对象。
    然后把上面的代码可整理成一个EJS文件模板,具体可以参考下面的打包文件模板, 用于将打包后的资源列表渲染成具体的打包文件。

创建命令目录

先创建一个命令目录,用于编写自己的webpack打包命令。
目录如下:


image.png

安装依赖包

由于ES6转ES5中需要用到babel,所以要用到一下插件

npm install  --save-dev 
@babel/generator 
@babel/parser 
@babel/traverse 
@babel/types 
babylon 
ejs 
tapable

相关插件说明:

  • @babel/parser 将源代码解析成 AST。
  • @babel/generator 将AST解码生 js代码。
  • @babel/core 包括了整个babel工作流,也就是说在@babel/core里面我们会使用到@babel/parser、transformer[s]、以及@babel/generator。
  • @babel/runtime 也是工具类,但是是为了babel编译时提供一些基础工具库。作用于transformer[s]阶段,当然这是一个工具库,如果要使用这个工具库,还需要引入@babel/plugin-transform-runtime,它才是transformer[s]阶段里面的主角。
  • @babel/template 也是工具类,主要用途是为parser提供模板引擎,更加快速的转化成AST
  • @babel/traverse 也是工具类,主要用途是来便利AST树,也就是在@babel/generator过程中生效。
  • @babel/types 也是工具类,主要用途是在创建AST的过程中判断各种语法的类型。

软链接

创建软链接,使用npm link将命令目录jd-pack映射到全局目录下,这样就可以使用全局命令打包了。

image.png

编写命令引导脚本

通过执行jd-pack命令执行的脚本,如下:

#! /usr/bin/env node

// 拿到webpack.config.js
let path = require('path');
let config = require(path.resolve('webpack.config.js'));
// 编译器
let Compiler = require("../lib/Compiler.js");
let compiler = new Compiler(config);

compiler.run();

编写编译器Compiler结构

编译器才是整个webpack的核心,它负责获取资源文件,解析资源文件(AST),最后生成打包的文件。
这里暂时不包括loader和plugin
首先编写Compiler文件结构,如下:

let fs = require('fs');
let path = require('path')
let babylon = require('babylon')
let t = require('@babel/types');
let traverse = require('@babel/traverse').default;
let generator = require('@babel/generator').default;
let ejs = require('ejs')
let {SyncHook} = require('tapable'); //发布订阅插件
//babylon 主要是把源码转换成ast抽象语法树
//@babel/traverse 遍历抽象语法树的工具,它会访问树中的所有节点,在进入每个节点时触发 enter 钩子函数,退出每个节点时触发 exit 钩子函数
//@babel/types 在创建AST的过程中判断各种语法的类型
//@babel/generator 将AST解码生 js代码
//@babel/parser 将源代码解析成 AST
class Compiler{
    constructor(config){
        //entry output
        this.config = config;
        // 文件入口
        this.entryId;  // ./src/index.js
        // 所有模块依赖
        this.modules = {}
        this.entry = config.entry;
        // 工作路径
        this.root = process.cwd();
    }
    buildModule(modulePath, isEntry){
    }
    emitFile(){// 发射文件
    }
    run(){
        //执行 
        this.buildModule(path.resolve(this.root, this.entry), true);
        //发射一个文件, 打包后的文件
        this.emitFile();
    }
}
module.exports = Compiler;

文件模块递归加载

接着实现文件模块加载方法buildModule,如下:

getSource(modulePath) {
        let content = fs.readFileSync(modulePath, 'utf8');
        return content;
}
buildModule(modulePath, isEntry){
        let source = this.getSource(modulePath);
        let moduleName = './' + path.relative(this.root, modulePath);
        if(isEntry) {
            this.entryId = moduleName; //保存入口名字
        }
        //改造source源码
        let {sourceCode, dependencies} = this.parse(source, path.dirname(moduleName));
        // console.log(sourceCode, dependencies)
        // 把相对路径和模块中内容对应起来
        this.modules[moduleName] = sourceCode;

        // dependencies.forEach(dep => {
        //     this.buildModule(path.join(this.root, dep), false);
        // })
    }

文件加载模块方法使用递归的方式加载所有的文件。

文件内容解析AST

ast语法树可以参考官网https://astexplorer.net/, 通过将代码解析成AST语法树,这里最主要的是将代码中的require方法替换为webpack自定义的webpack_require方法,如下:

parse(source, parentPath) { //AST解析语法树
        // console.log(source, parentPath)
        let ast = babylon.parse(source); //ast在线解析,如写require('./name.js')会解析成ast语法树 https://astexplorer.net/
        let dependencies = [];//依赖数组
        traverse(ast, {
            CallExpression(p){//调用表达式,如执行函数fun(),加载模块require()等
                let node = p.node;
                if (node.callee.name === 'require'){
                    node.callee.name = '__webpack_require__';
                    let moduleName = node.arguments[0].value;
                    moduleName = moduleName + (path.extname(moduleName)?'':'.js');
                    moduleName = './'+path.join(parentPath,moduleName); //'src/name.js'
                    dependencies.push(moduleName);
                    node.arguments = [t.stringLiteral(moduleName)];
                }
            }
        });
        let sourceCode = generator(ast).code;//将转换后的ast转为源码
        return {sourceCode, dependencies}
    }

打包文件模板

首先要拿到webpack默认打包生成的文件模板,然后把生所内容渲染在模板上即可,将webpack打包的文件内容精减后如下:

 (function(modules) {
    var installedModules = {};
    function __webpack_require__(moduleId) {
        if(installedModules[moduleId]) {
            return installedModules[moduleId].exports;
        }
        // Create a new module (and put it into the cache)
        var module = installedModules[moduleId] = {
            i: moduleId,
            l: false,
            exports: {}
        };
        modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
        module.l = true;
        return module.exports;
    }

    // Load entry module and return exports
    return __webpack_require__(__webpack_require__.s = "<%-entryId%>");
 })
/************************************************************************/
 ({
  <%for(let key in modules){%>
  "<%-key%>":
  (function(module, exports, __webpack_require__) {
    eval(`<%-modules[key]%>`);
  }),
  <%}%>

 });

发射打包的文件

执行到这里表示文件已经打包完毕,即可以生成文件到dist目录了,代码如下:

 emitFile(){// 发射文件
        //用数据渲染
        let main = path.join(this.config.output.path, this.config.output.filename);
        let templateStr = this.getSource(path.join(__dirname,'bundle.ejs'));
        let result = ejs.render(templateStr, {entryId: this.entryId, modules: this.modules})
        this.assets = {}
        this.assets[main] = result;
        fs.writeFileSync(main, this.assets[main]);
    }

完整源码如下

let fs = require('fs');
let path = require('path')
let babylon = require('babylon')
let t = require('@babel/types');
let traverse = require('@babel/traverse').default;
let generator = require('@babel/generator').default;
let ejs = require('ejs')
let {SyncHook} = require('tapable'); //发布订阅插件

class Compiler{
    constructor(config){
        //entry output
        this.config = config;
        // 文件入口
        this.entryId;  // ./src/index.js
        // 所有模块依赖
        this.modules = {}
        this.entry = config.entry;
        // 工作路径
        this.root = process.cwd();
    }
    getSource(modulePath) {
        let content = fs.readFileSync(modulePath, 'utf8');
        return content;
    }
    parse(source, parentPath) { //AST解析语法树
        // console.log(source, parentPath)
        let ast = babylon.parse(source); //ast在线解析,如写require('./name.js')会解析成ast语法树 https://astexplorer.net/
        let dependencies = [];//依赖数组
        traverse(ast, {
            CallExpression(p){//调用表达式,如执行函数fun(),加载模块require()等
                let node = p.node;
                if (node.callee.name === 'require'){
                    node.callee.name = '__webpack_require__';
                    let moduleName = node.arguments[0].value;
                    moduleName = moduleName + (path.extname(moduleName)?'':'.js');
                    moduleName = './'+path.join(parentPath,moduleName); //'src/name.js'
                    dependencies.push(moduleName);
                    node.arguments = [t.stringLiteral(moduleName)];
                }
            }
        });
        let sourceCode = generator(ast).code;//将转换后的ast转为源码
        return {sourceCode, dependencies}
    }
    buildModule(modulePath, isEntry){
        console.log(modulePath+"cccc");
        let source = this.getSource(modulePath);
        let moduleName = './' + path.relative(this.root, modulePath);
        console.log(source, modulePath+"cccc");
        if(isEntry) {
            this.entryId = moduleName; //保存入口名字
        }
        //改造source源码
        let {sourceCode, dependencies} = this.parse(source, path.dirname(moduleName));
        // console.log(sourceCode, dependencies)
        // 把相对路径和模块中内容对应起来
        this.modules[moduleName] = sourceCode;

        //递归解析更深的依赖
        dependencies.forEach(dep => {
            this.buildModule(path.join(this.root, dep), false);
        })
    }
    emitFile(){// 发射文件
        //用数据渲染
        let main = path.join(this.config.output.path, this.config.output.filename);
        let templateStr = this.getSource(path.join(__dirname,'bundle.ejs'));
        let result = ejs.render(templateStr, {entryId: this.entryId, modules: this.modules})
        this.assets = {}
        this.assets[main] = result;
        fs.writeFileSync(main, this.assets[main]);
    }
    run(){
        //执行 
        this.buildModule(path.resolve(this.root, this.entry), true);
        //发射一个文件, 打包后的文件
        this.emitFile(); 
    }
}

module.exports = Compiler;

❤️感谢大家
关注公众号「ITLove」即可加我好友,大家一起共同交流和进步。

你可能感兴趣的:(理解webpack原理,手写一个100行的webpack)