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 earlier
tailwindcss
andI wrote an article about it. whilealpine.js
The tagline is “Write JS like tailwindCSS”, whiletailwindcss
Is alsoapline.js
Sponsors of. - After the complete separation of the front and back ends, server-side rendering became a hot topic again. All but the most mainstream
SSR
In addition to the (server-side rendering + front-end hydration) solution, different solutions have emerged for different scenarios, for exampleJAM
andTALL.TALL
isLaravel
The main push of a fast full stack development program, isTailwindCSS
,Alpine.js
,Laravel
andLivewire
Is an acronym for. - The last reason, because in
react
andvue
He was part of a team that had developed something similar before the firealpine.js
Library, 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.