13

我一直在尝试学习如何编写对摇树友好的代码,但遇到了一个不可避免的副作用问题,我不确定如何处理。

在我的一个模块中,我访问全局Audio构造函数并使用它来确定浏览器可以播放哪些音频文件(类似于Modernizr 的做法)。每当我尝试摇树摇动我的代码时,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检测功能支持所需的元素。那么,如何以一种对摇树友好的方式访问全局浏览器函数/对象(我在这个库中做了很多工作)并且仍然允许我消除代码?

4

2 回答 2

6

您可以从 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 中窃取的一种方法是使用 reader monad,它为由简单函数组成的程序提供了一种依赖注入。

阅读器 monad 的更详细解释以及如何使用它在整个程序中线程化某些上下文超出了此答案的范围,但这里有一些链接,您可以阅读这些内容:

(免责声明:我没有彻底阅读或审查所有这些链接,我只是用谷歌搜索关键词并复制了一些引言看起来很有希望的链接)

于 2019-02-28T06:31:22.030 回答
3

您可以实现一个模块,为您提供问题建议的类似使用模式,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()+audioaudio``(如此处所示:Invoking a function without parentheses),这可以被认为更令人困惑。

最后,其他不涉及数组索引或函数调用的全局变量必须以与let audio = () => new Audio();.

于 2019-03-06T14:15:37.907 回答