我一直在努力学习如何编写树摇动友好的代码,但遇到了一个不可避免的副作用的问题,我不知道如何处理。
在我的一个模块中,我访问全局Audio
构造函数并使用它来确定浏览器可以播放哪些音频文件(类似于Modernizr does it)。每当我尝试树摇动我的代码时,即使我不在我的文件中导入模块,Audio
元素及其对它的所有引用都不会被消除。
let audio = new Audio(); // or document.createElement('audio')
let canPlay = {
ogg: audio.canPlayType('audio/ogg; codecs="vorbis"').replace(/^no$/, '');
mp3: audio.canPlayType('audio/mpeg; codecs="mp3"').replace(/^no$/, '');
// ...
};
我知道包含副作用的代码无法消除,但我找不到的是如何处理不可避免的副作用。我不能只是不访问全局对象来创建检测功能支持所需的audio
元素。那么我如何处理访问全局浏览器函数/对象(我在这个库中做了很多)的方式,树摇动友好,仍然允许我消除代码?
您可以从Haskell / PureScript的书中获取页面,并简单地限制自己在导入模块时发生任何副作用。而是导出一个代表例如副作用的thunk。在用户的浏览器中访问全局Audio
元素,并使用thunk产生的值参数化其他函数/值。
以下是您的代码段的外观:
// :: type IO a = () -!-> a
// :: IO Audio
let getAudio = () => new Audio();
// :: Audio -> { [MimeType]: Boolean }
let canPlay = audio => {
ogg: audio.canPlayType('audio/ogg; codecs="vorbis"').replace(/^no$/, '');
mp3: audio.canPlayType('audio/mpeg; codecs="mp3"').replace(/^no$/, '');
// ...
};
然后在主模块中,您可以使用适当的thunk来实例化您实际需要的全局变量,并将它们插入到使用它们的参数化函数/值中。
如何手动插入所有这些新参数是相当明显的,但它可能会变得乏味。有几种技术可以缓解这种情况;您可以再次从Haskell / PureScript中窃取的方法是使用读取器monad,这有助于对由简单函数组成的程序进行依赖注入。
读者monad的更详细的解释以及如何使用它来编写整个程序的一些上下文超出了这个答案的范围,但是这里有一些链接,你可以阅读这些内容:
(免责声明:我还没有彻底阅读或审查所有这些链接,我只是用谷歌搜索关键字并复制了一些引言看起来很有希望的链接)
您可以使用audio()
访问音频对象,并使用canPlay
,无需函数调用,实现一个模块,为您提供类似的使用模式。这可以通过在函数中运行Audio
构造函数来完成,如Asad建议的那样,然后在每次要访问它时调用该函数。对于canPlay
,我们可以使用Proxy,允许数组索引作为函数在引擎盖下实现。
我们假设我们创建了一个文件audio.js
:
let audio = () => new Audio();
let canPlay = new Proxy({}, {
get: (target, name) => {
switch(name) {
case 'ogg':
return audio().canPlayType('audio/ogg; codecs="vorbis"').replace(/^no$/, '');
case 'mp3':
return audio().canPlayType('audio/mpeg; codecs="mp3"').replace(/^no$/, '');
}
}
});
export {audio, canPlay}
这些是在各种index.js
文件上运行的结果,rollup index.js -f iife
:
import {} from './audio';
(function () {
'use strict';
}());
import {audio} from './audio';
console.log(audio());
(function () {
'use strict';
let audio = () => new Audio();
console.log(audio());
}());
import {canPlay} from './audio';
console.log(canPlay['ogg']);
(function () {
'use strict';
let audio = () => new Audio();
let canPlay = new Proxy({}, {
get: (target, name) => {
switch(name) {
case 'ogg':
return audio().canPlayType('audio/ogg; codecs="vorbis"').replace(/^no$/, '');
case 'mp3':
return audio().canPlayType('audio/mpeg; codecs="mp3"').replace(/^no$/, '');
}
}
});
console.log(canPlay['ogg']);
}());
此外,如果您希望保留问题中列出的属性,则无法按原计划实施audio
。 audio()
的其他短暂可能性是+audio
或audio``
(如图所示:Invoking a function without parentheses),可以认为更加混乱。
最后,其他不涉及数组索引或函数调用的全局变量必须以与let audio = () => new Audio();
类似的方式实现。