模拟打包工具

模拟打包工具

摘要

随着前端的不断发展,有很多打包工具,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}}

 到这里我们基本上完成来所有的功能。希望你能对打包工具能有更深刻的理解。

猜你喜欢

转载自www.cnblogs.com/teteWang-Ember/p/10209109.html