最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • alpine.js使用及原理简介

    正文概述 掘金(笨笨小撒)   2021-02-19   1479

    大家好。今天简单介绍下alpine.js的使用和原理。

    为什么会想到介绍alpine.js呢?有以下几个原因:

    • 此前刚接触了tailwindcss并写了篇文章做了简单介绍。而alpine.js的标语则是“像写tailwindcss一样写js”,同时tailwindcss也是apline.js的赞助者。
    • 前后端在经过彻底的分离之后,服务端渲染再次成为热门议题。除了最主流的SSR(服务端渲染+前端水合)方案之外,也出现了适合不同场景的不同方案,例如JAM和TALL。TALLLaravel主推的一套快速的全栈开发方案,是TailwindCSSAlpine.jsLaravelLivewire的首字母缩写。
    • 最后一个原因,因为在reactvue大火之前,自己所在的团队也曾经开发过类似alpine.js的库,难免有些亲切感。

    简介

    alpine.js官方的这两句简介足以概括其与当前主流前端框架的不同之处。apline.js主打的就是轻和快。

    TALL是传统的后端渲染机制,面对的用户也是以php开发者为主。不同于SSR的跨端组件,TALL以传统的后端模板机制完成页面渲染,前端再通过alpine.js提供交互。作为这套技术栈中前端重要的一环,轻量级、学习成本低,都是alpine.js的加分项。

    alpine.js无需安装,免去了webpackyarn之类的学习成本,类似vue的语法也非常容易上手。为了保持轻巧,alpine.js选择了一些不同的实现方式,例如不依赖虚拟 DOM,模板通过遍历 DOM 来解析等等,这些会在文章后半部分介绍。

    使用apline

    开始使用

    通常,我们只需在页面上引入alpine.js就可以了:

    <script src="https://cdn.jsdelivr.net/gh/alpinejs/alpine@v2.8.0/dist/alpine.js" defer></script>
    

    然后来看一个简单的例子:

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

    在我们的 HTML 中编写这段代码,alpine.js会在页面加载完成之后,将其初始化为组件。没错,我们几乎不需要额外写任何 JS,就实现了一个简单的组件。

    指令(Directives)

    alpine.js通过提供不同的指令,这里简单介绍几个:

    x-data

    提供组件的初始数据。alpine.js正是通过这个属性来界定组件边界的,一个带有x-data指令的 dom 元素会被编译成一个组件。

    除了内联的初始数据,我们也能调用函数:

    // 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 },
      }
    }
    

    x-init

    可以把x-init想象成 Vue 的mounted

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

    x-bind

    用来绑定属性,例如:

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

    x-on

    事件侦听,同样支持x-on:@两种形式,以及提供了例如selfpreventaway等修饰符:

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

    x-model

    类似v-model

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

    x-ref

    用来获取 dom 元素:

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

    x-for

    必须以template标签包裹单个根组件:

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

    x-spread

    类似 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
          },
        }
      }
    }
    

    x-cloak

    x-cloak属性会在组件初始化后被移除,因此可以添加以下css使得有这个属性的 DOM 元素在初始化后才展示:

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

    魔术属性

    在内联代码中,alpine.js提供了一些属性来协助我们完成功能

    $el

    用来获取组件的根元素:

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

    $refs

    用以获取子元素

    $event

    DOM 事件:

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

    $dispatch

    发出自定义事件:

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

    $nextTick

    alpine.js更新 DOM 后执行代码:

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

    $watch

    观察组件数据变化:

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

    apline代码浅析

    最后简单来看下apline.js的源码。作为一个总共不到2000行的库,alpine.js代码结构和流程可以说是比较清晰明了的。

    如果你大致了解过Vue或其他框架的原理,那么以下内容都是比较熟悉的了。

    初始化

    监听DOMContentLoaded事件:

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

    遍历所有包含x-data属性的 DOM 节点:

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

    并初始化组件(Component类):

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

    响应式数据

    首先初始化数据,使用getAttribute获取到x-data属性的值:

    const dataAttr = this.$el.getAttribute('x-data');
    const dataExpression = dataAttr === '' ? '{}' : dataAttr;
    
    this.unobservedData = componentForClone ? componentForClone.getUnobservedData() : saferEval(el, dataExpression, dataExtras);
    

    saferEval使用new Function来执行表达式以初始化数据:

    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
      });
    }
    

    接着,是响应式原理。这里主要涉及两个类:ReactiveMembraneReactiveProxyHandler

    一个组件包含一个ReactiveMembrane实例,其构造函数中将接收valueMutated回调:

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

    当数据被改动时,会调用valueMutated回调,从而调用组件的updateElements方法,更新 DOM(这里通过debounce来使得多个同步的数据修改可以一起被执行):

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

    ReactiveProxyHandler构造函数接收两个参数,一个是数据对象,另一个则是一个ReactiveMembrane实例:

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

    alpine.js的响应式是基于Proxy的,ReactiveProxyHandler实例会作为Proxy构造函数的第二个参数(这里还使用了懒初始化的技巧):

    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;
    },
    

    当从嵌套的对象、数组中读取值时,需要递归的创建ReactiveProxyHandler实例,绑定到同一个membrane上:

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

    当修改数据时,membranevalueMutated方法被调用,并最终更新 DOM:

    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;
    }
    

    DOM 渲染

    alpine.js的模板解析过程是通过遍历 DOM 树和元素节点的属性来完成的,更新时也不通过虚拟 DOM 这样的机制,而是直接修改 DOM。

    遍历 DOM

    DOM 的初始化和更新都需要从组件的根元素开始遍历,对遍历到的元素判断其是否为嵌套的组件,如果是则创建对应组件,如果不是则初始化/更新 DOM 元素,并在最后清理$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);
      });
    }
    

    walk方法通过firstElementChildnextElementSibling来遍历 DOM 树:

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

    组件的初始化和更新的不同之处在于,初始化需要获取并处理元素原本的 class,以及绑定事件:

    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);
    }
    

    registerListenersresolveBoundAttributes方法中,将遍历元素的属性,并通过对应的指令进行处理。

    指令

    on

    registerListeners中,如果判断指令为on,则调用registerListener进行事件绑定:

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

    registerListener中,需要根据不同的修饰符进行各种处理。这里就先不管这些修饰符,看下对于@click="open = !open"这段简单的代码会发生什么。

    对于以上场景,可以将registerListener简化为:

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

    这里modifiers修饰符为空数组,event就是clickel就是当前要绑定事件的 DOM 元素,express就是字符串open = !open

    首先通过addEventListener绑定事件,然后在事件回调中执行表达式,最终调用的是saferEvalNoReturn方法:

    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
      });
    }
    

    这里通过with将上下文设置为已经用Proxy代理过的组件数据,因此当expression对数据进行修改时,组件会触发重新渲染。

    model

    如果是x-model,则调用registerModelListener方法:

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

    需要判断 DOM 元素类型(如inputradio等)所对应的事件,以及如何从事件取值,并同样通过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)
        });
      });
    }
    

    这里通过registerListenerextraVars传入额外的参数rightSideOfExpression,来使得这里的listenerExpression可以正确的获取修改后的值。

    generateModelAssignmentFunction对不同输入元素类型就行判断,以正确获取值。例如对于input,就是对event.target.value再根据修饰符处理一下:

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

    text

    registerListeners处理了x-onx-model两种指令,其他的则在resolveBoundAttributes中处理。

    例如x-text

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

    这里先调用evaluateReturnExpression获取表达式的执行结果,随后调用handleTextDirective设置元素的textContent

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

    for

    handleForDirective用来处理x-for指令:

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

    handleForDirective中,首先需要解析表达式,获取遍历的目标数组、下标和当前值的名称:

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

    随后,遍历数组,根据key尝试重用元素,如果找到可重用的元素,就调用updateElements对其更新;否则,通过模板(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);
    

    其他的指令就不一一介绍了,逻辑也比较简单、直观。

    小结

    以上就是alpine.js使用和原理的一个简单介绍。在webpack、less / sass / css in js、三大框架、SSR等等成为前端主流技术栈的大潮下,还是出现了一些有着不同理念的工具和技术栈,了解一下也是挺有趣的。


    起源地下载网 » alpine.js使用及原理简介

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元