资讯专栏INFORMATION COLUMN

实现一个简易的webpack

darcrand / 2384人阅读

摘要:首先一段代码转化成的抽象语法树是一个对象,该对象会有一个顶级的属性第二个属性是是一个数组。最终完成整个文件依赖的处理。参考文章抽象语法树一看就懂的抽象语法树源码所有的源码已经上传

背景

随着前端复杂度的不断提升,诞生出很多打包工具,比如最先的gruntgulp。到后来的webpack Parcel。但是目前很多脚手架工具,比如vue-cli已经帮我们集成了一些构建工具的使用。有的时候我们可能并不知道其内部的实现原理。其实了解这些工具的工作方式可以帮助我们更好理解和使用这些工具,也方便我们在项目开发中应用。

一些知识点

在我们开始造轮子前,我们需要对一些知识点做一些储备工作。

模块化知识

首先是模块的相关知识,主要的是 es6 modulescommonJS模块化的规范。更详细的介绍可以参考这里 CommonJS、AMD/CMD、ES6 Modules 以及 webpack 原理浅析。现在我们只需要了解:

es6 modules 是一个编译时就会确定模块依赖关系的方式。

CommonJS的模块规范中,Node 在对 JS 文件进行编译的过程中,会对文件中的内容进行头尾包装,在头部添加(function (export, require, modules, __filename, __dirname){ 在尾部添加了 };。这样我们在单个JS文件内部可以使用这些参数。

AST 基础知识

什么是抽象语法树?

在计算机科学中,抽象语法树(abstract syntax tree 或者缩写为 AST),或者语法树(syntax tree),是源代码的抽象语法结构的树状表现形式,这里特指编程语言的源代码。树上的每个节点都表示源代码中的一种结构。之所以说语法是「抽象」的,是因为这里的语法并不会表示出真实语法中出现的每个细节。

大家可以通过Esprima 这个网站来将代码转化成 ast。首先一段代码转化成的抽象语法树是一个对象,该对象会有一个顶级的type属性Program,第二个属性是body是一个数组。body数组中存放的每一项都是一个对象,里面包含了所有的对于该语句的描述信息:

type:描述该语句的类型 --变量声明语句
kind:变量声明的关键字 -- var
declaration: 声明的内容数组,里面的每一项也是一个对象
    type: 描述该语句的类型 
    id: 描述变量名称的对象
        type:定义
        name: 是变量的名字
        init: 初始化变量值得对象
        type: 类型
        value: 值 "is tree" 不带引号
        row: ""is tree"" 带引号
进入正题 webpack 简易打包

有了上面这些基础的知识,我们先来看一下一个简单的webpack打包的过程,首先我们定义3个文件:

// index.js
import a from "./test"

console.log(a)

// test.js
import b from "./message"

const a = "hello" + b

export default a

// message.js
const b = "world"

export default b

方式很简单,定义了一个index.js引用test.jstest.js内部引用message.js。看一下打包后的代码:

(function (modules) {
  var installedModules = {};

  function __webpack_require__(moduleId) {
    if (installedModules[moduleId]) {
      return installedModules[moduleId].exports;
    }

    var module = installedModules[moduleId] = {
      i: moduleId,
      l: false,
      exports: {}
    };

    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": (function (module, __webpack_exports__, __webpack_require__) {

    "use strict";
    eval("__webpack_require__.r(__webpack_exports__);
/* harmony import */ var _test__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./test */ "./src/test.js");


console.log(_test__WEBPACK_IMPORTED_MODULE_0__["default"])


//# sourceURL=webpack:///./src/index.js?");

  }),
  "./src/message.js": (function (module, __webpack_exports__, __webpack_require__) {
    // ...
  }),
  "./src/test.js": (function (module, __webpack_exports__, __webpack_require__) {
    // ...
  })
});

看起来很乱?没关系,我们来屡一下。一眼看过去我们看到的是这样的形式:

(function(modules) {
  // ...
})({
 // ...
})

这样好理解了吧,就是一个自执行函数,传入了一个modules对象,modules 对象是什么样的格式呢?上面的代码已经给了我们答案:

{
  "./src/index.js": (function (module, __webpack_exports__, __webpack_require__) {
    // ...
  }),
  "./src/message.js": (function (module, __webpack_exports__, __webpack_require__) {
    // ...
  }),
  "./src/test.js": (function (module, __webpack_exports__, __webpack_require__) {
    // ...
  })
}

是这样的一个 路径 --> 函数 这样的 key,value 键值对。而函数内部是我们定义的文件转移成 ES5 之后的代码:

"use strict";
eval("__webpack_require__.r(__webpack_exports__);
/* harmony import */ var _test__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./test */ "./src/test.js");


console.log(_test__WEBPACK_IMPORTED_MODULE_0__["default"])


//# sourceURL=webpack:///./src/index.js?");

到这里基本上结构是分析完了,接着我们看看他的执行,自执行函数一开始执行的代码是:

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

调用了__webpack_require_函数,并传入了一个moduleId参数是"./src/index.js"。再看看函数内部的主要实现:

// 定义 module 格式   
var module = installedModules[moduleId] = {
      i: moduleId, // moduleId
      l: false, // 是否已经缓存
      exports: {} // 导出对象,提供挂载
};

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

这里调用了我们modules中的函数,并传入了 __webpack_require__函数作为函数内部的调用。module.exports参数作为函数内部的导出。因为index.js里面引用了test.js,所以又会通过 __webpack_require__来执行对test.js的加载:

var _test__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/test.js");

test.js内又使用了message.js所以,test.js内部又会执行对message.js的加载。message.js执行完成之后,因为没有依赖项,所以直接返回了结果:

var b = "world"
__webpack_exports__["default"] = (b)

执行完成之后,再一级一级返回到根文件index.js。最终完成整个文件依赖的处理。
整个过程中,我们像是通过一个依赖关系树的形式,不断地向数的内部进入,等返回结果,又开始回溯到根。

开发一个简单的 tinypack

通过上面的这些调研,我们先考虑一下一个基础的打包编译工具可以做什么?

转换ES6语法成ES5

处理模块加载依赖

生成一个可以在浏览器加载执行的 js 文件

第一个问题,转换语法,其实我们可以通过babel来做。核心步骤也就是:

通过babylon生成AST

通过babel-core将AST重新生成源码

/**
 * 获取文件,解析成ast语法
 * @param filename // 入口文件
 * @returns {*}
 */
function getAst (filename) {
  const content = fs.readFileSync(filename, "utf-8")

  return babylon.parse(content, {
    sourceType: "module",
  });
}

/**
 * 编译
 * @param ast
 * @returns {*}
 */
function getTranslateCode(ast) {
  const {code} = transformFromAst(ast, null, {
    presets: ["env"]
  });
  return code
}

接着我们需要处理模块依赖的关系,那就需要得到一个依赖关系视图。好在babel-traverse提供了一个可以遍历AST视图并做处理的功能,通过 ImportDeclaration 可以得到依赖属性:

function getDependence (ast) {
  let dependencies = []
  traverse(ast, {
    ImportDeclaration: ({node}) => {
      dependencies.push(node.source.value);
    },
  })
  return dependencies
}

/**
 * 生成完整的文件依赖关系映射
 * @param fileName
 * @param entry
 * @returns {{fileName: *, dependence, code: *}}
 */
function parse(fileName, entry) {
  let filePath = fileName.indexOf(".js") === -1 ? fileName + ".js" : fileName
  let dirName = entry ? "" : path.dirname(config.entry)
  let absolutePath = path.join(dirName, filePath)
  const ast = getAst(absolutePath)
  return {
    fileName,
    dependence: getDependence(ast),
    code: getTranslateCode(ast),
  };
}

到目前为止,我们也只是得到根文件的依赖关系和编译后的代码,比如我们的index.js依赖了test.js但是我们并不知道test.js还需要依赖message.js,他们的源码也是没有编译过。所以此时我们还需要做深度遍历,得到完成的深度依赖关系:

/**
 * 获取深度队列依赖关系
 * @param main
 * @returns {*[]}
 */
function getQueue(main) {
  let queue = [main]
  for (let asset of queue) {
    asset.dependence.forEach(function (dep) {
      let child = parse(dep)
      queue.push(child)
    })
  }
  return queue
}

那么进行到这一步我们已经完成了所有文件的编译解析。最后一步,就是需要我们按照webpack的思想对源码进行一些包装。第一步,先是要生成一个modules对象:

function bundle(queue) {
  let modules = ""
  queue.forEach(function (mod) {
    modules += `"${mod.fileName}": function (require, module, exports) { ${mod.code} },`
  })
  // ...
}

得到 modules 对象后,接下来便是对整体文件的外部包装,注册requiremodule.exports

(function(modules) {
      function require(fileName) {
          // ...
      }
     require("${config.entry}");
 })({${modules}})

而函数内部,也只是循环执行每个依赖文件的 JS 代码而已,完成代码:

function bundle(queue) {
  let modules = ""
  queue.forEach(function (mod) {
    modules += `"${mod.fileName}": function (require, module, exports) { ${mod.code} },`
  })

  const result = `
    (function(modules) {
      function require(fileName) {
        const fn = modules[fileName];

        const module = { exports : {} };

        fn(require, module, module.exports);

        return module.exports;
      }

      require("${config.entry}");
    })({${modules}})
  `;

  // We simply return the result, hurray! :)
  return result;
}

到这里基本上也就介绍完了,接下来就是输出编译好的文件了,这里我们为了可以全局使用tinypack包,我们还需要为其添加到全局命令(这里直接参考我的源码吧,不再赘述了)。我们来测试一下:

npm i tinypack_demo@1.0.7 -g

cd examples

tinypack

看一下输出的文件:

(function (modules) {
  function require(fileName) {
    const fn = modules[fileName];

    const module = {exports: {}};

    fn(require, module, module.exports);

    return module.exports;
  }

  require("./src/index.js");
})({
  "./src/index.js": function (require, module, exports) {
    "use strict";

    var _test = require("./test");

    var _test2 = _interopRequireDefault(_test);

    function _interopRequireDefault(obj) {
      return obj && obj.__esModule ? obj : {default: obj};
    }

    console.log(_test2.default);
  }, "./test": function (require, module, exports) {
    "use strict";

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

    var _message = require("./message");

    var _message2 = _interopRequireDefault(_message);

    function _interopRequireDefault(obj) {
      return obj && obj.__esModule ? obj : {default: obj};
    }

    var a = "hello" + _message2.default;
    exports.default = a;
  }, "./message": function (require, module, exports) {
    "use strict";

    Object.defineProperty(exports, "__esModule", {
      value: true
    });
    var b = "world";

    exports.default = b;
  },
})

再测试一下:

恩,基本上已经完成一个建议的 tinypack

参考文章

抽象语法树 Abstract syntax tree

一看就懂的JS抽象语法树

源码

tinypack 所有的源码已经上传 github

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/108042.html

相关文章

  • 一个简易DIY场景H5框架

    摘要:笔者也用操作的形式做了一个类似项目,现在整合成一个简易的框架分享给大家,只需要根据自己的需要完成布局与样式的编写,以及按下面指示操作,就可以生成一个自己的空间定制。 第一次发项目,以供大家学习参考,可能有些简陋,若有不合理处,请大神们指教。 最近在朋友圈出现了很多DIY类的H5游戏,通过添加拖拉摆放和组合元素,来DIY自定义一个场景并合成图片分享出去的新H5展示形式。笔者也用操作DOM...

    dongxiawu 评论0 收藏0
  • Webpack系列——手把手教你使用Webpack搭建简易React开发环境

    摘要:在这篇文章中我们开始利用我们之前所学搭建一个简易的开发环境,用以巩固我们之前学习的知识。 文章首发于我的github及个人博客,github请看https://github.com/huruji/blo...,转载请注明出处。 在这篇文章中我们开始利用我们之前所学搭建一个简易的React开发环境,用以巩固我们之前学习的Webpack知识。首先我们需要明确这次开发环境需要达到的效果:1、...

    cucumber 评论0 收藏0
  • 随我来基于webpack构建一个简易vue脚手架 (webpack系列二)

    摘要:构建文件清理构建目录下的文件打包工具编译文件模板函数编译与配合使用入口处理项目中的不同类型的模块。 前言 之前有写了一篇webpack的文章(认识篇) 猛戳,大家对于webpack基本概念(entry,output,loader,plugin,mode...)应该是有了较模糊的认识.今天希望在通过(对于vue-cli的效仿)搭建一个自己的脚手架的途中对于概念会有更深刻的认识. 目录 1...

    tomorrowwu 评论0 收藏0
  • webpack简易教程之loader

    摘要:一组链式的将按照先后顺序进行编译。在最后一个,返回所预期的。运行在中,并且能够执行任何可能的操作。用于对传递配置。分开的每个部分都相对于当前目录解析。 webpack自称能够打包任何文件,这句话咋听一下好像在吹牛逼,因为webpack本身只能理解JavaScript。但是由于webpack中有loader的存在,可以将所有类型的文件转换为webpack能够处理的有效模块,然后利用we...

    MobService 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<