边城

如何写 Node 和浏览器共用的 JavaScript 包 | Read the Tea Leaves

边城 · 2017-01-18翻译 · 1099阅读 原文链接

我看到很多在这个问题上发生混淆的情况,甚至经验丰富的 JavaScript 开发者都有可能错过了它的一些微妙之处。因此我觉得有必要写一篇简短的教程。

假设你有一个 JavaScript 模块想发布在 npm,这个模块既能在 Node 中使用也能在浏览器中使用。现在有一个问题,实现这个独特的模块时遇到一点困难,因为它针对 Node 和浏览器有不同的实现。

这种情况相当频繁,因为 Node 和浏览器间存在很多细小的环境差异。要正确实现显得有些棘手,尤其是在你想将其优化到最小以用于浏览器的情况下。

来构建一个 JS 包

先来写一个小小的 JavaScript 包,我们称之为 base64-encode-string。它接受一个字符串输出,对其进行 base64 编码后输出。

对于浏览器,很容易,只需要使用内置的 btoa 函数:

module.exports = function (string) {
  return btoa(string);
};

不过在 Node 中没有 btoa 函数。所以我们必须创建一个 Buffer,然后调用 buffer.toString()

module.exports = function (string) {
  return Buffer.from(string, 'binary').toString('base64');
};

两个实现都可以得到正确的 base64 编码后的字符串,例如:

var b64encode = require('base64-encode-string');
b64encode('foo');    // Zm9v
b64encode('foobar'); // Zm9vYmFy

现在我们只需要检测是运行在浏览器中还是运行在 Node 中,这样才能选用正确的版本。 Browserify 和 Webpack 都定义了 process.browser,在浏览器中返回 true,Node 中返回 false。所以操作起来很简单:

if (process.browser) {
  module.exports = function (string) {
    return btoa(string);
  };
} else {
  module.exports = function (string) {
    return Buffer.from(string, 'binary').toString('base64');
  };
}

把文件命名为 index.js,输入 npm publish,这样就搞定了,是吗?不错,它能工作,但不幸的是这个实现有很大的性能问题。

因为我们的 index.js 文件引用了 Node 内置的 processBuffer 模块,Browserify 和 Webpack 都会在结果中包含 the polyfills

这个只有 9 行的模块,Browserify 和 Webpack 却创建了 最小化 24.7KB 的文件 (最小化+gz 后是 7.6KB)。 那是一个很大的东西,而在浏览器中人需要一个使用 btoa 的表达式!

“browser”配置,我是多么爱你

如果你在 Browserify 和 Webpack 文件中搜索解决这个问题的技巧,你一定会发现 node-browser-resolve。这是一个关于 package.json 内的 "browser" 配置的说明,它用来定义针对浏览器构建时的替代模块。

使用这个技术,可以在 package.json 中添加:

{
  /* ... */
  "browser": {
    "./index.js": "./browser.js"
  }
}

两个函数分别放在两个结果文件中,index.jsbrowser.js

// index.js
module.exports = function (string) {
  return Buffer.from(string, 'binary').toString('base64');
};
// browser.js
module.exports = function (string) {
  return btoa(string);
};

进行了这样的修正之后,Browserify 和 Webpack 产生了 更容易阅读的结果:Browserify 的最小化后是 511 字节 (最小化+gz 后是 315 字节),Webpack 的最小化后是 550 字节(最小化+gz 后是 297 字节)。

当我们把包发布到 npm 上后,在 Node 中使用 require('base64-encode-string') 会取得 Node 版本,而使用 Browserify 或 Webapck 会取得浏览器版本。成功!

对于 Rollup 来说要麻烦一些,但不会有太多额外的工作。Rollup 用户需要使用 rollup-plugin-node-resolve 并在选项中设置 browsertrue

对于 jspm 来说,很不幸,它不支持“browser”选项,不过 jspm 用户可以绕过它,这样做:require('base64-encode-string/browser') 或者 jspm install npm:base64-encode-string -o "{main:'browser.js'}"。 或者,包作者可以在 package.json指定一个“jspm”选项

高级技巧 Advanced techniques

直接使用 "browser" 的方法工作良好,但对于更大的项目,我发现在 package.json 和代码间存在一个尴尬的耦合。比如,package.json 很快会变成这样:

{
  /* ... */
  "browser": {
    "./index.js": "./browser.js",
    "./widget.js": "./widget-browser.js",
    "./doodad.js": "./doodad-browser.js",
    /* etc. */
  }
}

每次你想浏览器化一个模块,你都得创建两个文件,同时还得记住在 "browser" 选项中添加一行来关联它们。还得小心不要写错什么!

同时,你可能会发现自己会提取一些特别小的模块,只因为不想进行 if (process.browser) {} 检查。当然这些 *-browser.js 文件越来越多,它们会导致在代码导航变得困难。

要解决这种繁重的情况,也还有一些不同的方案。我个人比较喜欢使用 Rollup 作为构建工具,它会自动把单个代码拆分成 index.jsbrowser.js 文件。它在打包你发布给客户的代码时带来了节约空间和时间的好处。

想这样做的话,安装 rolluprollup-plugin-replace,然后定义一个 rollup.config.js 文件:

import replace from 'rollup-plugin-replace';
export default {
  entry: 'src/index.js',
  format: 'cjs',
  plugins: [
    replace({ 'process.browser': !!process.env.BROWSER })
  ]
};

(我们会把 process.env.BROWSER 作为一个切换浏览器构建和 Node 构建的快捷方法。)

然后,创建 src/index.js 文件,里面包含一个使用普通 process.browser 条件的函数:

export default function base64Encode(string) {
  if (process.browser) {
    return btoa(string);
  } else {
    return Buffer.from(string, 'binary').toString('base64');
  }
}

接着在 package.json 中添加 prepublish 步骤用于生成文件:

{
  /* ... */
  "scripts": {
    "prepublish": "rollup -c > index.js && BROWSER=true rollup -c > browser.js"
  }
}

生成的文件相当简单易读:

// index.js
'use strict';

function base64Encode(string) {
  {
    return Buffer.from(string, 'binary').toString('base64');
  }
}

module.exports = base64Encode;
// browser.js
'use strict';

function base64Encode(string) {
  {
    return btoa(string);
  }
}

module.exports = base64Encode;

你会发现 Rollup 自动根据需要把 process.browser 变为 truefalse,然后去掉不用的代码。所以在针对浏览器的结果中没有对 processBuffer 的引用。

使用这些技术,你可以在代码中添加任意数量的 process.browser 开关,但是生成的结果总是集中在两个小文件 index.jsbrowser.js 中。它们分别只有 Node 相关的代码,或者只有浏览器相关的代码。

你也可以配置 Rollup 生成 ES 模块构建、IIFE 构建或者 UMD 构建。这里有个项目 marky 用一个简单的库演示了 Rollup 多目标构建。

本文所用的实际项目 (base64-encode-string) 已经 发布在 npm 上,你可以看看它是如何做的。源代码在GitHub上

相关文章