如何处理摇树代码中的副作用?

时间:2019-02-26 05:45:04

标签: javascript es6-modules rollupjs side-effects tree-shaking

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

在我的一个模块中,我访问全局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元素。那么,如何以对树摇动友好的方式仍然可以消除代码的方式来访问全局浏览器功能/对象(我在该库中做了很多工作)?

2 个答案:

答案 0 :(得分:5)

您可以从Haskell / PureScript的书中抽出一页,而只是限制自己,避免在导入模块时发生任何副作用。相反,您导出一个表示例如可以访问用户浏览器中的全局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以及如何使用它在整个程序中穿插某些上下文的更详细的解释超出了此答案的范围,但是在此处可以阅读以下内容的一些链接:

(免责声明:我还没有完全阅读或审查所有这些链接,我只是用谷歌搜索关键字,并复制了一些引人入胜的链接)

答案 1 :(得分:3)

您可以实现一个模块,以提供与您的问题类似的使用模式,即使用audio()访问音频对象,使用canPlay,而无需调用函数。可以通过按照Asad的建议在函数中运行Audio构造函数,然后在每次希望访问该函数时调用该函数来完成此操作。对于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']);

}());

此外,如果您希望保留问题中概述的属性,则无法按原计划实施audioaudio()的其他短期可能性是+audioaudio``(如此处所示:Invoking a function without parentheses),这被认为更加令人困惑。

最后,其他不涉及数组索引或函数调用的全局变量将必须以与let audio = () => new Audio();类似的方式实现。