mini-webpack 源码浅析_cory chase minipack web-

minipack的打包流程

可以分成两大部分

  • 生成模块依赖(循环引用等问题没有解决的~,只是原理解析)
  • 根据处理依赖进行打包
源码的分析
const fs = require('fs')
const path = require('path')
const babylon = require('babylon')// AST 解析器
const traverse = require('babel-traverse').default // 遍历工具
const { transformFromAst } = require('babel-core') // babel-core

let ID = 0

/\*\*
 \* 获得文件内容, 从而在下面做语法树分析
 \* @param {\*} filename
 \*/
function createAsset (filename) {
  const content = fs.readFileSync(filename, 'utf-8')
  const ast = babylon.parse(content, { // 解析内容至AST
    sourceType: 'module'
  })

  const dependencies = [] // 初始化依赖集, dependencies存放该文件依赖项的相对path

  traverse(ast, { // 声明traverse的statement, 这里进ImportDeclaration 这个statement内。然后对节点import的依赖值进行push进依赖集
    ImportDeclaration: ({ node }) => {
      dependencies.push(node.source.value)
    }
  })

  const id = ID++ // id自增

  const { code } = transformFromAst(ast, null, { // 再将ast转换为文件
    presets: ['env']
  })

  // 返回这么模块的所有信息,设置的id filename 依赖集 代码
  return {
    id,
    filename,
    dependencies,
    code
  }
}

/\*\*
 \*从entry入口进行解析依赖图谱
 \* @param {\*} entry
 \*/
function createGraph (entry) {
  const mainAsset = createAsset(entry) // 从入口文件开始
  const queue = [mainAsset] // 最初的依赖集

  for (const asset of queue) { // 一张图常见的遍历算法有广度遍历与深度遍历,这里采用的是广度遍历
    asset.mapping = {} // 给当前依赖做mapping记录
    const dirname = path.dirname(asset.filename)// 获得依赖模块地址
    asset.dependencies.forEach(relativePath => { // 刚开始只有一个asset 但是dependencies可能多个
      const absolutePath = path.join(dirname, relativePath)// 这边获得绝对路径
      const child = createAsset(absolutePath) // 递归依赖的依赖
      asset.mapping[relativePath] = child.id // 将当前依赖及依赖的依赖都放入到mappnig里
      queue.push(child) // 广度遍历借助队列
    })
  }

  return queue // 返回遍历完依赖的队列
}

/\*\*
 \* 将graph模块打包bundle输出
 \* @param {\*} graph
 \*/
function bundle (graph) {
  let modules = ''
  graph.forEach(mod => {
    modules += `${mod.id}: [
 function (require, module, exports) { ${mod.code} },
 ${JSON.stringify(mod.mapping)},
 ],`
  })

  // CommonJS风格
  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
}

module.exports = {
  bundle,
  createGraph
}

模块依赖生成

具体步骤

  • 给定入口文件
  • 根据入口文件分析依赖(借助bable获取)
  • 广度遍历依赖图获取依赖
  • 根据依赖图生成(模块id)key:(数组)value的对象表示
  • 建立require机制实现模块加载运行

一个简单的实例

原始代码

// 入口文件 entry.js
import message from './message.js'

console.log(message);


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

export default `hello ${name}!`


// name.js
export const name = 'world'


读取文件内容,分析依赖,第一步需要解析源码,生成抽象语法树。

  • 第一步,读取入口文件,生成 AST,递归生成依赖关系对象 graph

其中,createAsset 函数是解析js文本,生成每个文件对应的一个对象,其中 code 的代码是经过babel-preset-env转换后可在浏览器中执行的代码。

const { code } = transformFromAst(ast, null, {
    presets: ['env']
  })

createGraph 函数生成依赖关系对象。

[ 
  { 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: {} } 
	
]

有了依赖关系图,下一步就是将代码打包可以在浏览器中运行的包。

首先我们将依赖图解析成如下字符串(其实是对象没用{}包裹的格式):
关键代码是这句:

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

生成出来的代码如下:

  0: [
      function (require, module, exports) {
        // -------------- mod.code --------------
        "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) {
        // -------------- mod.code --------------
        "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) {
        // -------------- mod.code --------------
        "use strict";
        Object.defineProperty(exports, "\_\_esModule", {
          value: true
        });
        var name = exports.name = 'world';
        // --------------------------------------
      },
      {},
    ],

依赖的图生成的文件可以简化为:

modules = {
    0: [function code , {deps} ],
    1: [function code , {deps} ]
}

这里,我们比较下源码:

// 入口文件 entry.js
import message from './message.js';

console.log(message);

// ---
"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
import {name} from './name.js';

export default `hello ${name}!`;

// ---
"use strict";
Object.defineProperty(exports, "\_\_esModule", {
  value: true
});
var _name = require("./name.js");
exports.default = "hello " + _name.name + "!";


// name.js
export const name = 'world';

// ---
"use strict";
Object.defineProperty(exports, "\_\_esModule", {
  value: true
});
var name = exports.name = 'world';


可以看出,babel在转换原始code的时候,引入了require函数来解决模块引用问题。但是其实浏览器仍然是不认识的。因此还需要额外定义一个require函数(其实这部分和requirejs原理类似的模块化解决方案,其中原理其实也很简单)

得到这个字符串后,再最后拼接起来即最终结果。

最后,我们还需要定义一个自执行函数文本,并将上述字符串传入其中,拼接结果如下:

(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';
		},
		{},
	],
})

跳槽是每个人的职业生涯中都要经历的过程,不论你是搜索到的这篇文章还是无意中浏览到的这篇文章,希望你没有白白浪费停留在这里的时间,能给你接下来或者以后的笔试面试带来一些帮助。

也许是互联网未来10年中最好的一年。WINTER IS COMING。但是如果你不真正的自己去尝试尝试,你永远不知道市面上的行情如何。这次找工作下来,我自身感觉市场并没有那么可怕,也拿到了几个大厂的offer。在此进行一个总结,给自己,也希望能帮助到需要的同学。

面试准备

面试准备根据每个人掌握的知识不同,准备的时间也不一样。现在对于前端岗位,以前也许不是很重视算法这块,但是现在很多公司也都会考。建议大家平时有空的时候多刷刷leetcode。算法的准备时间比较长,是一个长期的过程。需要在掌握了大部分前端基础知识的情况下,再有针对性的去复习算法。面试的时候算法能做出来肯定加分,但做不出来也不会一票否决,面试官也会给你提供一些思路。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值