背景

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

一些知识点

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

模块化知识

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

  1. es6 modules 是一个编译时就会确定模块依赖关系的方式。
  2. CommonJS的模块规范中,Node 在对 JS 文件进行编译的过程中,会对文件中的内容进行头尾包装

    ,在头部添加(function (export, require, modules, __filename, __dirname){\n 在尾部添加了\n};。这样我们在单个JS文件内部可以使用这些参数。

AST 基础知识

什么是抽象语法树?

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

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

  1. type:描述该语句的类型 --变量声明语句
  2. kind:变量声明的关键字 -- var
  3. declaration: 声明的内容数组,里面的每一项也是一个对象
  4. type: 描述该语句的类型
  5. id: 描述变量名称的对象
  6. type:定义
  7. name: 是变量的名字
  8. init: 初始化变量值得对象
  9. type: 类型
  10. value: "is tree" 不带引号
  11. row: "\"is tree"\" 带引号

进入正题

webpack 简易打包

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

  1. // index.js
  2. import a from './test'
  3. console.log(a)
  4. // test.js
  5. import b from './message'
  6. const a = 'hello' + b
  7. export default a
  8. // message.js
  9. const b = 'world'
  10. export default b

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

  1. (function (modules) {
  2. var installedModules = {};
  3. function __webpack_require__(moduleId) {
  4. if (installedModules[moduleId]) {
  5. return installedModules[moduleId].exports;
  6. }
  7. var module = installedModules[moduleId] = {
  8. i: moduleId,
  9. l: false,
  10. exports: {}
  11. };
  12. modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
  13. // Flag the module as loaded
  14. module.l = true;
  15. // Return the exports of the module
  16. return module.exports;
  17. }
  18. // expose the modules object (__webpack_modules__)
  19. __webpack_require__.m = modules;
  20. // expose the module cache
  21. __webpack_require__.c = installedModules;
  22. // define getter function for harmony exports
  23. __webpack_require__.d = function (exports, name, getter) {
  24. if (!__webpack_require__.o(exports, name)) {
  25. Object.defineProperty(exports, name, {enumerable: true, get: getter});
  26. }
  27. };
  28. // define __esModule on exports
  29. __webpack_require__.r = function (exports) {
  30. if (typeof Symbol !== 'undefined' && Symbol.toStringTag) {
  31. Object.defineProperty(exports, Symbol.toStringTag, {value: 'Module'});
  32. }
  33. Object.defineProperty(exports, '__esModule', {value: true});
  34. };
  35. // create a fake namespace object
  36. // mode & 1: value is a module id, require it
  37. // mode & 2: merge all properties of value into the ns
  38. // mode & 4: return value when already ns object
  39. // mode & 8|1: behave like require
  40. __webpack_require__.t = function (value, mode) {
  41. /******/
  42. if (mode & 1) value = __webpack_require__(value);
  43. if (mode & 8) return value;
  44. if ((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
  45. var ns = Object.create(null);
  46. __webpack_require__.r(ns);
  47. Object.defineProperty(ns, 'default', {enumerable: true, value: value});
  48. if (mode & 2 && typeof value != 'string') for (var key in value) __webpack_require__.d(ns, key, function (key) {
  49. return value[key];
  50. }.bind(null, key));
  51. return ns;
  52. };
  53. // getDefaultExport function for compatibility with non-harmony modules
  54. __webpack_require__.n = function (module) {
  55. var getter = module && module.__esModule ?
  56. function getDefault() {
  57. return module['default'];
  58. } :
  59. function getModuleExports() {
  60. return module;
  61. };
  62. __webpack_require__.d(getter, 'a', getter);
  63. return getter;
  64. };
  65. // Object.prototype.hasOwnProperty.call
  66. __webpack_require__.o = function (object, property) {
  67. return Object.prototype.hasOwnProperty.call(object, property);
  68. };
  69. // __webpack_public_path__
  70. __webpack_require__.p = "";
  71. // Load entry module and return exports
  72. return __webpack_require__(__webpack_require__.s = "./src/index.js");
  73. })({
  74. "./src/index.js": (function (module, __webpack_exports__, __webpack_require__) {
  75. "use strict";
  76. eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _test__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./test */ \"./src/test.js\");\n\n\nconsole.log(_test__WEBPACK_IMPORTED_MODULE_0__[\"default\"])\n\n\n//# sourceURL=webpack:///./src/index.js?");
  77. }),
  78. "./src/message.js": (function (module, __webpack_exports__, __webpack_require__) {
  79. // ...
  80. }),
  81. "./src/test.js": (function (module, __webpack_exports__, __webpack_require__) {
  82. // ...
  83. })
  84. });

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

  1. (function(modules) {
  2. // ...
  3. })({
  4. // ...
  5. })

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

  1. {
  2. "./src/index.js": (function (module, __webpack_exports__, __webpack_require__) {
  3. // ...
  4. }),
  5. "./src/message.js": (function (module, __webpack_exports__, __webpack_require__) {
  6. // ...
  7. }),
  8. "./src/test.js": (function (module, __webpack_exports__, __webpack_require__) {
  9. // ...
  10. })
  11. }

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

  1. "use strict";
  2. eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _test__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./test */ \"./src/test.js\");\n\n\nconsole.log(_test__WEBPACK_IMPORTED_MODULE_0__[\"default\"])\n\n\n//# sourceURL=webpack:///./src/index.js?");

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

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

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

  1. // 定义 module 格式
  2. var module = installedModules[moduleId] = {
  3. i: moduleId, // moduleId
  4. l: false, // 是否已经缓存
  5. exports: {} // 导出对象,提供挂载
  6. };
  7. modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);

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

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

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

  1. var b = 'world'
  2. __webpack_exports__["default"] = (b)

执行完成之后,再一级一级返回到根文件index.js。最终完成整个文件依赖的处理。

整个过程中,我们像是通过一个依赖关系树的形式,不断地向数的内部进入,等返回结果,又开始回溯到根。

开发一个简单的 tinypack

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

  1. 转换ES6语法成ES5
  2. 处理模块加载依赖
  3. 生成一个可以在浏览器加载执行的 js 文件

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

  • 通过babylon生成AST
  • 通过babel-core将AST重新生成源码
  1. /**
  2. * 获取文件,解析成ast语法
  3. * @param filename // 入口文件
  4. * @returns {*}
  5. */
  6. function getAst (filename) {
  7. const content = fs.readFileSync(filename, 'utf-8')
  8. return babylon.parse(content, {
  9. sourceType: 'module',
  10. });
  11. }
  12. /**
  13. * 编译
  14. * @param ast
  15. * @returns {*}
  16. */
  17. function getTranslateCode(ast) {
  18. const {code} = transformFromAst(ast, null, {
  19. presets: ['env']
  20. });
  21. return code
  22. }

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

  1. function getDependence (ast) {
  2. let dependencies = []
  3. traverse(ast, {
  4. ImportDeclaration: ({node}) => {
  5. dependencies.push(node.source.value);
  6. },
  7. })
  8. return dependencies
  9. }
  10. /**
  11. * 生成完整的文件依赖关系映射
  12. * @param fileName
  13. * @param entry
  14. * @returns {{fileName: *, dependence, code: *}}
  15. */
  16. function parse(fileName, entry) {
  17. let filePath = fileName.indexOf('.js') === -1 ? fileName + '.js' : fileName
  18. let dirName = entry ? '' : path.dirname(config.entry)
  19. let absolutePath = path.join(dirName, filePath)
  20. const ast = getAst(absolutePath)
  21. return {
  22. fileName,
  23. dependence: getDependence(ast),
  24. code: getTranslateCode(ast),
  25. };
  26. }

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

  1. /**
  2. * 获取深度队列依赖关系
  3. * @param main
  4. * @returns {*[]}
  5. */
  6. function getQueue(main) {
  7. let queue = [main]
  8. for (let asset of queue) {
  9. asset.dependence.forEach(function (dep) {
  10. let child = parse(dep)
  11. queue.push(child)
  12. })
  13. }
  14. return queue
  15. }

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

  1. function bundle(queue) {
  2. let modules = ''
  3. queue.forEach(function (mod) {
  4. modules += `'${mod.fileName}': function (require, module, exports) { ${mod.code} },`
  5. })
  6. // ...
  7. }

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

  1. (function(modules) {
  2. function require(fileName) {
  3. // ...
  4. }
  5. require('${config.entry}');
  6. })({${modules}})

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

  1. function bundle(queue) {
  2. let modules = ''
  3. queue.forEach(function (mod) {
  4. modules += `'${mod.fileName}': function (require, module, exports) { ${mod.code} },`
  5. })
  6. const result = `
  7. (function(modules) {
  8. function require(fileName) {
  9. const fn = modules[fileName];
  10. const module = { exports : {} };
  11. fn(require, module, module.exports);
  12. return module.exports;
  13. }
  14. require('${config.entry}');
  15. })({${modules}})
  16. `;
  17. // We simply return the result, hurray! :)
  18. return result;
  19. }

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

  1. npm i tinypack_demo@1.0.7 -g
  2. cd examples
  3. tinypack

看一下输出的文件:

  1. (function (modules) {
  2. function require(fileName) {
  3. const fn = modules[fileName];
  4. const module = {exports: {}};
  5. fn(require, module, module.exports);
  6. return module.exports;
  7. }
  8. require('./src/index.js');
  9. })({
  10. './src/index.js': function (require, module, exports) {
  11. "use strict";
  12. var _test = require("./test");
  13. var _test2 = _interopRequireDefault(_test);
  14. function _interopRequireDefault(obj) {
  15. return obj && obj.__esModule ? obj : {default: obj};
  16. }
  17. console.log(_test2.default);
  18. }, './test': function (require, module, exports) {
  19. "use strict";
  20. Object.defineProperty(exports, "__esModule", {
  21. value: true
  22. });
  23. var _message = require("./message");
  24. var _message2 = _interopRequireDefault(_message);
  25. function _interopRequireDefault(obj) {
  26. return obj && obj.__esModule ? obj : {default: obj};
  27. }
  28. var a = 'hello' + _message2.default;
  29. exports.default = a;
  30. }, './message': function (require, module, exports) {
  31. "use strict";
  32. Object.defineProperty(exports, "__esModule", {
  33. value: true
  34. });
  35. var b = 'world';
  36. exports.default = b;
  37. },
  38. })

再测试一下:

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

参考文章

抽象语法树 Abstract syntax tree

一看就懂的JS抽象语法树

源码

tinypack 所有的源码已经上传 github

手把手教你撸一个简易的 webpack的更多相关文章

  1. 手把手教你撸一个 Webpack Loader

    文:小 boy(沪江网校Web前端工程师) 本文原创,转载请注明作者及出处 经常逛 webpack 官网的同学应该会很眼熟上面的图.正如它宣传的一样,webpack 能把左侧各种类型的文件(webpa ...

  2. 学以致用:手把手教你撸一个工具库并打包发布,顺便解决JS浮点数计算精度问题

    本文讲解的是怎么实现一个工具库并打包发布到npm给大家使用.本文实现的工具是一个分数计算器,大家考虑如下情况: \[ \sqrt{(((\frac{1}{3}+3.5)*\frac{2}{9}-\fr ...

  3. Go语言系列之手把手教你撸一个ORM(一)

    项目地址:https://github.com/yoyofxteam/yoyodata 欢迎星星,感谢 前言:最近在学习Go语言,就出于学习目的手撸个小架子,欢迎提出宝贵意见,项目使用Mysql数据库 ...

  4. C#多线程(16):手把手教你撸一个工作流

    目录 前言 节点 Then Parallel Schedule Delay 试用一下 顺序节点 并行任务 编写工作流 接口构建器 工作流构建器 依赖注入 实现工作流解析 前言 前面学习了很多多线程和任 ...

  5. Vue手把手教你撸一个 beforeEnter 钩子函数

    地址 :https://www.jb51.net/article/138821.htm 地址 :https://www.jb51.net/article/108964.htm

  6. 手把手教你撸个vue2.0弹窗组件

    手把手教你撸个vue2.0弹窗组件 在开始之前需要了解一下开发vue插件的前置知识,推荐先看一下vue官网的插件介绍 预览地址 http://haogewudi.me/kiko/inde... 源码地 ...

  7. 只有20行Javascript代码!手把手教你写一个页面模板引擎

    http://www.toobug.net/article/how_to_design_front_end_template_engine.html http://barretlee.com/webs ...

  8. iOS回顾笔记(05) -- 手把手教你封装一个广告轮播图框架

    html,body,div,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,bi ...

  9. PWA入门:手把手教你制作一个PWA应用

    摘要: PWA图文教程 原文:PWA入门:手把手教你制作一个PWA应用 作者:MudOnTire Fundebug经授权转载,版权归原作者所有. 简介 Web前端的同学是否想过学习app开发,以弥补自 ...

随机推荐

  1. Eclipse中java内存溢出

    1.点击Window --->Preferences,如下图  

  2. linux常用的BootLoader U-boot的前世今生

    U-Boot,全称 Universal Boot Loader,是遵循GPL条款的开放源码项目.U-Boot的作用是系统引导. U-Boot从FADSROM.8xxROM.PPCBOOT逐步发展演化而 ...

  3. D3.js的一些基础部分 (v3版本)

    最近公司有需求要做一些可视化的功能.之前一直都是用Echarts做的,但是Echarts难以满足我们的需求,经过多方请教,查找发现D3可以满足我们的需求.第一次接触到D3,发现这些图标的可交互性非常丰 ...

  4. Python模块——configparser

    configparser模块 该模块适用于配置文件的格式与windows ini文件类似,可以包含一个或多个节(section),每个节可以有多个参数(键=值) 创建文件 import configp ...

  5. GDB查看内存(x 命令)

    gdb查看内存命令 首先使用gdb [YourFileName].c进入gdb界面 使用examine命令,字母缩写为x查看内存地址的值.x命令语法 x/[number][format] <ad ...

  6. sql 指定时间 所在的周、月、季、年

    DECLARE @TodayDateTime DATETIMEDECLARE @strToday NVARCHAR(19) DECLARE @TodayBeginDateTime DATETIMEDE ...

  7. 关于resin的illegal utf8 encoding at (190)解决方式

    最近在项目开发中,出现了编码异常,内容如下:- illegal utf8 encoding at (190)com.caucho.jsp.JspParseException: illegal utf8 ...

  8. salesforce lightning零基础学习(十) Aura Js 浅谈三: $A、Action、Util篇

    前两篇分别介绍了Component类以及Event类,此篇将会说一下 $A , Action以及 Util.  一. Action Action类通常用于和apex后台交互,设置参数,调用后台以及对结 ...

  9. LoadRuner12.53教程(一)

    LoadRunner教程 L   o   a   d   R   u   n   n   e   r   jiào教   chéng程 Welcome to the LoadRunner tutori ...

  10. [HAOI 2015]按位或

    Description 题库链接 刚开始你有一个数字 \(0\) ,每一秒钟你会随机选择一个 \([0,2^n-1]\) 的数字,与你手上的数字进行或( \(\text{or}\) )操作.选择数字 ...