模拟打包工具
摘要
随着前端的不断发展,有很多打包工具,webpack gulp grunt parcel 。现在傻瓜式操作越来越多,脚手架可以为我们解决不少麻烦,所以很多人就不理解打包工具到底做了什么,怎么做的。但是深入的了解其中的原理,可以更好的使用这些工具。
了解基础知识
1. 模块化知识
a. es6 modules是一个编译时就会确定模块相互之间的依赖关系
b. CommonJs模块规范中,Node对Js文件编译的过程中,会对文件的内容进行从头尾包装,在头部添加(function (export, require, __filename, __dirname){\n,在尾部添加\n}; 这样我们在单个文件Js内部可以使用这些参数。具体nodeJs模块化可以参考这片文章。
2. AST基础知识
什么是抽象语法树
在计算机科学中,抽象语法树(abstract syntax tree 或者缩写为 AST),或者语法树(syntax tree),是源代码的抽象语法结构的树状表现形式,这里特指编程语言的源代码。树上的每个节点都表示源代码中的一种结构。之所以说语法是「抽象」的,是因为这里的语法并不会表示出真实语法中出现的每个细节。
有很多现成的工具可以使用,把代码转化ast代码。
看下webpack在打包的时候都干了些什么
1. 先定义3个文件:
index.js
import a from './test' console.log(a) test.js import b from './message' const a = 'hello' + b export default a message.js const b = 'world' export default b
上面三个文件就是test.js引用了index.js的变量a,message.js又引用了test.js的变量b。
看下webpack打包后生成的文件
(function (modules) { var installedModules = {}; function __webpack_require__(moduleId) { if (installedModules[moduleId]) { return installedModules[moduleId].exports; } var module = installedModules[moduleId] = { i: moduleId, l: false, exports: {} }; modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); // Flag the module as loaded module.l = true; // Return the exports of the module return module.exports; } // expose the modules object (__webpack_modules__) __webpack_require__.m = modules; // expose the module cache __webpack_require__.c = installedModules; // define getter function for harmony exports __webpack_require__.d = function (exports, name, getter) { if (!__webpack_require__.o(exports, name)) { Object.defineProperty(exports, name, {enumerable: true, get: getter}); } }; // define __esModule on exports __webpack_require__.r = function (exports) { if (typeof Symbol !== 'undefined' && Symbol.toStringTag) { Object.defineProperty(exports, Symbol.toStringTag, {value: 'Module'}); } Object.defineProperty(exports, '__esModule', {value: true}); }; // create a fake namespace object // mode & 1: value is a module id, require it // mode & 2: merge all properties of value into the ns // mode & 4: return value when already ns object // mode & 8|1: behave like require __webpack_require__.t = function (value, mode) { /******/ if (mode & 1) value = __webpack_require__(value); if (mode & 8) return value; if ((mode & 4) && typeof value === 'object' && value && value.__esModule) return value; var ns = Object.create(null); __webpack_require__.r(ns); Object.defineProperty(ns, 'default', {enumerable: true, value: value}); if (mode & 2 && typeof value != 'string') for (var key in value) __webpack_require__.d(ns, key, function (key) { return value[key]; }.bind(null, key)); return ns; }; // getDefaultExport function for compatibility with non-harmony modules __webpack_require__.n = function (module) { var getter = module && module.__esModule ? function getDefault() { return module['default']; } : function getModuleExports() { return module; }; __webpack_require__.d(getter, 'a', getter); return getter; }; // Object.prototype.hasOwnProperty.call __webpack_require__.o = function (object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; // __webpack_public_path__ __webpack_require__.p = ""; // Load entry module and return exports return __webpack_require__(__webpack_require__.s = "./src/index.js"); })({ "./src/index.js": (function (module, __webpack_exports__, __webpack_require__) { "use strict"; 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?"); }), "./src/message.js": (function (module, __webpack_exports__, __webpack_require__) { // ... }), "./src/test.js": (function (module, __webpack_exports__, __webpack_require__) { // ... }) });
一开始有个自执行函数
(function(modules) { //.... })({ ///.... })
通过key: value键值对,而函数内部是我们定义的文件转移成 ES5 之后的代码,通过eval
来执行,为了方便大家理解,我对eval
内的代码做了一下格式化:
"use strict"; __webpack_require__.r(__webpack_exports__); // 获取"./src/test.js" 依赖 var _test__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/test.js"); console.log(_test__WEBPACK_IMPORTED_MODULE_0__["default"])
然后就是自执行函数,函数中会加载文件路径index.js文件,然后通过_webpack_require_来引入相应的依赖 test.js ,一次test.js也会引入message.js,就像剥洋葱一层层剥开,然后再把值返回给index.js。
开发一个简单的打包demo
想下打包工具能干啥
1. 转化语法 为了兼容浏览器 把ES6语法转化为ES5
2. 处理模块加载的依赖
3. 生成一个浏览器可以加载执行的js文件
/** * 1. 找到入口文件 获取所有的文件 * 2. 遍历所有的文件,获取深度队列依赖关系 * 3. 解析成ast树 返回一个依赖数组 数组的组成:(1)文件名 (2)文件依赖 (3)文件代码 * 4. 把文件打包成一个个的模块 */
转换语法
通过babel中的babylon生成AST树
通过babel-core将ast重新生成浏览器认识的源码
/** * 获取文件,解析成ast语法 * @param filename * @returns {*} */ function getAst(filename) { const content = fs.readFileSync(filename, 'utf-8') return babylon.parse(content, { sourceType: 'module', }); } /** * 把ast转化成源码 * @param ast * @returns {*} */ function getTranslateCode(ast) { const {code} = transformFromAst(ast, null, { presets: ['env'] }); return code }
接下来处理依赖的关系,需要一个依赖的关系图,babel-traverse提供来一个可以遍历的AST视图,并作处理。
/** * 获取依赖 * @param {*} ast * */ function getDependence(ast) { let dependencies = [] traverse(ast, { ImportDeclaration: ({node}) => { dependencies.push(node.source.value); } }) return dependencies } /** * 生成完整的文件依赖关系映射 * @param filename * @param entry * @returns {{filename: *, dependence, code: *}} */ function parse(filename, entry) { let filePath = filename.indexOf('.js') === -1 ? filename + '.js' : filename let dirName = entry ? '' : path.dirname(config.entry) //找到入口文件开始解析 let absolutePath = path.join(dirName, filePath) const ast = getAst(absolutePath); return { filename, dependence: getDependence(ast), code: getTranslateCode(ast), } }
现在我们看似已经找到所有的依赖,其实不是 我们只是找到了根文件的依赖,根文件的依赖可能也存在,但是我们并没有找到,所以我们还需要进行深度遍历。
/** * 获取深度队列依赖关系 * @param main * @returns {*[]} */ function getQueue(main) { let queue = [main] for(let asset of queue) { asset.dependence.forEach(dep => { let child = parse(dep) queue.push(child) }); } return queue }
现在我们已经得到了所有的依赖关系,接下来是对代码进行包装。要生成一个modules对象
function bundle(queue) { let modules = '' queue.forEach(function(mod) { modules += `'${mod.filename}': function (require, module, exports) { ${mod.code} }` }) // 。。。。。 }
得到modules对象,接下来就是对整体文件的外部包装,注册require, module.exports
(function(modules) { function require(fileName) { const fn = modules[fileName]; const module = { exports: {} }; fn(require, module, module.exports); return module.exports; } require('${config.entry}); }){${modules}}
到这里我们基本上完成来所有的功能。希望你能对打包工具能有更深刻的理解。