Hello, everyone Today a brief introduction to alpine. Js use and principle.

Why did you want to introduce Alpine. Js? There are several reasons for this:

  • I just touched it earliertailwindcssandI wrote an article about it. whilealpine.jsThe tagline is “Write JS like tailwindCSS”, whiletailwindcssIs alsoapline.jsSponsors of.
  • After the complete separation of the front and back ends, server-side rendering became a hot topic again. All but the most mainstreamSSRIn addition to the (server-side rendering + front-end hydration) solution, different solutions have emerged for different scenarios, for exampleJAMandTALL.TALLisLaravelThe main push of a fast full stack development program, isTailwindCSS,Alpine.js,LaravelandLivewireIs an acronym for.
  • The last reason, because inreactandvueHe was part of a team that had developed something similar before the firealpine.jsLibrary, unavoidably some affinity.

Introduction to the

Alpine. Js provides responsive and declarative component authoring at a much lower cost than larger frameworks like React or Vue

Write js like tailwindcss

These two sentences in alpine. Js’s official profile are enough to sum up the differences between alpine and the current mainstream front-end frameworks. Apline.js is all about being light and fast.

TALL is a traditional back-end rendering mechanism, which is mainly targeted at PHP developers. Different from SSR’s cross-end components, TALL completes page rendering with the traditional back-end template mechanism, and the front end provides interaction through Alpine. Js. As an important front end of the technology stack, alpine. Js’s lightness and low cost of learning are pluses.

Alpine. Js requires no installation, eliminates the cost of learning webpack, YARN and the like, and vUe-like syntax is easy to use. To keep alpine. Js light, alpine has chosen a few different implementations, such as not relying on the virtual DOM, templates parsed by traversing the DOM, and so on, which are covered later in this article.

Using apline

Begin to use

Normally, we just need to import alpine. Js on the page:

<script src="https://cdn.jsdelivr.net/gh/alpinejs/[email protected]/dist/alpine.js" defer></script>
Copy the code

Here’s a simple example:

<div x-data="{ open: false }">
  <button @click="open = true">Open Dropdown</button>
  <ul
      x-show="open"
      @click.away="open = false"
  >
    Dropdown Body
  </ul>
</div>
Copy the code

Write this code in our HTML, and alpine. Js will initialize the page as a component after it has loaded. Yes, we implemented a simple component with almost no additional JS writing required.

Directives (Directives)

Alpine. Js provides different directives, but here are a few:

x-data

Provides initial data for the component. Alpine. Js defines component boundaries with this property. A DOM element with an X-data directive will be compiled into a component.

In addition to the inlined initial data, we can also call functions:

// html <div x-data="{ ... dropdown(), ... anotherMixin()}"> <button x-on:click="open">Open</button> <div x-show="isOpen()" x-on:click.away="close"> // Dropdown </div> </div> // js function dropdown() { return { show: false, open() { this.show = true }, close() { this.show = false }, isOpen() { return this.show === true }, } }Copy the code

x-init

X-init is mounted for Vue.

x-init="() => { // we have access to the post-dom-initialization state here // }"
Copy the code

x-bind

Used to bind properties, for example:

x-bind:class="{ 'hidden': myFlag }"

// x-bind:disabled="myFlag"
Copy the code

x-on

Event listening also supports the x-on: and @ forms and provides modifiers such as self, prevent, and away:

x-on:click="foo = 'bar'"
@input.debounce.750="fetchSomething()"
Copy the code

x-model

Similar to v – model:

<input type="text" x-model="foo">
<input x-model.number="age">
<input x-model.debounce="search">
Copy the code

x-ref

To get the DOM element:

<div x-ref="foo"></div><button x-on:click="$refs.foo.innerText = 'bar'"></button>
Copy the code

x-for

A single root component must be wrapped with the Template tag:

<template x-for="item in items" :key="item">
  <div x-text="item"></div>
</template>
Copy the code

x-spread

Similar to JSX {… Props}

// html
<div x-data="dropdown()">
  <button x-spread="trigger">Open Dropdown</button>
  <span x-spread="dialogue">Dropdown Contents</span>
</div>

// js
function dropdown() {
  return {
    open: false,
    trigger: {
      ['@click']() {
        this.open = true
      },
    },
    dialogue: {
      ['x-show']() {
        return this.open
      },
      ['@click.away']() {
        this.open = false
      },
    }
  }
}
Copy the code

x-cloak

The X-Cloak property is removed after the component is initialized, so you can add the following CSS to make the DOM element with the property display only after initialization:

[x-cloak] {
    display: none ! important;
}
Copy the code

The magic properties

In inline code, alpine. Js provides a few attributes to assist us with the functionality

$el

To get the root element of the component:

<div x-data>
  <button @click="$el.innerHTML = 'foo'">Replace me with "foo"</button>
</div>
Copy the code

$refs

To get child elements

$event

DOM events:

<input x-on:input="alert($event.target.value)">
Copy the code

$dispatch

Emit custom events:

<div @custom-event="console.log($event.detail.foo)">
  <button @click="$dispatch('custom-event', { foo: 'bar' })">
</div>
Copy the code

$nextTick

Execute the code after alpine. Js updates the DOM:

div x-data="{ fruit: 'apple' }">
  <button
    x-on:click="
      fruit = 'pear';
      $nextTick(() => { console.log($event.target.innerText) });
    "
    x-text="fruit"
  ></button>
</div>
Copy the code

$watch

Observe component data changes:

<div x-data="{ open: false }" x-init="$watch('open', value => console.log(value))">
    <button @click="open = ! open">Toggle Open</button>
</div>
Copy the code

Apline code analysis

Finally, a brief look at the source code of apline.js. As a library of less than 2,000 lines, alpine. Js code structure and flow are fairly straightforward.

If you’ve had a rough idea of how Vue or any other framework works, the following should be familiar.

Initialize the

Listen for the DOMContentLoaded event:

function domReady() { return new Promise(resolve => { if (document.readyState == "loading") { document.addEventListener("DOMContentLoaded", resolve); } else { resolve(); }}); }Copy the code

Traverse all DOM nodes that contain the X-data attribute:

discoverComponents: function discoverComponents(callback) {
  const rootEls = document.querySelectorAll('[x-data]');
  rootEls.forEach(rootEl => {
    callback(rootEl);
  });
},
Copy the code

And initialize the Component (Component class) :

initializeComponent: function initializeComponent(el) { // ... el.__x = new Component(el); / /... } / /... class Component { constructor(el, componentForClone = null) { // ... }}Copy the code

Responsive data

Initialize the data and use getAttribute to get the value of the X-data attribute:

const dataAttr = this.$el.getAttribute('x-data');
const dataExpression = dataAttr === '' ? '{}' : dataAttr;

this.unobservedData = componentForClone ? componentForClone.getUnobservedData() : saferEval(el, dataExpression, dataExtras);
Copy the code

SaferEval uses new Function to execute expressions to initialize data:

function saferEval(el, expression, dataContext, additionalHelperVariables = {}) { return tryCatch(() => { if (typeof expression === 'function') { return expression.call(dataContext); } return new Function(['$data', ...Object.keys(additionalHelperVariables)], `var __alpine_result; with($data) { __alpine_result = ${expression} }; return __alpine_result`)(dataContext, ... Object.values(additionalHelperVariables)); }, { el, expression }); }Copy the code

And then there’s the reactive principle. There are two main classes involved: ReactiveMembrane and ReactiveProxyHandler.

A component contains an instance of ReactiveMembrane that receives valueMutated callbacks in its constructor:

function wrap(data, mutationCallback) { let membrane = new ReactiveMembrane({ valueMutated(target, key) { mutationCallback(target, key); }}); / /... }Copy the code

When the data is changed, the valueMutated callback is called, which calls the component’s updateElements method to update the DOM (here debounce is used so that multiple synchronized data changes can be performed together) :

wrapDataInObservable(data) {
  var self = this;
  let updateDom = debounce(function () {
    self.updateElements(self.$el);
  }, 0);
  return wrap(data, (target, key) => {
    // ...
    updateDom();
  });
}
Copy the code

The ReactiveProxyHandler constructor takes two arguments, one a data object and the other an instance of ReactiveMembrane:

class ReactiveProxyHandler {
  constructor(membrane, value) {
      this.originalTarget = value;
      this.membrane = membrane;
  }
  // ...
}
Copy the code

Alpine. Js is proxy-based, with the ReactiveProxyHandler instance as the second argument to the Proxy constructor (using lazy initialization techniques) :

get reactive() {
    const reactiveHandler = new ReactiveProxyHandler(membrane, distortedValue);
    // caching the reactive proxy after the first time it is accessed
    const proxy = new Proxy(createShadowTarget(distortedValue), reactiveHandler);
    registerProxy(proxy, value);
    ObjectDefineProperty(this, 'reactive', { value: proxy });
    return proxy;
},
Copy the code

When reading values from nested objects or arrays, you need to recursively create ReactiveProxyHandler instances and bind them to the same membrane:

get(shadowTarget, key) {
  const { originalTarget, membrane } = this;
  const value = originalTarget[key];
  // ...
  return membrane.getProxy(value);
}
Copy the code

When data is modified, the valueMutated method of membrane is called, and DOM is finally updated:

set(shadowTarget, key, value) { const { originalTarget, membrane: { valueMutated } } = this; const oldValue = originalTarget[key]; if (oldValue ! == value) { originalTarget[key] = value; valueMutated(originalTarget, key); } else if (key === 'length' && isArray(originalTarget)) { valueMutated(originalTarget, key); } return true; }Copy the code

DOM rendering

Alpine. Js template parsing is done by traversing the DOM tree and element node attributes, and updating the DOM is not done through mechanisms such as the virtual DOM, but by modifying the DOM directly.

Traversing the DOM

DOM initialization and updating both require traversing from the root element of the component, determining if the element traversed is a nested component, creating the corresponding component if it is, initializing/updating the DOM element if it is not, and finally cleaning up the callback added by $nextTick:

initializeElements(rootEl, extraVars = () => {}) { this.walkAndSkipNestedComponents(rootEl, el => { // ... this.initializeElement(el, extraVars); }, el => { el.__x = new Component(el); }); this.executeAndClearRemainingShowDirectiveStack(); this.executeAndClearNextTickStack(rootEl); } updateElements(rootEl, extraVars = () => {}) { this.walkAndSkipNestedComponents(rootEl, el => { // ... this.updateElement(el, extraVars); }, el => { el.__x = new Component(el); }); this.executeAndClearRemainingShowDirectiveStack(); this.executeAndClearNextTickStack(rootEl); } walkAndSkipNestedComponents(el, callback, initializeComponentCallback = () => {}) { walk(el, el => { // We've hit a component. if (el.hasAttribute('x-data')) { // If it's not the current one. if (! el.isSameNode(this.$el)) { // Initialize it if it's not. if (! el.__x) initializeComponentCallback(el); // Now we'll let that sub-component deal with itself. return false; } } return callback(el); }); }Copy the code

The walk method traverses the DOM tree with firstElementChild and nextElementSibling:

function walk(el, callback) { if (callback(el) === false) return; let node = el.firstElementChild; while (node) { walk(node, callback); node = node.nextElementSibling; }}Copy the code

The difference between component initialization and update is that initialization requires fetching and processing the element’s original class, as well as binding events:

initializeElement(el, extraVars) {
  // To support class attribute merging, we have to know what the element's
  // original class attribute looked like for reference.
  if (el.hasAttribute('class') && getXAttrs(el, this).length > 0) {
    el.__x_original_classes = convertClassStringToArray(el.getAttribute('class'));
  }

  this.registerListeners(el, extraVars);
  this.resolveBoundAttributes(el, true, extraVars);
}

updateElement(el, extraVars) {
  this.resolveBoundAttributes(el, false, extraVars);
}
Copy the code

In the registerListeners and resolveBoundAttributes methods, the attributes of the element are traversed and processed by the corresponding instructions.

instruction

on

RegisterListeners are called for event binding if the notice is on:

case 'on':
  registerListener(this, el, value, modifiers, expression, extraVars);
  break;
Copy the code

In registerListener, a variety of processing is required depending on the modifier. Forget about the modifiers here, let’s see what happens to @click=”open =! What happens to this simple piece of code called open.

For the above scenario, registerListener can be simplified to:

function registerListener(component, el, event, modifiers, expression, extraVars = {}) {
  let handler = e => {
    runListenerHandler(component, expression, e, extraVars);
  };

  el.addEventListener(event, handler);
}
Copy the code

Here the modifiers are empty arrays, event is click, EL is the DOM element to which the event is currently bound, and Express is the string open =! The open.

The event is first bound via addEventListener, then the expression is executed in the event callback, and finally the saferEvalNoReturn method is called:

function saferEvalNoReturn(el, expression, dataContext, additionalHelperVariables = {}) { return tryCatch(() => { // ... return Promise.resolve(new AsyncFunction(['dataContext', ...Object.keys(additionalHelperVariables)], `with(dataContext) { ${expression} }`)(dataContext, ... Object.values(additionalHelperVariables))); }, { el, expression }); }Copy the code

Here, with sets the context to the component data that has already been proxyed, so when expression modifies the data, the component triggers rerendering.

model

If it is x-Model, the registerModelListener method is called:

case 'model':
  registerModelListener(this, el, modifiers, expression, extraVars);
  break;
Copy the code

We need to determine the event that corresponds to the DOM element type (input, radio, etc.) and how to value from the event, and also add a listener via registerListener:

function registerModelListener(component, el, modifiers, expression, extraVars) {
  var event = el.tagName.toLowerCase() === 'select' || ['checkbox', 'radio'].includes(el.type) || modifiers.includes('lazy') ? 'change' : 'input';
  const listenerExpression = `${expression} = rightSideOfExpression($event, ${expression})`;
  registerListener(component, el, event, modifiers, listenerExpression, () => {
    return _objectSpread2(_objectSpread2({}, extraVars()), {}, {
      rightSideOfExpression: generateModelAssignmentFunction(el, modifiers, expression)
    });
  });
}
Copy the code

This extra parameter rightSideOfExpression is passed in through registerListener extraVars so that listenerExpression here gets the modified value properly.

GenerateModelAssignmentFunction to different input element type line, to get the value correctly. For input, for example, the event.target.value modifier is applied:

const rawValue = event.target.value;
return modifiers.includes('number') ? safeParseNumber(rawValue) : modifiers.includes('trim') ? rawValue.trim() : rawValue;
Copy the code

text

RegisterListeners process both X-on and X-Model instructions, while others are handled by resolveBoundAttributes.

For example, x – text:

case 'text':
  var output = this.evaluateReturnExpression(el, expression, extraVars);
  handleTextDirective(el, output, expression);
  break;
Copy the code

EvaluateReturnExpression is called to get the result of the expression’s execution, followed by a call to handleTextDirective to set the element’s textContent:

function handleTextDirective(el, output, expression) {
  if (output === undefined && expression.match(/\./)) {
    output = '';
  }

  el.textContent = output;
}
Copy the code

for

HandleForDirective handles x-for directives:

case 'for':
  handleForDirective(this, el, expression, initialUpdate, extraVars);
  break;
Copy the code

In handleForDirective, we first parse the expression to get the target array, subscript, and name of the current value to iterate over:

let iteratorNames = typeof expression === 'function' ? parseForExpression(component.evaluateReturnExpression(templateEl, expression)) : parseForExpression(expression);
let items = evaluateItemsAndReturnEmptyIfXIfIsPresentAndFalseOnElement(component, templateEl, iteratorNames, extraVars);
Copy the code

We then iterate through the array, trying to reuse elements based on key, and if we find reusable elements, we call updateElements to update them. Otherwise, create a new element and initialize it with a templateEl:

let currentEl = templateEl; items.forEach((item, index) => { let iterationScopeVariables = getIterationScopeVariables(iteratorNames, item, index, items, extraVars()); let currentKey = generateKeyForIteration(component, templateEl, index, iterationScopeVariables); let nextEl = lookAheadForMatchingKeyedElementAndMoveItIfFound(currentEl.nextElementSibling, currentKey); // If we haven't found a matching key, insert the element at the current position. if (! nextEl) { nextEl = addElementInLoopAfterCurrentEl(templateEl, currentEl); // And transition it in if it's not the first page load. transitionIn(nextEl, () => {}, () => {}, component, initialUpdate); nextEl.__x_for = iterationScopeVariables; component.initializeElements(nextEl, () => nextEl.__x_for); // Otherwise update the element we found. } else { // Temporarily remove the key indicator to allow the normal "updateElements" to work. delete nextEl.__x_for_key; nextEl.__x_for = iterationScopeVariables; component.updateElements(nextEl, () => nextEl.__x_for); } currentEl = nextEl; currentEl.__x_for_key = currentKey; }); removeAnyLeftOverElementsFromPreviousUpdate(currentEl, component);Copy the code

Other instructions are not introduced, the logic is relatively simple, intuitive.

summary

The above is alpine. Js use and principle of a brief introduction. While Webpack, less/SASS/CSS in JS, the Big Three frameworks, SSR and so on have become mainstream technology stacks at the front end, there are still some tools and technology stacks with different ideas, which is interesting to know.