自制Rollup-WebWorker打包插件
发布于 7 年前 作者 stuartZhang 7765 次浏览 来自 分享

Rollup是什么?

Rollup是下一代的ES6 JS文件打包工具。和Webpack相似,Rollup支持扩展插件开发,能把模块化的多个JS文件打包成一个文件,还能打包CSS文件(这个功能我还一直没有尝试过)。但是,经常被Rich Harris拿出来炫耀的是Rollup的tree-shaking的能力。即,在打包过程中,Rollup能够自动过滤与剔除没有用到的JS代码和没有调用过的JS函数。 Rollup打包的底层逻辑是“内联”处理被import的ES6模块代码。我理解Rollup是把ES6模块当作是JAVA里的Inline Function来处理的。对于模块动态加载,或许Rollup打包器不能直接满足这个需求。而需要另一个ES6 API:System.import(…)。

Rollup-WebWorker打包插件 的 需求由来

Rollup自身强悍,也有丰富的第三方插件。请见。但是,这个活跃的生态系统似乎遗漏了我正在遇到的需求类型。简单地概括: 打包 运行在Web Worker里的,依赖传统JS库的,遵循ES6 Moulde规范的 JavaScript程序文件 成为 一个IIFE文件。 我的需求包括以下几个关键点:

  1. “运行在Web Worker里”意为着 Rollup基于external和globals配置参数的映射机制 对 我的需求 无效。
    1. 简单地说,Rollup的external和globals配置参数 被设计用来 把‘ES6 Import指令’先映射到 HTML <script>标签,再映射到 被引用JS文件构建的全局变量 上。
      • 比如,把 import io from ‘socket.io-client’; 指令 先映射到 <script src=“socket.io-client.js”>标签,再映射到 全局变量io。
    2. 但是,这一切对Web Worker是无效的。因为在Web Worker里不能定义<script>标签,所以仅能使用importScripts(…)函数在程序内手动导入依赖库。
    3. 于是,挑战1出现了。
  2. “依赖传统JS库”意为着 被依赖的JS文件 很有可能不是 CMD,AMD,UMD或ES 6模块文件。相反,所谓的传统JS库文件通常是被包装成为一个IIFE表达式(即,一个立即执行的大闭包),并且输出一个全局变量 作为 暴露API集的顶层命名空间。
    • 但是,挑战2并没有出现,因为‘Rollup的external和globals配置参数’就是被用来解决这个需求的。只不过,正如#1里已经提到的,‘Rollup的external和globals配置参数’要求你的JS文件运行在网页里,而不是运行在Web Worker里。
  3. “遵循ES6 Moulde规范”这需求最容易解决。只要在Rollup的插件链条的最后加上一个Rollup Babel Plugin就行了。
  4. “输出一个IIFE文件”这需求也简单。只需设置Rollup的打包输出格式参数“format”为“iife”即可。

Rollup-WebWorker打包插件 的 设计目标

至此,通过总结我的需求,新的Rollup插件需要完成如下几个工作步骤:

  1. 映射 一条ES 6 Module Import 指令 到 一个或多个 JS文件。
    • 比如,映射 import Zlib from ‘zlib’; 到 两个JS文件 ‘js/lib/gzip.min.js’和’js/lib/gunzip.min.js’。
  2. 生成一条importScripts(…)函数调用语句。
    • 比如,importScripts(‘js/lib/gzip.min.js’, ‘js/lib/gunzip.min.js’);
  3. 插入 第2步生成的importScripts(…)语句 到 打包结果文件的顶部第一句的位置。特别提示:importScripts(…)语句一定要被插入在IIFE闭包表达式之前,而绝对不能在IIFE的闭包函数体内。

Rollup-WebWorker打包插件 的 设计详细

  1. 实现Rollup Plugin的resolveId(importeeimportee, importer){…}成员函数。
    1. 捕获哪些 传统JS库 正在 ES6代码中 被依赖。即,知道import指令中from关键字后面的依赖模块名。 比如,从ES6指令import Zlib from ‘zlib’;抽取模块名’zlib’。
    2. 翻译 被捕获的依赖模块名 为 具体的一个或多个JS文件路径。即,通过 预传入的“模块名-文件名 映射表”,把’zlib’映射成 'js/lib/gzip.min.js’和’js/lib/gunzip.min.js’两个文件路径。
    3. 生成importScripts(…)函数调用语句。比如,importScripts(‘js/lib/gzip.min.js’, ‘js/lib/gunzip.min.js’);
  2. 实现Rollup Plugin的banner(){…}成员函数。
    1. 把 被生成的importScripts(…)语句 直接作为banner(){…}函数的返回字符串return出去即可。
    2. 最终,importScripts(…)语句就会出现在 打包结果文件的顶行第一句的位置。

Rollup-WebWorker打包插件 带来的改善

给Rollup写插件是不是so easy。更重要的是,仅只需要投入大约40到60分钟就能够立杆见影地解决工程构建过程中出现的棘手问题。仅这一点点的改善就确保了“网页JS编程”与“Web Worker开发”的一致编码风格与开发体验。即, 在ES6代码里,透明化对传统JS依赖的导入操作。 1. 无论依赖模块是AMD,CMD,UMD,ES6 Module,还是传统的IIFE闭包, 2. 无论JS程序是运行在网页渲染主线程,还是运行在Web Worker线程里, ES6 Module的Import指令都能够将它们导入你当前的运行环境上下文。

Rollup-WebWorker打包插件 源码

var _ = require('underscore'), path = require('path');
module.exports = function(options){
  var pluginName = 'Web Worker', optExternal, optPaths, importScripts = [], history = [], dirTopFilepath, relTopFilepath, topFilepath;
  return {
    'name': pluginName,
    'options': function(options){ // 从Rollup的配置中抽取external与paths配置参数。external与paths都是Rollup的标准配置项。
      optExternal = options.external || []; // 用法与语义都等同于Rollup对external参数的默认配置定义。
      optPaths = options.paths || {}; // 用法与语义都等同于Rollup对paths参数的默认配置定义。
      delete options.external;
      delete options.paths;
    },
    'resolveId': function(importee, importer){
      if (!importer) {
        topFilepath = importee; // 获得ES 6代码编译的入口文件的文件名
        relTopFilepath = path.relative(options.cwd, topFilepath);
        dirTopFilepath = path.dirname(relTopFilepath);
      }
      if (optExternal.indexOf(importee) < 0) {
        return null; // 将控件权移交给Rollup插件链条中的下一个Rollup插件实例。
      }
      if (history.indexOf(importee) > -1) {
        return false; // 此依赖已经处理过了,不再重复处理,在此处跳过。
      }
      var pathImportees = optPaths[importee];
      if (pathImportees) {
        if (!_.isArray(pathImportees)) {
          pathImportees = [pathImportees];
        }
        pathImportees.forEach(function(pathImportee){
          var refImportee = path.relative(dirTopFilepath, pathImportee);
          importScripts.push(refImportee); // 在此处,收集传统JS依赖库的文件名。
        });
      }
      history.push(importee);
      return false; 
    },
    'banner': function(){
      var polyfills = options.polyfills;
      if (!_.isArray(polyfills)) {
        polyfills = [polyfills];
      }
      polyfills.reverse().forEach(function(polyfill){
        var refImportee = path.relative(dirTopFilepath, polyfill);
        importScripts.unshift(refImportee);
      });
      return "importScripts('" + importScripts.join("', '").replace(/\\/g, '/') + "');"; // 生成importScripts(...)调用语句,并输出到 打包结果文件的 顶行第一句的位置。
    }
  };
};

Rollup-WebWorker打包插件 的使用

在这里,以Grunt为例。

var Babel = require('rollup-plugin-babel'), WebWorker = require('./src/js/lib/rollup-plugin-webworker');
grunt.config.merge({
  'rollup': {
    'worker': {
      'options': {
        'external': ['underscore', 'socket.io', 'zlib'], // 注册外部依赖的模块名称
        'globals': { // 注册 外部依赖的 模块名称 至 全局变量名 的映射关系表
          'underscore': '_',
          'zlib': 'Zlib',
          'socket.io': 'io'
        },
        'paths': { // 注册 外部依赖的 模块名称 至 文件路径 的映射关系表
          'underscore': 'js/lib/underscore.js', 
          'zlib': ['js/lib/gzip.min.js', 'js/lib/gunzip.min.js'],
          'socket.io': 'js/lib/socket.io-1.5.1.js'
        },
        'plugins': [WebWorker({ // 启用Rollup WebWorker打包插件 
          'cwd': 'src',
          'polyfills': ['js/lib/web-worker-runtime.js', 'js/lib/polyfill.js']
        }), Babel()]
      },
      'files': [{
        'cwd': 'src',
        'expand': true,
        'dest': '.build',
        'src': ['js/workers/*.js']
      }]
    }
  }
});

后面的计划

我正在学习如何在Github上创建工程,我想把这个rollup-webworker-plugin贡献到Rollup的第三方插件集中。

4 回复

@i5ting 狼叔。你使用Rollup吗?我的这个原创说不定对你也有帮助的。如果你感觉有帮助的话,给个赞吧。还有,置顶。

666不得不服

webpack2自带tree-shaking,版本号升2就是因为支持es6 Module…

@Smallpath 我正在关注webpack。一有机会我也会尝试webpack的。

回到顶部