Web前端   发布时间:2019-10-13  发布网站:大佬教程  code.js-code.com
大佬教程收集整理的这篇文章主要介绍了打包工具的核心原理(转自:https://juejin.im/entry/5b223ebd518825748b569bda)大佬教程大佬觉得挺不错的,现在分享给大家,也给大家做个参考。

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

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

从上面的描述可以看到,打包工具最核心的部分,其实就是处理好模块之间的依赖关系。

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

export <span style="color: #0000ff">const name = <span style="color: #800000">'<span style="color: #800000">World<span style="color: #800000">'

message.js import { name } <span style="color: #0000ff">from <span style="color: #800000">'<span style="color: #800000">./name.js<span style="color: #800000">'<span style="color: #000000">

export <span style="color: #0000ff">default Hello ${name}!

import message <span style="color: #0000ff">from <span style="color: #800000">'<span style="color: #800000">./message.js<span style="color: #800000">'<span style="color: #000000">

console.log(messagE)

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

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

@H_772_72@依赖关系解析

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

<span style="color: #0000ff">const fs = require(<span style="color: #800000">'<span style="color: #800000">fs<span style="color: #800000">'<span style="color: #000000">)
<span style="color: #0000ff">const path = require(<span style="color: #800000">'<span style="color: #800000">path<span style="color: #800000">'<span style="color: #000000">)
<span style="color: #0000ff">const babylon = require(<span style="color: #800000">'<span style="color: #800000">babylon<span style="color: #800000">'<span style="color: #000000">)
<span style="color: #0000ff">const traverse = require(<span style="color: #800000">'<span style="color: #800000">babel-traverse<span style="color: #800000">').<span style="color: #0000ff">default
<span style="color: #0000ff">const { transformFromAst } = require(<span style="color: #800000">'<span style="color: #800000">babel-core<span style="color: #800000">')

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

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

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

let ID = function createAsset (fileName) {
<span style="color: #008000">//
<span style="color: #008000"> 读取文件内容
<span style="color: #0000ff">const content = fs.readFileSync(filename,<span style="color: #800000">'<span style="color: #800000">utf-8<span style="color: #800000">'<span style="color: #000000">)

<span style="color: #008000">//<span style="color: #008000"> 转化成AST
<span style="color: #0000ff">const ast =<span style="color: #000000"> babylon.parse(content,{
sourceType: <span style="color: #800000">'<span style="color: #800000">module<span style="color: #800000">'<span style="color: #000000">,});

<span style="color: #008000">//<span style="color: #008000"> 该文件的所有依赖
<span style="color: #0000ff">const dependencies =<span style="color: #000000"> []

<span style="color: #008000">//<span style="color: #008000"> 获取依赖声明
<span style="color: #000000"> traverse(ast,{
ImportDeclaration: ({ node }) =><span style="color: #000000"> {
dependencies.push(node.source.value);
}
})

<span style="color: #008000">//<span style="color: #008000"> 转化ES6语法到ES5
<span style="color: #0000ff">const {CodE} = transformFromAst(ast,<span style="color: #0000ff">null<span style="color: #000000">,{
presets: [<span style="color: #800000">'<span style="color: #800000">env<span style="color: #800000">'<span style="color: #000000">],})

<span style="color: #008000">//<span style="color: #008000"> 分配ID
<span style="color: #0000ff">const id = ID++

<span style="color: #008000">//<span style="color: #008000"> 返回这个模块
<span style="color: #0000ff">return<span style="color: #000000"> {
id,filename,dependencies,code,}
}

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

{ id: ename: message.jsStrict";\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文件已经变成了一个典型的模块,且依赖已经被分析出来了。接下来我们就要递归这个过程,把“依赖中的依赖”也都分析出来,也就是下一节要讨论的建立依赖关系图集。

@H_772_72@建立依赖关系图集

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

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

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

{ : [function A () {},{ : ,:

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

{ mainAsset =<span style="color: #008000">//<span style="color: #008000"> 维护一个数组,传入第一个模块
<span style="color: #0000ff">const
queue =<span style="color: #000000"> [mainAsset]

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

<span style="color: #008000">//<span style="color: #008000"> 最后返回这个queue,也就是依赖关系图集
<span style="color: #0000ff">return<span style="color: #000000"> queue
}

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

[ { id: 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);{ message.js: { id: ename: message.jsStrict";\n\nObject.defineProperty(exports,"__esModule",{\n value: true\n});\n\nvar _name = require("./name.js");\n\nexports.default = "Hello " + _name.name + "!";{ : { id: ename: {\n value: true\n});\nvar name = exports.name = \'world\';{} } ]

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

@H_772_72@进行打包

上一步生成的依赖关系图集,接下来将通过CommomJS规范来实现加载。由于篇幅关系,本文不对CommomJS规范进行扩展,有兴趣的读者可以参@阮一峰 老师的一篇文章https://link.juejin.im/?target=http%3A%2F%2Fwww.ruanyifeng.com%2Fblog%2F2015%2F05%2Fcommonjs-in-browser.html" rel="nofollow noopener noreferrer" target="_blank">《浏览器加载 CommonJS 模块的原理与实现》,说得非常清晰。简单来说,就是通过构造一个立即执行函数(function () {})(),手动定义@H_257_35@module,exportsrequire变量,最后实现代码在浏览器运行的目的。

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

{ let modules = graph.forEach(mod =><span style="color: #000000"> {
modules
+=<span style="color: #000000"> ${mod.iD}: [ function (require,module,exports) { ${mod.codE} },${JSON.Stringify(mod.mapping)},],
})

<span style="color: #0000ff">const result =<span style="color: #000000"> `
(function(modules) {
function require(id) {
<span style="color: #0000ff">const [fn,mapping] =<span style="color: #000000"> modules[id];

    function localrequire(Name) {
      </span><span style="color: #0000ff"&gt;return</span><span style="color: #000000"&gt; require(mapping[name]);
    }

    </span><span style="color: #0000ff"&gt;const</span> module =<span style="color: #000000"&gt; { exports : {} };

    fn(localrequire,module.exports);

    </span><span style="color: #0000ff"&gt;return</span><span style="color: #000000"&gt; module.exports;
  }

  require(</span><span style="color: #800080"&gt;0</span><span style="color: #000000"&gt;);
})({${modules}})

`
<span style="color: #0000ff">return<span style="color: #000000"> result
}

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

{ function require(id) { [fn,mapping] =function localrequire(Name) { </span><span style="color: #0000ff"&gt;return</span><span style="color: #000000"&gt; require(mapping[name]); } </span><span style="color: #0000ff"&gt;const</span> module =<span style="color: #000000"&gt; { exports: {} }; fn(localrequire,module.exports); </span><span style="color: #0000ff"&gt;return</span><span style="color: #000000"&gt; module.exports;

}

require(<span style="color: #800080">0<span style="color: #000000">);
})({
<span style="color: #800080">0<span style="color: #000000">: [
function (require,exports) {
<span style="color: #800000">"<span style="color: #800000">use Strict<span style="color: #800000">"<span style="color: #000000">;

  </span><span style="color: #0000ff"&gt;var</span> _message = require(<span style="color: #800000"&gt;"</span><span style="color: #800000"&gt;./message.js</span><span style="color: #800000"&gt;"</span><span style="color: #000000"&gt;);

  </span><span style="color: #0000ff"&gt;var</span> _message2 =<span style="color: #000000"&gt; _interoprequireDefault(_messagE);

  function _interoprequireDefault(obj) { </span><span style="color: #0000ff"&gt;return</span> obj &amp;&amp; obj.__esModule ? obj : { <span style="color: #0000ff"&gt;default</span><span style="color: #000000"&gt;: obj }; }

  console.log(_message2.</span><span style="color: #0000ff"&gt;default</span><span style="color: #000000"&gt;);
},{ </span><span style="color: #800000"&gt;"</span><span style="color: #800000"&gt;./message.js</span><span style="color: #800000"&gt;"</span>: <span style="color: #800080"&gt;1</span><span style="color: #000000"&gt; },</span><span style="color: #800080"&gt;1</span><span style="color: #000000"&gt;: [
function (require,exports) {
  </span><span style="color: #800000"&gt;"</span><span style="color: #800000"&gt;use Strict</span><span style="color: #800000"&gt;"</span><span style="color: #000000"&gt;;

  Object.defineProperty(exports,</span><span style="color: #800000"&gt;"</span><span style="color: #800000"&gt;__esModule</span><span style="color: #800000"&gt;"</span><span style="color: #000000"&gt;,{
    value: </span><span style="color: #0000ff"&gt;true</span><span style="color: #000000"&gt;
  });

  </span><span style="color: #0000ff"&gt;var</span> _name = require(<span style="color: #800000"&gt;"</span><span style="color: #800000"&gt;./name.js</span><span style="color: #800000"&gt;"</span><span style="color: #000000"&gt;);

  exports.</span><span style="color: #0000ff"&gt;default</span> = <span style="color: #800000"&gt;"</span><span style="color: #800000"&gt;Hello </span><span style="color: #800000"&gt;"</span> + _name.name + <span style="color: #800000"&gt;"</span><span style="color: #800000"&gt;!</span><span style="color: #800000"&gt;"</span><span style="color: #000000"&gt;;
},{ </span><span style="color: #800000"&gt;"</span><span style="color: #800000"&gt;./name.js</span><span style="color: #800000"&gt;"</span>: <span style="color: #800080"&gt;2</span><span style="color: #000000"&gt; },</span><span style="color: #800080"&gt;2</span><span style="color: #000000"&gt;: [
function (require,{
    value: </span><span style="color: #0000ff"&gt;true</span><span style="color: #000000"&gt;
  });
  </span><span style="color: #0000ff"&gt;var</span> name = exports.name = <span style="color: #800000"&gt;'</span><span style="color: #800000"&gt;world</span><span style="color: #800000"&gt;'</span><span style="color: #000000"&gt;;
},{},})</span></pre>

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

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

@H_772_72@归纳总结

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

大佬总结

以上是大佬教程为你收集整理的打包工具的核心原理(转自:https://juejin.im/entry/5b223ebd518825748b569bda)全部内容,希望文章能够帮你解决打包工具的核心原理(转自:https://juejin.im/entry/5b223ebd518825748b569bda)所遇到的程序开发问题。

如果觉得大佬教程网站内容还不错,欢迎将大佬教程推荐给程序员好友。

本图文内容来源于网友网络收集整理提供,作为学习参考使用,版权属于原作者。
如您有任何意见或建议可联系处理。小编QQ:384754419,请注明来意。
标签: