前端进阶之路Web前端之路让前端飞

Riot - 你的下一个框架

2017-11-23  本文已影响141人  果汁凉茶丶

前端,你是文艺界的程序员

我为什么要用Riot

 优点明显,体积小,加载快,继承了react,Polymer等框架的优势,并简化他们,主要从以下几个方面考虑:

1. 自定义标签。

 Riot 在所有浏览器上支持自定义标签,我们能将页面组件化,一个自定义标签结构如下所示:

<todo>
  <!--布局-->
  <h3>{ item }</h3>

  <!-- css -->
  <style scoped>
    h3 { font-size: 14px; }
  </style>

  <!-- 业务逻辑 -->
  <script>
    var self = this;
    self.item = ‘Riot sample’;
  </script>
</todo>
2. 对阅读友好

 有了自定义标签功能后,我们可以用很简洁语言‘拼凑’出复杂的用户界面,加上语义化标签定义,在阅读的时候,很容易看清楚哪个标签是给html加入了什么组件,一共有多少个组件,这样一个页面大致实现什么功能,甚至不用看浏览器展示都能明白。你的代码可能是这样的

<body>
  <h1>Acme community</h1>
  <forum-header/>
  <forum-content>
    <forum-threads/>
    <forum-sidebar/>
  </forum-content>
  <forum-footer/>
  <script>riot.mount('*', { api: forum_api })</script>
</body>

 很清楚,页面被分成四三个模块,一个标题h1,一个header,一个content和一个footer,这样就在我们脑海构成一个基础的网页模型
 Riot 标签首先被 编译 成标准 JavaScript,然后在浏览器中运行。

3. 虚拟 DOM
4. 与标准保持一致

5. 友好的语法

(1).强大的简写语法

class={ enabled: is_enabled, hidden: hasError() }

(2).语义化强,不需要费脑记忆

render, state, constructor 或 shouldComponentUpdate

(3).直接插值

Add #{ items.length + 1 } 或 class="item { selected: flag }"

(4).用 <script> 标签来包含逻辑代码不是必需的
(5).紧凑的 ES6 方法定义语法

6. 麻雀小,五脏全

最小化是 Riot 区别于其它库的重要特点,它所提供的 API 方法比其他库要少 10 至 100倍。
react.min.js – 34.89KB
polymer.min.js – 49.38KB
riot.min.js – 10.38KB

Riot 拥有创建现代客户端应用的所有必需的成分:


学前三两个FAQ

1. 标签名中必须使用横线 (-) 吗?

 W3C 规范要求在标签名中使用横线。所以 <person> 需要写成 <my-person>. 如果你关心 W3C 的话就遵循这个规则. 其实两者都能跑

2. 为什么源码中没有分号?

 不写分号使代码更整洁。这与我们的整体的最小化哲学是一致的。同样的原因,我们使用单引号,也建议你在使用riot时不要使用分号和双引号。

3. 为什么使用 == 运算符?

 运算符本身没有好坏之分,如果你知道它的工作原理,巧妙的使用能简化你的代码,如node.nodeValue = value == null ? '' : value 这将导致 0false 被显示而 nullundefined 显示为空字符串。这正是我们想要的!

4. 使用 onclick?

 onclick只是比较‘过时’。将JS和HTML放在同一个模块里比美学重要。Riot的最小化语法使事件处理器看起来很象样儿。


开始学习 Riot

一、自定义标签

1. 直观感受

Riot 自定义标签是构建用户界面的单元。它们构成了应用的”视图”部分。我们先从一个实现TODO应用的例子来感受一下Riot的各个功能:(自定义标签会被 编译 成 JavaScript.)

<todo>
  <h3>{ opts.title }</h3>
  
  <ul>
    <li each={ items }>
      <label class={ completed: done }>
        <input type="checkbox" checked={ done } onclick={ parent.toggle }> { title }
      </label>
    </li>
  </ul>
  
  <form onsubmit={ add }>
    <input name="input" onkeyup={ edit }>
    <button disabled={ !text }>Add #{ items.length + 1 }</button>
  </form>

  <script>
    this.disabled = true
    this.items = opts.items
    edit(e) {
      this.text = e.target.value
    }
    add(e) {
      if (this.text) {
        this.items.push({ title: this.text })
        this.text = this.input.value = ''
      }
    }
    toggle(e) {
      var item = e.item
      item.done = !item.done
      return true
    }
  </script>
</todo>
2. 标签语法

Riot标签是布局(HTML)与逻辑(JavaScript)的组合。以下是基本的规则:

注意: 自定义标签文件里的标签定义总是从某一行的行首开始,前面不能放空格。内置标签定义(定义在 document body中) 必须正确缩进,所有的自定义标签拥有相同的最低级的缩进级别, 不建议将tab与空格混合使用

<!--正确-->
<t-tag>
</t-tag>

<!--正确-->
<t-tag></t-tag>

<!--错误 没从行首开始-->
  <t-tag>
  </t-tag>
3. 省略 script 标签

 可以省略 <script> 标签,如果这么做,逻辑会认为从最后一个 HTML 标签结束处开始

<todo>
  <!-- 布局 -->
  <h3>{ opts.title }</h3>
  // 逻辑
  this.items = [1, 2, 3]
</todo>
4. 预处理

 可以使用 type 属性来指定预处理器. 现在可选的type值包括“coffee”, “typescript”, “es6”“none”. 也可以为 language 加上 “text/” 前缀, 如 “text/coffee”

<script type="coffee">
  # coffeescript 标签逻辑
</script>
5. 标签 css

 在标签内部可以放置一个 style 标签,Riot.js 会自动将它提取出来并放到 <head> 部分,因为放到了head部分,所以其他文件也能调用到该样式

<todo>
  <!-- 布局 -->
  <h3>{ opts.title }</h3>

  <style>
    todo { display: block }
    todo h3 { font-size: 120% }
    /** 本标签其它专有的css **/
  </style>
</todo>
6. 局部 CSS

 支持局部 CSS

<todo>
  <!-- 布局 -->
  <h3>{ opts.title }</h3>

  <style scoped>
    :scope { display: block }
    h3 { font-size: 120% }
    /** 本标签其它专有的css **/
  </style>
</todo>

 css的提取和移动只执行一次,无论此自定义标签被初始化多少次。 为了能够方便地覆盖CSS,你可以指定Riot在<head>中的哪个位置插入标签所定义的css:

<style type="riot"></style>

 例如,在某些场景下可以指定将riot组件库的标签css放在normalize.css后面,而放在网站的整体主题CSS之前,这样可以覆盖组件库的默认风格。

二、自定义标签的加载

1. 直观体验

 自定义标签实例被创建后,就可以象这样将其加载到页面上:

<body>

  <!-- 将自定义标签放在body内部的任何地方 -->
  <todo></todo>

  <!-- 引入 riot.js -->
  <script src="riot.min.js"></script>

  <!-- 引入标签定义文件 -->
  <script src="todo.js" type="riot/tag"></script>

  <!-- 加载标签实例 -->
  <script>riot.mount('todo')</script>

</body>

 放置在页面 body 中的自定义标签必须使用正常关闭方式: <todo></todo> ,自关闭的写法: <todo/> 不支持。

2. mount 方法的使用方法

 将自定义标签放在 <body> 后,我们还需要调用riot.mount()才能将其加载进来。一个html文档中可以包含一个自定义标签的多个实例。

// mount 页面中所有的自定义标签
riot.mount('*')

// mount 自定义标签到指定id的html元素
riot.mount('#my-element')

// mount 自定义标签到选择器选中的html元素
riot.mount('todo, forum, comments')
3. 标签生命周期

自定义标签的创建过程是这样的:

  1. 创建标签实例
  2. 标签定义中的JavaScript被执行
  3. HTML 中的表达式被首次计算并首次触发 “update” 事件
  4. 标签被加载 (mount) 到页面上,触发 “mount” 事件

加载完成后,表达式会在以下时机被更新:

  1. 当一个事件处理器被调用(如上面ToDo示例中的toggle方法)后自动更新。你也可以在事件处理器中设置 e.preventUpdate = true 来禁止这种行为。
  2. 当前标签实例的 this.update() 方法被调用时
  3. 当前标签的任何一个祖先的 this.update() 被调用时. 更新从父亲到儿子单向传播。
  4. riot.update() 方法被调用时, 会更新页面上所有的表达式。

每次标签实例被更新,都会触发“update” 事件。
由于表达式的首次计算发生在加载之前,所以不会有类似 <img src={ src }> 计算失败之类的意外问题。

4. 监听生命周期事件

标签定义内部可以这样监听各种生命周期事件:

<todo>
  this.on('before-mount', function() {
    // 标签被加载之前
  })

  this.on('mount', function() {
    // 标签实例被加载到页面上以后
  })

  this.on('update', function() {
    // 允许在更新之前重新计算上下文数据
  })

  this.on('updated', function() {
      // 标签模板更新后
    })

  this.on('before-unmount', function() {
    // 标签实例被删除之前
  })

  this.on('unmount', function() {
    // 标签实例被从页面上删除后
  })

  // 想监听所有事件?
  this.on('all', function(eventName) {
    console.info(eventName)
  })
</todo>
5. 访问 DOM 元素

 Riot 允许开发人员通过 this 实例直接访问设置了 name 属性的元素,也提供了各种简化的属性方法如 if="{...}",但偶尔你还是需要直接完成这些内置手段所不支持的DOM操作。

6. 如何使用 jQuery, Zepto, querySelector, 等等

 如果需要在Riot中访问DOM,要注意 DOM 元素的初始化发生在第一个 update() 事件被触发之后,这意味着在这之前试图选择这个元素将都失败。

<example-tag>
  <p id="findMe">Do I even Exist?</p>

  <script>
  var test1 = document.getElementById('findMe')
  console.log('test1', test1)  // 失败

  this.on('update', function(){
    var test2 = document.getElementById('findMe')
    console.log('test2', test2) // 成功
  })
  </script>
</example-tag>

 你可能并不打算在每次update时都去取一下你想要的元素,而更倾向于在 mount 事件中做这件事。

<example-tag>
  <p id="findMe">Do I even Exist?</p>

  <script>
  var test1 = document.getElementById('findMe')
  console.log('test1', test1)  // 失败

  this.on('update', function(){
    var test2 = document.getElementById('findMe')
    console.log('test2', test2) // 成功,每次更新都会触发
  })

  this.on('mount', function(){
    var test3 = document.getElementById('findMe')
    console.log('test3', test3) // 成功,实例被加载以后,只触发一次
  })
  </script>
</example-tag>
7. 基于上下文的 DOM 查询

 现在我们知道了如何在处理 updatemount 事件时获取 DOM 元素,现在我们可以利用这一点,将 根元素 (我们所创建的 riot 标签实例) 作为DOM元素查询的上下文。

<example-tag>
  <p id="findMe">Do I even Exist?</p>
  <p>Is this real life?</p>
  <p>Or just fantasy?</p>

  <script>
  this.on('mount', function(){
    // Contexted jQuery
    $('p', this.root)

    // Contexted Query Selector
    this.root.querySelectorAll('p')
  })
  </script>
</example-tag>
8. 标签选项(参数)

mount 方法的第二个参数用来传递标签选项

<script>
  riot.mount('todo', { title: 'My TODO app', items: [ ... ] })
</script>

 在标签内部,通过 opts 变量来访问这些参数,如下:

<my-tag>
  <!-- 在HTML中访问参数 -->
  <h3>{ opts.title }</h3>

  // 在 JavaScript 中访问参数
  var title = opts.title
</my-tag>
9. Mixin

 Mixin 可以将公共代码不同标签之间方便地共享。

var OptsMixin = {
    init: function() {
      this.on('updated', function() { console.log('Updated!') })
    },

    getOpts: function() {
        return this.opts
    },

    setOpts: function(opts, update) {
        this.opts = opts

        if(!update) {
            this.update()
        }

        return this
    }
}

<my-tag>
    <h1>{ opts.title }</h1>

    this.mixin(OptsMixin)   // 用 mixin() 加上mixin名字来将mixin混入标签.
</my-tag>

 上例中,我们为所有 my-tag 标签实例混入了 OptsMixin ,它提供 getOptssetOpts 方法. init 是个特殊方法,用来在标签载入时对mixin进行初始化。 (init 方法不能混入此mixin的标签中访问)

var my_tag_instance = riot.mount('my-tag')[0]

console.log(my_tag_instance.getOpts()) // 输出<my-tag>的所有的标签选项

 标签的mixin可以是 object – {'key': 'val'} var mix = new function(...) – 混入任何其它类型的东西将报错.

现在:my-tag 定义又加入了一个 getId 方法,以及OptMixin中除init以外的所有其它方法

function IdMixin() {
    this.getId = function() {
        return this._id
    }
}

var id_mixin_instance = new IdMixin()

<my-tag>
    <h1>{ opts.title }</h1>

    this.mixin(OptsMixin, id_mixin_instance)
</my-tag>

 由于定义在标签这个级别,mixin不仅仅扩展了你的标签的功能, 也能够在重复的界面中使用. 每次标签被加载时,即使是子标签, 标签实例也获得了mixin中的代码功能.

10. 共享 mixin

 为了能够在文件之间和项目之间共享mixin,提供了 riot.mixin API. 你可以像这样全局性地注册mixin :

riot.mixin('mixinName', mixinObject)

用 mixin() 加上mixin名字来将mixin混入标签.

<my-tag>
    <h1>{ opts.title }</h1>

    this.mixin('mixinName')
</my-tag>

表达式

1, 直观理解

在 HTML 中可以混合写入表达式,用花括号括起来。[ style 标签中的表达式将被忽略.]

{ /* 某个表达式 */ }

表达式可以放在html属性里,也可以作为文本节点嵌入:

<h3 id={ /* 属性表达式 */ }>
  { /* 嵌入表达式 */ }   // 文本节点
</h3>

当然,并不是什么表达式都是能嵌入,因为Riot只支持属性(值)表达式和嵌入的文本表达式,以下将会执行失败。

<input type="checkbox" { true ? 'checked' : ''}>

表达式是 100% 纯 JavaScript. 一些例子:

{ title || 'Untitled' }
{ results ? 'ready' : 'loading' }
{ new Date() }
{ message.length > 140 && 'Message is too long' }
{ Math.round(rating) }

 建议的设计方法是使表达式保持最简从而使你的HTML尽量干净。如果你的表达式变得太复杂,建议你考虑将它的逻辑转移到 “update” 事件的处理逻辑中. 例如:

<my-tag>
  <!-- `val` 的值在下面的代码中计算 .. -->
  <p>{ val }</p>

  // 每次更新时计算
  this.on('update', function() {
    this.val = some / complex * expression ^ here
  })
</my-tag>
2. 布尔属性

如果表达式的值为非真,则布尔属性 (checked, selected 等..) 将不被渲染:
<input checked={ null }> 渲染为<input>
这与 W3C 有很大区别,W3C规范是只要布尔属性存在即为true,即使他的值为或者false

3. class 属性简化写法

Riot 为 CSS class 名称提供了特殊语法. 看一个例子

<p class={ foo: true, bar: 0, baz: new Date(), zorro: 'a value' }></p>

该表达式最终被计算为 “foo baz zorro”.,只有表达式中为真值的属性名会被加入到class名称列表中. 这种用法并不限于用在计算class名称的场合。

4. 转义

用以下的写法来对花括号进行转义:

\\{ this is not evaluated \\}  输出为  { this is not evaluated }
5. 渲染原始HTML

Riot 表达式只能渲染不带HTML格式的文本值。如果真的需要,可以写一个自定义标签来做这件事. 例如:

<raw>
  <span></span>

  this.root.innerHTML = opts.content
</raw>

这个标签定义以后,可以被用在其它的标签里. 例如

<my-tag>
  <p>原始HTML: <raw content="{ html }"/> </p>

  this.html = 'Hello, <strong>world!</strong>'
</my-tag>

嵌套标签

我们来定义一个父标签 <account> ,其中嵌套一个子标签 <subscription>:

<account>
  <subscription  plan={ opts.plan } show_details="true" />
</account>

// 子标签
<subscription>
  <h3>{ opts.plan.name }</h3>

  // 取得标签选项
  var plan = opts.plan, 
      show_details = opts.show_details   // 取出子标签的标签属性

  // 访问父标签实例
  var parent = this.parent   // 获取父标签的标签实例

</subscription>

注意: 我们使用下划线的风格(而不是驼峰风格)对 show_details 进行命名,由于浏览器的约定,驼峰风格的命名会被自动转换成小写.
如果在页面上加载 account 标签,带 plan 选项,调用riot.mount()方法。

<body>
  <account></account>
</body>

<script>
  riot.mount('account', { plan: { name: 'small', term: 'monthly' } })
</script>

注意: 嵌套的标签只能定义在自定义父标签里,如果定义在页面上,将不会被初始化。


嵌套 HTML

“HTML transclusion” 是处理页面上标签内部 HTML 的方法. 通过内置的 <yield> 标签来实现.

<my-tag>
  <p>Hello <yield/></p>
  this.text = 'world'
</my-tag>

页面上放置自定义标签,并包含嵌套的 HTML

<my-tag>
  <b>{ text }</b>
</my-tag>

结果得到

<my-tag>
  <p>Hello <b>world</b><p>
</my-tag>

DOM元素与name自动绑定

 我感觉这个功能真的是帅炸了。当html被定义好了之后,带有 ref 属性的DOM元素将自动被绑定到上下文中,这样就可以从JavaScript中方便地访问它们:

<login>
  <form ref="login" onsubmit={ submit }>
    <input ref="username">
    <input ref="password">
    <button ref="submit">
  </form>

  // 获取 HTML 元素
  var form = this.refs.login,
    username = this.refs.username.value,
    password = this.refs.password.value,
    button = this.refs.submit

</login>

当然,因为DOM已经被绑定到上下文中,所以我们也可以直接在HTML中以表达式形式引用:

<div>{ username.value }</div>

事件处理器

1. 一般处理

响应DOM事件的函数称为 “事件处理器”.

<login>
  <form onsubmit={ submit }>
  </form>

  // 上面的表单提交时调用此方法
  submit(e) {

  }
</login>

 以”on” (onclick, onsubmit, oninput等…)开头的属性的值是一个函数名,当相应的事件发生时,此函数被调用. 函数也可以通过表达式来动态定义:

<form onsubmit={ condition ? method_a : method_b }>

 在此函数中,this指向当前标签实例。当处理器被调用之后, this.update() 将被自动调用,将所有可能的变化体现到 UI 上。

2. 阻止默认行为

 如果事件的目标元素不是checkboxradio按钮,默认的事件处理器行为是 自动取消事件 . 意思是它总会自动调用 e.preventDefault() , 因为通常都需要调用它,而且容易被遗忘。如果要让浏览器执行默认的操作,在处理器中返回 true 就可以了.

submit() {
  return true
}
3. 事件对象

 事件处理器的第一个参数是标准的事件对象。事件对象的以下属性已经被Riot进行了跨浏览器兼容


渲染条件 - show / hide / if

可以基于条件来决定显示或隐藏元素。例如:

<div if={ is_premium }>
  <p>This is for premium users only</p>
</div>

同样, 渲染条件中的表达式也可以是一个简单属性,或一个完整的 JavaScript 表达式. 有以下选择:

判断用的操作符是 == 而非 ===. 例如: 'a string' == true.


循环

1. 循环是用 each 属性来实现:
<todo>
  <ul>
    <li each={ items } class={ completed: done }>
      <input type="checkbox" checked={ done }> { title }
    </li>
  </ul>

  this.items = [
    { title: 'First item', done: true },
    { title: 'Second item' },
    { title: 'Third item' }
  ]
</todo>

 定义 each 属性的html元素根据对数组中的所有项进行重复。 当数组使用 push(), slice()splice 方法进行操作后,新的元素将被自动添加或删除。

2. 上下文

 循环中的每一项将创建一个新的上下文(标签实例);如果有嵌套的循环,循环中的子标签都会继承父循环中定义了而自己未定义的属性和方法。Riot通过这种方法来避免重写不应在父标签中重写的东西。
从子上下文中可以通过显式地调用 parent 变量来访问上级上下文.

<todo>
  <div each={ items }>
    <h3>{ title }</h3>
    <a onclick={ parent.remove }>Remove</a>
  </div>

  this.items = [ { title: 'First' }, { title: 'Second' } ]
  remove(event) {

  }
</todo>

 该例中,除了 each 属性外,其它都属于子上下文, 因此 title 可以被直接访问而 remove 需要从 parent. 中访问,因为remove方法并不是循环元素的属性.
 每一个循环项都是一个标签实例. Riot 不会修改原始数据项,因此不会为其添加新的属性。

3. 循环项的事件处理器

事件处理器中可以通过 event.item 来访问单个集合项。这种办法采用了事件委托机制,极大减少了对DOM的访问。
下面我们来实现上方的 remove 函数:

<todo>
  <div each={ items }>
    <h3>{ title }</h3>
    <a onclick={ parent.remove }>Remove</a>
  </div>

  this.items = [ { title: 'First' }, { title: 'Second' } ]

  remove(event) {

    // 循环项
    var item = event.item

    // 在集合中的索引
    var index = this.items.indexOf(item)

    // 从集合中删除
    this.items.splice(index, 1)
  }
</todo>

 事件处理器被执行后,当前标签实例会自动调用 this.update()(你也可以在事件处理器中设置 e.preventUpdate = true 来禁止这种行为)从而导致所有循环项也被更新. 父亲会发现集合中被删除了一项,从而将对应的DOM结点从document中删除。

4. 循环自定义标签

自定义标签也可以被循环

<todo-item each="{ items }" data="{ this }"></todo-item>

当前循环项可以用 this 来引用,你可以用它来将循环项作为一个参数传递给循环标签。

5. 非对象数组

数组元素不要求是对象. 也可以是字符串或数字. 这时可以用 { name, i in items } 写法

<my-tag>
  <p each="{ name, i in arr }">{ i }: { name }</p>

  this.arr = [ true, 110, Math.random(), 'fourth']
</my-tag>

name 是元素的名字,i 是索引. 这两个变量的变量名可以自由选择。

6. 对象循环

也可以对普通对象做循环. 例如:

<my-tag>
  <p each="{ name, value in obj }">{ name } = { value }</p>

  this.obj = {
    key1: 'value1',
    key2: 1110.8900,
    key3: Math.random()
  }
</my-tag>

 不太建议使用对象循环,因为在内部实现中,Riot使用 JSON.stringify 来探测对象内容的改变. 整个 对象都会被检查,只要有一处改变,整个循环将会被重新渲染. 会很慢. 普通的数组要快得多,而且只有变化的部分会在页面上体现。

7. 循环的高级技巧

 在 riot v2.3 中,为了使循环渲染更可靠,DOM 结点的移动,插入和删除总是与数据集合同步的: 这种策略会导致渲染过程比之前的版本慢一些。要使用更快的渲染算法,可以在循环结点上加上 no-reorder 属性。

<loop>
  <div each="{ item in items }" no-reorder>{ item }</div>
</loop>

使用标准 HTML 元素作为标签 | #riot-tag

页面body中的标准 HTML 元素也可以作为riot标签来使用,只要加上 riot-tag 属性.

<ul riot-tag="my-tag"></ul>

这为用户提供了一种选择,与css框架的兼容性更好. 这些标签将被与其它自定义标签一样进行处理。

riot.mount('my-tag')

会将 my-tag 标签 加载到 ul 元素上


服务端渲染 | #server-side

Riot 支持服务端渲染,使用 Node/io.js 可以方便地引用标签定义并渲染成 html:

var riot = require('riot')
var timer = require('timer.tag')

var html = riot.render(timer, { start: 42 })

console.log(html) // <timer><p>Seconds Elapsed: 42</p></timer>

循环和条件渲染都支持.

上一篇下一篇

猜你喜欢

热点阅读