最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

javascript - How to deal with side effects in tree shaking code? - Stack Overflow

programmeradmin1浏览0评论

I've been trying to learn how to write code that is tree shaking friendly, but have run into a problem with unavoidable side effects that I'm not sure how to deal with.

In one of my modules, I access the global Audio constructor and use it to determine which audio files the browser can play (similar to how Modernizr does it). Whenever I try to tree shake my code, the Audio element and all references to it do not get eliminated, even if I don't import the module in my file.

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$/, '');
  // ...
};

I understand that code that contains side effects cannot be eliminated, but what I can't find is how to deal with unavoidable side effects. I can't just not access a global object to create an audio element needed to detect feature support. So how do I handle accessing global browser functions/objects (which I do a lot in this library) in a way that is tree shaking friendly and still allows me to eliminate the code?

I've been trying to learn how to write code that is tree shaking friendly, but have run into a problem with unavoidable side effects that I'm not sure how to deal with.

In one of my modules, I access the global Audio constructor and use it to determine which audio files the browser can play (similar to how Modernizr does it). Whenever I try to tree shake my code, the Audio element and all references to it do not get eliminated, even if I don't import the module in my file.

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$/, '');
  // ...
};

I understand that code that contains side effects cannot be eliminated, but what I can't find is how to deal with unavoidable side effects. I can't just not access a global object to create an audio element needed to detect feature support. So how do I handle accessing global browser functions/objects (which I do a lot in this library) in a way that is tree shaking friendly and still allows me to eliminate the code?

Share Improve this question edited Feb 26, 2019 at 7:04 Daniel 11.2k12 gold badges54 silver badges93 bronze badges asked Feb 26, 2019 at 5:45 Steven LambertSteven Lambert 5,9212 gold badges32 silver badges46 bronze badges 5
  • 1 Does it get eliminated if you instead export a let audio = () => new Audio() thunk? – user1726343 Commented Feb 28, 2019 at 5:50
  • Sorry, I'm not sure I follow. Would the consumer have to call the audio function and set the canPlay themself? – Steven Lambert Commented Feb 28, 2019 at 6:26
  • Yes, the consumer would call audio themselves to obtain the Audio value, and then they would plug it into canPlay, which would have to be parametrized to accept an Audio value. – user1726343 Commented Feb 28, 2019 at 7:01
  • 1 Can you provide an example of how you are exporting your module functions? I think wrapping what you've provided thus far in a single function should allow tree-shaking, but this depends on how you're exporting. – willascend Commented Mar 3, 2019 at 18:47
  • Since I'm still learning, I've been exporting a single default object/class for most of the code, following lodash es as an example template. In this particular case, my library isn't just a library of single functions though, but handles things like keyboard events, mouse events, and asset loading. – Steven Lambert Commented Mar 4, 2019 at 15:55
Add a ment  | 

2 Answers 2

Reset to default 6 +100

You could take a page out of Haskell/PureScript's book, and simply restrict yourself from having any side effects occur when you import a module. Instead, you export a thunk that represents the side effect of e.g. getting access to the global Audio element in the user's browser, and parametrize the other functions/values wrt the value that this thunk produces.

Here's what it would look like for your code snippet:

// :: 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$/, '');
  // ...
};

Then in your main module you can use the appropriate thunks to instantiate the globals you actually need, and plug them into the parametrized functions/values that consume them.

It's fairly obvious how to plug all these new parameters manually, but it can get tedious. There's several techniques to mitigate this; an approach you can again steal from Haskell/PureScript is to use the reader monad, which facilitates a kind of dependency injection for programs consisting of simple functions.

A much more detailed explanation of the reader monad and how to use it to thread some context throughout your program is beyond the scope of this answer, but here are some links where you can read about these things:

  • https://github./monet/monet.js/blob/master/docs/READER.md
  • https://www.youtube./embed/ZasXwtTRkio?rel=0
  • https://www.fpplete./blog/2017/06/readert-design-pattern

(disclaimer: I haven't thoroughly read or vetted all of these links, I just googled keywords and copied some links where the introduction looked promising)

You can implement a module to give you a similar usage pattern that your question suggests, using audio() to access the audio object, and canPlay, without a function call. This can be done by running the Audio constructor in a function, as Asad suggested, and then calling that function every time you wish to access it. For the canPlay, we can use a Proxy, allowing the array indexing to be implemented under the hood as a function.

Let's assume we create a file 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}

These are the results of running on various index.js files, 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']);

}());

Additionally, there is no way to implement audio as originally intended if you wish to preserve the properties outlined in the question. Other short possibilities to audio() are +audio or audio`` (as shown here: Invoking a function without parentheses), which can be considered to be more confusing.

Finally, other global variables that don't involve an array index or function call will have to be implemented in similar ways to let audio = () => new Audio();.

发布评论

评论列表(0)

  1. 暂无评论