Skip to content

专门为在现有的由服务器框架呈现的 HTML 页面上进行少量的交互准备的轻量级 JS 库。

License

Notifications You must be signed in to change notification settings

a112121788/pvue

Repository files navigation

Pvue

pvuevue 的优化的替代发行版。它提供了与标准 Vue 相同的模板语法和响应式模型, 不过它是专门为在现有的由服务器框架呈现的 HTML 页面上进行少量的交互准备的轻量级 JS 库。

  • 仅 ~8kb
  • Vue 兼容的模板语法
  • 基于 DOM, 原地变换
  • @vue/reactivity 驱动

快速上手

pvue 无需构建可使用,只需从 CDN 加载即可:

<script src="https://cdn.jsdelivr.net/gh/a112121788/pvue/dist/pvue.iife.js" defer init></script>

<div v-data="{ count: 0 }">
  {{ count }}
  <button @click="count++">inc</button>
</div>

<div v-data="{ open: false }">
  <button @click="open=!open">Toggle</button>
  <span v-show="open">
      Content...
    </span>
</div>
  • 使用 v-data 在页面上标记应该由 pvue 控制的区域。
  • defer 属性使脚本在解析完 HTML 内容后执行。
  • init 属性告诉 pvue 自动查询并初始化页面上所有有 v-data 的元素。

手动初始化

如果你不想要自动初始化,可以去掉 init 属性,将脚本移到 <body> 的末尾:

<script src="https://cdn.jsdelivr.net/gh/a112121788/pvue/dist/pvue.iife.js"></script>
<script>
  Pvue.createApp().mount()
</script>

生产环境 CDN 地址

  • 最新版: https://cdn.jsdelivr.net/gh/a112121788/pvue/dist/pvue.iife.js
  • 具体某个版本:https://cdn.jsdelivr.net/gh/a112121788/[email protected]/dist/pvue.iife.js

根域

createApp 函数接受作为所有表达式的根作用域的数据对象,这可以用来引导简单的一次性应用程序:

<script src="https://cdn.jsdelivr.net/gh/a112121788/pvue/dist/pvue.iife.js"></script>

<script>
  Pvue.createApp({
    // exposed to all expressions
    count: 0,
    // getters
    get plusOne() {
      return this.count + 1
    },
    // methods
    increment() {
      this.count++
    }
  }).mount()
</script>

<!-- v-data value can be omitted -->
<div v-data>
  <p>{{ count }}</p>
  <p>{{ plusOne }}</p>
  <button @click="increment">increment</button>
</div>

注:v-data 不需要取值,只是作为 pvue 处理元素的提示。

显式装载目标

你可以指定挂载目标 (选择器或元素),将 pvue 限制为页面的该区域:

Pvue.createApp().mount('#only-this-div')

这也意味着你可以在同一页面上有多个pvue应用来控制不同的地域:

Pvue.createApp({
  // root data for app one
}).mount('#app1')

Pvue.createApp({
  // root data for app two
}).mount('#app2')

生命周期事件

你可以监听每个元素特殊的 vue:mountedvue:unounted 生命周期事件:

<div
  v-if="show"
  @vue:mounted="console.log('mounted on: ', $el)"
  @vue:unmounted="console.log('unmounted: ', $el)"
></div>

v-effect

使用 v-effect 执行 reactive 内联语句:

<div v-data="{ count: 0 }">
  <div v-effect="$el.textContent = count"></div>
  <button @click="count++">++</button>
</div>

v-effect 使用 count,这是一个反应性的数据源,所以每当 count 发生变化时,它都会重新运行。

另一个示例:

<input v-effect="if (todo === editedTodo) $el.focus()" />

组件

pvue 中,"组件" 的概念是不同的,因为它更基本。

首先,可以使用函数创建可重用的作用域逻辑:

<script src="https://cdn.jsdelivr.net/gh/a112121788/pvue/dist/pvue.iife.js"></script>
<script>
  function Counter(props) {
    return {
      count: props.initialCount,
      inc() {
        this.count++
      },
      mounted() {
        console.log(`I'm mounted!`)
      }
    }
  }

  Pvue.createApp({
    Counter
  }).mount()
</script>

<div v-data="Counter({ initialCount: 1 })" @vue:mounted="mounted">
  <p>{{ count }}</p>
  <button @click="inc">increment</button>
</div>

<div v-data="Counter({ initialCount: 2 })">
  <p>{{ count }}</p>
  <button @click="inc">increment</button>
</div>

带模板的组件

如果你还想重用一个模板,可以在 data 对象上提供一个特殊的 $template 键。该值可以是模板字符串,也可以是 <template> 元素的 ID 选择符:

<script src="https://cdn.jsdelivr.net/gh/a112121788/pvue/dist/pvue.iife.js"></script>
<script type="module">
  function Counter(props) {
    return {
      $template: '#counter-template',
      count: props.initialCount,
      inc() {
        this.count++
      }
    }
  }

  Pvue.createApp({
    Counter
  }).mount()
</script>

<template id="counter-template">
  My count is {{ count }}
  <button @click="inc">++</button>
</template>

<!-- reuse it -->
<div v-data="Counter({ initialCount: 1 })"></div>
<div v-data="Counter({ initialCount: 2 })"></div>

与内联字符串相比,推荐使用 <template> 方法,因为从原生模板元素克隆会更高效。

全局状态管理

你可以使用 reactive 方法 (从 @vue/reactivity 重新导出) 创建全局单一状态:

<script src="https://cdn.jsdelivr.net/gh/a112121788/pvue/dist/pvue.iife.js"></script>
<script>
  const store = reactive({
    count: 0,
    inc() {
      this.count++
    }
  })

  // manipulate it here
  store.inc()

  Pvue.createApp({
    // share it with app datas
    store
  }).mount()
</script>

<div v-data="{ localCount: 0 }">
  <p>Global {{ store.count }}</p>
  <button @click="store.inc">increment</button>

  <p>Local {{ localCount }}</p>
  <button @click="localCount++">increment</button>
</div>

自定义指令

支持自定义指令,但具有不同的接口:

const myDirective = (ctx) => {
  // the element the directive is on
  ctx.el
  // the raw value expression
  // e.g. v-my-dir="x" then this would be "x"
  ctx.exp
  // v-my-dir:foo -> "foo"
  ctx.arg
  // v-my-dir.mod -> { mod: true }
  ctx.modifiers
  // evaluate the expression and get its value
  ctx.get()
  // evaluate arbitrary expression in current data
  ctx.get(`${ctx.exp} + 10`)

  // run reactive effect
  ctx.effect(() => {
    // this will re-run every time the get() value changes
    console.log(ctx.get())
  })

  return () => {
    // cleanup if the element is unmounted
  }
}

// register the directive
Pvue.createApp().directive('my-dir', myDirective).mount()

v-html 的实现方式如下:

const html = ({ el, get, effect }) => {
  effect(() => {
    el.innerHTML = get()
  })
}

自定义分隔符 (0.3+)

你可以通过向根作用域传递 $delimiters 来使用自定义分隔符。这在与同时使用 mustaches 的服务器端模板语言一起工作时非常有用:

createApp({
  $delimiters: ['${', '}']
}).mount()

使用插件

你可以编写自定义指令,然后将其作为包分发,然后将它添加到创建 vue 中,如:

<div v-data="{counter: 0}" v-log="inside petite-vue data">
  <button @click="counter++">increase</button>
</div>

<script type="module">
  import log from './log'
  import { createApp } from 'https://cdn.jsdelivr.net/gh/a112121788/pvue/dist/pvue.es.js'

  createApp().use(log).mount()
</script>

类似于 vue 插件代码的插件代码:

// inside log.js plugin file
export default {
  install: (app, options) => {
    app.directive('log', ({ exp }) => {
      console.log(exp)
    })
  }
}

示例

查看 示例目录.

特性

pvue

  • v-data
  • v-effect
  • @vue:mounted & @vue:unmounted events

不同的行为

  • 在表达式中,$el 指向指令绑定到的当前元素(而不是组件根元素)
  • createApp() 接受全局状态而不是组件
  • 组件被简化为对象返回函数
  • 自定义指令具有不同的接口

Vue 兼容

  • {{ }} 文本绑定(可配置自定义定界符)
  • v-bind(包括 : 速记和类/样式特殊处理)
  • v-on(包括 @ 速记和所有修饰语)
  • v-model(所有输入类型 + 非字符串 :value 绑定)
  • v-if / v-else / v-else-if
  • v-for
  • v-show
  • v-html
  • v-text
  • v-pre
  • v-once
  • v-cloak
  • reactive()
  • nextTick()
  • refs

不支持特性

一些特征被丢弃,因为它们在渐进增强的上下文中具有相对低的效用/大小比。如果你需要这些功能,你可能只需要使用标准的 Vue。

  • ref(), computed() 等。
  • Render 函数 (pvue 没有虚拟 dom)
  • 收集类型的反应性(Map、Set 等,因尺寸较小而删除)
  • 过渡、保活、传送、悬念
  • v-for 深度解构
  • v-on="object"
  • v-is & <component :is="xxx">
  • v-bind:style 自动前缀

与标准 Vue 的差别

“pvue” 的意义不仅仅在于小。它是关于使用预期用例的最佳实现(渐进增强)。

标准 Vue 可以在有或没有构建步骤的情况下使用。当使用构建设置时(例如使用单文件组件),我们预编译所有模板,因此在运行时不需要进行模板处理。 多亏了 tree-shaking,我们可以在标准 Vue 中提供可选功能,在不使用时不会使你的捆绑包膨胀。 这是标准 Vue 的最佳使用,但由于它涉及构建设置,因此更适合于构建交互相对频繁的 SPA 或应用程序。

当使用标准 Vue 而不需要构建步骤并安装到 DOM 模板中时,它的最佳性要低得多,因为:

  • 我们必须将 Vue 模板编译器发送到浏览器(额外大小为 13kb)
  • 编译器必须从已经实例化的 DOM 中检索模板字符串
  • 然后,编译器将字符串编译为 JavaScript 呈现函数
  • 然后,Vue 用渲染函数生成的新 DOM 替换现有 DOM 模板。

pvue 通过遍历现有 DOM 并将细粒度的反应性效果直接附加到元素来避免所有这些开销。DOM 是模板。这意味着“pvue”在渐进增强场景中更有效。

这也是 Vue 1 的工作方式。这里的权衡是,这种方法与 DOM 耦合,因此不适用于平台无关的渲染或 JavaScript SSR。 我们还失去了处理高级抽象的呈现函数的能力。然而,正如你可能知道的那样,在渐进增强的环境中很少需要这些功能。

与 Alpine 的差别

pvue is indeed addressing a similar data to Alpine, but aims to be (1) even more minimal and (2) more Vue-compatible.

pvue 确实解决了与 Alpine 类似的范围,但其目的是(1)更加最小化和(2)更加兼容 Vue。

  • pvue 大约是 Alpine 的一半。

  • pvue 没有 transition 系统(也许这可以是一个选择加入插件)。

  • 虽然 Alpine 在很大程度上类似于 Vue 的设计,但在许多情况下,其行为与 Vue 本身不同。在未来,它也可能与 Vue 的差异更大。 这是好的,因为 Alpine 不应该限制其设计严格遵循 Vue,它应该有自由在一个对其目标有意义的方向上发展。

相比之下,pvue将尽可能与标准 Vue 行为保持一致,以便在需要时减少移动到标准 Vue 的摩擦。 它旨在成为 Vue 生态系统的一部分**以涵盖标准 Vue 目前优化程度较低的渐进增强用例。

安全和 CSP

pvue计算模板中的 JavaScript 表达式。这意味着如果pvue安装在 DOM 中包含来自用户数据的未经清理的 HTML 的区域上, 则可能会导致 XSS 攻击,如果你的页面呈现用户提交的 HTML,你应该更喜欢使用 显式装载目标初始化 pvue, 以便它只处理由你控制的部分。你还可以为v-data属性清理任何用户提交的 HTML。

pvue 使用 new Function()计算表达式,这在严格的 CSP 设置中可能是禁止的。 没有计划提供一个 CSP 构建,因为它涉及到一个表达式解析器,这违背了轻量级的目的。如果你有严格的 CSP 要求,你可能应该使用标准 Vue 并预编译模板。

版权

MIT

About

专门为在现有的由服务器框架呈现的 HTML 页面上进行少量的交互准备的轻量级 JS 库。

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published