「Alpine.js」petite-vue 前瞻版本 – 现代网页技术中的 jQuery

这是我参与新手入门的第 3 篇文章


介绍

Your new, lightweight, JavaScript framework.

你的新型轻量级 JavaScript 框架。

  • 简单
  • 轻量
  • 如地狱一般强大

Alpine 是一个坚固的、小型的工具,用于在标记中直接组合行为。可以把它想象成现代网络中的 jQuery。输入一个脚本标签,然后开始。

Alpine 是 15 个指令、6 个属性和 2 个方法的集合。

要想了解 Alpine 是什么以及它能做什么,最好的办法就是亲自看看!


其实我更愿意把 Alpine.js 当做 Vue 的一个周边生态,因为它实在和 Vue 太像了,并且最新版本使用 @vue/reactivity 就像我说的,它简直就是 petite-vue 的前瞻版本,但是它却提供了一个很好的思路,至少对于我来说是有启发的,这点在我粗略阅读了它的源码之后更甚,我觉得学习一个库,或者单指轮子,我们不应该单纯学习并且拿来用就好了,我们应当学习它的思想,实现,以做到不变应万变,所以这期我选择先介绍 Alpine 的使用(虽然很像 Vue,但是仍然能获取到一些知识),后续分享我阅读源码的体会。


从零学习使用 Alpine.js

生命周期

Alpine.initializing

Alpine 加载之前

document.addEventListener('alpine:init', () => {
    Alpine.data(...)
})
复制代码

Alpine.initialized

Alpine 加载之后

document.addEventListener('alpine:initialized', () => {
    //
})
复制代码

15 个指令

x-data

在 Alpine 中,一切都从 x-data 开始。

x-data 将 HTML 块 定义成为组件,并为其提供响应式数据。

<div x-data="{ open: false }"></div>
复制代码

作用域

像 JS 一样,Alpine 中定义的数据也拥有自己的作用域,x-data 指令中定义的属性对其所有子元素都可用。

因为 x-data 会被当成 JavaScript 对象计算,所以在其中你可以定义方法甚至 getters

<div x-data="{ foo: 'bar' }">
    <span x-text="foo"><!-- Will output: "bar" --></span>

    <div x-data="{ bar: 'baz' }">
        <span x-text="foo"><!-- Will output: "bar" --></span>

        <div x-data="{ foo: 'bob' }">
            <span x-text="foo"><!-- Will output: "bob" --></span>
        </div>
    </div>
</div>
复制代码

无数据组件

有时,你想创建一个 Alpine 组件,但你不需要任何数据,在这种情况下,你总是可以传入一个空对象甚至不传:

<div x-data="{}"...

<div x-data...
复制代码

数据重用

详见 Alpine.data()

x-init

x-init 是在组件 DOM 初始化之前执行的函数。
下面的实例 1 会在初始化之前打印 I'm being initialized!,当然我们也可以在 x-init 中定义一些常见的初始化操作,例如向后端请求数据并赋值:

<div x-init="console.log('I\'m being initialized!')"></div>

<div
    x-data="{ posts: [] }"
    x-init="posts = await (await fetch('/posts')).json()"
>...</div>
复制代码

$nextTick

当然了有时候我们希望 x-init 能在渲染之后再执行,就像 react 中的 useEffect(..., []) 或者 Vue 中的 mount,使用 Alpine 中的 $nextTick 同样可以:

<div x-init="$nextTick(() => { ... })"></div>
复制代码

独立 x-init

您可以将 x-init 添加到 x-data 注册的组件外的任何元素。

<div x-data>
    <span x-init="console.log('I can initialize')"></span>
</div>

<span x-init="console.log('I can initialize too')"></span>
复制代码

自动执行的 init() 方法

如果你需要同时使用 x-datax-init,你可以在 x-data 中定义效果相同的 init() 函数:

<div
  x-data="{
    init() {
        console.log('I am called automatically')
    }
  }"
>
  ...
</div>
复制代码

对于使用 Alpine.data() 语法注册的组件也是如此。

Alpine.data('dropdown', () => ({
    init() {
        console.log('I will get evaluated when initializing each "dropdown" component.')
    },
}))
复制代码

x-show

x-show 是 Alpine 中最有用、最强大的指令之一。它提供了一种表达方式来显示和隐藏 DOM 元素。

<div x-data="{ open: false }">
    <button x-on:click="open = ! open">Toggle Dropdown</button>

    <div x-show="open">
        Dropdown Contents...
    </div>
</div>
复制代码

可使用 x-cloak 来避免初始化闪烁,详见 x-cloak

可搭配 x-transition 开启动画效果,详见 x-transition

x-bind

x-bind 允许您根据 JavaScript 表达式的结果设置元素的 HTML 属性并且缩写 :

<div x-data="{ placeholder: 'Type here...' }">
  <input type="text" x-bind:placeholder="placeholder">
</div>

<input type="text" :placeholder="placeholder">
复制代码

常用来绑定 class 或者 style,通常有字符串或者对象两种写法,不同于其他属性,class 采用合并而不是覆盖。

x-on

x-on 允许更容易地调度事件, 如果嫌麻烦你也可以使用缩写 @

<button x-on:click="alert('Hello World!')">Say Hi</button>

<button @click="alert('Hello World!')">Say Hi</button>
复制代码

$event

如果希望从表达式访问 JavaScript 事件对象,可以使用 Alpine 的 $event 属性。

此外,Alpine 还将事件对象传递给引用的任何方法。

<button @click="alert($event.target.getAttribute('message'))" message="Hello World">Say Hi</button>

<button @click="handleClick">...</button>

<script>
    function handleClick(e) {
        // Now you can access the event object (e) directly
    }
</script>
复制代码

键盘事件

Alpine 使得监听特定键上的 keydownkeyup 事件变得很容易。

<input type="text" @keyup.enter="alert('Submitted!')">
复制代码

这是一个监听器,它在按住 Shift 键并按下 Enter时运行,而不是单独按下 Enter 时运行。

<input type="text" @keyup.shift.enter="alert('Submitted!')">
复制代码

通过将 KeyboardEvent.key 公开的任何有效密钥名转换为 keybab-case,可以直接使用它们作为修饰符。

<input type="text" @keyup.page-down="alert('Submitted!')">
复制代码

为了便于参考,这里列出了您可能需要监听的常用键。

Modifier Keyboard Key
.shift Shift
.enter Enter
.space Space
.ctrl Ctrl
.cmd Cmd
.meta Cmd on Mac, Ctrl on Windows
.alt Alt
.up .down .left .right Up / Down / Left / Right arrows
.esc Escape
.tab Tab
.caps-lock Caps Lock

自定义事件

Alpine 事件侦听器是本地 DOM 事件侦听器的包装器。因此,它们可以监听任何 DOM 事件,包括自定义事件。

下面是一个组件的示例,它分派一个自定义 DOM 事件并侦听它。

<div x-data @foo="alert('Button Was Clicked!')">
  <button @click="$event.target.dispatchEvent(new CustomEvent('foo', { bubbles: true }))">...</button>
</div>
复制代码

修饰符

.prevent

.prevent 相当于在浏览器事件对象的侦听器中调用 .preventDefault()

.stop

.prevent 类似,.stop 相当于在浏览器事件对象的侦听器内部调用 .stoppropagation() 阻止事件传播冒泡。

.outside
<div x-data="{ open: false }">
  <button @click="open = ! open">Toggle</button>

  <div x-show="open" @click.outside="open = false">Contents...</div>
</div>
复制代码

在上面的例子中,通过单击 “Toggle” 按钮显示下拉列表内容后,您可以通过单击页面内容之外的任何地方来关闭。

这是因为 .outside 正在侦听不是来自它注册的元素的单击。

值得注意的是,.outside 表达式只有在它所注册的元素在页面上可见时才会被计算。

.window

.window 修饰符出现时,Alpine 将在页面的 窗口对象 而不是元素本身上注册事件监听器。

<div @keyup.escape.window="...">...</div>
复制代码
.document

.document 的工作方式类似于 .window,它只在文档全局中注册监听器,而不是在 window 全局中。

.once

使处理程序只被调用一次

.debounce

防抖

.throttle

节流

.self

通过将 .self 添加到事件侦听器中,可以确保事件源于声明它的元素,而不是源于子元素。

<button @click.self="handleClick">
  Click Me

  <img src="..." />
</button>
复制代码

在上面的例子中,我们在 <button> 标签中有一个 <img> 标签。通常,在 <button> 元素中产生的任何点击(例如 <img>)都会被按钮上的 @click 侦听器选中。但是,在本例中,因为我们添加了一个 .self,所以只有单击按钮本身才会调用 handleClick。只有源自 <img> 元素的点击将不会被处理。

.camel

有时您可能希望侦听驼峰大小写事件,例如我们示例中的 customEvent 。因为不支持在 HTML 属性中使用 camelCase,所以 Alpine 需要在内部添加 .camel 修饰符来使用 camelCase 事件名。通过添加 .camel, Alpine 现在监听的是 customEven t而不是 custom-event。

<div @custom-event.camel="handleCustomEvent">...</div>
复制代码
.passive

浏览器优化了页面上的滚动,使之快速平滑,即使在页面上执行 JavaScript 时也是如此。但是,未正确实现的触摸和滚轮侦听器可能会阻止此优化并导致网站性能不佳。如果您正在侦听触摸事件,则必须向侦听器中添加 .passive 以避免阻止滚动性能。

<div @touchstart.passive="...">...</div>
复制代码

x-text

x-text 将元素的文本内容设置为给定表达式的结果。

<div x-data="{ username: 'calebporzio' }">
  Username: <strong x-text="username"></strong>
</div>
复制代码

x-html

x-htmlinnerHTML 元素设置为给定表达式的结果。

⚠️ 仅用于受信任的内容,从不用于用户提供的内容。⚠️

⚠️ 从第三方动态呈现HTML很容易导致XSS漏洞。⚠️

<div x-data="{ username: '<strong>calebporzio</strong>' }">
  Username: <span x-html="username"></span>
</div>
复制代码

x-model

双向绑定

x-model 允许您将输入元素的值绑定到 Alpine 数据。

<div x-data="{ message: '' }">
  <input type="text" x-model="message">

  <span x-text="message">
</div>
复制代码

修饰符

.lazy

对于文本输入,默认情况下,x-model 每次击键都会更新属性。通过添加 .lazy 修饰符,您可以强制 x-model 输入仅在用户不关注输入元素时更新属性。

这对于像实时表单验证这样的事情非常方便,在用户“制表符”离开字段之前,你可能不希望显示输入验证错误。

<input type="text" x-model.lazy="username">
<span x-show="username.length > 20">The username is too long.</span>
复制代码
.number

默认情况下,通过 x-model 存储在属性中的任何数据都存储为字符串。要强制 Alpine 将值存储为 JavaScript 数字,请添加 .number 修饰符。

<input type="text" x-model.number="age">
<span x-text="typeof age"></span>
复制代码

.debounce & .throttle

防抖和节流

x-for

Alpine的 x-for 指令允许您通过遍历列表来创建 DOM 元素。下面是一个基于数组创建颜色列表的简单示例。

<ul x-data="{ colors: ['Red', 'Orange', 'Yellow'] }">
  <template x-for="(color, index) in colors">
    <li>
      <span x-text="index + ': '"></span>
      <span x-text="color"></span>
    </li>
  </template>
</ul>
复制代码

关于 x-for 有两个值得注意的规则:

  • x-for 必须在 <template> 元素上声明
  • <template> 元素 必须 只有一个根元素

keys

如果要重新排序项,为每个 x-for 迭代指定键值是很重要的。如果没有动态键,Alpine 可能很难跟踪重新排序的内容,并会导致奇怪的副作用。

<ul
  x-data="{ colors: [
    { id: 1, label: 'Red' },
    { id: 2, label: 'Orange' },
    { id: 3, label: 'Yellow' },
]}"
>
  <template x-for="color in colors" :key="color.id">
    <li x-text="color.label"></li>
  </template>
</ul>
复制代码

在一个范围内迭代

如果需要简单地循环 n 次,而不是遍历数组,Alpine 提供了一个简短的语法。

<ul>
  <template x-for="i in 10">
    <li x-text="i"></li>
  </template>
</ul>
复制代码

x-transition

Alpine 提供 x-transition 指令,在显示或隐藏元素之间创建平滑的过渡。

<div x-data="{ username: '<strong>calebporzio</strong>' }">
  Username: <span x-html="username"></span>
</div>
复制代码

Transition helper

提供一个简单的示例:

<div x-data="{ open: false }">
  <button @click="open = ! open">Toggle</button>

  <span x-show="open" x-transition>
      Hello ?
  </span>
</div>
复制代码
.duration

你可以用.duration修饰符为一个转换配置你想要的持续时间:

<div ... x-transition.duration.500ms>
复制代码

上面的 <div> 在进入时转换为 500 毫秒,离开时转换为 250 毫秒。

如果你想要分别指定显示和消失时间:

<div ...
  x-transition:enter.duration.500ms
  x-transition:leave.duration.400ms
>
复制代码
.delay

通过 .delay 修饰符设置延时:

<div ... x-transition.delay.50ms>
复制代码
.opacity & .scale

x-transtion 默认使用了缩放和透明度做渐入渐出效果,你可以通过 .opacity & .scale 设置单一的效果:

<!-- 只开启透明度效果 -->
<div ... x-transition.opacity>

<!-- 只开启缩放效果 -->
<div ... x-transition.scale>
复制代码

.scale 还可以配置其缩放值的特性:

<div ... x-transition.scale.80>
复制代码

上面的代码片段将把元素缩放 80%。

你当然也可以单独设置显示和消失的缩放值:

<div ...
  x-transition:enter.scale.80
  x-transition:leave.scale.90
>
复制代码
.origin

要自定义缩放转换的原点,你可以使用 .origin 修饰符:

<div ... x-transition.scale.origin.top | bottom | left | right >
复制代码

你也可以自由组合 .origin.top.right

x-effect

x-effect 用于在表达式的某个依赖项发生更改触发计算表达式。你可以将它视为一个监视程序,但是不必指定要监视的属性,它将监视其中使用的所有属性。

<div x-data="{ label: 'Hello' }" x-effect="console.log(label)">
  <button @click="label += ' World!'">Change Message</button>
</div>
复制代码

x-ignore

默认情况下,Alpine 会抓取 标记了 x-data 或者 x-init 的元素的整个 DOM 树,但是因为某种原因,你不希望这么做,那么你可以给元素标记 x-ignore

<div x-data="{ label: 'From Alpine' }">
  <div x-ignore>
      <span x-text="label"></span>
  </div>
</div>
复制代码

<span> 标签内将不会出现 From Alpine 文本。

x-ref

x-ref$refs 结合使用是一个非常有用的工具,可以方便地直接访问 DOM 元素。它最有用的是作为 getElementByIdquerySelector 等 api 的替代品。

<button @click="$refs.text.remove()">Remove Text</button>

<span x-ref="text">Hello ?</span>
复制代码

x-cloak

x-cloak 通过隐藏它所附加的元素来解决初始化显示问题,直到 Alpine 被完全加载到页面上。

但是,要想让 x-cloak 有效,您必须在页面中添加以下 CSS。

[x-cloak] { display: none !important; }
复制代码

现在,下面的示例将隐藏 <span> 标记,直到 Alpine 将其文本内容设置为 message 属性。

<span x-cloak x-text="message"></span>
复制代码

x-if

x-if 用来切换页面上的元素,类似于 x-show,但是它是完全添加或删除元素,而不是在 CSS display 属性上设置 hidden,由于行为上的这种差异,x-if 不应该直接应用于元素,而是应用于包含元素的 <template>。这样,一旦元素从页面中删除,Alpine 就可以保留它的记录。

<template x-if="open">
  <div>Contents...</div>
</template>
复制代码

x-show 不同,x-if 不支持使用 x-transition 进行切换。

6 个属性

$el

$el 是一个魔术属性,可用于检索当前 DOM 节点。

<button @click="$el.innerHTML = 'Hello World!'">Replace me with "Hello World!"</button>
复制代码

$refs

$refs 用来检索组件内部有 x-ref 标记的 DOM 元素。当你想要手动操作 DOM 元素时,这非常有用,这经常被用来简洁局部地代替 document.querySelector

<button @click="$refs.text.remove()">Remove Text</button>

<span x-ref="text">Hello ?</span>
复制代码

$store

详情请见 Alpine.data()

$watch

用来监视组件属性变化

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

获取旧的被替换的值

$watch 跟踪正在监视的属性的前一个值,您可以使用回调的可选第二个参数访问它,如下所示:

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

$dispatch

$dispatch 是一个调度浏览器事件的快捷方式。

<div @notify="alert('Hello World!')">
  <button @click="$dispatch('notify')">
      Notify
  </button>
</div>
复制代码

$dispatch 的底层是一个冗长的 API element.dispatchEvent(new CustomEvent(...))

关于事件传播的注意事项

注意,由于事件冒泡,当你需要捕获在相同嵌套层次结构下的节点分派的事件时,你需要使用 .window 修饰符:

<!-- ? Won't work -->
<div x-data>
<span @notify="..."></span>
<button @click="$dispatch('notify')">
<div>

<!-- ✅ Will work (because of .window) -->
<div x-data>
<span @notify.window="..."></span>
<button @click="$dispatch('notify')">
<div>
复制代码

第一个示例不起作用,因为当调度自定义事件时,它将传播到它的共同祖先 div,而不是它的兄弟 span。第二个示例起作用,因为兄弟节点在窗口级别侦听 notify,而自定义事件最终将跳转到该级别。

调度其他组件的事件

利用上述 .window 可以实现调度其他组件事件

<div
  x-data="{ title: 'Hello' }"
  @set-title.window="title = $event.detail"
>
  <h1 x-text="title"></h1>
</div>

<div x-data>
  <button @click="$dispatch('set-title', 'Hello World!')">...</button>
</di
<!-- When clicked, the content of the h1 will set to "Hello World!". -->
复制代码

调度 x-modal

您还可以使用 $dispatch() 来触发 x-model 数据绑定的数据更新。例如:

<div x-data="{ title: 'Hello' }">
  <span x-model="title">
      <button @click="$dispatch('input', 'Hello World!')">
      <!-- After the buttons pressed, `x-model` will catch the bubbling "input" event, and update title. -->
      <!-- 按下按钮后,`x-model` 将捕获冒泡的 "input" 事件,并更新标题。-->
  </span>
</div>
复制代码

这方便了定制输入组件,这些组件的值可以通过 x-model 设置。

$nextTick

$nextTick 允许你在 Alpine 进行了响应式 DOM 更新后执行给定的表达式。当你希望在 DOM 的任何数据更新之后与它进行交互时,这是非常有用的。

<div x-data="{ title: 'Hello' }">
  <button
    @click="
    title = 'Hello World!';
    $nextTick(() => { console.log($el.innerText) });
    "
    x-text="title"
  ></button>
</div>
复制代码

2 个方法

Alpine.data()

Alpine.data() 提供了在你的应用程序中重用 x-data 的解决办法

 <div x-data="dropdown">
   <button @click="toggle">...</button>

   <div x-show="open">...</div>
 </div>

 <script>
   document.addEventListener('alpine:init', () => {
     Alpine.data('dropdown', () => ({
       open: false,
       toggle() {
         this.open = !this.open
       }
     }))
   })
 </script>
复制代码

我们将直接定义在 x-data 中的属性提到了 Alpine 组件对象当中。

从包中注册

如果你选择构建你的 Alpine 代码,你可以参考如下步骤注册组件:

import Alpine from `alpinejs`
import dropdown from './dropdown.js'

Alpine.data('dropdown', dropdown)

Alpine.start()
复制代码
// dropdown.js
export default function () => ({
  open: false,

  toggle() {
      this.open = ! this.open
  }
})
复制代码

使用魔法属性

如果你在组件对象中访问魔法方法或属性,你可以使用 this 上下文:

Alpine.data('dropdown', () => ({
    open: false,

    init() {
        this.$watch('open', () => {...})
    }
}))
复制代码

x-bind 封装指令

<div x-data="dropdown">
    <button x-bind="trigger"></button>

    <div x-bind="dialogue"></div>
</div>
复制代码
Alpine.data('dropdown', () => ({
    open: false,

    trigger: {
        ['@click']() {
            this.open = ! this.open
        },
    },

    dialogue: {
        ['x-show']() {
            return this.open
        },
    },
}))
复制代码

Alpine.store()

Alpine 提供了 Alpine.store() API 用来全局管理状态。

使用 script 标签

<script>
    document.addEventListener('alpine:init', () => {
        Alpine.store('darkMode', {
            on: false,

            toggle() {
                this.on = ! this.on
            }
        })
    })
</script>
复制代码

使用包管理

import Alpine from 'alpinejs'

Alpine.store('darkMode', {
  on: false,

  toggle() {
    this.on = !this.on
  }
})

Alpine.start()
复制代码

获取 stores

Alpine 提供 $store 来获取全局对象:

<div x-data :class="$store.darkMode.on && 'bg-black'">...</div>

<button x-data @click="$store.darkMode.toggle()">Toggle Dark Mode</button>
复制代码

进阶

响应式

Alpine 是“响应性的”,在这种意义上,当您更改一段数据时,所有依赖于该数据的东西都会自动做出“反应”。在Alpine 中发生的每一点反应,都是因为 Alpine 的核心中有两个非常重要的反应函数:Alpine.reactive()Alpine.effect()

Alpine.reactive()

让我们先看看 Alpine.reactive()。这个函数接受一个 JavaScript 对象作为它的参数,并返回该对象的“响应”版本。例如:

let data = { count: 1 }

let reactiveData = Alpine.reactive(data)
复制代码

Alpine.effect()

Alpine.effect 接受单个回调函数。一旦 Alpine.effect 被调用,它将运行提供的函数,但会查找与响应数据的任何交互。如果它检测到一个交互(来自前面提到的代理的 get 或 set),它将持续追踪它,并确保在将来数据更改时重新运行回调。例如:

let data = Alpine.reactive({ count: 1 })

Alpine.effect(() => {
  console.log(data.count)
})
复制代码

继承

自定义指令

Alpine 允许你使用 Alpine.directive() API注册你自己的自定义指令。

方法签名
Alpine.directive('[name]', (el, { value, modifiers, expression }, { Alpine, effect, cleanup }) => {})
复制代码
parameter explaination
name The name of the directive. The name “foo” for example would be consumed as x-foo
el The DOM element the directive is added to
value If provided, the part of the directive after a colon. Ex: 'bar' in x-foo:bar
modifiers An array of dot-separated trailing additions to the directive. Ex: ['baz', 'lob'] from x-foo.baz.lob
expression The attribute value portion of the directive. Ex: law from x-foo="law"
Alpine The Alpine global object
effect A function to create reactive effects that will auto-cleanup after this directive is removed from the DOM
cleanup A function you can pass bespoke callbacks to that will run when this directive is removed from the DOM
简单的例子

下面是我们将要创建的一个简单指令的例子,名为 x-uppercase

Alpine.directive('uppercase', el => {
    el.textContent = el.textContent.toUpperCase()
})
复制代码
<div x-data>
    <span x-uppercase>Hello World!</span>
</div>
复制代码
计算表达式

注册自定义指令时,可能需要计算用户提供的 JavaScript 表达式:

<div x-data="{ message: 'Hello World!' }">
    <div x-log="message"></div>
</div>
复制代码

您需要检索消息的实际值,方法是使用 x-data 范围将其作为 JavaScript 表达式计算。幸运的是,Alpine 公开了一个 evaluate() API 计算 JavaScript表达式。

Alpine.directive('log', (el, { expression }, { evaluate }) => {
    // expression === 'message'

    console.log(
        evaluate(expression)
    )
})
复制代码
引入响应式

在前面的 x-log 示例的基础上,我们假设希望 x-log 记录 message 的值,如果值发生变化,也要记录它。

我们可以调整 x-lo g的实现,并引入两个新的 api 来实现这一点: evaluateLater()effect()

Alpine.directive('log', (el, { expression }, { evaluateLater, effect }) => {
    let getThingToLog = evaluateLater(expression)

    effect(() => {
        getThingToLog(thingToLog => {
            console.log(thingToLog)
        })
    })
})
复制代码

这里,我们不是立即计算消息并检索结果,而是将字符串表达式 message 转换为可以在任何时候运行的实际JavaScript 函数。如果要多次计算一个 JavaScrip t表达式,强烈建议首先生成一个 JavaScript 函数并使用它,而不是直接调用 evaluate()。原因是将普通字符串解释为 JavaScript 函数的过程开销很大,在不必要的时候应该避免。

Cleaning Up

可能出于某种原因你需要在自定义指令从元素上被移除时做些什么, Alpine 也提供了一个 cleanup() 方法供你使用:

Alpine.directive('...', (el, {}, { cleanup }) => {
    let handler = () => {}

    window.addEventListener('click', handler)

    cleanup(() => {
        window.removeEventListener('click', handler)
    })

})
复制代码

自定义 magics

Alpine 允许您使用 Alpine.magic() 注册自定义 “magics”(属性或方法)。您注册的任何 magic 都可以
在 Alipine 代码中以 $ 为前缀使用。

方法签名
Alpine.magic('[name]', (el, { Alpine }) => {})
复制代码
parameter explaination
name The name of the magic. The name “foo” for example would be consumed as $foo
el The DOM element the magic was triggered from
Alpine The Alpine global object
Magic 属性
Alpine.magic('now', () => {
    return (new Date).toLocaleTimeString()
})
复制代码
<span x-text="$now"></span>
复制代码
Magic 方法
Alpine.magic('clipboard', () => {
    return subject => navigator.clipboard.writeText(subject)
})
复制代码
<button @click="$clipboard('hello world')">Copy "Hello World"</button>
复制代码

编写和分享插件

您可以使用 Alpine 官方的”plugin-blueprint”包快速开始。复制仓库并运行 npm install && npm run build 就可以获得插件。

否则,让我们手工创建一个名为 Foo 的 Alpine 插件,它包含一个指令 (x-Foo) 和一个魔法属性 ($Foo)。

Script Include
<html>
  <script src="/js/foo.js" defer></script>
  <script src="/js/alpine.js" defer></script>

  <div x-data x-init="$foo()">
    <span x-foo="'hello world'">
  </div>
</html>
复制代码

注意我们的脚本是在 Alpine 之前引入的。这很重要,否则,Alpine 在我们的插件加载时就已经初始化了。

// /js/foo.js
document.addEventListener('alpine:init', () => {
  window.Alpine.directive('foo', ...)

  window.Alpine.magic('foo', ...)
})
复制代码
Bundle module
import Alpine from 'alpinejs'

import foo from 'foo'
Alpine.plugin(foo)

window.Alpine = Alpine
window.Alpine.start()
复制代码

你会注意到一个新的 API:Alpine.plugin()。这是 Alpine 公开的一个便捷方法。

// foo
export default function (Alpine) {
  Alpine.directive('foo', ...)
  Alpine.magic('foo', ...)
}
复制代码

Async

Alpine 以在大多数正常函数都支持异步函数情况构建的。

例如,假设您有一个名为 getLabel() 的简单函数,用作 x-text 指令的输入:

function getLabel() {
  return 'Hello World!'
}
复制代码
<span x-text="getLabel()"></span>
复制代码

因为 getLabel 是同步的,所以一切都按照预期工作。现在假设 getLabel 发出一个网络请求来检索标签,但是不能立即返回一个(异步的)。通过使 getLabel 成为一个异步函数,您可以使用 JavaScript 的 await 语法从 Alpine 调用它。

async function getLabel() {
  let response = await fetch('/api/label')

  return await response.text()
}
复制代码
<span x-text="await getLabel()"></span>
复制代码

此外,如果您喜欢在 Alpine 中调用没有末尾圆括号的方法,您可以省略它们,Alpine 将检测到提供的函数是异步的,并相应地处理它。例如:

<span x-text="getLabel"></span>
复制代码

CSP (Content-Security Policy)

为了使 Alpine 能够以 JavaScript 表达式的形式从 HTML 属性执行纯字符串,例如 x-on:click="console.log()",它需要依赖违反 “unsafe eval” 内容安全策略。

实际上,Alpine 本身并不使用 eval(),因为它速度慢而且有问题。相反,它使用了更好的函数声明,但仍然违反了”unsafe eval”。

为了适应需要这种 CSP 环境,Alpine 提供了一种替代构建,它不会违反 “unsafe-eval”,但具有更严格的语法。

安装

Script 标签
<html>
  <script src="alpinejs/alpinejs-csp/cdn.js" defer></script>
</html>
复制代码
模块导入
import Alpine from '@alpinejs/csp'

window.Alpine = Alpine
window.Alpine.start()
复制代码

限制

因为 Alpine 不能再将字符串解释为普通的 JavaScript,它必须手动解析和构造 JavaScript 函数。由于这个限制,您必须使用 Alpine.data 来注册 x-data 对象,并且必须仅通过键引用其中的属性和方法。

例如,像这样的内联组件将不能工作。

<!-- Bad -->
<div x-data="{ count: 1 }">
  <button @click="count++">Increment</button>

  <span x-text="count"></span>
</div>
复制代码

然而,如果将表达式分解为外部 api,以下内容对于 CSP 构建是有效的:

<!-- Good -->
<div x-data="counter">
  <button @click="increment">Increment</button>

  <span x-text="count"></span>
</div>
复制代码
Alpine.data('counter', () => ({
  count: 1,

  increment() {
    this.count++
  }
}))
复制代码

做一个新拟态计算器

截屏2021-07-11 12.41.13.png

<script defer src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"></script>

<div id="app" x-data="data" x-cloak>
  <div class="calculator">
    <div class="result" style="grid-area: result" x-text="equation"></div>

    <button style="grid-area: ac" @click="clear">AC</button>
    <button style="grid-area: plus-minus" @click="calculateToggle">±</button>
    <button style="grid-area: percent" @click="calculatePercentage">%</button>
    <button style="grid-area: add" @click="append('+')">+</button>
    <button style="grid-area: subtract" @click="append('-')">-</button>
    <button style="grid-area: multiply" @click="append('×')">×</button>
    <button style="grid-area: divide" @click="append('÷')">÷</button>
    <button style="grid-area: equal" @click="calculate">=</button>

    <button style="grid-area: number-1" @click="append(1)">1</button>
    <button style="grid-area: number-2" @click="append(2)">2</button>
    <button style="grid-area: number-3" @click="append(3)">3</button>
    <button style="grid-area: number-4" @click="append(4)">4</button>
    <button style="grid-area: number-5" @click="append(5)">5</button>
    <button style="grid-area: number-6" @click="append(6)">6</button>
    <button style="grid-area: number-7" @click="append(7)">7</button>
    <button style="grid-area: number-8" @click="append(8)">8</button>
    <button style="grid-area: number-9" @click="append(9)">9</button>
    <button style="grid-area: number-0" @click="append(0)">0</button>

    <button style="grid-area: dot" @click="append('.')">.</button>
  </div>
</div>
复制代码
[x-cloak] {
  display: none !important;
}

body {
  display: flex;
  justify-content: center;
  align-items: center;
  min-height: 100vh;
  background-color: #eee;
}

.calculator {
  --button-width: 80px;
  --button-height: 80px;

  display: grid;
  grid-template-areas:
    'result result result result'
    'ac plus-minus percent divide'
    'number-7 number-8 number-9 multiply'
    'number-4 number-5 number-6 subtract'
    'number-1 number-2 number-3 add'
    'number-0 number-0 dot equal';
  grid-template-columns: repeat(4, var(--button-width));
  grid-template-rows: repeat(6, var(--button-height));

  box-shadow: -8px -8px 16px -10px rgba(255, 255, 255, 1),
    8px 8px 16px -10px rgba(0, 0, 0, 0.15);
  padding: 24px;
  border-radius: 20px;
}

.calculator button {
  margin: 8px;
  padding: 0;
  border: 0;
  display: block;
  outline: none;
  border-radius: calc(var(--button-height) / 2);
  font-size: 24px;
  font-family: Helvetica;
  font-weight: normal;
  color: #999;
  background: linear-gradient(
    135deg,
    rgba(230, 230, 230, 1) 0%,
    rgba(246, 246, 246, 1) 100%
  );
  box-shadow: -4px -4px 10px -8px rgba(255, 255, 255, 1),
    4px 4px 10px -8px rgba(0, 0, 0, 0.3);
}

.calculator button:active {
  box-shadow: -4px -4px 10px -8px rgba(255, 255, 255, 1) inset,
    4px 4px 10px -8px rgba(0, 0, 0, 0.3) inset;
}

.result {
  text-align: right;
  line-height: var(--button-height);
  font-size: 48px;
  font-family: Helvetica;
  padding: 0 20px;
  color: #666;
}
复制代码
document.addEventListener('alpine:init', () => {
  Alpine.data('data', () => ({
    equation: '0',
    isDecimalAdded: false,
    isOperatorAdded: false,
    isStarted: false,
    isOperator(character) {
      return ['+', '-', '×', '÷'].indexOf(character) > -1
    },
    append(character) {
      if (this.equation === '0' && !this.isOperator(character)) {
        if (character === '.') {
          this.equation += '' + character
          this.isDecimalAdded = true
        } else {
          this.equation = '' + character
        }

        this.isStarted = true
        return
      }

      if (!this.isOperator(character)) {
        if (character === '.' && this.isDecimalAdded) {
          return
        }

        if (character === '.') {
          this.isDecimalAdded = true
          this.isOperatorAdded = true
        } else {
          this.isOperatorAdded = false
        }

        this.equation += '' + character
      }

      if (this.isOperator(character) && !this.isOperatorAdded) {
        this.equation += '' + character
        this.isDecimalAdded = false
        this.isOperatorAdded = true
      }
    },

    calculate() {
      let result = this.equation
        .replace(new RegExp('×', 'g'), '*')
        .replace(new RegExp('÷', 'g'), '/')

      this.equation = parseFloat(eval(result).toFixed(9)).toString()
      this.isDecimalAdded = false
      this.isOperatorAdded = false
    },

    calculateToggle() {
      if (this.isOperatorAdded || !this.isStarted) {
        return
      }

      this.equation = this.equation + '* -1'
      this.calculate()
    },

    calculatePercentage() {
      if (this.isOperatorAdded || !this.isStarted) {
        return
      }

      this.equation = this.equation + '* 0.01'
      this.calculate()
    },

    clear() {
      this.equation = '0'
      this.isDecimalAdded = false
      this.isOperatorAdded = false
      this.isStarted = false
    }
  }))
})
复制代码

新拟态学习自 CodingStartup

写在最后

源码分享,敬请期待。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享