Wednesday, May 8, 2024
HomeJavaScriptHow Livewire works (a deep dive)

How Livewire works (a deep dive)


Oct 2021

The expertise of utilizing Livewire appears magical. It’s as in case your front-end HTML can name your PHP code and all the things simply works.

Lots goes into making this magic occur. Let me present you what’s occurring:

Our Instance

For this writeup, we’re going to make use of the instance of a easy counter element. Right here’s what the Livewire element appears to be like like:

Counter.php

<?php

namespace AppHttpLivewire;

class Counter extends LivewireComponent
{
    public $depend = 0;

    public operate increment()
    {
        $this->depend++;
    }

    public operate render()
    {
        return view('livewire.counter');
    }
}

counter.blade.php

<div>
    <h1>Depend: {{ $depend }}</h1>

    <button wire:click on="increment">Increment</button>
</div>

On this instance, after we load the web page we see “Depend: 0“ and after we hit the “Increment” button, the “0” magically turns right into a “1”.

Now that we’ve established one thing concrete, let’s speak about how Livewire makes this occur.

The Preliminary Render

Let’s say we need to embrace this Livewire element in a totally commonplace blade view. Right here’s how that might look:

<html>
    <livewire:counter />

    @livewireScripts
</html>

Whenever you load this web page, Laravel processes this Blade file like another, besides that Livewire does some hackery to get Blade to transform <livewire:counter /> into: @livewire('counter').

On this easy case, the @livewire('counter') directive compiles right down to code that appears like this: echo LivewireLivewire::mount('counter')->html();

Earlier than we dig into what’s occurring in mount() we’ll simply keep outdoors and have a look at the results of that decision:

<div wire:id="44Njb4Yue0jBTzpzRlUf" wire:initial-data="{&quot;fingerprint&quot;:{&quot;id&quot;:&quot;44Njb4Yue0jBTzpzRlUf&quot;,&quot;title&quot;:&quot;counter&quot;,&quot;locale&quot;:&quot;en&quot;,&quot;path&quot;:&quot;take a look at&quot;,&quot;methodology&quot;:&quot;GET&quot;},&quot;results&quot;:{&quot;listeners&quot;:[]},&quot;serverMemo&quot;:{&quot;kids&quot;:[],&quot;errors&quot;:[],&quot;htmlHash&quot;:&quot;402ed05a&quot;,&quot;information&quot;:{&quot;depend&quot;:0},&quot;dataMeta&quot;:[],&quot;checksum&quot;:&quot;1dc6e1fbb14c1a5cf8c138bb6b09dd99493dee38a9a89a8133901d1d38f40eac&quot;}}">
    <h1>Depend: 0</h1>

    <button wire:click on="increment">Increment</button>
</div>
<!-- Livewire Part wire-end:44Njb4Yue0jBTzpzRlUf -->

As you may see, Livewire renders the element as you’d count on, nevertheless it additionally pumps the HTML with plenty of metadata for its personal inner functions.

We’ll dig into this within the subsequent part, however whereas we’re right here let’s additionally briefly have a look at what @livewireScripts does.

Primarily this Blade directive compiles down to 2 <script> tags that load all of the JavaScript Livewire must operate.

I’ll pass over all of the pointless particulars of that and simply present you a stripped down model:

<script src="http://calebporzio.com/livewire/livewire.js?id=36e5f3515222d88e5c4a"></script>
<script>
window.livewire = new Livewire();

doc.addEventListener("DOMContentLoaded", operate () {
    window.livewire.begin();
}
});
</script>

As you may see, Livewire masses its personal JavaScript, then initializes it when the web page is prepared.

The Web page Initialization

The cool factor about Livewire is even in case you have JavaScript disabled, the element renders in plain HTML. That is nice for getting content material on a web page immediately and making serps comfortable.

Now that the content material is on the web page and within the browser, let’s have a look at what occurs when Livewire’s JavaScript is initialized and the Livewire.begin() methodology known as.

Right here’s a bit of verbatim snippet of Livewire’s begin() operate in JavaScript:

begin() {
    DOM.rootComponentElementsWithNoParents().forEach(el => {
        this.parts.addComponent(new Part(el, this.connection))
    })

    ...
}

Livewire makes use of doc.querySelectorAll to get the foundation components of Livewire parts on the web page. It does this by in search of the presence of a [wire:id] attribute.

As soon as it finds them, it initializes them by passing them right into a devoted class constructor referred to as Part.

Every Livewire element on a web page has its personal occasion of the Part class in JavaScript reminiscence. If there was a god class in Livewire, it will be this.

When Part initializes, it extracts all of the metadata embedded within the HTML and shops it in reminiscence.

Extra particularly, it will get the contents of the [wire:initial-data] property that shipped with the web page (recall from a earlier snippet):

<div wire:id="44Njb4Yue0jBTzpzRlUf" wire:initial-data="...">

Right here’ s the precise JavaScript code from the constructor of Part that does this:

const initialData = JSON.parse(this.el.getAttribute('wire:initial-data'))
this.el.removeAttribute('wire:initial-data')

this.fingerprint = initialData.fingerprint
this.serverMemo = initialData.serverMemo
this.results = initialData.results

Now Livewire’s JavaScript is aware of the next details about the “counter” element:

  • It’s fingerprint (an object containing a element’s title, id, and so on…)
  • It’s serverMemo (persistent information concerning the element)
  • It’s results (unintended effects that ought to get run on web page load)

Perceive these three properties is important to understanding Livewire’s internal workings. We’ll revisit them later in additional element.

The ultimate stage of initialization is for Livewire to stroll by way of all of the DOM nodes in a element and search for any Livewire attributes.

In our case, we now have a button with a wire:click on attribute:

<button wire:click on=“increment”>Increment</button>

When Livewire sees this ingredient, it registers a click on occasion listener on it, with a handler that triggers an AJAX request to the server.

Earlier than we transfer onto that, a fast overview of the place we’re:

We now have HTML on the web page that appears like this:

<div wire:id="44Njb4Yue0jBTzpzRlUf">
    <h1>Depend: 0</h1>

    <button wire:click on="increment">Increment</button>
</div>

And in reminiscence, JavaScript has an occasion of the Part class with all the info we’d like about this Livewire element.

There may be additionally an occasion listener connected to the <button> ingredient now.

Let’s transfer on to the following massive idea: performing an replace. Which in our case appears to be like like clicking the button.

The Web page Replace

Relatively than strolling by way of all the things that occurs when the button is clicked, let’s simply have a look at the AJAX request that will get despatched to the server, and likewise the AJAX response that comes again.

We are able to speak extra particulars in a minute, nevertheless it is likely to be useful so that you can see this from an outside-in perspective:

AJAX Request

{
    "fingerprint": {
        "id": "44Njb4Yue0jBTzpzRlUf",
        "title": "counter",
        "locale": "en",
        "path": "",
        "methodology": "GET"
    },
    "serverMemo": {
        "kids": [],
        "errors": [],
        "htmlHash": "402ed05a",
        "information": {
            "depend": 0
        },
        "dataMeta": [],
        "checksum": "18a19f65fabc363e6b74d9c5a3338d6906a07f0281a3e91b4ebca491d5917702"
    },
    "updates": [
        {
            "type": "callMethod",
            "payload": {
                "id": "kwfdh",
                "method": "increment",
                "params": []
            }
        }
    ]
}

There’s a LOT occurring right here, so we’ll dedicate a complete part to this request object, however the vital factor to node is the serverMemo.information object and the updates array.

These are the 2 issues that ought to look intuitive to you.

AJAX Response

{
    "results": {
        "html": "<div wire:id="tkAIyMxrzcymYe2Z5OTq">n    <h1>Depend: 1</h1>n     n    <button wire:click on="increment">Increment</button>n</div>n",
        "soiled": [
            "count"
        ]
    },
    "serverMemo": {
        "htmlHash": "a7613101",
        "information": {
            "depend": 1
        },
        "checksum": "6e8f9599e47d3725f5470db6d38f8ee3d141214996576dca6720198d45a866a3"
    }
}

Now that the server has accomplished its factor, the response that comes again comprises the brand new HTML that ought to present up on the web page AND the brand new information represented in JSON.

Once more, I’ll dedicate a complete part to what’s occurring right here, however let’s begin with a deep dive on the request:

The Request

I’m unsure there’s any higher solution to method this half than simply explaining every particular person merchandise within the request payload. Let’s do it:

fingerprint

"fingerprint": {
    "id": "44Njb4Yue0jBTzpzRlUf",
    "title": "counter",
    "locale": "en",
    "path": "",
    "methodology": "GET"
},

That is information related to a element that makes it distinctive and offers important non-changing details about it.

Along with the title and id of the element, there’s additionally details about the locale of the appliance and details about the trail of the unique web page this element was loaded on.

This info is definitely a vital a part of Livewire’s safety system. When the server will get this information, it would search for the unique route of the web page load, extract any authentication middleware (and different middleware) and apply these to each subsequent request from this element.

This fashion, if a element is loaded on a web page with particular authorization, somebody can’t make an AJAX request to that element from a unique web page with out that authorization.

serverMemo

"serverMemo": {
    ...
}

The “server memo” is all information that DOES change all through a element’s lifecycle. This information is used to inform Livewire the way to “hydrate” or boot up our Counter.php element class as if it’s been operating within the backend this complete time.

There are heaps right here, however I’m solely going to cowl what’s related for this information:

serverMemo.information

"information": {
    "depend": 0
},

The info object is likely one of the most vital and clear items of knowledge we have to ship again to the server. That is how Livewire’s PHP facet is aware of it must set public $depend to 0 on the following request.

serverMemo.information

"dataMeta": [],

We aren’t utilizing dataMeta on this request, nevertheless it’s price mentioning. This array shops deeper details about information. For instance, we set the $depend property to an Eloquent Assortment, dataMeta would retailer a observe about that in order that JavaScript would simply see the info as a plain array, however PHP would no to “hydrate” it again into an Eloquent assortment for every request.

checksum

"checksum": "18a19f65fabc363e6b74d9c5a3338d6906a07f0281a3e91b4ebca491d5917702"

That is THE most vital safety characteristic in Livewire. Every element payload is signed with a secured checksum hash generated from the complete payload. This fashion if something tampers with the info used to ship again to the server, the backend will be capable of inform that and can throw an exception.

updates

"updates": [
    {
        "type": "callMethod",
        "payload": {
            "id": "kwfdh",
            "method": "increment",
            "params": []
        }
    }
]

“updates” is a listing of directions to carry out on the element within the backend. In our case, we’re calling the “increment” methodology. However that is the place any wire:mannequin updates or dispatched occasions would come by way of.

Phew, okay, now that we now have a little bit of context, let’s have a look at what occurs on the server when it receives this request payload:

Hydrating From The Request

Okay, It’s PHP time. I’m not going to put in writing out each single PHP operation that occurs in a Livewire request since you and I each would hate that.

As a substitute we’ll simply have a look at the highlights.

When the payload is available in from the browser, PHP creates a brand new Livewire Request object to retailer all the info:

<?php

namespace Livewire;

class Request
{
    public $fingerprint;
    public $updates;
    public $memo;

Now, with this information, Livewire fetches a uncooked occasion of the Livewire element in query:

$this->occasion = app('livewire')->getInstance($instance->request->title(), $instance->request->id());

$this->occasion is an occasion of the particular Counter.php Livewire class. Nonetheless, it hasn’t been “hydrated” but (full of all the info or “state” from the frontend)

The remainder of the magic occurs in a way inside Livewire’s service supplier (LivewireServiceProvider.php) referred to as: registerHydrationMiddleware(). Right here’s a style of what’s inside:

LifecycleManager::registerHydrationMiddleware([

    /* This is the core middleware stack of Livewire. It's important */
    /* to understand that the request goes through each class by the */
    /* order it is listed in this array, and is reversed on response */
    /*                                                               */
    /* ↓    Incoming Request                  Outgoing Response    ↑ */
    /* ↓                                                           ↑ */
    /* ↓    Secure Stuff                                           ↑ */
    /* ↓ */ SecureHydrationWithChecksum::class, /* --------------- ↑ */
    /* ↓ */ NormalizeServerMemoSansDataForJavaScript::class, /* -- ↑ */
    /* ↓ */ HashDataPropertiesForDirtyDetection::class, /* ------- ↑ */
    /* ↓                                                           ↑ */
    /* ↓    Hydrate Stuff                                          ↑ */
    /* ↓ */ HydratePublicProperties::class, /* ------------------- ↑ */
    /* ↓ */ CallPropertyHydrationHooks::class, /* ---------------- ↑ */
    /* ↓ */ CallHydrationHooks::class, /* ------------------------ ↑ */
    /* ↓                                                           ↑ */
    /* ↓    Update Stuff                                           ↑ */
    /* ↓ */ PerformDataBindingUpdates::class, /* ----------------- ↑ */
    /* ↓ */ PerformActionCalls::class, /* ------------------------ ↑ */
    /* ↓ */ PerformEventEmissions::class, /* --------------------- ↑ */
    /* ↓                                                           ↑ */
    /* ↓    Output Stuff                                           ↑ */
    /* ↓ */ RenderView::class, /* -------------------------------- ↑ */
    /* ↓ */ NormalizeComponentPropertiesForJavaScript::class, /* - ↑ */

]);

Simply so we’re clear, that is precise copy/pasted supply code above. My purpose was to exhibit the idea of “hydrating” and “dehydrating” immediately within the code.

You possibly can see by the code above that the request travels by way of these middlewares a method on the way in which in, after which within the reverse order on the way in which you.

To grasp how Livewire “hydrates” a element’s properties, we’ll take a fast peek inside HydratePublicProperties.

Listed below are a number of snippets yanked out to exhibit:

class HydratePublicProperties implements HydrationMiddleware
{
    use SerializesAndRestoresModelIdentifiers;

    public static operate hydrate($occasion, $request)
    {
        $publicProperties = $request->memo['data'] ?? [];
        ...
        foreach ($publicProperties as $property => $worth) {
            ...
            $instance->$property = $worth;
            ...
        }
    }

    ....
}

As you may see by the above code, after this “hydration middleware” runs, our Counter.php element class could have its properties set from the front-end state.

Calling The “increment” methodology

Now that our element is “hydrated” with the correct state, it’s time to truly name our “increment” methodology on it. That get’s dealt with within the PerformActionCalls::class middleware. Right here’s a stripped-down model to see it in motion:

class PerformActionCalls implements HydrationMiddleware
{
    public static operate hydrate($occasion, $request)
    {
        foreach ($request->updates as $replace) {
            ...
            $id = $replace['payload']['id'];
            $methodology = $replace['payload']['method'];
            $params = $replace['payload']['params'];
              ...
            $instance->callMethod($methodology, $params);
        }
    }
}

As you may see, every replace triggers ->callMethod on the element. The implementation of this methodology isn’t actually vital, as you may think about, it…nicely…calls that methodology on the category!

Render Time

Now that we’ve taken within the request, hydrated up the element, AND referred to as the tactic we meant to name, it’s time to render the contents of our element out to HTML then ship it again to the front-end.

On the finish of the hydration middleware stack, there’s one referred to as RenderView::class that’s accountable for rendering a element.

Relatively than strolling you thru each single PHP name, I’m going to only paste (and rework for simplicity) in related snippets so you may get the gist of how a Livewire element’s view will get rendered:

// Get the Blade view from the element.
$view = $this->render();
// Move all of it the general public properties as information (like $depend)
$view->with($this->getPublicPropertiesDefinedByClass());
// Render the Blade to plain HTML
$html = $view->render();
// Within the RenderView middleware, add the HTML to the response payload
data_set($response, 'results.html', $html);

After including the HTML to the response, Livewire has different middlewares that add extra issues into the response. One in every of them (HydratePublicProperties) will get the brand new public property values from the element and provides the info to the response payload.

Let’s check out the Response payload extra deeply earlier than speak front-end once more:

The Response

Right here is the total Response from earlier on this article so you may see the massive image:

AJAX Response

{
    "results": {
        "html": "<div wire:id="tkAIyMxrzcymYe2Z5OTq">n    <h1>Depend: 1</h1>n     n    <button wire:click on="increment">Increment</button>n</div>n",
        "soiled": [
            "count"
        ]
    },
    "serverMemo": {
        "htmlHash": "a7613101",
        "information": {
            "depend": 1
        },
        "checksum": "6e8f9599e47d3725f5470db6d38f8ee3d141214996576dca6720198d45a866a3"
    }
}

Most of this payload is self-explanatory. There are two ideas right here: “serverMemo” and “results”.

The “serverMemo” object is all the brand new “state” of the element till it goes again to the server for one more request. That is issues like the info.

Discover there’s a “checksum” included as nicely. This has been up to date for this new payload and might be handed to the backend on the following request for safety.

At this level, it’s additionally price noting that Livewire tries its greatest to solely ship the minimal quantity of knowledge mandatory. For instance, you’ll discover an “htmlHash” property.

This can be a hash of the HTML being despatched over. This fashion we will consider on the following request if the HTML is completely different and solely ship the total HTML if it’s modified. In any other case, we will save on the response payload dimension.

The identical goes with the info. Livewire will solely ship the info that’s completely different for every response. Consider it extra like a “diff” of the info.

Now that we’ve seen the total backend cycle, let’s have a look at what the front-end does with this info to show the quantity “0” to “1”.

Handing The Response In JS

When the response comes again, Livewire already is aware of the element that despatched it out, so it could match up the response with that element and let IT deal with the response.

There’s a methodology in JS on the element class referred to as handleResponse. Here’s a pattern of it to exhibit what it does:

handleResponse(message) {
    let response = message.response
    this.updateServerMemoFromResponseAndMergeBackIntoResponse(message)

    ...

    if (response.results.html) {
        ...
        this.handleMorph(response.results.html.trim())
    }
}

As you may see, the response comes again from the server and Livewire’s front-end element object updates itself with all the brand new information (from the “serverMemo”).

After it’s all synced up, it’s lastly time to govern the DOM of the particular web page. Turning the quantity “0” into the quantity “1”.

This occurs contained in the handleMorph() operate. Let’s speak about morphing HTML

Morphing The HTML

With a purpose to flip the “0” right into a “1” on the web page, we COULD simply change all of the HTML contained in the element with the brand new HTML from the server.

Nonetheless, it is a unhealthy concept for plenty of causes, primarily that it will wipe out any short-term state within the DOM like textual content in textual content inputs.

So as an alternative, we use a bundle referred to as “morphdom” to intelligently determine what elements of the particular DOM are completely different from the HTML from the server and ONLY manipulate the precise DOM within the locations there’s a mismatch.

This mechanism deserves a complete article by itself so I gained’t go into an excessive amount of element on it right here. Be at liberty to supply dive Livewire (that goes for all of this) to be taught extra concerning the internal workings.

After morphdom runs, the HTML is up to date to “1” and we’re accomplished!

Wrapping Up

Phew! What a experience.

I hope that was useful for these of you in search of deeper data with out studying and understanding each line of code in Livewire.

This text simply skimmed the floor. There are various extra deep mechanisms that come collectively to make Livewire work easily. Possibly they’ll be the subject of future articles, possibly not. However you may at all times see all of them by diving by way of the supply code your self.

Completely satisfied Livewireing!
Caleb


I ship out an e mail occasionally about cool stuff I am engaged on or launching. Should you dig, go forward and join!

RELATED ARTICLES

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Most Popular

Recent Comments