浏览器模块化之路(一),了解一下?

1,642 阅读4分钟

解决什么问题

浏览器本身不支持模块化

怎么解决

  1. seajs / require

是一种在线"编译"模块的方案,相当于在页面上加载一个 CMD/AMD 解释器。这样浏览器就认识了 define、exports、module 这些东西。也就实现了模块化。

  1. webpack (预编译模块的方案)「把模块转换成函数」

你在本地直接写JS,不管是 AMD / CMD / ES6 风格的模块化,它都能认识,并且编译成浏览器认识的JS。

  1. es6

ES6在语言标准的层面上, 实现了模块功能, 而且实现得相当简单, 完全可以取代CommonJS和AMD规范, 是浏览器和服务器通用的模块解决方案

浏览器模块化发展过程

从amd cmd 到 (commonjs), 再到浏览器支持模块化es6。

浏览器加载 CommonJS 模块的实现

  • IIFE
  • webpack的实现

IIFE

浏览器不兼容Commonjs的根本原因,是因为缺少NodeJs的4个环境变量

  • require
  • exports
  • module
  • global

解决办法

让浏览器支持,这几个环境变量。我们来定义这几个变量。

方案(立即执行函数+变量定义)

var module = {
    exports:{}
};

(function(module, exports) {
  exports.multiply = function (n) { return n * 1000 };
}(module, module.exports))

var f = module.exports.multiply;
f(5) // 5000

上面代码向一个立即执行函数提供 module 和 exports 两个外部变量,模块就放在这个立即执行函数里面。模块的输出值放在 module.exports 之中,这样就实现了模块的加载。

阮老师的实现

https://github.com/ruanyf/tiny-browser-require/blob/master/require.js

webpack的实现

webpack是如何支持commonjs的?

我们都知道,webpack作为一个构建工具,解决了前端代码缺少模块化能力的问题。我们写的代码,经过webpack构建和包装之后,能够在浏览器以模块化的方式运行。这些能力,都是因为webpack对我们的代码进行了一层包装,本文就以webpack生成的代码入手,分析webpack是如何实现模块化的。

PS: webpack的模块不仅指js,包括css、图片等资源都可以以模块看待,但本文只关注js。

分析

webpack打包的代码,整体可以简化成下面的结构:

(function (modules) {/* 省略函数内容 */})
([
function (module, exports, __webpack_require__) {
    /* 模块a.js的代码 */
},
function (module, exports, __webpack_require__) {
    /* 模块b.js的代码 */
}
]);

可以看到,整个打包生成的代码是一个IIFE(立即执行函数),函数内容我们待会看,我们先来分析函数的参数。

函数参数是我们写的各个模块组成的数组,只不过我们的代码,被webpack包装在了一个函数的内部,也就是说我们的模块,在这里就是一个函数。为什么要这样做,是因为浏览器本身不支持模块化,那么webpack就用函数作用域来hack模块化的效果。

如果你debug过node代码,你会发现一样的hack方式,node中的模块也是函数,跟模块相关的参数exports、require,或者其他参数__filename和__dirname等都是通过函数传值作为模块中的变量,模块与外部模块的访问就是通过这些参数进行的,当然这对开发者来说是透明的。

同样的方式,webpack也控制了模块的module、exports和require,那么我们就看看webpack是如何实现这些功能的。

下面是摘取的函数内容,并添加了一些注释:

// 1、模块缓存对象
var installedModules = {};
// 2、webpack实现的require
function __webpack_require__(moduleId) {
    // 3、判断是否已缓存模块
    if(installedModules[moduleId]) {
        return installedModules[moduleId].exports;
    }
    // 4、缓存模块
    var module = installedModules[moduleId] = {
        i: moduleId,
        l: false,
        exports: {}
    };
    // 5、调用模块函数
    modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
    // 6、标记模块为已加载
    module.l = true;
    // 7、返回module.exports
    return module.exports;
}
// 8、require第一个模块
return __webpack_require__(__webpack_require__.s = 0);


参考

  1. segmentfault.com/a/119000001…
  2. www.ruanyifeng.com/blog/2015/0…

总结

  • 浏览器模块化,从模块解释器到模块打包器,到浏览器支持模块化
  • webpack把每一个模块文件打包成一个函数,当然也有多个模块文件合并成一个函数的处理方式
  • 因为浏览器本身不支持模块化,所以webpack实现了模块化的功能,把我们commonjs写的模块转成了函数(webpack就用函数作用域来hack模块化的效果)
  • webpack 对于commonjs与es6模块混写的处理,后续分析

后续文章

1. webpack是如何支持es模块的

webpack对于es模块的实现,也是基于自己实现的__webpack_require__ 和__webpack_exports__ ,装换成类似于commonjs的形式。对于es模块和commonjs混用的情况,则需要通过__webpack_require__.n的形式做一层包装来实现。

2. webpack的 code splitting通过promise实现模块的动态加载

webpack的模块化不仅支持commonjs和es module,还能通过code splitting实现模块的动态加载。根据wepack官方文档,实现动态加载的方式有两种:import和require.ensure。

webpack通过__webpack_require__.e函数实现了动态加载,再通过webpackJsonp函数实现异步加载回调,把模块内容以promise的方式暴露给调用方,从而实现了对code splitting的支持。