How Livewire works (a deep dive)

The experience of using Livewire seems magical. It’s as if your front-end HTML can call your PHP code and everything just works.

A lot goes into making this magic happen. Let me show you what’s going on:

Our Example

For this writeup, we’re going to use the example of a simple counter component. Here’s what the Livewire component looks like:

Counter.php

<?php

namespace App\Http\Livewire;

class Counter extends \Livewire\Component
{
    public $count = 0;

    public function increment()
    {
        $this->count++;
    }

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

counter.blade.php

<div>
    <h1>Count: {{ $count }}</h1>

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

In this example, when we load the page we see “Count: 0“ and when we hit the “Increment” button, the “0” magically turns into a “1”.

Now that we’ve established something concrete, let’s talk about how Livewire makes this happen.

The Initial Render

Let’s say we want to include this Livewire component in a completely standard blade view. Here’s how that would look:

<html>
    <livewire:counter />

    @livewireScripts
</html>

When you load this page, Laravel processes this Blade file like any other, except that Livewire does some hackery to get Blade to convert <livewire:counter /> into: @livewire('counter').

In this simple case, the @livewire('counter') directive compiles down to code that looks like this: echo \Livewire\Livewire::mount('counter')->html();

Before we dig into what’s happening in mount() we’ll just stay outside and look at the result of that call:

<div wire:id="44Njb4Yue0jBTzpzRlUf" wire:initial-data="{&quot;fingerprint&quot;:{&quot;id&quot;:&quot;44Njb4Yue0jBTzpzRlUf&quot;,&quot;name&quot;:&quot;counter&quot;,&quot;locale&quot;:&quot;en&quot;,&quot;path&quot;:&quot;test&quot;,&quot;method&quot;:&quot;GET&quot;},&quot;effects&quot;:{&quot;listeners&quot;:[]},&quot;serverMemo&quot;:{&quot;children&quot;:[],&quot;errors&quot;:[],&quot;htmlHash&quot;:&quot;402ed05a&quot;,&quot;data&quot;:{&quot;count&quot;:0},&quot;dataMeta&quot;:[],&quot;checksum&quot;:&quot;1dc6e1fbb14c1a5cf8c138bb6b09dd99493dee38a9a89a8133901d1d38f40eac&quot;}}">
    <h1>Count: 0</h1>

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

As you can see, Livewire renders the component as you’d expect, but it also pumps the HTML with lots of metadata for its own internal purposes.

We’ll dig into this in the next section, but while we’re here let’s also briefly look at what @livewireScripts does.

Essentially this Blade directive compiles down to two <script> tags that load all the JavaScript Livewire needs to function.

I’ll leave out all the unnecessary details of that and just show you a stripped down version:

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

document.addEventListener("DOMContentLoaded", function () {
    window.livewire.start();
}
});
</script>

As you can see, Livewire loads its own JavaScript, then initializes it when the page is ready.

The Page Initialization

The cool thing about Livewire is even if you have JavaScript disabled, the component renders in plain HTML. This is great for getting content on a page instantly and making search engines happy.

Now that the content is on the page and in the browser, let’s look at what happens when Livewire’s JavaScript is initialized and the Livewire.start() method is called.

Here’s a little verbatim snippet of Livewire’s start() function in JavaScript:

start() {
    DOM.rootComponentElementsWithNoParents().forEach(el => {
        this.components.addComponent(new Component(el, this.connection))
    })

    ...
}

Livewire uses document.querySelectorAll to get the root elements of Livewire components on the page. It does this by looking for the presence of a [wire:id] attribute.

Once it finds them, it initializes them by passing them into a dedicated class constructor called Component.

Each Livewire component on a page has its own instance of the Component class in JavaScript memory. If there was a god class in Livewire, it would be this.

When Component initializes, it extracts all the metadata embedded in the HTML and stores it in memory.

More specifically, it gets the contents of the [wire:initial-data] property that shipped with the page (recall from a previous snippet):

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

Here’ s the actual JavaScript code from the constructor of Component 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.effects = initialData.effects

Now Livewire’s JavaScript knows the following information about the “counter” component:

  • It’s fingerprint (an object containing a component’s name, id, etc…)
  • It’s serverMemo (persistent data about the component)
  • It’s effects (side effects that should get run on page load)

Understand these three properties is essential to understanding Livewire’s inner workings. We will revisit them later in more detail.

The final stage of initialization is for Livewire to walk through all the DOM nodes in a component and look for any Livewire attributes.

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

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

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

Before we move onto that, a quick review of where we are:

We have HTML on the page that looks like this:

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

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

And in memory, JavaScript has an instance of the Component class with all the data we need about this Livewire component.

There is also an event listener attached to the <button> element now.

Let’s move on to the next big concept: performing an update. Which in our case looks like clicking the button.

The Page Update

Rather than walking through everything that happens when the button is clicked, let’s just look at the AJAX request that gets sent to the server, and also the AJAX response that comes back.

We can talk more details in a minute, but it might be helpful for you to see this from an outside-in perspective:

AJAX Request

{
    "fingerprint": {
        "id": "44Njb4Yue0jBTzpzRlUf",
        "name": "counter",
        "locale": "en",
        "path": "",
        "method": "GET"
    },
    "serverMemo": {
        "children": [],
        "errors": [],
        "htmlHash": "402ed05a",
        "data": {
            "count": 0
        },
        "dataMeta": [],
        "checksum": "18a19f65fabc363e6b74d9c5a3338d6906a07f0281a3e91b4ebca491d5917702"
    },
    "updates": [
        {
            "type": "callMethod",
            "payload": {
                "id": "kwfdh",
                "method": "increment",
                "params": []
            }
        }
    ]
}

There’s a LOT going on here, so we’ll dedicate a whole section to this request object, but the important thing to node is the serverMemo.data object and the updates array.

These are the two things that should look intuitive to you.

AJAX Response

{
    "effects": {
        "html": "<div wire:id=\"tkAIyMxrzcymYe2Z5OTq\">\n    <h1>Count: 1<\/h1>\n     \n    <button wire:click=\"increment\">Increment<\/button>\n<\/div>\n",
        "dirty": [
            "count"
        ]
    },
    "serverMemo": {
        "htmlHash": "a7613101",
        "data": {
            "count": 1
        },
        "checksum": "6e8f9599e47d3725f5470db6d38f8ee3d141214996576dca6720198d45a866a3"
    }
}

Now that the server has done its thing, the response that comes back contains the new HTML that should show up on the page AND the new data represented in JSON.

Again, I’ll dedicate an entire section to what’s going on here, but let’s start with a deep dive on the request:

The Request

I’m not sure there’s any better way to approach this part than just explaining each individual item in the request payload. Let’s do it:

fingerprint

"fingerprint": {
    "id": "44Njb4Yue0jBTzpzRlUf",
    "name": "counter",
    "locale": "en",
    "path": "",
    "method": "GET"
},

This is data associated with a component that makes it unique and provides essential non-changing information about it.

In addition to the name and id of the component, there is also information about the locale of the application and information about the path of the original page this component was loaded on.

This information is actually an essential part of Livewire’s security system. When the server gets this data, it will look up the original route of the page load, extract any authentication middleware (and other middleware) and apply those to every subsequent request from this component.

This way, if a component is loaded on a page with special authorization, someone can’t make an AJAX request to that component from a different page without that authorization.

serverMemo

"serverMemo": {
    ...
}

The “server memo” is all data that DOES change throughout a component’s lifecycle. This data is used to tell Livewire how to “hydrate” or boot up our Counter.php component class as if it’s been running in the backend this whole time.

There are lots here, but I’m only going to cover what’s relevant for this guide:

serverMemo.data

"data": {
    "count": 0
},

The data object is one of the most important and clear pieces of data we need to send back to the server. This is how Livewire’s PHP side knows it needs to set public $count to 0 on the next request.

serverMemo.data

"dataMeta": [],

We aren’t using dataMeta on this request, but it’s worth mentioning. This array stores deeper information about data. For example, we set the $count property to an Eloquent Collection, dataMeta would store a note about that so that JavaScript would just see the data as a plain array, but PHP would no to “hydrate” it back into an Eloquent collection for each request.

checksum

"checksum": "18a19f65fabc363e6b74d9c5a3338d6906a07f0281a3e91b4ebca491d5917702"

This is THE most important security feature in Livewire. Each component payload is signed with a secured checksum hash generated from the entire payload. This way if anything tampers with the data used to send back to the server, the backend will be able to tell that and will throw an exception.

updates

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

“updates” is a list of instructions to perform on the component in the backend. In our case, we’re calling the “increment” method. But this is where any wire:model updates or dispatched events would come through.

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

Hydrating From The Request

Ok, It’s PHP time. I’m not going to write out every single PHP operation that happens in a Livewire request because you and I both would hate that.

Instead we’ll just look at the highlights.

When the payload comes in from the browser, PHP creates a new Livewire Request object to store all the data:

<?php

namespace Livewire;

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

Now, with this data, Livewire fetches a raw instance of the Livewire component in question:

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

$this->instance is an instance of the actual Counter.php Livewire class. However, it hasn’t been “hydrated” yet (filled with all the data or “state” from the frontend)

The rest of the magic happens in a method inside Livewire’s service provider (LivewireServiceProvider.php) called: registerHydrationMiddleware(). Here’s a taste 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, /* - ↑ */

]);

Just so we’re clear, this is actual copy/pasted source code above. My goal was to demonstrate the concept of “hydrating” and “dehydrating” directly in the code.

You can see by the code above that the request travels through these middlewares one way on the way in, and then in the reverse order on the way you.

To understand how Livewire “hydrates” a component’s properties, we’ll take a quick peek inside HydratePublicProperties.

Here are a few snippets yanked out to demonstrate:

class HydratePublicProperties implements HydrationMiddleware
{
    use SerializesAndRestoresModelIdentifiers;

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

    ....
}

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

Calling The “increment” method

Now that our component is “hydrated” with the proper state, it’s time to actually call our “increment” method on it. That get’s handled in the PerformActionCalls::class middleware. Here’s a stripped-down version to see it in action:

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

As you can see, each update triggers ->callMethod on the component. The implementation of this method isn’t really important, as you can imagine, it…well…calls that method on the class!

Render Time

Now that we’ve taken in the request, hydrated up the component, AND called the method we intended to call, it’s time to render the contents of our component out to HTML then send it back to the front-end.

At the end of the hydration middleware stack, there is one called RenderView::class that is in charge of rendering a component.

Rather than walking you through every single PHP call, I’m going to just paste (and rework for simplicity) in relevant snippets so you can get the gist of how a Livewire component’s view gets rendered:

// Get the Blade view from the component.
$view = $this->render();
// Pass it all the public properties as data (like $count)
$view->with($this->getPublicPropertiesDefinedByClass());
// Render the Blade to plain HTML
$html = $view->render();
// In the RenderView middleware, add the HTML to the response payload
data_set($response, 'effects.html', $html);

After adding the HTML to the response, Livewire has other middlewares that add more things into the response. One of them (HydratePublicProperties) gets the new public property values from the component and adds the data to the response payload.

Let’s take a look at the Response payload more deeply before talk front-end again:

The Response

Here is the full Response from earlier in this article so you can see the big picture:

AJAX Response

{
    "effects": {
        "html": "<div wire:id=\"tkAIyMxrzcymYe2Z5OTq\">\n    <h1>Count: 1<\/h1>\n     \n    <button wire:click=\"increment\">Increment<\/button>\n<\/div>\n",
        "dirty": [
            "count"
        ]
    },
    "serverMemo": {
        "htmlHash": "a7613101",
        "data": {
            "count": 1
        },
        "checksum": "6e8f9599e47d3725f5470db6d38f8ee3d141214996576dca6720198d45a866a3"
    }
}

Most of this payload is self-explanatory. There are two concepts here: “serverMemo” and “effects”.

The “serverMemo” object is all the new “state” of the component until it goes back to the server for another request. This is things like the data.

Notice there is a “checksum” included as well. This has been updated for this new payload and will be passed to the backend on the next request for security.

At this point, it’s also worth noting that Livewire tries its best to only send the minimum amount of data necessary. For example, you’ll notice an “htmlHash” property.

This is a hash of the HTML being sent over. This way we can evaluate on the next request if the HTML is different and only send the full HTML if it’s changed. Otherwise, we can save on the response payload size.

The same goes with the data. Livewire will only send the data that is different for each response. Think of it more like a “diff” of the data.

Now that we’ve seen the full backend cycle, let’s look at what the front-end does with this information to turn the number “0” to “1”.

Handing The Response In JS

When the response comes back, Livewire already knows the component that sent it out, so it can match up the response with that component and let IT handle the response.

There is a method in JS on the component class called handleResponse. Here is a sample of it to demonstrate what it does:

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

    ...

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

As you can see, the response comes back from the server and Livewire’s front-end component object updates itself with all the new data (from the “serverMemo”).

After it’s all synced up, it’s finally time to manipulate the DOM of the actual page. Turning the number “0” into the number “1”.

This happens inside the handleMorph() function. Let’s talk about morphing HTML

Morphing The HTML

In order to turn the “0” into a “1” on the page, we COULD just replace all the HTML inside the component with the new HTML from the server.

However, this is a bad idea for lots of reasons, mainly that it would wipe out any temporary state in the DOM like text in text inputs.

So instead, we use a package called “morphdom” to intelligently figure out what parts of the actual DOM are different from the HTML from the server and ONLY manipulate the actual DOM in the places there is a mismatch.

This mechanism deserves an entire article on its own so I won’t go into too much detail on it here. Feel free to source dive Livewire (that goes for all of this) to learn more about the inner workings.

After morphdom runs, the HTML is updated to “1” and we are done!

Wrapping Up

Phew! What a ride.

I hope that was helpful for those of you looking for deeper knowledge without reading and understanding every line of code in Livewire.

This article just skimmed the surface. There are many more deep mechanisms that come together to make Livewire work smoothly. Maybe they’ll be the topic of future articles, maybe not. But you can always see all of them by diving through the source code yourself.

Happy Livewireing! Caleb

 

via https://calebporzio.com/how-livewire-works-a-deep-dive

How Livewire works (a deep dive)
标签: