9am / 9am.github.io

9am Blog 🕘 ㍡
https://9am.github.io/
MIT License
3 stars 0 forks source link

Call APIs Before It Existed? Sure! #6

Open 9am opened 2 years ago

9am commented 2 years ago
tools hits
9am commented 2 years ago

The Problem

Working as a frontend engineer, there'll be one day that you need to use some 3rd-party services, like error monitoring or user action analyzing, except that your company decides to develop their own version. For all of those services, they need to embed some JS SDK code in the page, so they can send messages back to their server. Usually, the code looks like this:

<script src="https://xxx-cdn/1.3.6/sdk.min.js"></script>
<script>
    window.sdk.init(...config)
    window.sdk.api()
    ...
</script>

This is an old-fashion way of exposing API to others before the bundler or ES module. But it's nice and simple, which doesn't require developers to install any dependency. Just copy the code to the HTML template, and it will work like a charm. But...is it good enough?

Here is the problem: The developer wouldn't know when the SDK will be ready.

The API calling and the clicking before SDK is ready are not executed right.

ss-0

Edit old-way

Emm..., the code snippet from the beginning seems not to have this problem. Well, that's because <script src="https://xxx-cdn/1.3.6/sdk.min.js"></script> block the HTML parsing until the <script> got loaded and executed. It's bad for performance. Left us with no choice but async or defer.

<script defer /> might be the answer. Sure that defer will keep the execution order of <script>s. But the API calling script must be defer too to make that work. And even if we do that, all the script will be executed until DOMContentLoaded. That's bad if the SDK needs to launch ASAP for reasons like sending the PV beacon, etc...

For <script async />, the most obvious solution is to listen to the load event. Call the API in the event handler. But to register the handler anywhere we want, we have to expose another global variable. And the API call becomes ugly with the extra code.

<script>
    (() => {
        s = document.createElement('script')
        s.src = 'sdk.min.js'
        s.async = 1 // delay the execution
        const t = document.getElementsByTagName('script')
        t.parentElement.insertBefore(s, t)
    })()
</script>
...
<script>
    s.addEventListener('load', () => {
        sdk.api()
    })
</script>

Another brutal solution is to check the existence of window.sdk until it's ready. Uuuuuuugly.

<script async src="sdk.min.js"></script>
...
<script>
    const tid = setInterval(() => {
        if (window.sdk) {
            window.sdk.api()
            clearInterval(tid);
        }
    }, 10)
</script>

So, imagine that you are the SDK owner. Is there a better way to let the user call the API anywhere they want, even before the SDK loading script?

<script>
    window.sdk.api(1)
</script>
...
<script async src="sdk.min.js></script>
...
<script>
    window.sdk.api(2)
</script>

Solution

If we consider the javascript runtime an SDK, which is available from the beginning of the HTML parsing. We can take advantage of that.

  1. sdk.min.js

    (() => {
        const sdk = (queue = []) => {
            const hi = (name) => {
                console.log('Hi ', name);
            };
            const hello = (name) => {
                console.log('Hello ', name);
            };
            const api = {
                hi,
                hello,
            };
            // override the push() after loading
            const push = ([fn, ...args]) => {
                if (!api[fn]) {
                    return;
                }
                api[fn].apply(this, args);
            };
            // flush the queue
            queue.forEach((task) => push(task));
            queue = [];
            return {
                ...api,
                push,
            };
        };
    
        // passing the queue to SDK
        window._sdk = sdk(window._sdk);
    })();
  2. index.html

    <script>
        (() => {
            // using a native javascript API to 'save' the SDK API callings
            // before the SDK loading complete
            // like Array.push
            window._sdk = []
    
            // embed the SDK with async
            const s = document.createElement('script');
            s.src = 'sdk.min.js';
            s.async = 1; // delay the execution
            const t = document.getElementsByTagName('script');
            t.parentElement.insertBefore(s, t);
        })()
    </script>
    ...
    <script>
        // using 'push' to call API at anytime
        window._sdk.push('hi', '9am')
    
        // if the SDK is not ready, it'll be queued in window_sdk[]
        // otherwise it'll get called directly
        window._sdk.push('hello', '9am')
    </script>

Click the button before SDK is ready, you'll find that they got called as expected.

ss-1

Edit call-it-before-existed

Hope you enjoy it. I'll see you next time.


@9am 🕘