基于 Turbolinks 的跨平台开发

黄增光(@chloerei) <chloerei@gmail.com>

vulcan salute

“生生不息,繁荣昌盛。”(Live long and prosper)

关于我

  • Ruby China 社区创始人之一(No.1 会员)

  • 大疆 Ruby on Rails 开发

  • Alipay gem 维护者

Rails is Omakase

omakase
cake

Rails has Two Default Stacks

  • Omakase Stack

  • Prime stack

Prime Stack

prime stack

前后端分离是未来?

前后端带来新问题

  • 首屏加载速度

  • SEO

  • 开发复杂度

chrome timeline example

首屏加载速度

ruby china home timeline
discourse home timeline

Ruby China (Turbolinks)

Discourse (Ember.js)

延迟用户感知

0–100 ms

即时

100–300 ms

稍微延迟

300–1000 ms

等待处理

1,000+ ms

注意力分散

10,000+ ms

终止任务

加快首屏加载的方法

  • 首屏插入数据,避免 API 请求。

  • 使用服务端渲染。✨

SEO

<section id='main'>
</section>

<script>
  App.init(...);
</script>

SEO 解决办法

  • 服务端输出专供搜索引擎的内容。

  • 使用服务端渲染。✨

今年前端流行:Isomorphic(同构)

同构是在数学对象之间定义的一类映射,它能揭示出在这些对象的属性或者操作之间存在的关系。若两个数学结构之间存在同构映射,那么这两个结构叫做是同构的。一般来说,如果忽略掉同构的对象的属性或操作的具体定义,单从结构上讲,同构的对象是完全等价的。

— wikipedia
jack chen

同构 JavaScript 应用是可以同时跑在客户端和服务端的 JavaScript 应用。前端和后端共享同样的代码。

— http://isomorphic.net/

说好的前后端分离呢?

“前后端分离”有歧义

  • 服务端和客户端分离 🙅

  • 业务层和展示层分离 🙆

开发复杂度

isomorphic javascript

#JavaScriptEE

javascript ee

系统开发的复杂度多半是引入了系统组件之间的界限

我们要追求的系统:包含所有功能,容易发布,简单理解的单一整合系统。

The Rails Doctrine
— DHH

Turbolinks 是 Rails 最被误解的组件。

ruby china search

Turbolinks 是 Web 换页加速器

有多快?

turbolinks timeline
no turbolinks timeline

Ruby China (Turbolinks)

Ruby China (No Turbolinks)

Turbolinks 做了什么

  • 捕获 <a> 点击事件。

  • 用 xhr 请求服务端内容。

  • 替换当前页面的 <body>,合并 <head>

  • 处理浏览器历史记录等等……

Turbolinks 让网站成为单页应用(SPA)

安装(Rails 默认)

#= require turbolinks

已经在使用 Turbolinks 了!

等等,我的 JavaScript 出问题了! 😱

Turbolinks 两大“坑”

  • Turbolinks Event

  • Turbolinks Caching

Turbolinks Event

  • 现象:我的代码不执行了!

  • 原因:Turbolinks 开启后,DOMContentLoaded 事件只在首屏触发。

Turbolinks Event

解决:使用 turbolinks:* Event

$(document).on 'turbolinks:load', ->
  # codes here...

将事件委托到 document

# 每次换页遍历绑定 #my-element
$(document).on 'turbolinks:load' ->
  $('#my-element').on 'click', ->
    # codes here...

# 只绑定一次
$(document).on 'click', '#my-element', ->
  # codes here...

使用 UJS 和 SJR

Turbolinks Caching

  • 现象:我的代码被重复执行了!

  • 原因:Turbolinks 内部维护了缓存,点击浏览器前进、后退的时候会读取缓存,此时 JavaScript 可能对同一处元素重复处理。

Turbolinks Caching

解决:让 JavaScript 操作幂等。

$(document).on 'turbolinks:load', ->
  if !$('#element').attr('data-is-done')
    # do something
    $('#element').attr('data-is-done', true)

还有,初始化元素的时机不一定在 Turbolinks 事件内,例如 Ajax。

Custom Elements(实验性 Web 标准)

# 定义 element
class MyElement extends HTMLElement
  constructor: ->
    # element 创建

  connectedCallback: ->
    # element 插入 DOM

  disconnectedCallback: ->
    # element 脱离 DOM

window.customElements.define('my-element', MyElement)
<my-element></my-element>

Trix:基于 Custom Element 的富文本编辑器

<form …>
  <input id="x" type="hidden" name="content">
  <trix-editor input="x"></trix-editor>
</form>

README

依然需要写 JavaScript。

复杂的组件可以使用前端框架,但没必要整站都用。

它要求你从全局的角度看待问题。

Ruby China 一直在使用 Turbolinks。

移动应用

Facebook HTML5 Mobile App

facebook

失败原因

  • 移动设备性能不足

  • 无法调用本地接口

时代在进步

  • 移动设备性能更好了

  • 混合移动应用: Native 导航, Web 内容

Basecamp 3

basecamp

Turbolinks Android & Turbolinks iOS

Ruby China 移动应用 🎉

已上架 App Store、Play Store。

ruby china android webview
ruby china android webview 2

TurbolinksView

  • 移动端原生组件。

  • 多个 Activity 共享一个 WebView。

  • 提供 WebView 和 Native 交互的接口。

安装

repositories {
    jcenter()
}

dependencies {
    compile 'com.basecamp:turbolinks:1.0.3'
}

Layout

<LinearLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <com.basecamp.turbolinks.TurbolinksView
        android:id="@+id/turbolinks_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

</LinearLayout>

初始化 TurbolinksSession

@Override
protected void onCreate(Bundle savedInstanceState) {
    turbolinksView = (TurbolinksView) findViewById(R.id.turbolinks_view);

    TurbolinksSession.getDefault(this)
                     .activity(this)
                     .adapter(this)
                     .view(turbolinksView)
                     .visit("https://ruby-china.org");
}

Turbolinks.visit 回调

@Override
public void visitProposedToLocationWithAction(String location, String action) {
  String path = Uri.parse(location).getPath();

  // ...

  if (path.matches("/topics/\\d+")) {
    Intent intent = new Intent(this, TopicActivity.class);
    intent.putExtra(INTENT_URL, location);
    this.startActivity(intent);
  }

  // ...
}

再加上原生工具栏、菜单、按钮等等……

ruby china android

定制移动页面

  • Responsive web design

  • Action Pack Variants

用 Java 操作 WebVIew

private void topicCreate() {
    TurbolinksSession.getDefault(this).getWebView().evaluateJavascript(
            "$('form[tb=\"edit-topic\"]').submit();",
            null
    );
}

从 WebView 调用 Java

TurbolinksSession.getDefault()
  .addJavascriptInterface(this, "MyCustomJavascriptInterface");

快速开发

通过 Native 渐进增强

依然需要 Android & iOS 开发人员。

劣势 😟

  • 缺少开源代码参考。

  • 跟团队已有的技术栈不符。

  • 不适合交互复杂的移动应用。

Turbolinks Mobile 提供了一个选择。

开源 🎉

多谢 😘