webpack简易打包原理

567 阅读3分钟

关于最近学习webapck打包原理的一些新的分享 

我们都知道webpack是静态模块打包器,他能根据我们的项目入口,找到模块与模块之间的依赖,帮我们构建出我们最终想要的代码, 并顺利的在浏览器运行,但是平时工作中可能我们只知道这些东西

 1.webpack需要配置很多loader,插件... 

 2.webpack源码复杂,生命周期特别多... 

 3.想深入了解无处下手

 那现在就来看看webpack是如何进行打包,以及依赖查找的 

 模块化打包 

最终打包结果得到两样东西

1.一个与模块依赖的对象参数

2.自执行函数

(function (modules) {
  function exec(id) {
    let [fn, mapping] = modules[id];
    console.log(fn, mapping)
    let module = { exports: {} };
    fn && fn(require, module.exports, module);
    function require(path) {
      //根据模块路径,返回模块执行的结果
      return exec(mapping[path]);
    }
    return module.exports;
  }
  exec(0)
})(
  {
    0: [
      function (require, exports, module) {
        let action = require('./action.js').action;
        let name = require('./name.js').name;

        let message = `${name} is ${action}`;
        console.log(message);
      },
      { "./action.js": 1, "./name.js": 2 }
    ], 1: [
      function (require, exports, module) {
        let action = '我是action模块';

        exports.action = action;
      },
      {}
    ], 2: [
      function (require, exports, module) {
        exports.name = '我是name模块';

      },
      {}
    ],
  }
)

各个模块具体内容

index.js

let action = require('./action.js').action;
let name = require('./name.js').name;
let message = `${name} and ${action}`;
console.log(message);

action.js

let action = '我是action模块';exports.action = action;

name.js

exports.name = '我是name模块';

需要构建的数据结构

我们发现一个模块的定义的数据结构是这样的

  • 模块自增id
  • 被一个函数包裹的模块内容,为了在浏览器能运行,重写了require方法,requier作为一个模块的参数传递
  • 与模块相关的依赖模块

实现这个数据结构

模块定义

  • 唯一id

  • 文件路径

  • 包含依赖文件的数组

  • 模块内容

  • id与模块间的引用关系

我们先来尝试处理前面三个

const fs = require('fs');
const path = require('path'); let ID = 0;
/** * 递归获取模块依赖  
 * @param {*} str 模块内容   
 *  @return 模块依赖的数组集合  
 */
function getDependencies(str) {
  let reg = /require\(['"](.+?)['"]\)/g;//正则匹配ruquire后面的路径    
  let result = null; let dependencies = [];
  while (result = reg.exec(str)) { dependencies.push(result[1]); }
  return dependencies;
}
/** * 获取模块对象  
 * @param {*} filename 文件路径  
 * @return 模块对象  
 */
function createAsset(filename) {
  let fileContent = fs.readFileSync(filename, 'utf-8');
  const id = ID++;
  return {
    id: id,
    filename: filename,
    dependencies: getDependencies(fileContent), //当前模块所依赖模块    
    code: `function(require, exports, module) {               
      ${fileContent}        
    }`
  }
}

 createAsset返回值,但是这个返回值无法帮我们拿到模块之间的引用,也就是这个对象{'./action.js': 1},如果我知道了这个对象引用的id为1,那么我们就能拿到完整的模块集合了

 {
  id: 0,
  filename: './src/index.js',
  dependencies: [ './action.js', './name.js' ],
  code: 'function(require, exports, module) { \n' +
    "        let action = require('./action.js').action;\n" +
    "let name = require('./name.js').name;\n" +
    '\n' +
    'let message = `${name} is ${action}`;\n' +
    'console.log(message);\n' +
    '    }'
}

通过id获取模块引用

  • id与模块间的引用关系

使用let of 进行遍历,let of会继续遍历新添加的元素

/**通过id获取模块引用  
 *  @param {*} filename 文件路径  
 *  @return 模块对象 
 */
function createGraph(filename) {
  let asset = createAsset(filename);  //模块 
  let queue = [asset];  // 把模块放入数组,遍历,push,遍历...    
  for (let asset of queue) {
    const dirname = path.dirname(asset.filename);
    asset.mapping = {};
    asset.dependencies.forEach(relativePath => {
      const absolutePath = path.join(dirname, relativePath);
      const child = createAsset(absolutePath);
      asset.mapping[relativePath] = child.id;
      queue.push(child);
    });
  } return queue;
}

最终可以拿到这样的数据

[{  
 id: 0, filename: './src/index.js',  
 dependencies: ['./action.js', './name'], 
 mapping: { './action.js': 1, './name': 2 },  // 模块与id引用关系    
 code: 'function(require, exports, module) {                
     let action = require(\'./action.js\').action;       
     let name = require(\'./name\').name;       
     let message = `${name} is ${action}`; console.log(message);  }'
},
 {  1,      // 模块id为1         
    xxx.js  //模块路径   
}]

最后我们再通过循环构建出一个开始想要的一个模块集合,加一个处理引用的递归函数,再拼成一个完成的代码,走你!

function createBundle(graph) {
  let modules = '';
  graph.forEach(mod => {
    modules += `${mod.id}: [
      ${mod.code},
      ${JSON.stringify(mod.mapping)}
    ],`;
  });
  const result = `(function(modules){
    function exec(id) {
      let [fn, mapping] = modules[id];
      console.log(fn, mapping)
      let module = { exports: {} };
      fn && fn(require, module.exports, module);
      function require(path) {
        //根据模块路径,返回模块执行的结果
        return exec(mapping[path]);
      }
      return module.exports;
    }
    exec(0)
  })(
    {${modules}}
  )`
  fs.writeFileSync('./dist/bundle.js', result);
}createBundle(createGraph('./src/index.js'))

最后打包生成的文件

(function (modules) {
  function exec(id) {
    let [fn, mapping] = modules[id];
    console.log(fn, mapping)
    let module = { exports: {} };
    fn && fn(require, module.exports, module);
    function require(path) {
      //根据模块路径,返回模块执行的结果
      return exec(mapping[path]);
    }
    return module.exports;
  }
  exec(0)
})(
  {
    0: [
      function (require, exports, module) {
        let action = require('./action.js').action;
        let name = require('./name.js').name;

        let message = `${name} is ${action}`;
        console.log(message);
      },
      { "./action.js": 1, "./name.js": 2 }
    ], 1: [
      function (require, exports, module) {
        let action = '我是action模块';

        exports.action = action;
      },
      {}
    ], 2: [
      function (require, exports, module) {
        exports.name = '我是name模块';

      },
      {}
    ],
  }
)

理一下执行顺序

1.exec(0)会拿到模块的入口文件,也就是id为0的这项,并执行fn()

2.exec(mapping[path]),  mapping是结构的第二个模块的引用对象

{ "./action.js": 1, "./name.js": 2 }

path能拿到key,mapping[path]得到模块对应的id,继续递归执行下一个模块

最后在浏览器跑一下,完美

最后想说,其实总的代码是挺简单的,稍微花点时间,我觉得每个人都看得懂,但是这么一点代码能带来非常多的收获,我觉得是很值得一看,结束!