webpack核心功能实现

2021 M02 14

前言

站在面试角度,webpack核心功能实现无非是plugin,loader。 更深入一点,自然是同步加载和异步加载。 如果说到加分项,那优化配置,提高打包速度也都算,但这部分不再本系列中。

本篇为源码系列核心实现第六篇,对应下图webpack部分。

src

同步加载

概述

不论是同步加载还是异步加载,webpack打包结果都是一个IIFE。 该IIFE接收一个类型为对象的modules参数, 该参数是一个以模块路径为key,函数为value的对象,其中函数体是文件内容。 对于每个require语法,都会被替换为webpack_require,每个文件路径都会补全为src下相关路径。 module(打包前),chunk(打包过程中),bundle(打包后) 是同一套代码在webpack打包不同时机的不同称呼。

实现

直接丢代码了,关键位置已经加了注释,代码不多,应该比较好理解。

(function (modules) {

    //模块缓存
    var installedModules = {}

    //自己实现的__webpack_require__ 取代原生的require
    function __webpack_require__(moduleId) {

        //命中缓存 直接返回
        if (installedModules[moduleId]) {
            return installedModules[moduleId].exports
        }
        //未命中缓存 添加
        var module = installedModules[moduleId] = {
            id: moduleId,//模块id
            load: false,//默认模块没有被加载,
            exports: {},//导出的内容
        }

        //加载对应模块
        //这里的this指向为module.exports貌似是一种规范
        modules[moduleId].call(
          module.exports, module, 
          module.exports, __webpack_require__
          );

        module.load = true//已经加载

        return module.exports

    }

    //加载入口模块
    return __webpack_require__('./src/index.js')

})(

    {
        './src/index.js': (function (module, exports, __webpack_require__) {
            const title = __webpack_require__('./src/title.js')
            console.log(title)
        }),
        './src/title.js': (function (module, exports,) {
            module.exports = 'hello'
        })


    })

注意:同步加载用了闭包做缓存,在面试时候这是一个很好的闭包使用场景举例。

异步加载

概述

首先要明确一点,异步加载包含同步加载。 最先被加载的入口文件index.js就是同步加载,异步加载指的是其他模块。 整个异步加载过程其实有三个重要环节:同步加载,e函数,t函数。

同步加载好理解,e函数和t函数是什么东西呢?

e函数内部会通过创建script并指定src去加载其他模块,最后会把加载的依赖结果处理成promise返回。 t函数内部会进行esmodule和commonjs的兼容处理。

打包前vs打包后


//打包前
let btn = document.createElement('button')
btn.innerText = '按我'
document.body.appendChild(btn)

btn.addEventListener('click', () => {
    import(/* webpackChunkName:'title' */'./title').then(res => {
        console.log(res.default)
    })
})


//打包后

(function (modules) {
//...    
}, {
    "./src/index.js":

        (function (module, exports, __webpack_require__) {

            let btn = document.createElement('button');
            btn.innerText = '按我';
            document.body.appendChild(btn);
            btn.addEventListener('click', () => {
                __webpack_require__.e(/*! import() | title */ "title")
                    .then(__webpack_require__.t.bind(null, 
                    /*! ./title */ "./src/title.js", 7))
                    .then(res => {
                        console.log(res.default);
                    });
            });

        })
})



e函数之所以能够.then,是因为已经成了promise。 根据promise的链式调用原理,e函数return的结果会交给t函数继续处理。 同理,t函数的处理结果也会再次向后传递。 而此时的结果必然是兼容处理后的,带有default属性的对象。

异步加载的实现



  // The module cache
  var installedModules = {};

  // object to store loaded and loading chunks
  // undefined = chunk not loaded, null = chunk preloaded/prefetched
  // Promise = chunk loading, 0 = chunk loaded
  var installedChunks = {
    "main": 0
  };


  function __webpack_require__(moduleId) {

    // Check if module is in cache
    if (installedModules[moduleId]) {
      return installedModules[moduleId].exports;
    }
    // Create a new module (and put it into the cache)
    var module = installedModules[moduleId] = {
      i: moduleId,
      l: false,
      exports: {}
    };

    // Execute the module function
    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;
  }


  __webpack_require__.e = function (chunkId) {

    var promises = []

    var installedChunkData = installedChunks[chunkId]

     //同样的资源加载后不会加载第二次
    if (installedChunkData !== 0) {
      var promise = new Promise(function (resolve, reject) {
        installedChunkData = installedChunks[chunkId] = [resolve, reject]
      })

      promises.push(installedChunkData[2] = promise);

      var script = document.createElement('script')
      script.src = chunkId + '.bundle.js'
      document.head.appendChild(script)
    }


    return Promise.all(promises)


  }


  var jsonpArray = window["webpackJsonp"] = window["webpackJsonp"] || [];

  //核心是push方法
  jsonpArray.push = webpackJsonpCallback;


  // 异步加载的模块结构如下

(window["webpackJsonp"] = window["webpackJsonp"] || []).push([["say"], {

  "./src/say.js":
    (function (module, exports) {
      module.exports = 'say import ';
    })

}]);

//该模块被加载就会执行push方法,也就是执行webpackJsonpCallback

// webpackJsonpCallback 接收一个data参数
// data形如 [[chunkId1,chunkId2],{}]
  function webpackJsonpCallback(data) {
    var chunkIds = data[0];
    var moreModules = data[1];

    var moduleId, chunkId, i resolves = [];
    for ( var = 0; i < chunkIds.length; i++) {
         chunkId = chunkIds[i];
         //存储resolves回调数组
        resolves.push(installedChunks[chunkId][0]);//[resolve,reject,promise]
      }
        //将所有加载后的chunkId标识为已加载
      installedChunks[chunkId] = 0;
    }

  //将异步加载的模块与原模块合并,后续会在t函数内部执行一次webpack_require加载
    for (moduleId in moreModules) {
        modules[moduleId] = moreModules[moduleId];
    }

   //取出resolves回调数组并逐步执行
    while (resolves.length) {
      resolves.shift()();
    }


//t函数会加载异步合并的代码并进行esmodule和commonjs语法的兼容处理
//经过t函数处理后的结果必然是一个带有default属性的对象
//promsie中,返回的内容会在下一个then中获取

// 默认进入时mode是7
 // 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;
  };


  };


关键部分已经给出注释,但可能细节上还需要动手多测试,验证。

loader

loader概述

webpack本身只能处理js文件,loader就是扩展webpack打包能力用的, 告诉它非js格式文件该怎么处理。比如css相关的style-loader,css-loader。

loader其实和node中的pipe很像,如同流水线,一个loader之后可以接另一个loader。 但最终,必须是一个js形式收尾。

实现一个简易loader

loader的本质是一个函数,该函数通过source参数接收输入,通过返回值输出。 下面以一个markdown-loader为例,举例说明一个loader是如何实现的。

const marked = require('marked')
module.exports = source => {
  const html = marked(source)
  return  `module.exports = ${JSON.stringify(html)}`
}

这就完事了?是的,就是如此简单。 测试的话,这个loader可以在webpack的配置文件中以本地路径形式加载。

plugin

plugin概述

Webpack 的插件本质就是各种钩子,这就像组件生命周期一样,不同阶段存在不同钩子函数。 webpack赋予了这些钩子函数特定的能力,直接拿过来用就行了。

实现一个简易plugin

plugin可以写成一个类的形式,然后身上有个apply方法,这个是webpack约定好的。 下面写一个简单的小插件,用于打印构建过程涉及的文件名。

class GetFileListPlugin {
    apply(compiler) {
        compiler.hooks.emit.tap('GetFileListPlugin ', compilation => {
            // compilation => 可以理解为此次打包的上下文
            for (const name in compilation.assets) {
                console.log(name)
                const contents = compilation.assets[name].source()
                compilation.assets[name] = {
                    //用于返回新内容 
                    source: () => contents,
                    //返回内容大小
                    size: () => noComments.length
                }
            }
        })

    }
}

module.exports = GetFileListPlugin

源码压缩包

再会

情如风雪无常,

却是一动既殇。

感谢你这么好看还来阅读我的文章,

我是冷月心,下期再见。