从零开始手写webpack(1)

从零开始手写webpack(1)

webpack简介

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

上面是 webpack中文网 对webpack的一句话介绍。其中几个关键词,现代javascrpt打包器 ,说的比较清楚, webpack 就是一个 打包 js代码的打包器。至于webpack能打包 图片、css、less、scss 等其他文件, 那都是loader或者plugin的功能。本文就是从零开始手写一个简单的 js打包器

动手前的思考

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

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

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

webpack打包js步骤

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

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

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

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

分析webpack打包出来的文件


(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?");

/***/ })

});

这是webpack打包后的文件,经过简化处理。简单分析一下。

  • 整个文件内的代码是一个大的IIFE函数

(function(modules) { })

({});

  • 传入的参数就是入口文件及其依赖文件的代码

(function(modules) { })

({

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

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

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

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

/*! no exports provided */

/***/ (function(module, __webpack_exports__, __webpack_require__) {

"use strict";

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

/***/ })

});

可以看出传入的参数是一个对象字面量,key 为文件路径, value 是一个函数,函数内部是 我们编写的代码源码。 这就是modules接收到的数据。

  • 函数体内所做的事就是加载模块,执行模块,缓存模块。

(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;

    }

    // Load entry module and return exports

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

})

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

({});

通过注释,可以看出来,函数体内,就是加载传入的模块,并且缓存这个模块。加载函数就是__webpack_require__, 在内部实现是加载模块并且执行模块,缓存这个模块,并且将当前模块标记为已经加载过。后续需要这个模块则直接读缓存。这个缓存的 key 是文件路径, value 是文件源码。 其他的代码暂且可以不用管。这个代码就能在浏览器端运行了。所以我们要做的就是想办法来生成这个文件。

开发前的小知识

为了方便调试,使用npm link来创建动态链接,实际就是软连接, 使用方法可以查看以下两个示例:

  1. 示例1

  2. 示例2

开始着手开发

首先,我们需要获取配置文件,这个就比较简单了。获取webpack.config.js就行了。


// pack.js

const path = require('path');

const config = require(path.resolve('webpack.config.js'));

然后我们需要编写一个编译器,这里我单独抽一个文件来写编译器,这个编译器只需要执行run方法,即可执行。


// pack.js

const path = require('path');

const config = require(path.resolve('webpack.config.js'));

const Compiler = require('../lib/Compiler');

const compiler = new Compiler(config);

compiler.run();

那么编译器的代码基本结构就有了


// lib/Compiler.js

class Compiler {

run() {

        console.log('run')

    }

}

module.exports = Compiler

我们在编译器里面需要做的是,拿到webpack.config.js里面的配置信息,解析入口,解析文件依赖关系,发射文件。

首先解析入口。


// lib/Compiler.js

const path = require('path');

class Compiler {

    constructor(config) {

        this.config = config;

        // 需要保存入口文件的路径

        this.entryId;

        // 需要保存所有模块的依赖

        this.modules = {};

        // 入口路径

        this.entry = config.entry;

        // 工作路径

        this.root = process.cwd();

    }

    run() {

        // 执行并且创建模块的依赖关系

        this.buildModule(path.resolve(this.root, this.entry), true);

        // 发射一个打包后的文件

        this.emitFile()

    }

    getSource(modulePath) {

        const content = fs.readFileSync(modulePath, 'utf8');

        return content;

    }

    /**

    * 构建模块

    */

    buildModule(modulePath, isEntry) {



    }

    /**

    * 发射文件

    */

    emitFile() {



    }

}

module.exports = Compiler

构建模块时,我们需要拿到模块的内容(我们编写的源码)。这个通过getSource函数拿到即可。我们还需要拿到模块id,即上文提到的文件路径。

那么buildModule函数可以写成


buildModule(modulePath, isEntry) {

    // 拿到模块的内容

    const source = this.getSource(modulePath);

    // 拿到模块的id modulePath - this.root

    const moduleName = './' + path.relative(this.root, modulePath);

    if (isEntry) {

        this.entryId = moduleName

    }

}

通过这一段代码,就能拿到入口文件的模块id模块内容了。也就是上文的打包后文件传入的模块列表中的参数了。接下来,我们需要做的就是解析入口文件里面的文件依赖,解析依赖文件的依赖,递归解析出所有文件的依赖。

举个例子:


// 入口文件 index.js

const a = require('./a');

const b = require('./b');

// a.js

console.log('aaaaaa');

// b.js

const c = require('./common/c');

console.log('bbbbb');

// common/c.js

console.log('cccccc');

那么我们就得把 index.jsa.jsb.jsc.js,通过require函数的参数分析出来依赖关系。这个时候,AST 抽象语法树便要发挥作用了。

我们并不用去实现一个,使用babylon.js这个库就好了。具体请看babylon

我们拿到语法树后,还需要遍历语法树,原因是 require 这个函数在浏览器端是没有的,直接这么把源码打包进去,在浏览器端是不能运行的。这也是为什么 webpack 打包后的文件里面有个 __webpack_require__ 函数,就是为了在浏览器端能加载模块的,所以我们要做的就是把源码中的require函数替换为__webpack_require__。另外,我们还需要用到两个库, @babel/types@babel/generator

这一步, 我们总共需要用到四个库, babylon@babel/traverse@babel/types@babel/generator

代码为:


/**

* 解析文件

*/

parse(source, parentPath){

    // AST 解析语法树

    const ast = babylon.parse(source);

    const dependencies = [];

    traverse(ast, {

        CallExpression(p) {

            // 对应的节点

            const {node} = p;

            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);

                dependencies.push(moduleName);

                node.arguments = [t.stringLiteral(moduleName)]

            }

        }

    });

    const sourceCode = generator(ast).code;

    return {

        sourceCode,

        dependencies

    }

}

到了这一步,我们已经实现了获取模块id模块内容替换require函数。 接下来我们递归获取即可。

完整代码就是:


const path = require('path');

const fs = require('fs');

// babylon 主要把源码转换成ast

const babylon = require('babylon');

// @babel/traverse

const traverse = require('@babel/traverse').default;

// @babel/types

const t = require('@babel/types');

// @babel/generator

const generator = require('@babel/generator').default;

class Compiler {

    constructor(config) {

        this.config = config;

        // 需要保存入口文件的路径

        this.entryId;

        // 需要保存所有模块的依赖

        this.modules = {};

        // 入口路径

        this.entry = config.entry;

        // 工作路径

        this.root = process.cwd();

    }

    run() {

        // 执行并且创建模块的依赖关系

        this.buildModule(path.resolve(this.root, this.entry), true);

        // 发射一个打包后的文件

        this.emitFile()

    }

    /**

    * 根据路径来获取文件的内容

    */

    getSource(modulePath) {

        const content = fs.readFileSync(modulePath, 'utf8');

        return content;

    }

    /**

    * 构建模块

    */

    buildModule(modulePath, isEntry) {

        // 拿到模块的内容

        const source = this.getSource(modulePath);

        // 拿到模块的id modulePath - this.root

        const moduleName = './' + path.relative(this.root, modulePath);

        if (isEntry) {

            this.entryId = moduleName

        }

        // 解析,需要把source源码进行改造,返回一个依赖列表

        const { sourceCode, dependencies } = this.parse(source, path.dirname(moduleName));

        this.modules[moduleName] = sourceCode;

        // 把相对路径和模块中的内容 对应起来

        dependencies.forEach(dep => {

            // 附模块的递归加载

            this.buildModule(path.join(this.root, dep), false);

        });

    }

    /**

    * 解析文件

    */

    parse(source, parentPath){

        // AST 解析语法树

        const ast = babylon.parse(source);

        const dependencies = [];

        traverse(ast, {

            CallExpression(p) {

                // 对应的节点

                const {node} = p;

                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);

                    dependencies.push(moduleName);

                    node.arguments = [t.stringLiteral(moduleName)]

                }

            }

        });

        const sourceCode = generator(ast).code;

        return {

            sourceCode,

            dependencies

        }

    }

    /**

    * 发射文件

    */

    emitFile() {



    }

}

module.exports = Compiler

接下来,我们需要做的就是发射文件了。首先我们需要准备一个webpack打包后的模板,并且增加一个渲染引擎,这里我选择 ejs

模板为:


// main.ejs

(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;

    }

    // Load entry module and return exports

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

})

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

({

    <% for(let key in modules){ %>

"<%- key %>":

(function(module, exports, __webpack_require__) {

eval(`<%- modules[key] %>`)

}),

<% } %>

})

然后我们只需要将拿到的模块id以及模块内容渲染到模板中,在发射到一个文件即可,输出文件为配置项中的output.filename.

emitFile函数实现为


/**

* 发射文件

*/

emitFile() {

    // 拿到输出的目录

    const main = path.join(this.config.output.path, this.config.output.filename);

    const templatStr = this.getSource(path.join(__dirname, 'main.ejs'));

    const code = ejs.render(templatStr, { entryId: this.entryId, modules: this.modules })

    this.assets = {};

    // 资源中,路径对应的代码

    this.assets[main] = code;

    fs.writeFileSync(main, this.assets[main]);

}

整个Compile.js的完整代码为:


const path = require('path');

const fs = require('fs');

const ejs = require('ejs');

// babylon 主要把源码转换成ast

const babylon = require('babylon');

// @babel/traverse

const traverse = require('@babel/traverse').default;

// @babel/types

const t = require('@babel/types');

// @babel/generator

const generator = require('@babel/generator').default;

class Compiler {

    constructor(config) {

        this.config = config;

        // 需要保存入口文件的路径

        this.entryId;

        // 需要保存所有模块的依赖

        this.modules = {};

        // 入口路径

        this.entry = config.entry;

        // 工作路径

        this.root = process.cwd();

    }

    run() {

        // 执行并且创建模块的依赖关系

        this.buildModule(path.resolve(this.root, this.entry), true);

        // 发射一个打包后的文件

        this.emitFile()

    }

    /**

    * 根据路径来获取文件的内容

    */

    getSource(modulePath) {

        const content = fs.readFileSync(modulePath, 'utf8');

        return content;

    }

    /**

    * 构建模块

    */

    buildModule(modulePath, isEntry) {

        // 拿到模块的内容

        const source = this.getSource(modulePath);

        // 拿到模块的id modulePath - this.root

        const moduleName = './' + path.relative(this.root, modulePath);

        if (isEntry) {

            this.entryId = moduleName

        }

        // 解析,需要把source源码进行改造,返回一个依赖列表

        const { sourceCode, dependencies } = this.parse(source, path.dirname(moduleName));

        this.modules[moduleName] = sourceCode;

        // 把相对路径和模块中的内容 对应起来

        dependencies.forEach(dep => {

            // 附模块的递归加载

            this.buildModule(path.join(this.root, dep), false);

        });

    }

    /**

    * 解析文件

    */

    parse(source, parentPath){

        // AST 解析语法树

        const ast = babylon.parse(source);

        const dependencies = [];

        traverse(ast, {

            CallExpression(p) {

                // 对应的节点

                const {node} = p;

                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);

                    dependencies.push(moduleName);

                    node.arguments = [t.stringLiteral(moduleName)]

                }

            }

        });

        const sourceCode = generator(ast).code;

        return {

            sourceCode,

            dependencies

        }

    }

    /**

    * 发射文件

    */

    emitFile() {

        // 拿到输出的目录

        const main = path.join(this.config.output.path, this.config.output.filename);

        const templatStr = this.getSource(path.join(__dirname, 'main.ejs'));

        const code = ejs.render(templatStr, { entryId: this.entryId, modules: this.modules })

        this.assets = {};

        // 资源中,路径对应的代码

        this.assets[main] = code;

        fs.writeFileSync(main, this.assets[main]);

    }

}

module.exports = Compiler

这样我们的打包器,就能打包代码,并且使代码运行在浏览器端了。

下一步

下一步将增加loaderplugin的支持。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,937评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,503评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,712评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,668评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,677评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,601评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,975评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,637评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,881评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,621评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,710评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,387评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,971评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,947评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,189评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,805评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,449评论 2 342

推荐阅读更多精彩内容

  • GitChat技术杂谈 前言 本文较长,为了节省你的阅读时间,在文前列写作思路如下: 什么是 webpack,它要...
    萧玄辞阅读 12,669评论 7 110
  • 版权声明:本文为博主原创文章,未经博主允许不得转载。 webpack介绍和使用 一、webpack介绍 1、由来 ...
    it筱竹阅读 11,025评论 0 21
  • 写在开头 先说说为什么要写这篇文章, 最初的原因是组里的小朋友们看了webpack文档后, 表情都是这样的: (摘...
    Lefter阅读 5,269评论 4 31
  • 作者:小 boy (沪江前端开发工程师)本文原创,转载请注明作者及出处。原文地址:https://www.smas...
    iKcamp阅读 2,740评论 0 18
  • 产后恢复的小建议,别错过! 对于辣妈们来说,产后的身材恢复就跟宝贝的成长一样重要。其实产后恢复的不仅是曼妙的身材,...
    妈妈修复阅读 171评论 0 0