10分鐘快速進階rollup.js

sam9831發表於2018-11-21

前言

上一篇教程中,為大家介紹了rollup.js的入門技巧,沒有讀過的小夥伴可以點選這裡,本次我們將繼續對rollup.js的進階技巧進行探討,想直接看結論的小夥伴可以直接看最後一章。

rollup.js外掛

rollup.js的外掛採用可拔插設計,它幫助我們增強了rollup.js的基礎功能,下面我將重點講解四個rollup.js最常用的外掛。

resolve外掛

為什麼需要resolve外掛?

上一篇教程中,我們打包的物件是本地的js程式碼和庫,但實際開發中,不太可能所有的庫都位於本地,我們會通過npm下載遠端的庫。這裡我專門準備了一些測試庫,供大家學習rollup.js使用,首先下載測試庫:

npm i -S sam-test-data
複製程式碼

sam-test-data庫預設提供了一個UMD模組,對外暴露了兩個變數a和b以及一個random函式,a是0到9之間的一個隨機整數,b是0到99之間的一個隨機整數,random函式的引數是一個整數,如傳入100,則返回一個0到99之間的隨機整數,在本地建立測試外掛程式碼的資料夾:

mkdir src/plugin
複製程式碼

建立測試程式碼:

touch src/plugin/main.js
複製程式碼

寫入以下程式碼:

import * as test from 'sam-test-data'
console.log(test)
export default test.random
複製程式碼

先不使用rollup.js打包,直接通過babel-node嘗試執行程式碼:

babel-node
> require('./src/plugin/main.js')
{ a: 1, b: 17, random: [Function: random] }
{ default: [Function: random] }
> require('./src/plugin/main.js').default(100)
41
複製程式碼

可以看到程式碼可以正常執行,下面我們嘗試通過rollup.js打包程式碼,新增一個新的配置檔案:

touch rollup.plugin.config.js
複製程式碼

寫入以下內容:

import { comment } from './comment-helper-es'

export default {
  input: './src/plugin/main.js',
  output: [{
    file: './dist/index-plugin-cjs.js',
    format: 'cjs',
    banner: comment('welcome to imooc.com', 'this is a rollup test project'),
    footer: comment('powered by sam', 'copyright 2018')
  }, {
    file: './dist/index-plugin-es.js',
    format: 'es',
    banner: comment('welcome to imooc.com', 'this is a rollup test project'),
    footer: comment('powered by sam', 'copyright 2018')
  }]
}
複製程式碼

這裡我提供了一個comment-helper-es模組,暴露了一個comment方法,自動讀取我們的引數,並幫助生成註釋,同時會在註釋上方和下方新增等長的分隔符,感興趣的小夥伴可以直接拿去用:

export function comment() {
  if (arguments.length === 0) {
    return // 如果引數為0直接返回
  }
  let maxlength = 0
  for (let i = 0; i < arguments.length; i++) {
    const length = arguments[i].toString().length
    maxlength = length > maxlength ? length : maxlength // 獲取最長的引數
  }
  maxlength = maxlength === 0 ? maxlength : maxlength + 1 // 在最長引數長度上再加1,為了美觀
  let seperator = ''
  for (let i = 0; i < maxlength; i++) {
    seperator += '=' // 根據引數長度生成分隔符
  }
  const c = []
  c.push('/**\n') // 新增註釋頭
  c.push(' * ' + seperator + '\n') // 新增註釋分隔符
  for (let i = 0; i < arguments.length; i++) {
    c.push(' * ' + arguments[i] + '\n') // 加入引數內容
  }
  c.push(' * ' + seperator + '\n') // 新增註釋分隔符
  c.push(' **/') // 新增註釋尾
  return c.join('') // 合併引數為字串
}
複製程式碼

通過rollup.js打包:

$ rollup -c rollup.plugin.config.js 

./src/plugin/main.js → ./dist/index-plugin-cjs.js, ./dist/index-plugin-es.js...
(!) Unresolved dependencies
https://rollupjs.org/guide/en#warning-treating-module-as-external-dependency
sam-test-data (imported by src/plugin/main.js)
created ./dist/index-plugin-cjs.js, ./dist/index-plugin-es.js in 13ms
複製程式碼

可以看到程式碼生成成功了,但是sam-test-data被當做一個外部的模組被引用,我們檢視dist/index-plugin-es.js原始碼:

/**
 * ==============================
 * welcome to imooc.com
 * this is a rollup test project
 * ==============================
 **/
import * as test from 'sam-test-data';
import { random } from 'sam-test-data';

console.log(test);

var main = random;

export default main;
/**
 * ===============
 * powered by sam
 * copyright 2018
 * ===============
 **/
複製程式碼

和我們原本寫的程式碼幾乎沒有區別,只是通過es6的解構賦值將random函式單獨從sam-test-data獲取,然後賦給變數main並暴露出來。大家試想,如果我們正在編寫一個Javascript類庫,使用者在引用我們庫的時候,還需要手動去下載這個庫所有的依賴,這是多麼糟糕的體驗。為了解決這個問題,將我們編寫的原始碼與依賴的第三方庫進行合併,rollup.js為我們提供了resolve外掛。

resolve外掛的使用方法

首先,安裝resolve外掛:

npm i -D rollup-plugin-node-resolve
複製程式碼

修改配置檔案rollup.plugin.config.js:

import resolve from 'rollup-plugin-node-resolve'

export default {
  input: './src/plugin/main.js',
  output: [{
    file: './dist/index-plugin-cjs.js',
    format: 'cjs'
  }, {
    file: './dist/index-plugin-es.js',
    format: 'es'
  }],
  plugins: [
    resolve()
  ]
}
複製程式碼

重新打包:

$ rollup -c rollup.plugin.config.js 

./src/plugin/main.js → ./dist/index-plugin-cjs.js, ./dist/index-plugin-es.js...
created ./dist/index-plugin-cjs.js, ./dist/index-plugin-es.js in 28ms
複製程式碼

可以看到警告消除了,我們重新檢視dist/index-plugin-es.js原始碼:

const a = Math.floor(Math.random() * 10);
const b = Math.floor(Math.random() * 100);
function random(base) {
  if (base && base % 1 === 0) {
    return Math.floor(Math.random() * base) 
  } else {
    return 0
  }
}

var test = /*#__PURE__*/Object.freeze({
  a: a,
  b: b,
  random: random
});

console.log(test);

var main = random;

export default main;
複製程式碼

很明顯sam-test-data庫的原始碼已經與我們的原始碼整合了。

tree-shaking

下面我們修改src/plugin/main.js的原始碼:

import * as test from 'sam-test-data'
export default test.random
複製程式碼

原始碼中去掉了console.log(test),重新打包:

rollup -c rollup.plugin.config.js
複製程式碼

再次檢視dist/index-plugin-es.js原始碼:

function random(base) {
  if (base && base % 1 === 0) {
    return Math.floor(Math.random() * base) 
  } else {
    return 0
  }
}

var main = random;

export default main;
複製程式碼

我們發現關於變數a和b的定義沒有了,因為原始碼中並沒有用到這兩個變數。這就是ES模組著名的tree-shaking機制,它動態地清除沒有被使用過的程式碼,使得程式碼更加精簡,從而可以使得我們的類庫獲得更快的載入速度(容量小了,自然載入速度變快)。

external屬性

有些場景下,雖然我們使用了resolve外掛,但我們仍然某些庫保持外部引用狀態,這時我們就需要使用external屬性,告訴rollup.js哪些是外部的類庫,修改rollup.js的配置檔案:

import resolve from 'rollup-plugin-node-resolve'

export default {
  input: './src/plugin/main.js',
  output: [{
    file: './dist/index-plugin-cjs.js',
    format: 'cjs'
  }, {
    file: './dist/index-plugin-es.js',
    format: 'es'
  }],
  plugins: [
    resolve()
  ],
  external: ['sam-test-data']
}
複製程式碼

重新打包:

rollup -c rollup.plugin.config.js
複製程式碼

檢視dist/index-plugin-es.js原始碼:

import { random } from 'sam-test-data';

var main = random;

export default main;
複製程式碼

可以看到雖然使用了resolve外掛,sam-test-data庫仍被當做外部庫處理

commonjs外掛

為什麼需要commonjs外掛?

rollup.js預設不支援CommonJS模組,這裡我編寫了一個CommonJS模組用於測試,該模組的內容與sam-test-data完全一致,差異僅僅是前者採用了CommonJS規範,先安裝這個模組:

npm i -S sam-test-data-cjs
複製程式碼

新建一個程式碼檔案:

touch src/plugin/main-cjs.js
複製程式碼

寫入如下程式碼:

import test from 'sam-test-data-cjs'
console.log(test)
export default test.random
複製程式碼

這段程式碼非常簡單,接下來修改rollup.js的配置檔案:

import resolve from 'rollup-plugin-node-resolve'

export default {
  input: './src/plugin/main-cjs.js',
  output: [{
    file: './dist/index-plugin-cjs.js',
    format: 'cjs'
  }, {
    file: './dist/index-plugin-es.js',
    format: 'es'
  }],
  plugins: [
    resolve()
  ]
}
複製程式碼

執行打包:

rollup -c rollup.plugin.config.js 

./src/plugin/main-cjs.js → ./dist/index-plugin-cjs.js, ./dist/index-plugin-es.js...
[!] Error: 'default' is not exported by node_modules/_sam-test-data-cjs@0.0.1@sam-test-data-cjs/index.js
https://rollupjs.org/guide/en#error-name-is-not-exported-by-module-
src/plugin/main-cjs.js (1:7)
1: import test from 'sam-test-data-cjs'
          ^
複製程式碼

可以看到預設情況下,rollup.js是無法識別CommonJS模組的,此時我們需要藉助commonjs外掛來解決這個問題。

commonjs外掛的使用方法

首先安裝commonjs外掛:

npm i -D rollup-plugin-commonjs
複製程式碼

修改rollup.js的配置檔案:

import resolve from 'rollup-plugin-node-resolve'
import commonjs from 'rollup-plugin-commonjs'

export default {
  input: './src/plugin/main-cjs.js',
  output: [{
    file: './dist/index-plugin-cjs.js',
    format: 'cjs'
  }, {
    file: './dist/index-plugin-es.js',
    format: 'es'
  }],
  plugins: [
    resolve(),
    commonjs()
  ]
}
複製程式碼

重新執行打包:

rollup -c rollup.plugin.config.js
複製程式碼

打包成功後,我們檢視dist/index-plugin-es.js原始碼:

const a = Math.floor(Math.random() * 10);
const b = Math.floor(Math.random() * 100);
function random(base) {
  if (base && base % 1 === 0) {
    return Math.floor(Math.random() * base) 
  } else {
    return 0
  }
}
var _samTestDataCjs_0_0_1_samTestDataCjs = {
  a, b, random
};

console.log(_samTestDataCjs_0_0_1_samTestDataCjs);
var mainCjs = _samTestDataCjs_0_0_1_samTestDataCjs.random;

export default mainCjs;
複製程式碼

可以看到CommonJS模組被整合到程式碼中了,通過babel-node嘗試執行打包後的程式碼:

babel-node 
> require('./dist/index-plugin-es')
{ a: 7, b: 45, random: [Function: random] }
{ default: [Function: random] }
> require('./dist/index-plugin-es').default(1000)
838
複製程式碼

程式碼執行成功,說明我們的程式碼打包成功了。

CommonJS與tree-shaking

我們修改src/plugin/main-cjs.js的原始碼,驗證一下CommonJS模組是否支援tree-shaking特性:

import test from 'sam-test-data-cjs'
export default test.random
複製程式碼

與resolve中tree-shaking的案例一樣,我們去掉console.log(test),重新執行打包後,再檢視打包原始碼:

const a = Math.floor(Math.random() * 10);
const b = Math.floor(Math.random() * 100);
function random(base) {
  if (base && base % 1 === 0) {
    return Math.floor(Math.random() * base) 
  } else {
    return 0
  }
}
var _samTestDataCjs_0_0_1_samTestDataCjs = {
  a, b, random
};

var mainCjs = _samTestDataCjs_0_0_1_samTestDataCjs.random;

export default mainCjs;
複製程式碼

可以看到原始碼中仍然定義了變數a和b,說明CommonJS模組不能支援tree-shaking特性,所以建議大家使用rollup.js打包時,儘量使用ES模組,以獲得更精簡的程式碼。

UMD與tree-shaking

UMD模組與CommonJS類似,也是不能夠支援tree-shaking特性的,這裡我提供了一個UMD測試模組sam-test-data-umd,感興趣的小夥伴可以自己驗證一下。有的小夥伴可能會問,sam-test-data也是一個UMD模組,為什麼它能夠支援tree-shaking?我們開啟sam-test-data的package.json一探究竟:

{
  "name": "sam-test-data",
  "version": "0.0.4",
  "description": "provide test data",
  "main": "dist/sam-test-data.js",
  "module": "dist/sam-test-data-es.js"
}
複製程式碼

可以看到main屬性指向dist/sam-test-data.js,這是一個UMD模組,但是module屬性指向dist/sam-test-data-es.js,這是一個ES模組,rollup.js預設情況下會優先尋找並載入module屬性指向的模組。所以sam-test-data的ES模組被優先載入,從而能夠支援tree-shaking特性。我們看一下rollup.js官方的說明:

在 package.json 檔案的 main 屬性中指向當前編譯的版本。如果你的 package.json 也具有 module 欄位,像 Rollup 和 webpack 2 這樣的 ES6 感知工具(ES6-aware tools)將會直接匯入 ES6 模組版本。

babel外掛

為什麼需要babel外掛?

在src/plugin目錄下建立一個新檔案main-es.js:

touch src/plugin/main-es.js
複製程式碼

寫入如下程式碼:

import { a, b, random } from 'sam-test-data-es'

console.log(a, b, random)
export default (base) => {
  return random(base)
}
複製程式碼

程式碼中採用了ES6的新特性:箭頭函式,修改配置檔案:

import resolve from 'rollup-plugin-node-resolve'
import commonjs from 'rollup-plugin-commonjs'

export default {
  input: './src/plugin/main-es.js',
  output: [{
    file: './dist/index-plugin-cjs.js',
    format: 'cjs'
  }, {
    file: './dist/index-plugin-es.js',
    format: 'es'
  }],
  plugins: [
    resolve(),
    commonjs()
  ]
}
複製程式碼

重新執行打包:

rollup -c rollup.plugin.config.js 
複製程式碼

檢視dist/index-plugin-es.js原始碼:

var mainEs = (base) => {
  return random(base)
};

export default mainEs;
複製程式碼

可以看到箭頭函式被保留下來,這樣的程式碼在不支援ES6的環境下將無法執行。我們期望在rollup.js打包的過程中就能使用babel完成程式碼轉換,因此我們需要babel外掛。

babel外掛的使用方法

首先安裝babel外掛:

npm i -D rollup-plugin-babel
複製程式碼

修改配置檔案,增加babel外掛的引用:

import resolve from 'rollup-plugin-node-resolve'
import commonjs from 'rollup-plugin-commonjs'
import babel from 'rollup-plugin-babel'

export default {
  input: './src/plugin/main-es.js',
  output: [{
    file: './dist/index-plugin-cjs.js',
    format: 'cjs'
  }, {
    file: './dist/index-plugin-es.js',
    format: 'es'
  }],
  plugins: [
    resolve(),
    commonjs(),
    babel()
  ]
}
複製程式碼

重新打包:

rollup -c rollup.plugin.config.js
複製程式碼

再次檢視dist/index-plugin-es.js原始碼:

var mainEs = (function (base) {
  return random(base);
});

export default mainEs;
複製程式碼

可以看到箭頭函式被轉換為了function,babel外掛正常工作。

json外掛

為什麼需要json外掛

在src/plugin下建立一個新檔案main-json.js:

touch src/plugin/main-json.js
複製程式碼

把package.json當做一個模組來引入,並列印package.json中的name和main屬性:

import json from '../../package.json'

console.log(json.name, json.main)
複製程式碼

使用bable-node嘗試執行main-json.js:

$ babel-node src/plugin/main-json.js 
rollup-test index.js
複製程式碼

可以看到name和main欄位都被列印出來了,babel-node可以正確識別json模組。下面修改rollup.js的配置檔案:

import resolve from 'rollup-plugin-node-resolve'
import commonjs from 'rollup-plugin-commonjs'
import babel from 'rollup-plugin-babel'

export default {
  input: './src/plugin/main-json.js',
  output: [{
    file: './dist/index-plugin-cjs.js',
    format: 'cjs'
  }, {
    file: './dist/index-plugin-es.js',
    format: 'es'
  }],
  plugins: [
    resolve(),
    commonjs(),
    babel()
  ]
}
複製程式碼

重新打包:

$ rollup -c rollup.plugin.config.js 

./src/plugin/main-json.js → ./dist/index-plugin-cjs.js, ./dist/index-plugin-es.js...
[!] Error: Unexpected token (Note that you need rollup-plugin-json to import JSON files)
複製程式碼

可以看到預設情況下rollup.js不支援匯入json模組,所以我們需要使用json外掛來支援。

json外掛的使用方法

下載json外掛:

npm i -D rollup-plugin-json
複製程式碼

修改配置檔案:

import resolve from 'rollup-plugin-node-resolve'
import commonjs from 'rollup-plugin-commonjs'
import babel from 'rollup-plugin-babel'
import json from 'rollup-plugin-json'

export default {
  input: './src/plugin/main-json.js',
  output: [{
    file: './dist/index-plugin-cjs.js',
    format: 'cjs'
  }, {
    file: './dist/index-plugin-es.js',
    format: 'es'
  }],
  plugins: [
    resolve(),
    commonjs(),
    babel(),
    json()
  ]
}
複製程式碼

重新打包:

rollup -c rollup.plugin.config.js
複製程式碼

檢視dist/index-plugin-cjs.js原始碼,可以看到json檔案被解析為一個物件進行處理:

var name = "rollup-test";
var version = "1.0.0";
var description = "";
var main = "index.js";
var scripts = {
	test: "echo \"Error: no test specified\" && exit 1"
};
var author = "";
var license = "ISC";
var devDependencies = {
	"@babel/core": "^7.1.6",
	"@babel/plugin-external-helpers": "^7.0.0",
	"@babel/preset-env": "^7.1.6",
	rollup: "^0.67.3",
	"rollup-plugin-babel": "^4.0.3",
	"rollup-plugin-commonjs": "^9.2.0",
	"rollup-plugin-json": "^3.1.0",
	"rollup-plugin-node-resolve": "^3.4.0"
};
var dependencies = {
	epubjs: "^0.3.80",
	loadsh: "^0.0.3",
	"sam-test-data": "^0.0.4",
	"sam-test-data-cjs": "^0.0.1",
	"sam-test-data-es": "^0.0.1",
	"sam-test-data-umd": "^0.0.1"
};
var json = {
	name: name,
	version: version,
	description: description,
	main: main,
	scripts: scripts,
	author: author,
	license: license,
	devDependencies: devDependencies,
	dependencies: dependencies
};

console.log(json.name, json.main);
複製程式碼

uglify外掛

uglify外掛可以幫助我們進一步壓縮程式碼的體積,首先安裝外掛:

npm i -D rollup-plugin-uglify
複製程式碼

修改rollup.js的配置檔案:

import resolve from 'rollup-plugin-node-resolve'
import commonjs from 'rollup-plugin-commonjs'
import babel from 'rollup-plugin-babel'
import json from 'rollup-plugin-json'
import { uglify } from 'rollup-plugin-uglify'

export default {
  input: './src/plugin/main.js',
  output: [{
    file: './dist/index-plugin-cjs.js',
    format: 'cjs'
  }],
  plugins: [
    resolve(),
    commonjs(),
    babel(),
    json(),
    uglify()
  ]
}
複製程式碼

這裡要注意的是uglify外掛不支援ES模組和ES6語法,所以只能打包成非ES格式的程式碼,如果碰到ES6語法則會出現報錯:

$ rollup -c rollup.plugin.config.js 

./src/plugin/main.js → ./dist/index-plugin-cjs.js, ./dist/index-plugin-es.js...
  19 | var main = random;
  20 | 
> 21 | export default main;
     |       ^ Unexpected token: keyword (default)
[!] (uglify plugin) Error: Unexpected token: keyword (default)
複製程式碼

所以這裡我們採用sam-test-data進行測試,因為這個模組採用了babel進行編譯,其他幾個模組uglify都不支援(因為其他幾個模組使用了const,const也是ES6特性,uglify不能支援),所以大家在自己編寫類庫的時候要注意使用babel外掛進行編譯。配置完成後重新打包:

$ rollup -c rollup.plugin.config.js 

./src/plugin/main.js → ./dist/index-plugin-cjs.js...
created ./dist/index-plugin-cjs.js in 679ms
複製程式碼

檢視dist/index-plugin-cjs.js原始碼:

"use strict";var a=Math.floor(10*Math.random()),b=Math.floor(100*Math.random());function random(a){return a&&a%1==0?Math.floor(Math.random()*a):0}var test=Object.freeze({a:a,b:b,random:random});console.log(test);var main=random;module.exports=main;
複製程式碼

可以看到程式碼被最小化了,體積也減小了不少。

rollup.js watch

命令列模式

rollup.js的watch模式支援監聽程式碼變化,一旦修改程式碼後將自動執行打包,非常方便,使用方法是在打包指令後新增--watch即可:

$ rollup -c rollup.plugin.config.js  --watch

rollup v0.67.1
bundles ./src/plugin/main-json.js → dist/index-plugin-cjs.js, dist/index-plugin-es.js...
created dist/index-plugin-cjs.js, dist/index-plugin-es.js in 24ms

[2018-11-20 22:26:24] waiting for changes...
複製程式碼

API模式

rollup.js支援我們通過API來啟動watch模式,在專案根目錄下建立以下檔案:

  • rollup-watch-input-options.js:輸入配置
  • rollup-watch-output-options.js:輸出配置
  • rollup-watch-options.js:監聽配置
  • rollup-watch.js:呼叫rollup.js的API啟動watch模式

為了讓node能夠執行我們的程式,所以採用CommonJS規範,rollup-watch-input-options.js程式碼如下:

const json = require('rollup-plugin-json')
const resolve = require('rollup-plugin-node-resolve')
const commonjs = require('rollup-plugin-commonjs')
const babel = require('rollup-plugin-babel')
const uglify = require('rollup-plugin-uglify').uglify

module.exports = {
  input: './src/plugin/main.js',
  plugins: [
    json(),
    resolve({
      customResolveOptions: {
        moduleDirectory: 'node_modules' // 僅處理node_modules內的庫
      }
    }),
    babel({
      exclude: 'node_modules/**' // 排除node_modules
    }),
    commonjs(),
    uglify() // 程式碼壓縮
  ]
}
複製程式碼

rollup-watch-output-options.js程式碼如下:

module.exports = [{
  file: './dist/index-cjs.js',
  format: 'cjs',
  name: 'sam-cjs'
}]
複製程式碼

rollup-watch-options.js程式碼如下:

module.exports = {
  include: 'src/**', // 監聽的資料夾
  exclude: 'node_modules/**' // 排除監聽的資料夾
}
複製程式碼

rollup-watch.js程式碼如下:

const rollup = require('rollup')
const inputOptions = require('./rollup-watch-input-options')
const outputOptions = require('./rollup-watch-output-options')
const watchOptions = require('./rollup-watch-options')

const options = {
  ...inputOptions,
  output: outputOptions,
  watchOptions
} // 生成rollup的options

const watcher = rollup.watch(options) // 呼叫rollup的api啟動監聽

watcher.on('event', event => {
  console.log('重新打包中...', event.code)
}) // 處理監聽事件

// watcher.close() // 手動關閉監聽
複製程式碼

通過node直接啟動監聽:

$ node rollup-watch.js 
重新打包中... START
重新打包中... BUNDLE_START
重新打包中... BUNDLE_END
重新打包中... END
複製程式碼

之後我們再修改src/plugin/main.js的原始碼,rollup.js就會自動對程式碼進行打包。

總結

本教程詳細講解了rollup.js的外掛、tree-shaking機制和watch模式,涉及知識點整理如下:

  • rollup.js外掛
    • resolve外掛:整合外部模組
    • commonjs外掛:支援CommonJS模組
    • babel外掛:編譯ES6語法,使低版本瀏覽器可以識別
    • json外掛:支援json模組
    • uglify:程式碼最小化打包(不支援ES模組)
  • tree-shaking:只有ES模組才支援,大幅精簡程式碼量
  • watch模式:支援命令列和API模式,實時監聽程式碼變更

相關文章