Rollup

339 阅读15分钟

背景. 为什么会出现rollup?

为什么会出现rollup? webpack中的import本质上并不是es6标准的import,而是webpack自己定义了一个import,因此在打包文件的时候会出现大量的webpack_import函数体,这无疑增加了打包文件的体积。而rollup是面向ES6规范的,因此rollop中的import就是ES6中的import,所以无需在“二次处理”import,直接就可以获取模块中的导出。 例如:以下为ES6代码,使用了import关键字。

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

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

问题:那么如果另一个模块叶定义了const b,那么最后导入main.js文件中不是有两个b变量,造成了变量污染?

然后我们看下rollup的打包文件,很自然就拿到了导出值 b = 'xx',说明rollup中的import是交给ES6底层去处理的,不是自己处理。这样打包出的文件可以利用这一点,将众多的模块“很干净”的合并到一个文件中。

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

但是反观“杜兰特”:webpack生成的打包文件就很大了:

/******/ (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); 

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

总结: 由此可以看出,rollup最大的特色就是使用了ES6规范中import来导入模块内容,这样极大的节省了打包后的体积(免去了模块化处理代码),此外还基于ES6实现了代码的静态分析tree-shaking。可以说是彻底拥抱了ES6+!

webpack解惑: 问题1:为什么webpack打包体积比rollup大? 答:因为 webpack 本身维护了一套模块系统,将所有的模块都被封装成function (module, webpack_exportswebpack_require){}的形式,然后每个模块都统一存储在对象中,且对应自己的模块ID,这样不同模块就能被区分。每个模块的导出值记录在module.exports中。这套模块系统兼容了所有前端历史进程下的模块规范,包括amd、commonjs、es6等

问题2:为什么webpack支持import和require两种引入模块的方式
mport的本质其实就是require,webpack在进行静态分析时会将其转为require。因此webpack中的import和require本质上都是require。(这也侧面印证了webpack是基于node的,node的模块规范是commonjs,该规范就是使用require和module.exports来引入和导出模块的,虽然es6 module是静态的依赖,但是webpack并没有使用静态导入,而是在自己静态分析阶段解析出“import”为“require”,以达到欺骗效果。)
参考:www.jianshu.com/p/812347b4a…

一、rollup的配置

1.1 rollup.config.js文件的基本配置

需要在package.json中的 script字段 进行配置:

  "scripts": {
    "build": "node_modules/.bin/rollup -c ./build/rollup.config.js",
    "dev": "node_modules/.bin/rollup -w -c ./build/rollup.config.js"
  },
// 相应命令:
1) -c命令为`--config`的缩写,表示设置`rollup`打包的配置。
2) -w命令为`--watch`的缩写,在本地开发环境添加`-w`参数可以监控源文件的变化,自动重新打包。

为什么rollup.config.js文件可以执行相应的配置,是rollup和webpack一样自动去寻找config文件还是w我们认为在命令行中设置的呢? 可以参考以下文章 blog.cjw.design/blog/old/ro…

(1) 配置plugin插件 和 presets预设

  • presets plugins插件的集合,如['es2015’] 数组,表示插件集合

  • plugins 按需引进,拆成细小粒度的插件,如['transform-es2015-arrow-functions'、'transform-es2015-template-literals'] 数组,表示插件。

1.babel插件

babel是保证es6代码可以在不同版本的浏览器进行兼容运行的重要工具。而babel配置和核心就在于「preset」和「plugins」,这两项的配置决定了可以使用哪些es的语法特性。

plugins和preset的区别
babel插件为了尽可能的实现按需引入,因此拆分了很多plugin插件,但是当我们需要实现“ES6到ES5的转化”功能的时候,涉及到很多拆分的小的plugins插件,此时我们如果依然一个个导入配置插件 - plugins的话,就会造成一些繁琐。因此如果当前项目需要用到 ES6 转 ES5 的大部分功能,那么更适合直接引用插件的集合 - presets

plugin和presets的执行顺序:

module.exports = {
  presets: [E,D],
  plugins: [A,B,C]
}

总结:presets是多个plugin功能组合后的包

目前babel维护着四个presets集合包,分别是:preset-env、preset-flow、preset-react、preset-typescript。而在rollup中我们主要需要使用到的是babel的preset-env这个集合plugins。可以从env看出,这个是babel为了解决市面上不同版本的浏览器兼容问题,专门配置的一个浏览器版本列表,然后会根据这个版本列表自动抓华为当前版本浏览器可以运行的js。

(2) 配置本地http服务

和webpack一样,rollup如果需要本地服务,那么需要rollup-plugin-serve插件实现。

1.package.json文件进行开启http服务命令配置

"scripts": {
    "dev": "node_modules/.bin/rollup -w -c ./build/rollup.config.js"
  },
// 相应命令:
1) -c命令为`--config`的缩写,表示设置`rollup`打包的配置。
2) -w命令为`--watch`的缩写,在本地开发环境添加`-w`参数可以监控源文件的变化,自动重新打包。

2.rollup.config.js文件中配置http服务器端口

const path = require('path');
const { babel } = require('@rollup/plugin-babel');
const serve = require('rollup-plugin-serve'); // 1.引入serve插件

const resolveFile = function(filePath) {
  return path.join(__dirname, '..', filePath)
}

module.exports = {
  input: resolveFile('src/index.js'),
  output: {
    file: resolveFile('dist/index.js'),
    format: 'umd',
    sourcemap: true,
  }, 
  plugins: [
    babel({
      presets: ['@babel/preset-env']
    }),
    serve({ // 2.在plugin中配置serve服务器的端口和文件
      open: true,  // 运行时自动打开浏览器
      headers: { "Access-Control-Allow-Origin": "*" }, // 本地服务允许跨域 
      port: 3001, // 设置网络服务监听端口
      contentBase: [resolveFile('example'), resolveFile('dist')] // 本地服务的运行文件根目录
    })
  ],
}

执行npm run dev之后的控制台显示如下:访问localhost:3001相当于访问到路径/Users/luweidong/Desktop/RollupProjects/example和/dist/index.js image.png

1.2 两种打包方式

(1) bin命令打包

我们在执行rollup打包的时候,其实是在执行json文件中的script命令,而这个命令实际上是在执行node_modules下的bin文件夹下面的rollup文件。

1.package.json中的「bin」字段
一般来说,我们的常规开发项目不会需要bin字段,但是如果是npm包的话,那么其对应的json文件就需要bin字段。 当我们使用npm install安装npm包时候,如果这个包json文件存在bin字段,那么npm就会通过这个npm包中bin字段对应可执行文件路径,找到该可执行文件,然后复制到项目的node_modules下的bin目录下。
我们在运行npm run start的时候,实际上是在执行json文件中的script字段中的“dstart”配置,"scripts": {"start": "webpack"},而实际上则是"scripts": {"start": "node_modules/.bin webpack"}, 因为npm已经默认给script字段加上了node_moudles/.bin前缀

如下所示,rollup的package.json文件的bin字段值: image.png 然后npm会通过路径dist/bin/rollup找到对应可执行文件,复制到项目的node_module目录下: image.png 以此看来,我们执行npm run build的时候,最终就是执行node_modules/.bin/rollup这个可执行文件,然后按照这个可执行文件的代码去自动操作打包。

"scripts": {
    "dev": "node_modules/.bin/rollup -w -c ./build/rollup.config.dev.js",
    "build": "node_modules/.bin/rollup -c ./build/rollup.config.prod.js"
  },

此外,对应vue的命令也是通过bin字段去配置的,如果执行vue create命令去创建项目时候,本质上也是执行对应的bin文件,然后创建项目的。
总结:bin字段值是npm包可执行文件路径,npm install之后会将可执行文件放置在node_module中,然后在script中的字段就是执行bin可执行文件。 www.jianshu.com/p/53feedd72…

(2) rollup-API打包

rollup-API打包,即为rollup.js的API在Node.js代码中执行编译代码。如下所示为

// package.json文件
  "scripts": {
    "build": "node ./build/build.js"
  },
// build/build.js文件
const rollup = require('rollup');
const config = require('./rollup.config'); // 引入config.js相关配置

const inputOptions = config;
const outputOptions = config.output;

async function build() {
  // create a bundle
  const bundle = await rollup.rollup(inputOptions);

  console.log(`[INFO] 开始编译 ${inputOptions.input}`);  

  // generate code and a sourcemap
  const { code, map } = await bundle.generate(outputOptions);

  console.log(`[SUCCESS] 编译结束 ${outputOptions.file}`);  

  // or write the bundle to disk
  await bundle.write(outputOptions);
}

build();

二、js模块化编译

Rollup支持将ES6代码编译成支持的模块化形式,具体包括:AMD、commonjs、UMD、IIFE四种模块化表现形式

2.1 amd 编译

2.2 cjs 编译

2.3 umd 编译

三、CSS编辑

四、框架编译(Rollup打包vue)

step1: 安装rollup和vue3

在根目录下安装rollup和vue3

   npm install rollup  
   npm install --save vue@next  

step2: 构建必要插件

主要编译解析插件

1.处理vue文件

npm i --save-dev rollup-plugin-vue @vue/compiler-sfc

通过这两个插件来联合解析处理vue文件(相当于webpack中的vue-loader)。因为rollup只能解析js文件,因此对于框架文件的解析需要对应的编译插件。vue组件中包含三块,template、script、style等三块代码,分别表示模版、脚本、样式三块。安装好这两个插件后,会把单文件组件编译为标准的 JavaScript(js文件中含有CSS-style)。
如下所示,是一个vue文件通过SFC解析后得到的js文件,文件中存一个js对象。其中vue的style 部分则是被 parse 成一个数组styles,它的类型是SFCStyleBlock[]。为什么 style 的 parse 结果会是一个数组呢?这是因为我们可以在 .vue 文件中写多个 style 块。(然后在会将这个AST合并为render)。
因此,如果没有单独配置的话,vue组件内的样式还是会跟script生成的js文件打包在一起。 image.png 我们在webpack中使用vue-loader来解析.vue文件,其底层也是调用了@vue/compiler-sfc 插件使用:我们在rollup.config.js配置文件中,引入这个rollup-plugin-vue插件,并在plugins中进行注册,就可以正常编译、打包.vue文件了

// rollup.config.js配置文件
import vue from 'rollup-plugin-vue' // 引入可以解析vue的插件
export default {
  ...
  plugins:[
    vue()
  ]
}

2.处理样式

npm i --save-dev rollup-plugin-css-only

插件rollup-plugin-css-only 是一个用于提取并输出 CSS 样式文件的插件。当使用 Rollup 构建项目时,通常将 CSS 代码打包到 JavaScript 文件中。但在某些情况下,例如在使用服务端渲染时,您可能需要将 CSS 样式文件分离出来。上面的vue文件拆分出来了js和css代码,那么我们就可以使用这个插件,将css单独打包出来。但是这里需要注意的是,虽然可以抽离出vue的style样式到单独的文件中,但是是将所有的vue-style合并到一个css文件的。每个样式中[data-v-hash值]来实现vue-scoped。 使用rollup-plugin-css-only 插件了。至此插件配置为

// rollup.config.js配置文件
import vue from 'rollup-plugin-vue' // 引入可以解析vue的插件
import css from 'rollup-plugin-css-only' // 抽离css为单独文件
export default {
  ...
  plugins:[
    vue({css: false}),
    css()
  ]
}

3.编译npm模块

npm i --save-dev @rollup/plugin-commonjs @rollup/plugin-node-resolve @rollup/plugin-replace

  1. @rollup/plugin-commonjs 插件
    作用:支持对commonjs模块的打包。由于rollup本身只支持ES模块进行打包,但是如果我们在开发中引用的依赖包是commonjs模块的npm包,那么rollup在解析引入的这个npm包是无法成功的,因此需要这个插件将这个CommonJS 模块转换为 ES 模块,然后再一起打包。这个插件还支持一些高级功能,比如解析动态导入语法和处理循环依赖。通过在 Rollup 配置文件中添加 @rollup/plugin-commonjs,你可以更方便地使用 Rollup 打包项目中的 CommonJS 模块。

  2. @rollup/plugin-node-resolve 插件
    主要解决了以下两个问题:
    问题1.Node.js 模块识别 - 如 Rollup 打包项目时,可能会遇到一些需要从node_modules 目录导入的模块,而这些模块并不是采用 ES module 规范,或者它们的入口文件并未指定为 ES module。在这种情况下,为了确保 Rollup 能够正确找到并导入这些模块,您可以通过配置 rollup-plugin-node-resolve 插件来将其转换为适用于 Rollup 的 ES module 格式。但是,如果遇到commonjs模块的话,该插件并不能将其转为es-module,还是需要借助@rollup/plugin-commonjs来将commonjs转为ES-Module。此外,插件会解析这些导入对应的文件路径。它支持查找 .js, .json, .node 文件,以及其他可配置的扩展名。
    问题2别名(aliases)和自定义模块解析逻辑 - 如果你的项目使用了模块别名或者有特定的模块解析规则,你可以通过配置此插件来实现。这样可以使 Rollup 更好地适应各种开发环境和团队规范。

  3. @rollup/plugin-replace 插件
    作用:在打包过程中替换代码中的特定字符串,我们常用的环境变量替换就是使用该插件。如下图为替换环境变量:将替换所有包含在构建中的文件中的每个实例中的字符串‘process.env.NODE_ENV’ 替换为:‘production’。对于复杂的值,请使用 JSON.stringify。

import replace from '@rollup/plugin-replace';
export default {
  input: 'src/index.js',
  output: {
     dir: 'output',
      format: 'cjs'
  },
  plugins: [
      nodeResolve(),
      commonjs(),
      replace({
          'process.env.NODE_ENV': JSON.stringify('production'),
          // 上面的配置将替换所有包含在构建中的文件中的每个实例 `process.env.NODE_ENV` 为 `production`
      })
  ]
};

4.编译ES6+模块

npm i --save-dev @rollup/plugin-buble @rollup/plugin-babel @babel/core @babel/preset-env

  1. @rollup/plugin-buble插件: 编译简单的ES6代码
  2. @rollup/plugin-buble插件:rollup的ES6编译插件
  3. @babel/core 插件:是官方 babel 编译核心模块
  4. @babel/preset-env 插件:是官方 babel 编译解析ES6+ 语言的扩展模块

babel插件为了尽可能的实现按需引入,因此拆分了很多babel-plugin插件。但是当我们需要实现“ES6到ES5的转化”功能的时候,涉及到很多拆分的小的plugins插件,此时我们如果依然一个个导入配置插件 - plugins的话,就会造成一些繁琐。因此babel推出了plugins的功能集合presets,而@babel/preset-env就是一个常用的presets。(依赖于plugin-buble && core插件),因此我们不用在plugins中一次调用plugin-buble插件和core插件,而是直接配置preset-env插件即可。

plugins: [
  babel({
     presets: ['@babel/preset-env'] // 配置presets相当于连续调用plugins().
   }),
 ],

5.其他安装

1)编译本地开发服务插件:npm i --save-dev rollup-plugin-serve
2)编译代码混淆插件:npm i --save-dev rollup-plugin-uglify

step3: package.json文件配置

项目下的package.json文件

{
  "name": "rollup-vue",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "dev": "node_modules/.bin/rollup -w -c ./build/rollup.config.dev.js",
    // 执行命令指向build中的配置文件
    "build": "node_modules/.bin/rollup -c ./build/rollup.config.prod.js"
  },
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@babel/core": "^7.9.6",
    "@babel/preset-env": "^7.9.6",
    "@rollup/plugin-babel": "^5.0.0",
    "@rollup/plugin-buble": "^0.21.3",
    "@rollup/plugin-commonjs": "^11.1.0",
    "@rollup/plugin-node-resolve": "^7.1.3",
    "@rollup/plugin-replace": "^2.3.2",
    "@vue/compiler-sfc": "^3.0.0-beta.6",
    "rollup": "^2.7.6",
    "rollup-plugin-css-only": "^2.0.0",
    "rollup-plugin-serve": "^1.0.1",
    "rollup-plugin-uglify": "^6.0.4",
    "rollup-plugin-vue": "^6.0.0-alpha.7"
  },
  "dependencies": {
    "vue": "^3.0.0-beta.6"
  }
}

step4: rollup.config.js 配置 && 环境配置

配置文件build目录:包含统一的配置文件,以及区分dev和prod环境的配置。 image.png

1. rollup.config.js

// rollup.config.js
const path = require('path');
const buble = require('@rollup/plugin-buble');
const { babel } = require('@rollup/plugin-babel');
const nodeResolve = require('@rollup/plugin-node-resolve');
const commonjs = require('@rollup/plugin-commonjs');
const vue = require('rollup-plugin-vue');
const css = require('rollup-plugin-css-only');
const replace = require('@rollup/plugin-replace')

const resolveFile = function(filePath) {
  return path.join(__dirname, '..', filePath)
}

// const isProductionEnv = process.env.NODE_ENV === 'production';

const babelOptions = {
  "presets": ['@babel/preset-env'],
}

module.exports = [
  {
    input: resolveFile('src/index.js'),
    // input: resolveFile('src/App.vue'),
    output: {
      file: resolveFile('dist/index.js'),
      format: 'iife',
      name: 'App'
    }, 
    // external: ['vue'],
    plugins: [
      vue({css: false}),
      css(),
      nodeResolve(),
      commonjs(),
      replace({
        'process.env.NODE_ENV': JSON.stringify( 'production' )
      }),
      babel(babelOptions),
      buble(),
    ],
  },
]

2. rollup.config.dev.js -开发环境配置

process.env.NODE_ENV = 'development';

const path = require('path');
const serve = require('rollup-plugin-serve');
const configList = require('./rollup.config');

const resolveFile = function(filePath) {
  return path.join(__dirname, '..', filePath)
}
const PORT = 3001;

const devSite = `http://127.0.0.1:${PORT}`;
const devPath = path.join('example', 'index.html');
const devUrl = `${devSite}/${devPath}`;

setTimeout(()=>{
  console.log(`[dev]: ${devUrl}`)
}, 1000);

configList.map((config, index) => {

  config.output.sourcemap = true;

  if( index === 0 ) {
    config.plugins = [
      ...config.plugins,
      ...[
        serve({
          port: PORT,
          contentBase: [resolveFile('')]
        })
      ]
    ]
  }
  
  return config;
})


module.exports = configList;

3. rollup.config.prod.js 线上配置

process.env.NODE_ENV = 'production';

const { uglify } = require('rollup-plugin-uglify');
const configList = require('./rollup.config');

const resolveFile = function(filePath) {
  return path.join(__dirname, '..', filePath)
}

configList.map((config, index) => {

  config.output.sourcemap = false;
  config.plugins = [
    ...config.plugins,
    ...[
      uglify()
    ]
  ]

  return config;
})

module.exports = configList;

step5. 开发文件

image.png

1. index.js文件

import Vue from 'vue/index.js';
import App from './App.vue';

Vue.createApp(App).mount('#App');

2. APP.vue文件

<template>
    <div>
      <h1>Hello {{ name }}</h1>
  
      12312312
      <!-- <Text></Text> -->
    </div>
  
  </template>
  
  <script>
//   import Text from './text.vue'
  export default {
    components: {
      Text
    },
    data() {
      return { name: 'World!' }
    }
  }
  </script>
  
  <style scoped>
  h1 {
    color: red;
  }
  </style>

step6. example测试

image.png

1. index.html文件

<html>
  <head>
    <script src="https://cdn.bootcss.com/babel-polyfill/6.26.0/polyfill.js"></script>
    <link rel="stylesheet" href="./../dist/index.css" />
  </head>
  <body>
    <p>hello rollup + vuejs</p>
    <div id="App"></div>
    <script src="./../dist/index.js"></script>
  </body>
</html>

五、多文件输入输出编译(一套代码多种编译)

单入口单出口、多入口/多出口blog.csdn.net/iamyang0511…


export default [
  {
    input: 'main-a.js',
    output: {
      file: 'dist/bundle-a.js',
      format: 'cjs'
    }
  },
  {
    input: 'main-b.js',
    output: [
      {
        file: 'dist/bundle-b1.js',
        format: 'cjs'
      },
      {
        file: 'dist/bundle-b2.js',
        format: 'es'
      }
    ]
  }
];

单入口/多出口 www.51cto.com/article/781…

六、按需加载实现

问题四: rollup 的 plugin 机制是怎么样的 ?如何实现一个自定义 plugin ?

问题五: rollup 的整个工作过程是怎么样的 ?

问题6: 如何使用babel

babel中的plugin和presets是

问题七: rollup 的 treeshaking 原理是什么 ?

rollup使用场景

如果你的项目是以插件或库给用户引入使用,Rollup 是最佳选择,因为它可以将代码转成不同的模块,然后用户根据自身项目模块语法来引入你的代码。

未完待续

参考文献:
juejin.cn/post/712575…
www.51cto.com/article/781…