fequan-netease



fequan-netease

2 8


fequan-netease

前端圈-走进我司(网易)的Slide

On Github leeluolee / fequan-netease

漫谈Web前端的『组件化』

What, Why, How

Hi, 我叫 郑海波

@leeluolee

@拴萝卜的棍子

网易杭州 - 前端技术部

Agenda

组件 数据驱动的组件框架 实践经验分享

什么是组件?

component , widget , module , plugin ...etc

Component

"A component in the Unified Modeling Language represents a modular part of a system, that encapsulates its content and whose manifestation is replaceable within its environment. A component defines its behavior in terms of provided and required interfaces"

Tab

Tab页内容...blablabla
tab1 content
tab2 content
tab3 content

jQuery

$( ".selector" ).tabs( "option", "show", { 

    effect: "blind", 

    duration: 800 
});

NEJ


var tb = p._$$Tab._$allocate({

    list: e._$getChildren( e._$get('box')),

    index:1,

    onchange:function(_event){

    }
});

tb._$go(2);

YUI

YUI().use('tabview', function(Y) {

    var tabview = new Y.TabView({
        children: [{
            label: 'foo',
            content: '<p>foo content</p>'
        }, {
            label: 'bar',
            content: '<p>bar content</p>'
        }, {
            label: 'baz',
            content: '<p>baz content</p>'
        }]
    });

    tabview.render('#demo');

});

实现不统一, 使用不统一

Web Component

2011年,由Alex Russell提出

是系列规范

  • Custom Element: 自定义HTML元素

  • shadow DOM: 封装

  • HTML Imports: 打包一切

  • HTML Template: Lazy的DOM模板

Custom Element

<x-foo> Custom Elememnt </x-foo>
var xFoo = document.createElement('x-foo');

定义新元素

var XFooProto = Object.create(HTMLElement.prototype);

// 生命周期相关
XFooProto.readyCallback = function() {
  this.textContent = "I'm an x-foo!";
};

// 设置 JS 方法
XFooProto.foo = function() { alert('foo() called'); };

var XFoo = document.register('x-foo', {prototype: XFooProto});

Vanilla JS + Vanilla DOM!

Shadow Dom

Your user agent does not support the HTML5 Video element.

DevTool > Settings > General> show shadow DOM

Light DOM

h3 outer

<div><h3>Light DOM</h3></div>

<script>
var node = document.querySelector('div');

var root = node.createShadowRoot();

root.innerHTML = 
        '<style>h3{ color: red; }</style>' + 
        '<h3> h3 in shadowRoot </h3>';
</script》

Are We Componentized Yet?

Advantage

  • 一致性: Vanlila DOM
  • 可移植性
  • 所写即所得
  • 浏览器内建的生命周期

Disadvantage

  • Not Data Driven
  • Imprective

Data-Binding

var tabPane = document.createElement('tab-pane');

tabPane.setAttribute('title', 'hello');
tabPane.setAttribute('content', 'hello');

tab.appendChild(tabPane);

VS

tabs.push({
    title: 'hello',
    content: 'hello'
})

Declarative VS Imprective

<pagination 
    current={current} total={maxCount/20} 
    on-nav={this.nav(1)}></pagination>
<pagination 
    current={current} total={maxCount/20} 
    on-nav={this.nav(2)}></pagination>

VS

<bootstrap-pagination id='pagination'></bootstrap-pagination>
<script>
// 获取元素
var pagination = document.querySelector('#pagination');
// 绑定事件
pagination.addEventListener('pagination-nav', function(event){
    // blablablabal
})
// 设置属性
$.ajax('/blogs').then(function( json ){
    pagination.setAttribute('current', 0)
    pagination.setAttribute('total', json.length / 20)
})

</script》

"是数据驱动允许将两个组件通过声明式编程建立内在联系"

  • Data-Driven是因(gong)

  • Declarative是果(shou)

视图层本就是由数据所驱动

stories
stories[1]
stories[1].vote
stories[1].comments.length

矛盾在于DOM操作是碎片的命令式

这不是Web Component的错

"Foundation 与 Framework 职责是不同的"

We Need

Framework

v = f(d)

何种框架技术满足这个公式 ?

答案是 ......

字符串模板

{%if login %} 
    <h2>{{user.name}} </h2>
{% endif %}

+

{
    login: true,
    user: {
        name: '@leeluolee'
    }
}

===

<h2>@leeluolee</h2>

Advantage.

  • Simple Usage ( compile + render )

  • Natrual Isomorphic( 100% dom 无关 )

  • Powerful Syntax

  • 100% Stateless "v=f(d)"

  • "High Performance"

Disadvantage.

  • Hidden danger ( XSS, invalid tag... etc )

  • 100% Stateless (??)

  • "Low Performance" (???!)

MDV technology to rescue

Model Driven View

MDV

A proposal for extending HTML and the DOM APIs to support a sensible separation between the UI (DOM) of a document or application and its underlying data (model). Updates to the model are reflected in the DOM and user input into the DOM is immediately assigned to the model

from Google

Polymer

Everything is Component

<link rel="import"
      href="bower_components/polymer/polymer.html">
<!-- import the iron-input custom element -->
<link rel="import"
      href="bower_components/iron-input/iron-input.html">

<dom-module id="editable-name-tag">

  <template>
    <p>
    This is a <strong>{{owner}}</strong>'s editable-name-tag.
    </p>
    <!-- iron-input exposes a two-way bindable input value -->
    <input is="iron-input" bind-value="{{owner}}"
      placeholder="Your name here...">
  </template>

  <script>
    Polymer({
      is: "editable-name-tag",
      properties: {
        owner: {
          type: String,
          value: "Daniel"
        }
      }
    });
  </script》

</dom-module>

React

Nothing But Component

"I definitely think it’s the wrong programming paradigm. I really hope that [web components] do not succeed"

from Pete Hunt

Share Context Between Logic And View

Virtual DOM

  • 脏检查发生在View层抽象Virtual DOM上
  • Diff计算差异并映射到View层, 实现局部更新
  • View层可以独立实现到各平台 (依赖JS Binding)
  • 不依赖模板, 配合JSX可以有类似书写体验.

One Way Data-Flow

  • Two-Way Binding 不再被认为是最佳实践

Huge

  • Foudation ( React Native, Flux, Immutable.js..etc)

  • Community ( 3w+ stars , 5000+ issues )

  • Size... ( 40kb Gzip & without JSX )

Angular

The next generation JavaScript language that will kill ALL the frameworks!

Feature

  • Dirty Check(Pull)
  • 100% 的 Plain Object
  • Complex Concepts(directive,service,controller..etc)
  • Huge Community
  • Everything Except Componenet (v1.x)
  • Low Performance (maybe)

Regularjs

Create data-driven Components based on Living Template

Living Template

Living Template 是一种组合技术

  • String-based Template的头
  • 类似React的身体
  • DOM-based Template的尾巴

http://leeluolee.github.io/2014/10/10/template-engine/

Define A Component

Declarative Way

<div class='example-container'>
  <pophover title='pophover title' placement='bottom'>
    pophover content
  </pophover>
</div>

Imprective Way

let pophover = new Pophover({

    data: {
        title: 'pophover title',
        placement: 'bottom'
    },

    $body: 'pophover content'

}).$inject('.example-container')

声明式(标签化)只是『接口』的一种形式

Feature

  • Safe

  • 20KB(Gzip)

  • Living Template( String-based + Dom-based )

  • Dirty Check(性能?)

  • Composite Component( != 标签化 )

  • Fully Tested( IE6+ )

  • Widely Used in 我厂( 网易杭州 )

MDV总结: 框架的Model层

  • Pull: Regular, Angular
    • 潜在性能问题
    • Stateless的Plain Object
  • Push: Polymer(Object.observe), Vue(Object.defineProperty)
    • 高效的模型(MayBe)
    • 非100%的Plain Object, 基于binding.
  • Pull(view): React

纯粹的v = f(d) 止步于DOM 操作.

MDV总结: 框架的Foudation

  • Polymer: 基于Web Component
  • Angular: 基于DOM的解析(innerHTML)+自建的编译(link)流程
  • React: 自有流程
  • Regular: 自有流程

regular-register

将Component 转化为真正的Custom Element !!

https://github.com/regularjs/customelements-register

组件实践

与具体框架无关

永远不要提前将业务组件化

组件是重构的结果,非提前设计所能及也

  • 组件化前, 共处一个Context

  • 组件化后, 灵活性降低,分处于不同Context,通过事件和Data Flow维系

组件封装

组件分类

  • Non Visual: 只有业务逻辑,没有View

    • 如带翻页列表: 抽象业务逻辑, 一般由其子类实现View
    • 如<polymer-ajax></polymer-ajax>: 纯功能,没有View
  • Visual: 业务逻辑 + View

Non Visual Component is Easy ,Visual Component is Hard

  • 组件最大的不稳定性来自于展现层(模板)
  • 继承或混入可以解决成员函数和变量的扩展复用, 但无法解决模板的扩展复用

如何解决模板的复用问题?

组合

A + B = AB

组合的野生例子: ul + li

属于无序列表的li元素

<ul class="pagination">
  <li><a href="">Prev</a></li>
  <li><a href="">1</a></li>
  <li><a href="">...</a></li>
  <li><a href="">10</a></li>
  <li><a href="">Next</a></li>
</ul>

组合的野生例子: ol + li

属于有序列表的li元素

<ol class="rank">
  <li><a href="">1</a></li>
  <li><a href="">2</a></li>
  <li><a href="">3</a></li>
  <li><a href="">4</a></li>
  <li><a href="">5</a></li>
</ol>

组件亦是如此

<dropdown-button title='向上的箭头'  style=success dropup on-select={}>
  <select-item header>我是标题</select-item>
  <select-item><span class='icon'></span>我是列表项1</select-item>
  <select-item divider />
  <select-item disabled>我是禁止的</select-item>
  <select-item>我是列表项2</select-item>
  <select-item divider />
  <select-item>我是列表项3</select-item>
</dropdown-button>
  • select-item的职责: 响应点击、 失效等行为
  • dropdown-button的职责: 展现样式, 抛出select事件

组合可以更复杂...

<modal title='筛选器'>
    <tab>
      <tab.pane title='列表筛选'  selected>
        <text-search select={select}   ref=list ></text-search>
      </tab.pane>
      <tab.pane title='文本筛选' > 
        <text-filter match ={match}   ref=text ></text-filter>
      </tab.pane>
      <tab.pane title='条件筛选' >
        <condition-filter condition={condition}  measures={measures} ref=condition ></condition-filter>
      </tab.pane>
      <tab.pane title='高级筛选' >
        <advanced-filter top={top} measures={measures} ref=advanced ></advanced-filter>
      </tab.pane>
    </tab>
</modal>

组合: 只提供能力

<div class="item {mark.labels.length? 'z-act':''}">
 <dropable name='mark.labels' direct=y 
  on-toucheddrop={this.drop('mark.labels', $event)}  >
  <div class="box">
    {#list mark.labels as lpill}
      <dragable target={TARGET_PILL} on-dragend='dragend' >
        <pill pill={lpill} index={lpill_index} isolate = 1 />
      </dragable>
    {/list}
  </div>
</dropable>
</div>
  • dropable: 为 包裹区域 提供放置能力
  • dragable: 为 包裹区域 提供拖拽能力

DEMO

One Way Data-Flow

为何我们需要单向数据流的组件架构

  • 可控性

Flux

瞬间被后浪排死在沙滩的前浪

Redux

State Manager, Only 2KB (gzip)

http://rackt.org/redux/

流程

Store.dispatch(Action) // 派发Action Middleware(Action) // 处理异步逻辑、Log等 Reducer(State, Action) View = F(State);

Example

import { createStore } from 'redux';
// reducer
function counter(state = 0, action) {
  switch (action.type) {
  case 'INCREMENT':
    return state + 1;
  case 'DECREMENT':
    return state - 1;
  default:
    return state;
  }
}
// Its API is { subscribe, dispatch, getState }.
let store = createStore(counter);

// You can subscribe to the updates manually, or use bindings to your view layer.
store.subscribe(() =>
  console.log(store.getState())
);

store.dispatch({ type: 'INCREMENT' });

让我们翻译成VanillaJS...

state++

先苦后甜, 还是先甜后『狗带』

三原则

  • 规定State本身是不变数据类型
  • 规定Reducer为纯函数
  • 规则只能有单个Store(它整合了Dispather)

从数据类型、Mutation、数据源、Data-Flow保证了状态本身的可预测性(predictable)

Redux与JS的矛盾点

JS中的Object,Array等是可变数据结构

可变数据

var a = {selected: true, content: 'hello'};

// action 1
a.selected = false;

// action 2

a.selected = true;

a === a;

往往你在调试action 1时, 数据已经被action 2改变

不可变数据

var a = {selected: true, content: 'hello'};

// action1
var a1 = Object.assign({}, a , {
  selected: true
}) 

a !== a1

深层对象修改

  • deepSet(state, 'blogs[1].title', 'hello')

当我们把每一步的数据都保存下来

Time Machine!

如何结合特定的View层框架

store.subcribe(callback)

特别适合基于Pull流程(脏检查)的View层框架

Regular< ReduxMixin >Redux

var ReduxMixin = {
    events: {
        $config: function(){
            store.subcribe( function(){

                    this.mapStateToData( store.getState(), this.data);
                    this.$update()// 触发脏检查

                }.bind(this) )
        }
    },
    mapStateToData: function(){
        throw Error('You need implement mapStateToData')
    }
}
ReportApp.implement( ReduxApp );

Thank You

漫谈Web前端的『组件化』 What, Why, How