打包工具的配置教程见的多了,但它们的运行原理你知道吗?

2018-06-15 admin

clipboard.png

前端模块化成为了主流的今天,离不开各种打包工具的贡献。社区里面对于webpack,rollup以及后起之秀parcel的介绍层出不穷,对于它们各自的使用配置分析也是汗牛充栋。为了避免成为一位“配置工程师”,我们需要来了解一下打包工具的运行原理,只有把核心原理搞明白了,在工具的使用上才能更加得心应手。

本文基于parcel核心开发者@ronami的开源项目minipack而来,在其非常详尽的注释之上加入更多的理解和说明,方便读者更好地理解。

1、打包工具核心原理

顾名思义,打包工具就是负责把一些分散的小模块,按照一定的规则整合成一个大模块的工具。与此同时,打包工具也会处理好模块之间的依赖关系,最终这个大模块将可以被运行在合适的平台中。

打包工具会从一个入口文件开始,分析它里面的依赖,并且再进一步地分析依赖中的依赖,不断重复这个过程,直到把这些依赖关系理清挑明为止。

从上面的描述可以看到,打包工具最核心的部分,其实就是处理好模块之间的依赖关系,而minipack以及本文所要讨论的,也是集中在模块依赖关系的知识点当中。

为了简单起见,minipack项目直接使用ES modules规范,接下来我们新建三个文件,并且为它们之间建立依赖:

/* name.js */

export const name = 'World'
/* message.js */

import { name } from './name.js'

export default `hello ${name}!`
/* entry.js */

import message from './message.js'

console.log(message)

它们的依赖关系非常简单:entry.jsmessage.jsname.js,其中entry.js将会成为打包工具的入口文件。

但是,这里面的依赖关系只是我们人类所理解的,如果要让机器也能够理解当中的依赖关系,就需要借助一定的手段了。

2、依赖关系解析

新建一个js文件,命名为minipack.js,首先引入必要的工具。

/* minipack.js */

const fs = require('fs')
const path = require('path')
const babylon = require('babylon')
const traverse = require('babel-traverse').default
const { transformFromAst } = require('babel-core')

接下来,我们会撰写一个函数,这个函数接收一个文件作为模块,然后读取它里面的内容,分析出其所有的依赖项。当然,我们可以通过正则匹配模块文件里面的import关键字,但这样做非常不优雅,所以我们可以使用babylon这个js解析器把文件内容转化成抽象语法树(AST),直接从AST里面获取我们需要的信息。

得到了AST之后,就可以使用babel-traverse去遍历这棵AST,获取当中关键的“依赖声明”,然后把这些依赖都保存在一个数组当中。

最后使用babel-coretransformFromAst方法搭配babel-preset-env插件,把ES6语法转化成浏览器可以识别的ES5语法,并且为该js模块分配一个ID。

let ID = 0

function createAsset (filename) {
  // 读取文件内容
  const content = fs.readFileSync(filename, 'utf-8')

  // 转化成AST
  const ast = babylon.parse(content, {
    sourceType: 'module',
  });

  // 该文件的所有依赖
  const dependencies = []

  // 获取依赖声明
  traverse(ast, {
    ImportDeclaration: ({ node }) => {
      dependencies.push(node.source.value);
    }
  })

  // 转化ES6语法到ES5
  const {code} = transformFromAst(ast, null, {
    presets: ['env'],
  })

  // 分配ID
  const id = ID++

  // 返回这个模块
  return {
    id,
    filename,
    dependencies,
    code,
  }
}

运行createAsset('./example/entry.js'),输出如下:

{ id: 0,
  filename: './example/entry.js',
  dependencies: [ './message.js' ],
  code: '"use strict";\n\nvar _message = require("./message.js");\n\nvar _message2 = _interopRequireDefault(_message);\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }\n\nconsole.log(_message2.default);' }

可见entry.js文件已经变成了一个典型的模块,且依赖已经被分析出来了。接下来我们就要递归这个过程,把“依赖中的依赖”也都分析出来,也就是下一节要讨论的建立依赖关系图集。

3、建立依赖关系图集

新建一个名为createGragh()的函数,传入一个入口文件的路径作为参数,然后通过createAsset()解析这个文件使之定义成一个模块。

接下来,为了能够挨个挨个地对模块进行依赖分析,所以我们维护一个数组,首先把第一个模块传进去并进行分析。当这个模块被分析出还有其他依赖模块的时候,就把这些依赖模块也放进数组中,然后继续分析这些新加进去的模块,直到把所有的依赖以及“依赖中的依赖”都完全分析出来。

与此同时,我们有必要为模块新建一个mapping属性,用来储存模块、依赖、依赖ID之间的依赖关系,例如“ID为0的A模块依赖于ID为2的B模块和ID为3的C模块”就可以表示成下面这个样子:

{
  0: [function A () {}, { 'B.js': 2, 'C.js': 3 }]
}

搞清楚了个中道理,就可以开始编写函数了。

function createGragh (entry) {
  // 解析传入的文件为模块
  const mainAsset = createAsset(entry)

  // 维护一个数组,传入第一个模块
  const queue = [mainAsset]

  // 遍历数组,分析每一个模块是否还有其它依赖,若有则把依赖模块推进数组
  for (const asset of queue) {
    asset.mapping = {}
    // 由于依赖的路径是相对于当前模块,所以要把相对路径都处理为绝对路径
    const dirname = path.dirname(asset.filename)
    // 遍历当前模块的依赖项并继续分析
    asset.dependencies.forEach(relativePath => {
      // 构造绝对路径
      const absolutePath = path.join(dirname, relativePath)
      // 生成依赖模块
      const child = createAsset(absolutePath)
      // 把依赖关系写入模块的mapping当中
      asset.mapping[relativePath] = child.id
      // 把这个依赖模块也推入到queue数组中,以便继续对其进行以来分析
      queue.push(child)
    })
  }

  // 最后返回这个queue,也就是依赖关系图集
  return queue
}

可能有读者对其中的for...of ...循环当中的queue.push有点迷,但是只要尝试过下面这段代码就能搞明白了:

var numArr = ['1', '2', '3']

for (num of numArr) {
  console.log(num)
  if (num === '3') {
    arr.push('Done!')
  }
}

尝试运行一下createGraph('./example/entry.js'),就能够看到如下的输出:

[ { id: 0,
    filename: './example/entry.js',
    dependencies: [ './message.js' ],
    code: '"use strict";\n\nvar _message = require("./message.js");\n\nvar _message2 = _interopRequireDefault(_message);\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }\n\nconsole.log(_message2.default);',
    mapping: { './message.js': 1 } },
  { id: 1,
    filename: 'example/message.js',
    dependencies: [ './name.js' ],
    code: '"use strict";\n\nObject.defineProperty(exports, "__esModule", {\n  value: true\n});\n\nvar _name = require("./name.js");\n\nexports.default = "Hello " + _name.name + "!";',
    mapping: { './name.js': 2 } },
  { id: 2,
    filename: 'example/name.js',
    dependencies: [],
    code: '"use strict";\n\nObject.defineProperty(exports, "__esModule", {\n  value: true\n});\nvar name = exports.name = \'world\';',
    mapping: {} } ]

现在依赖关系图集已经构建完成了,接下来就是把它们打包成一个单独的,可直接运行的文件啦!

4、进行打包

上一步生成的依赖关系图集,接下来将通过CommomJS规范来实现加载。由于篇幅关系,本文不对CommomJS规范进行扩展,有兴趣的读者可以参考@阮一峰 老师的一篇文章《浏览器加载 CommonJS 模块的原理与实现》,说得非常清晰。简单来说,就是通过构造一个立即执行函数(function () {})(),手动定义moduleexportsrequire变量,最后实现代码在浏览器运行的目的。

接下来就是依据这个规范,通过字符串拼接去构建代码块。

function bundle (graph) {
  let modules = ''

  graph.forEach(mod => {
    modules += `${mod.id}: [
      function (require, module, exports) { ${mod.code} },
      ${JSON.stringify(mod.mapping)},
    ],`
  })

  const result = `
    (function(modules) {
      function require(id) {
        const [fn, mapping] = modules[id];

        function localRequire(name) {
          return require(mapping[name]);
        }

        const module = { exports : {} };

        fn(localRequire, module, module.exports);

        return module.exports;
      }

      require(0);
    })({${modules}})
  `
  return result
}

最后运行bundle(createGraph('./example/entry.js')),输出如下:

(function (modules) {
  function require(id) {
    const [fn, mapping] = modules[id];

    function localRequire(name) {
      return require(mapping[name]);
    }

    const module = { exports: {} };

    fn(localRequire, module, module.exports);

    return module.exports;
  }

  require(0);
})({
  0: [
    function (require, module, exports) {
      "use strict";

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

      var _message2 = _interopRequireDefault(_message);

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

      console.log(_message2.default);
    },
    { "./message.js": 1 },
  ], 1: [
    function (require, module, exports) {
      "use strict";

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

      var _name = require("./name.js");

      exports.default = "Hello " + _name.name + "!";
    },
    { "./name.js": 2 },
  ], 2: [
    function (require, module, exports) {
      "use strict";

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

这段代码将能够直接在浏览器运行,输出“Hello world!”。

至此,整一个打包工具已经完成。

5、归纳总结

经过上面几个步骤,我们可以知道一个模块打包工具,第一步会从入口文件开始,对其进行依赖分析,第二步对其所有依赖再次递归进行依赖分析,第三步构建出模块的依赖图集,最后一步根据依赖图集使用CommonJS规范构建出最终的代码。明白了当中每一步的目的,便能够明白一个打包工具的运行原理。

最后再次感谢@ronami的开源项目minipack,其源码有着更为详细的注释,非常值得大家阅读。

原文链接:https://segmentfault.com/a/1190000015291911

本站文章除注明转载外,均为本站原创或编译。欢迎任何形式的转载,但请务必注明出处。

转载请注明:文章转载自 JavaScript中文网 [https://www.javascriptcn.com]

本文地址:https://www.javascriptcn.com/read-34082.html

文章标题:打包工具的配置教程见的多了,但它们的运行原理你知道吗?

相关文章
10个强大的纯CSS3动画案例分享
我们的网页外观主要由CSS控制,编写CSS代码可以任意改变我们的网页布局以及网页内容的样式。CSS3的出现,更是可以让网页增添了不少动画元素,让我们的网页变得更加生动有趣,并且更易于交互。本文分享了10个非常炫酷的CSS3动画案例,希望大家...
2015-11-16
Android中Okhttp3实现上传多张图片同时传递参数
之前上传图片都是直接将图片转化为io流传给服务器,没有用框架传图片。 最近做项目,打算换个方法上传图片。 Android发展到现在,Okhttp显得越来越重要,所以,这次我选择用Okhttp上传图片。 Okhttp目前已经更新到Okhttp...
2017-03-17
Node.js 2014这一年发生了什么
Node.js 的 2014 年充满了不幸和争议. 这一年 Noder 们经历了太多的伤心事, 经历了漫长的等待, 经历了沉重的分裂之痛. 也许 Noder 们不想回忆14年 Node.js land 发生的事情, 但正因为痛才更有铭记的价...
2015-11-12
从2014年的发展来展望JS的未来将会如何
<font face="寰�杞�闆呴粦, Arial, sans-serif ">2014骞达紝杞�浠惰�屼笟鍙戝睍杩呴€燂紝鍚勭�嶈��瑷€灞傚嚭涓嶇┓锛屼互婊¤冻鐢ㄦ埛涓嶆柇鍙樺寲鐨勯渶姹傘€傝繖浜涜��...
2015-11-12
12个你未必知道的CSS小知识
虽然CSS并不是一种很复杂的技术,但就算你是一个使用CSS多年的高手,仍然会有很多CSS用法/属性/属性值你从来没使用过,甚至从来没听说过。 1.CSS的color属性并非只能用于文本显示 对于CSS的color属性,相信所有Web开发人员...
2015-11-12
ajax为什么令人惊异?ajax的优缺点
使用Ajax的最大优点,就是能在不更新整个页面的前提下维护数据。这使得Web应用程序更为迅捷地回应用户动作,并避免了在网络上发送那些没有改变的信息。 Ajax不需要任何浏览器插件,但需要用户允许JavaScript在浏览器上执行。就像DHT...
2015-11-12
HTML5的5个不错的开发工具推荐
HTML5规范终于在今年正式定稿,对于从事多年HTML5开发的人员来说绝对是一个重大新闻。数字天堂董事长,DCloud CEO王安也发表了文章,从开发者和用户两个角度分析了HTML对两个人群的优势。其实,关于HTML5的开发工具,我们以往的...
2015-11-12
JavaScript教程:JS中的原型
Keith Peters 几年前发表的一篇博文,关于学习没有“new”的世界,其中解释了使用原型继承代替构造函数。两者都是纯粹的原型编码。 标准方法(The Standard Way) 一直以来,我们学习的在 JavaScript 里创建对...
2015-11-12
AJAX的浏览器支持
AJAX 的要点是 XMLHttpRequest 对象。 不同的浏览器创建 XMLHttpRequest 对象的方法是有差异的。 IE 浏览器使用 ActiveXObject,而其他的浏览器使用名为 XMLHttpRequest 的 Jav...
2015-11-12
Riot.js:不足1KB的MVP客户端框架
Riot.js是一款MVP(模型-视图-呈现)开源客户端框架,其最大的特点就是体积非常小,不足1KB,虽然体积小,但它可以帮助用户构建大规模的Web应用程序。 Riot.js是由Moot公司开发,目前最新版本为v0.9.2,遵循MIT开源许...
2016-03-11
回到顶部