打包工具的核心原理(转自:https://juejin.im/entry/5b223ebd518825748b569bda)
打包工具就是负责把一些分散的小模块,按照一定的规则整合成一个大模块的工具。与此同时,打包工具也会处理好模块之间的依赖关系,最终这个大模块将可以被运行在合适的平台中。
打包工具会从一个入口文件开始,分析它里面的依赖,并且再进一步地分析依赖中的依赖,不断重复这个过程,直到把这些依赖关系理清挑明为止。
从上面的描述可以看到,打包工具最核心的部分,其实就是处理好模块之间的依赖关系。
为了简单起见,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.js → message.js → name.js,其中entry.js将会成为打包工具的入口文件。
但是,这里面的依赖关系只是我们人类所理解的,如果要让机器也能够理解当中的依赖关系,就需要借助一定的手段了。
依赖关系解析
新建一个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-core的transformFromAst方法搭配babel-preset-env插件,把ES6语法转化成浏览器可以识别的ES5语法,并且为该js模块分配一个ID。
let ID = 
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: ,
  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文件已经变成了一个典型的模块,且依赖已经被分析出来了。接下来我们就要递归这个过程,把“依赖中的依赖”也都分析出来,也就是下一节要讨论的建立依赖关系图集。
建立依赖关系图集
新建一个名为createGragh()的函数,传入一个入口文件的路径作为参数,然后通过createAsset()解析这个文件使之定义成一个模块。
接下来,为了能够挨个挨个地对模块进行依赖分析,所以我们维护一个数组,首先把第一个模块传进去并进行分析。当这个模块被分析出还有其他依赖模块的时候,就把这些依赖模块也放进数组中,然后继续分析这些新加进去的模块,直到把所有的依赖以及“依赖中的依赖”都完全分析出来。
与此同时,我们有必要为模块新建一个mapping属性,用来储存模块、依赖、依赖ID之间的依赖关系,例如“ID为0的A模块依赖于ID为2的B模块和ID为3的C模块”就可以表示成下面这个样子:
{
  : [function A () {}, { 'B.js': , 'C.js':  }]
}
搞清楚了个中道理,就可以开始编写函数了。
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
}
尝试运行一下createGraph('./example/entry.js'),就能够看到如下的输出:
[ { id: ,
    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':  } },
  { id: ,
    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':  } },
  { id: ,
    filename: 'example/name.js',
    dependencies: [],
    code: '"use strict";\n\nObject.defineProperty(exports, "__esModule", {\n  value: true\n});\nvar name = exports.name = \'world\';',
    mapping: {} }
 ]
现在依赖关系图集已经构建完成了,接下来就是把它们打包成一个单独的,可直接运行的文件啦!
进行打包
上一步生成的依赖关系图集,接下来将通过CommomJS规范来实现加载。由于篇幅关系,本文不对CommomJS规范进行扩展,有兴趣的读者可以参考@阮一峰 老师的一篇文章《浏览器加载 CommonJS 模块的原理与实现》,说得非常清晰。简单来说,就是通过构造一个立即执行函数(function () {})(),手动定义module,exports和require变量,最后实现代码在浏览器运行的目的。
接下来就是依据这个规范,通过字符串拼接去构建代码块。
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();
    })({${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();
})({
  : [
    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":  },
  ], : [
    function (require, module, exports) {
      "use strict";
      Object.defineProperty(exports, "__esModule", {
        value: true
      });
      var _name = require("./name.js");
      exports.default = "Hello " + _name.name + "!";
    },
    { "./name.js":  },
  ], : [
    function (require, module, exports) {
      "use strict";
      Object.defineProperty(exports, "__esModule", {
        value: true
      });
      var name = exports.name = 'world';
    },
    {},
  ],
})
这段代码将能够直接在浏览器运行,输出“Hello world!”。
至此,整一个打包工具已经完成。
归纳总结
经过上面几个步骤,我们可以知道一个模块打包工具,第一步会从入口文件开始,对其进行依赖分析,第二步对其所有依赖再次递归进行依赖分析,第三步构建出模块的依赖图集,最后一步根据依赖图集使用CommonJS规范构建出最终的代码。明白了当中每一步的目的,便能够明白一个打包工具的运行原理
打包工具的核心原理(转自:https://juejin.im/entry/5b223ebd518825748b569bda)的更多相关文章
- 透过实现小型打包工具理解webpack
		
面试经常有问到 webpack,偶遇一篇比较有实用价值的且有利于理解的文章,现总结如下: 本篇文章中要实现的这个迷你打包工具,它主要能实现如下两个功能: ①.将 ES6 转换成 ES5: ②.支持在 ...
 - Maven 核心原理
		
Maven 核心原理 标签 : Java基础 Maven 是每一位Java工程师每天都会接触的工具, 但据我所知其实很多人对Maven理解的并不深, 只把它当做一个依赖管理工具(下载依赖.打包), M ...
 - 开源框架TLog核心原理架构解析
		
前言 最近在做TLog 1.2.5版本的迭代,许多小伙伴之前也表示说很想参与开源项目的贡献.为了让项目更好更快速的迭代新特性以及本着发扬开源精神互相学习交流,很有幸招募到了很多小伙伴与我一起前行. 为 ...
 - 【算法】(查找你附近的人)  GeoHash核心原理解析及代码实现
		
本文地址 原文地址 分享提纲: 0. 引子 1. 感性认识GeoHash 2. GeoHash算法的步骤 3. GeoHash Base32编码长度与精度 4. GeoHash算法 5. 使用注意点( ...
 - Atitit.项目修改补丁打包工具 使用说明
		
Atitit.项目修改补丁打包工具 使用说明 1.1. 打包工具已经在群里面.打包工具.bat1 1.2. 使用方法:放在项目主目录下,执行即可1 1.3. 打包工具的原理以及要打包的项目列表1 1. ...
 - iOS 本地自动打包工具
		
1.为什么要自动打包工具? 每修改一个问题,测试都让你打包一个上传fir , 你要clean -> 编译打包 -> 上传fir -> 通知测试.而且打包速度好慢,太浪费时间了.如果有 ...
 - Unity 游戏框架搭建 (十一) 简易AssetBundle打包工具(一)
		
最近在看Unity官方的AssetBundle(以下简称AB)的教程,也照着做了一遍,不过做出来的AssetBundleManager的API设计得有些不太习惯.目前想到了一个可行的解决方案.AB相关 ...
 - 多渠道打包工具Walle源码分析
		
一.背景 首先了解多渠道打包工具Walle之前,我们需要先明确一个概念,什么是渠道包. 我们要知道在国内有无数大大小小的APP Store,每一个APP Store就是一个渠道.当我们把APP上传到A ...
 - 手写webpack核心原理,再也不怕面试官问我webpack原理
		
手写webpack核心原理 目录 手写webpack核心原理 一.核心打包原理 1.1 打包的主要流程如下 1.2 具体细节 二.基本准备工作 三.获取模块内容 四.分析模块 五.收集依赖 六.ES6 ...
 
随机推荐
- 实现MySQL数据库的实时备份
			
实现MySQL数据库的实时备份 使用MySQL Replication 吴剑 2018-08-03 原创文章,转载必需注明出处:http://www.cnblogs.com/wu-jian 吴剑 ht ...
 - Angular 4+ 修仙之路
			
Angular 4.x 快速入门 Angular 4 快速入门 涉及 Angular 简介.环境搭建.插件表达式.自定义组件.表单模块.Http 模块等 Angular 4 基础教程 涉及 Angul ...
 - [转] sqlserver 中查看trigger的disabled/enabled情况
			
本文转自:http://blog.csdn.net/miqi770/article/details/48708199 SELECT t.name AS TableName, tr.name AS Tr ...
 - 封装简单的API——微信小程序
			
前几天自己琢磨微信小程序的基本开发,里边用到的技术包括WebAPI,也就是方法的封装. 当然也可以用ASP.NET MVC WCF来写接口.更简单应该就是 WinForm 简单易部署. 这里用的是 2 ...
 - [转]如何选择Html.RenderPartial和Html.RenderAction
			
Html.RenderPartial与Html.RenderAction这两个方法都是用来在界面上嵌入用户控件的. Html.RenderPartial是直接将用户控件嵌入到界面上: <%Htm ...
 - [转] .NET出现频率非常高的笔试题
			
又到了金三银四的跳槽季,许多朋友又开始跳槽了,这里我简单整理了一些出现频率比较高的.NET笔试题,希望对广大求职者有所帮助. 一..net基础 1. a=10,b=15,请在不使用第三方变量的情况下 ...
 - Dapper的简单使用(初学者归纳)
			
Dapper的简单使用(初学者归纳) //引用:using System;using System.Collections.Generic;using System.Linq;using System ...
 - springboot+mybatis遇到BUG:自动注入失败
			
今天用springboot+mybatis写一个小demo遇到如下错误 Error starting ApplicationContext. To display the conditions rep ...
 - MyBatis别名
			
Spring的别名管理比较规范,有严格的接口规范,SimpleAliasRegistry实现 -> AliasRegistry接口,而且是线程安全的,Map也用的是ConcurrentHashM ...
 - 【SSH网上商城项目实战15】线程、定时器同步首页数据(类似于博客定期更新排名)
			
转自:https://blog.csdn.net/eson_15/article/details/51387378 上一节我们做完了首页UI界面,但是有个问题:如果我在后台添加了一个商品,那么我必须重 ...