如何使用 Google Closure Compiler 浏览您的 Node.js 库

问题描述

我有这个简单的 Node.js 库:

mylib/
|- inc.js
|- index.js
|- is_number.js
|- package.json

mylib/is_number.js

module.exports = x => typeof x === 'number';

mylib/inc.js

const is_number = require('./is_number');

module.exports = x => is_number(x) ? x + 1 : x;

mylib/index.js mainpackage.json 属性的值)

module.exports = {
  inc: require('./inc'),utils: {
    is_number: require('./is_number')
  }
};

示例:

const mylib = require('mylib');

mylib.inc(41);
//=> 42

mylib.utils.is_number(42);
//=> true

如何使用 Google Closure Compiler 来“浏览”我的 Node.js 库,以便它也能在浏览器中运行?例如,

<script src="mylib/browser.min.js"></script>
<script>
const mylib = window.mylib;

mylib.inc(41);
//=> 42

mylib.utils.is_number(42);
//=> true
</script>

解决方法

这个答案的规范帖子是这个Gist

TL;博士

  1. 创建mylib/index_browser.js

    window.mylib = {
      inc: require('./inc'),utils: {
        is_number: require('./is_number')
      }
    };
    
  2. 创建mylib/externs.js

    /** @externs */
    var mylib;
    var inc;
    var utils;
    var is_number;
    
  3. 那么:

    $ cc --compilation_level ADVANCED \
        --language_out ES5 \
        --process_common_js_modules \
        --module_resolution NODE \
        --externs mylib/externs.js \
        --isolation_mode IIFE \
        --js mylib/index_browser.js mylib/inc.js mylib/is_number.js \
        --js_output_file mylib/browser.min.js
    

    其中 cc 是您的 Google Closure Compiler 实例的别名;请参阅下面的示例


在我们开始之前:

我写这个别名是为了更容易调用 Google Closure Compiler (CC)

$ alias cc="java -jar /devtools/closure-compiler/compiler.jar"
$ cc --version
Closure Compiler (http://github.com/google/closure-compiler)
Version: v20210106

浏览器版本的库将编译为 ES5。

分步说明

您的第一次尝试可能如下所示:只需编译导出文件 mylib/index.js

$ cc --compilation_level ADVANCED \
     --language_out ES5 \
     --js mylib/index.js
mylib/index.js:1:0: ERROR - [JSC_UNDEFINED_VARIABLE] variable module is undeclared
  1| module.exports = {
     ^^^^^^

mylib/index.js:2:7: ERROR - [JSC_UNDEFINED_VARIABLE] variable require is undeclared
  2|   inc: require('./inc'),^^^^^^^

2 error(s),0 warning(s)

如果 CC 不知道 modulerequire 这不是一个好的开始。

幸运的是,我们只缺少 --process_common_js_modules 标志:

$ cc --compilation_level ADVANCED \
     --language_out ES5 \
     --process_common_js_modules \
     --js mylib/index.js
mylib/index.js:2:7: ERROR - [JSC_JS_MODULE_LOAD_WARNING] Failed to load module "./inc"
  2|   inc: require('./inc'),^

mylib/index.js:4:15: ERROR - [JSC_JS_MODULE_LOAD_WARNING] Failed to load module "./is_number"
  4|     is_number: require('./is_number')
                    ^

2 error(s),0 warning(s)

仍然不是很好,但这次错误有所不同:

  1. CC 不知道你在说什么require
  2. CC 不知道这两个其他模块在哪里

我们需要 --module_resolution 标志并告诉 CC 其他模块在哪里:

$ cc --compilation_level ADVANCED \
     --language_out ES5 \
     --process_common_js_modules \
     --module_resolution NODE \
     --js mylib/index.js mylib/inc.js mylib/is_number.js 

然而输出为空...

为什么?在 ADVANCED 编译模式下,CC 会删除任何未使用的代码。实际情况是这样:到目前为止,所有这些东西都没有使用过!

让我们检查一个不太激进的编译模式:

$ cc --compilation_level WHITESPACE_ONLY --formatting PRETTY_PRINT \
     --language_out ES5 \
     --process_common_js_modules \
     --module_resolution NODE \
     --js mylib/index.js mylib/inc.js mylib/is_number.js 
var module$mylib$index = {default:{}};
module$mylib$index.default.inc = module$mylib$inc.default;
module$mylib$index.default.utils = {is_number:module$mylib$is_number.default};
var module$mylib$inc = {};
var is_number$$module$mylib$inc = module$mylib$is_number.default;
module$mylib$inc.default = function(x) {
  return (0,module$mylib$is_number.default)(x) ? x + 1 : x;
};
var module$mylib$is_number = {};
module$mylib$is_number.default = function(x) {
  return typeof x === "number";
};

我们可以看到,即使 ADVANCED 编译模式没有删除所有内容,这无论如何也不是很有用。例如,window.mylib 在哪里?

我设法让我的库在 window.mylib 可用并使用最积极的编译模式编译的唯一方法是为浏览器创建一个单独的导出文件。

从此mylib/index.js

module.exports = {
  inc: require('./inc'),utils: {
    is_number: require('./is_number')
  }
};

到这个mylib/index_browser.js

window.mylib = {
  inc: require('./inc'),utils: {
    is_number: require('./is_number')
  }
};

当您添加到 window 对象时,CC 知道此代码可能被访问,因此无法再安全地删除它。

让我们用这个文件再试一次:

$ cc --compilation_level ADVANCED --formatting PRETTY_PRINT \
     --language_out ES5 \
     --process_common_js_modules \
     --module_resolution NODE \
     --js mylib/index_browser.js mylib/inc.js mylib/is_number.js
function b(a) {
  return "number" === typeof a;
}
;window.g = {h:function(a) {
  return b(a) ? a + 1 : a;
},j:{i:b}};

这看起来更好,但有一个主要问题:CC 把所有的名字都弄乱了!

别担心!我们只需要告诉 CC 应该保留哪些名称。这就是 externs 文件的用途。

mylib/externs.js

/** @externs */
var foo;
var inc;
var utils;
var is_number;

我们需要另一个标志:--externs

$ cc --compilation_level ADVANCED --formatting PRETTY_PRINT \
     --language_out ES5 \
     --process_common_js_modules \
     --module_resolution NODE \
     --externs mylib/externs.js \
     --js mylib/index_browser.js mylib/inc.js mylib/is_number.js
function b(a) {
  return "number" === typeof a;
}
;window.mylib = {inc:function(a) {
  return b(a) ? a + 1 : a;
},utils:{is_number:b}};

到达那里...

一个明显的改进是将所有这些都包装在一个 IIFE 中,以避免不必要地污染全局范围。

我们需要 --isolation_mode 标志:

$ cc --compilation_level ADVANCED --formatting PRETTY_PRINT \
     --language_out ES5 \
     --process_common_js_modules \
     --module_resolution NODE \
     --externs mylib/externs.js \
     --isolation_mode IIFE \
     --js mylib/index_browser.js mylib/inc.js mylib/is_number.js
(function(){function b(a) {
  return "number" === typeof a;
}
;window.mylib = {inc:function(a) {
  return b(a) ? a + 1 : a;
},utils:{is_number:b}};
}).call(this);

太棒了!

剩下要做的就是将其保存到文件中并删除格式以节省一些额外的字节:

$ cc --compilation_level ADVANCED \
     --language_out ES5 \
     --process_common_js_modules \
     --module_resolution NODE \
     --externs mylib/externs.js \
     --isolation_mode IIFE \
     --js mylib/index_browser.js mylib/inc.js mylib/is_number.js \
     --js_output_file mylib/browser.min.js

mylib/browser.min.js

(function(){function b(a){return"number"===typeof a};window.mylib={inc:function(a){return b(a)?a+1:a},utils:{is_number:b}};}).call(this);