likes
comments
collection
share

rollup打包产物解析及原理(对比webpack)

作者站长头像
站长
· 阅读数 35

rollup打包产物解析及原理(对比webpack)

rollup定位

rollup比webpack晚出2年,对比webpack肯定是有差异化的

我们可以查看官网rollupjs.org/guide/en/#o…

得到以下几个特点

rollup 使用流程

浏览器环境使用的应用程序的话:

  1. 无需考虑浏览器兼容问题的话
    • 开发者写esm代码 -> rollup通过入口,递归识别esm模块 -> 最终打包成一个或多个bundle.js -> 浏览器直接可以支持引入<script type="module">
  2. 需考虑浏览器兼容问题的话
    • 可能会比较复杂,需要用额外的polyfill库,或结合webpack使用

打包成npm包的话:

  • 开发者写esm代码 -> rollup通过入口,递归识别esm模块 -> (可以支持配置输出多种格式的模块,如esm、cjs、umd、amd)最终打包成一个或多个bundle.js
    • (开发者要写cjs也可以,需要插件@rollup/plugin-commonjs) 初步看来
  • 很明显,rollup 比较适合打包js库(react、vue等的源代码库都是rollup打包的)或 高版本无需往下兼容的浏览器应用程序(现在2022年了,时间越往后,迁移到rollup会越多,猜测)
  • 这样打包出来的库,可以充分使用上esm的tree shaking,使源库体积最小

举个小🌰简单的对比一下 webpack打包和rollup打包

此demo是纯esm的写法

// 入口main。js
import { b } from './test/a'
console.log(b + 1)
console.log(1111)

// './test/a'
export const b = 'xx'
export const bbbbbbb = 'xx'

rollup打包效果(非常干净,无注入代码)

const b = 'xx';
console.log(b + 1);
console.log(1111);

webpack打包效果(有很多注入代码)

  • 实际上,我们自己写的代码在最下面。上面注入的大段代码 都是webpack自己的兼容代码目的是自己实现require,modules.exports,export,让浏览器可以兼容cjs和esm语法
  • (可以理解为,webpack自己实现polyfill支持模块语法,rollup是利用高版本浏览器原生支持esm(所以rollup无需代码注入)
/******/ (function(modules) { // webpackBootstrap
/******/   // The module cache
/******/   var installedModules = {};
/******/
/******/   // The require function
/******/   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;
/******/   }
/******/
/******/
/******/   // 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 = 0);
/******/ })
/************************************************************************/
/******/ ([
/* 0 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {

"use strict";
// ESM COMPAT FLAG
__webpack_require__.r(__webpack_exports__);

// CONCATENATED MODULE: ./src/test/a.js
const b = 'xx';
const bbbbbbb = 'xx';

// CONCATENATED MODULE: ./src/main.js
console.log(b + 1);
console.log(1111); 

/***/ })
/******/ ]);

两者处理源代码模块的对比

纯esm纯cjs两者混用
webpack支持(有代码注入)支持(有代码注入)支持(有代码注入)
rollup支持(无注入)原生不支持(需增加插件@rollup/plugin-commonjs)原生不支持(需增加插件@rollup/plugin-commonjs)

rollup的初衷也是希望开发者去写esm,而不是cjs。因为esm是javascript的新标准,是未来,有很多优点,高版本浏览器也支持

两者处理对外暴露模块,非常不一样!!(解释rollup为什么适合打包库)

上面的demo 加上export 导出

// 入口main。js
import { b } from './test/a'
console.log(b + 1)
console.log(1111)
export { // 新增导出
  b
}

// './test/a'
export const b = 'xx'
export const bbbbbbb = 'xx'

rollup打包 导出(非常干净,无注入代码)

  • rollup本身不去做polyfill
  • rollup的配置文件无需特殊配置,而且还可以支持多种模块导出(esm,cjs,umd,amd)
    // rollup.config.js
    const OUTPUT_DIR = 'dist'
    const INPUT_FILE = 'src/main.js'
    export default[
     // esm
      {
        input: INPUT_FILE,
        output: {
          file: OUTPUT_DIR + '/esm/index.js',
          format: 'esm' // 导出esm模块 
        }
      },
      // commonjs
      {
        input: INPUT_FILE,
        output: {
          file: OUTPUT_DIR + '/cjs/index.js',
          format: 'cjs' // 导出cjs模块 
        }
      },
      // umd
      {
        input: INPUT_FILE,
        output: {
          file: OUTPUT_DIR + '/umd/index.js',
          format: 'umd' // 导出umd模块 
        }
      },
    ]
    
    打包的到 esm 和 cjs
    // esm
    const b = 'xx';
    console.log(b + 1);
    console.log(1111);
    export { b };
    
    // cjs
    const b = 'xx';
    console.log(b + 1);
    console.log(1111);
    exports.b = b;
    
    // umd (兼容3种写法:cjs,amd,global(global可以初步理解为直接通过window传值))
    (function (global, factory) {
        typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
        typeof define === 'function' && define.amd ? define(['exports'], factory) :
        (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.aa = {}));
    })(this, (function (exports) { 'use strict';
        const b = 'xx';
        console.log(b + 1);
        console.log(1111);
        exports.b = b;
        Object.defineProperty(exports, '__esModule', { value: true });
    }));
    

webpack 导出 (区别巨大,注入代码较多,导出esm支持的不太好)

  • webpack需配置 (此处是 webpack 4.x)

    output: {
      ...,
      library: 'myLib', // 暴露出去的变量的名字
      libraryTarget: 'commonjs',
    }
    

    webpack暂时只能支持导出 cjs 或 更往前兼容的包(umd)

    不支持esm(实验性) rollup打包产物解析及原理(对比webpack)

    我们此处导出 cjs的包, 和rollup对比一下

    • 注入代码特别多,比较冗余
    exports["myLib"] =
    /******/ (function(modules) { // webpackBootstrap
    /******/   // The module cache
    /******/   var installedModules = {};
    /******/
    /******/   // The require function
    /******/   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;
    /******/   }
    /******/
    /******/
    /******/   // 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 = 0);
    /******/ })
    /************************************************************************/
    /******/ ([
    /* 0 */
    /***/ (function(module, __webpack_exports__, __webpack_require__) {
    
    "use strict";
    // ESM COMPAT FLAG
    __webpack_require__.r(__webpack_exports__);
    
    // EXPORTS  这一行是处理esm的导出,因为我们用的是 export { b: xx }, 如果我们用cjs的导出 比如 module.exports = { b: xx }, 此处就会没有,会更简单,直接是 module.exports = { b: xx }
    __webpack_require__.d(__webpack_exports__, "b", function() { return /* reexport */ b; });
    
    // CONCATENATED MODULE: ./src/test/a.js
    const b = 'xx';
    const bbbbbbb = 'xx';
    
    // CONCATENATED MODULE: ./src/main.js
    console.log(b + 1);
    console.log(1111);
    
    /***/ })
    /******/ ]);
    

    注意看 倒数第10多行,有个

    // EXPORTS 
    __webpack_require__.d(__webpack_exports__, "b", function() { return /* reexport */ b; });
    
    这一行是处理esm的导出,因为我们用的是 export { b: xx },
    如果我们用cjs的导出 比如 module.exports = { b: xx }, 此处就会没有此行,会更简单,直接是 module.exports = { b: xx }   ( webpack会自己模拟实现 module.exports
    

为什么webpack需要注入这么多代码?

因为webpack比rollup早出2年,诞生在esm标准出来前,commonjs出来后

  • 当时的浏览器只能通过script标签加载模块
    • script标签加载代码是没有作用域的,只能在代码内 用iife的方式 实现作用域效果
      • 这就是webpack打包出来的代码 大结构都是iife的原因
      • 并且每个模块都要装到function里面,才能保证互相之间作用域不干扰。
      • 这就是为什么 webpack打包的代码为什么乍看会感觉乱,找不到自己写的代码的真正原因
  • 关于webpack的代码注入问题,是因为浏览器不支持cjs,所以webpack要去自己实现require和module.exports方法(才有很多注入)(webpack自己实现polyfill)
    • 这么多年了,甚至到现在2022年,浏览器为什么不支持cjs
      • cjs是同步的,运行时的,node环境用cjs,node本身运行在服务器,无需等待网络握手,所以同步处理是很快的
      • 浏览器是 客户端,访问的是服务端资源,中间需要等待网络握手,可能会很慢,所以不能 同步的 卡在那里等服务器返回的,体验太差
  • 后续出来esm后,webpack为了兼容以前发在npm上的老包(并且当时心还不够决绝,导致这种“丑结构的包”越来越多,以后就更不可能改这种“丑结构了”),所以保留这个iife的结构和代码注入,导致现在看webpack打包的产物,乍看结构比较乱且有很多的代码注入,自己写的代码都找不到

rollup诞生于esm标准出来后,就是针对esm设计的,也没有历史包袱,所以可以做到真正的“打包”(精简,无额外注入)

  • (根据npm版本上传显示最早上传时间: webpack是2013年左右,rollup是2015.5

rollup如何打包第三方依赖 和 懒加载模块 和 公共模块?

和webpack打包一样,有两种:单chunk包 和 多chunk包

  1. 单chunk包

    无额外配置,一般会把所有js打成一个包。打包外部依赖(第三方)也是一样的。比如:

    // 入口 main.js
    import Axios from 'axios'
    Axios.get()
    console.log(1111)
    
    ------ 打包后的结果 ------
    // 最终会把axios的源代码 和 main.js 主代码,打包到一个文件内,无额外代码注入 
    // 以下是截取了一头一尾,中间省略
    import require$$1$1 from 'http';
    import require$$2 from 'https';
    import require$$0$1 from 'url';
    import require$$3 from 'assert';
    import require$$4 from 'stream';
    import require$$0 from 'tty';
    import require$$1 from 'util';
    import require$$7 from 'zlib';
    
    var axios$1 = {exports: {}};
    
    var bind$2 = function bind(fn, thisArg) {
      return function wrap() {
        var args = new Array(arguments.length);
        for (var i = 0; i < args.length; i++) {
          args[i] = arguments[i];
        }
        return fn.apply(thisArg, args);
      };
    };
    
    ...
    ...
    ...
    
    axios$1.exports = axios;
    
    // Allow use of default import syntax in TypeScript
    axios$1.exports.default = axios;
    
    var _axios_0_18_1_axios = axios$1.exports;
    _axios_0_18_1_axios.get();
    console.log(1111);
    

    此处rollup打包有个注意点

    • 很多第三方依赖很早就有了,所以用的是commonjs模块导出,rollup打包的话,需要安装插件@rollup/plugin-node-resolve。因为是cjs的包,所以也不存在tree shaking

      • 插件原理是,把cjs的包,转成esm包,在打包
    • 现在比较流行的monorepo,就是完全用esm写库+rollup打包,可以很轻易的做到tree shaking,让核心库变的更小,解析速度更快,还可以对外提供工具,扩大影响力

  2. 多个chunk包(代码分离)

    1. 配置多个入口,此法比较简单,可自行测试
      // rollup.config.js
       input: {            
           index: 'src/main.js',            
           other: 'src/other.js',            
       },
      
    2. 代码分离 (动态import,懒加载, import(xxx).then(module => {})
    • 此处有一个官方的例子,再清楚不过了

      // 入口 main.js
      /* DYNAMIC IMPORTS 动态import
          Rollup supports automatic chunking and lazy-loading  Rollup支持自动分块和懒加载
          via dynamic imports utilizing the import mechanism   通过dynamic imports动态导入
          of the host system. 
          */
      if (displayMath) {
          import('./maths.js').then(function (maths) {
                  console.log(maths.square(5));
                  console.log(maths.cube(5));
          });
      }
      
      // './maths.js'
      import square from './square.js';
      export {default as square} from './square.js';
      export function cube (x ) {
              return square(x) * x;
      }
      
      // './square.js'
      export default function square ( x ) {
              return x * x;
      }
      
      
      ---------------- 打包结果 ----------------
      // main.js
      'use strict';
      /* DYNAMIC IMPORTS 动态import
          Rollup supports automatic chunking and lazy-loading  Rollup支持自动分块和懒加载
          via dynamic imports utilizing the import mechanism   通过dynamic imports动态导入
          of the host system. 
          */
      if (displayMath) {
              // 打包成cjs模块的话,import替换成 Promise + require
              // Promise.resolve(require('../chunk-0ee5c472.js')).then(function (maths) {
              import('../chunk-c4d97f01.js').then(function (maths) {
                      console.log(maths.square(5));
                      console.log(maths.cube(5));
              });
      }
      
      // '../chunk-0ee5c472.js'
      'use strict';
      function square ( x ) {
              return x * x;
      }
      function cube (x ) {
              return square(x) * x;
      }
      exports.cube = cube;
      exports.square = square;
      

      对于代码分割,还有一种方法可以通过 output.manualChunks 选项显式告诉 Rollup 哪些模块要分割成单独的块

      总结

      • 动态import,rollup对比webpack 打包后的模块格式的支持度

        打包后的模块格式:esmcjsamdumd
        webpack不支持,实验中支持支持支持
        rollup支持支持支持不支持
      • 实现原理,对比webpack:

        • webpack是自己实现的“动态import“(借助promise + script标签 + window对象 + 模拟import方法)
        • rollup是 (打包成esm模块)利用浏览器(chorme63 以上)天然支持动态import
          • 或 (打包成cjs模块)promise + cjs的require
      • 此处有个很重要细节点

        • rollup打的包,如果要用 动态import(现在vue和react的单页项目 特别流行用动态import加载路由,算硬需求了),注意 如果要在浏览器上跑,首先要是esm的包(浏览器不支持cjs),然后浏览器版本要注意(chorme63 以上)
          • 因为rollup不做额外代码注入,完全利用高版本浏览器原生支持import(所以代码特别干净,webpack会做大量的兼容 自己实现require和import) rollup打包产物解析及原理(对比webpack)

    1. rollup如何处理公共模块?(比如, a、b、c 3个模块 同时依赖 d)

      有2种情况:

      1. 源代码内 不存在 动态import,那么会打成一个chunk(a、b、c、d 4个模块都在一包内,d只正常有一份)

      2. 源代码内 存在 懒加载模块,并且懒加载的模块也访问了公共依赖,比如

        // 入口 main.js
        import {deepCopy} from '@xxx/methods/deepCopy.js' // 这是放在公司的npm域内的一个包,可以理解为export一个简单的deepCopy函数
        console.log(deepCopy(a))
        import('./test/a').then(e => {
          console.log(e)
        })
        
        // './test/a' 懒加载模块 也依赖 同一公共模块
        import {deepCopy} from '@xxx/methods/deepCopy.js'
        const a = {a: 1}
        export const b = deepCopy(a)
        
        ---------- 是否会把 公共依赖  打包2份呢?  --------------
        答案是no,rollup还是牛p,公共依赖只会出来一份,然后对外 export  (此处举例是导出esm格式, 亲测导出cjs格式一样的可以,此处就不赘述,有兴趣可以自己test一下)
        
        生成的目录结构,有3个文件
            a-19173be8.js
            main.js
            main-219c2eaf.js
        
        // main.js
        import './main-219c2eaf.js';
        
        // main-219c2eaf.js
        const deepCopy = function (obj) {
          // do ..
        };
        console.log(deepCopy(a));
        import('./a-19173be8.js').then(e => {
          console.log(e);
        });
        
        // a-19173be8.js
        import { d as deepCopy } from './main-219c2eaf.js';
        const a = {a: 1};
        const b = deepCopy(a);
        export { b };
        

        总结:对于公共依赖,rollup不会出现重复打包的情况!并且完全无注入代码!无需额外配置。 对比webpack的话,webpack需要配置 optimization.splitChunks (webpack4.x 以上)

总结 rollup vs webpack

rollup 诞生在esm标准出来后

  • 出发点就是希望开发者去写esm模块,这样适合做代码静态分析,可以做tree shaking减少代码体积,也是浏览器除了script标签外,真正让JavaScript拥有模块化能力。是js语言的未来
  • rollup完全依赖高版本浏览器原生去支持esm模块,所以无额外代码注入,打包后的代码结构也是清晰的(不用像webpack那样iife)
    • 目前浏览器支持模块化只有3种方法:
      1. ①script标签(缺点没有作用域的概念)
      2. ②script标签 + iife + window + 函数作用域(可以解决作用域问题。webpack的打包的产物就这样)
      3. ③esm (什么都好,唯一缺点 需要高版本浏览器)

webpack 诞生在esm标准出来前,commonjs出来后

  • 当时的浏览器只能通过script标签加载模块
    • script标签加载代码是没有作用域的,只能在代码内 用iife的方式 实现作用域效果
      • 这就是webpack打包出来的代码 大结构都是iife的原因
      • 并且每个模块都要装到function里面,才能保证互相之间作用域不干扰。
      • 这就是为什么 webpack打包的代码为什么乍看会感觉乱,找不到自己写的代码的真正原因
  • 关于webpack的代码注入问题,是因为浏览器不支持cjs,所以webpack要去自己实现require和module.exports方法(才有很多注入)
    • 这么多年了,甚至到现在2022年,浏览器为什么不支持cjs
      • cjs是同步的,运行时的,node环境用cjs,node本身运行在服务器,无需等待网络握手,所以同步处理是很快的
      • 浏览器是 客户端,访问的是服务端资源,中间需要等待网络握手,可能会很慢,所以不能 同步的 卡在那里等服务器返回的,体验太差
  • 后续出来esm后,webpack为了兼容以前发在npm上的老包(并且当时心还不够决绝,导致这种“丑结构的包”越来越多,以后就更不可能改这种“丑结构了”),所以保留这个iife的结构和代码注入,导致现在看webpack打包的产物,乍看结构比较乱且有很多的代码注入,自己写的代码都找不到

最终使用推荐


笔者建议,最好自己上手打包 调试,得到的打包产物 并仔细分析。一时看不懂的话,也可以收藏本文,过段时间在看,先了解前置知识

最后,感谢爱学习的你,谢谢点赞!

转载自:https://juejin.cn/post/7054752322269741064
评论
请登录