我看到很多在这个问题上发生混淆的情况,甚至经验丰富的 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'); // Zm9vb64encode('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 内置的process和Buffer模块,Browserify 和 Webpack 都会在结果中包含thepolyfills。
这个只有 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.js和browser.js:
// index.jsmodule.exports = function (string) { return Buffer.from(string, 'binary').toString('base64');};
// browser.jsmodule.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并在选项中设置browser为true。
对于 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.js和browser.js文件。它在打包你发布给客户的代码时带来了节约空间和时间的好处。
想这样做的话,安装rollup和rollup-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
|