Skip to content

ESNEXT 原生框架范式探究(01) #8

@Lucifier129

Description

@Lucifier129

前言

托付于 Node.jsHMTL5JavaScript 发展得越来越快。

几年前,就有人提出「你或许不需要 jQuery」 ,近期也有「你可能不需要 underscore」以及 Hax 的 nodash 仓库。

正当风靡的 ReactJs 提出 Flux 架构后,社区涌现出许多不同版本跟理念的 Flux 实现,目前风头最劲的要数基于 ES7+redux,它的理念除了hot loading之外,最重要的就是pure function(纯函数)。

作为一门语言,javascript 的自理能力不断增强;那么势必存在一种组织范式,使得不用框架,也能像之前有框架辅助那样,写出模块化、组件化以及语义清晰的代码。

我对原生框架范式的思考,会在esnext-framework仓库里积累。

所谓的原生框架范式,主旨是「用理念代替 API;如非必要,勿增 API」。我目前的探索方式是,在 babelstage = 0 模式提供的一切便利下,寻找一个舒适的前端编程模式;每一个范式探究,都以一个TODOMVC DEMO为产物和体现。

本文是小试牛刀的[范式01]。

es6.template 视图模板组件化

在 Mozilla 的 ES6 In Depth: Template strings 文章里提到:

Template strings are no replacement for Mustache and Nunjucks, partly because they don’t have built-in syntax for loops or conditionals.

然后演示了如何用 Tagged templates (标签模板提供模板字符串的循环和条件语句),看起来自己打自己脸,好不快乐。

我认为,es6.template 没有提供循环和判断语句,并不是大问题,因为 ${expression} 语法支持任何 javascript 语句,甚至可以嵌套 template 字符串;我们自己写循环,操作空间更强。

在 reactjs 的 jsx 语法里也可以写任意纯 js 语句,我们早就见怪不怪,并且乐得其所。与其自己制造方言,倒不如用纯 js 语法。

所以,我认为 es6.template 起码可以作为轻量级 web 应用的视图模板的选择之一。

它的大概写法如下:

//component/todo.js
export default data => {
    let { completed, time, id, title } = data
    return `
<li class="${ completed }" data-id="${ id }" data-helper="todo" title="${ time }">
    <div class="view">
        <input class="toggle" type="checkbox" ${ completed ? 'checked' : '' }>
                <label>${ title }</label>
                <button class="destroy"></button>
    </div>
    <input class="edit">
</li>
    `
}

component/todo.js返回一个渲染字符串函数,在 component/todos.js 循环展开,这样实现组合作用。

import todo from 'component/todo'

export default data => {
    return `
<ul id="todo-list">
    ${ data.map(todo).join('') //纯 js 循环语句 }
</ul>
    `
}

es7.functionBind 与 函数式 helper

es7.functionBind 暂不了解的可以点击这里。这个特性,算不上强大,只是语法糖性质,编译到 ES3 环境也能正常运行。

然而,有了::语法,我们的代码可以变得更富有表现力,我们的关注点更能纯粹。甚至,我们整个思维模式都可能因其而改变。

虽然 js 模块化开发已经有几年的历史,但直到今天,我们也还在继续命名空间思路。$_ 符号四处可见。trine 开始做出改变,它用 :: 取代 _ ,让代码更整洁,显得更函数式。我们可以在这条道路上,走得更远。

一直以来,往原型链上添加属性和方法,都是很诱惑的事情;它的副作用是覆盖原生行为与污染全局变量带来的不确定性。如果用上 :: 的话,我们就可以保持 this method 的顺序和格式,得到整洁的外观,而又不会污染代码运行时的确定性。

我们只是从this.method( )变成this::method(),别提多方便了。

我们可以这样编写我们的函数方法:

//type helper
export let isArray = function() {
    return Array.isArray(this)
}

export let isFunction = function() {
    return Object.prototype.toString.call(this) === '[object Function]'
}

export let isObject = function() {
    return Object.prototype.toString.call(this) === '[object Object]'
}

export let isString = function() {
    return Object.prototype.toString.call(this) === '[object String]'
}

export let isNumber = function() {
    return Object.prototype.toString.call(this) === '[object Number]'
}

export let isBoolean = function() {
    return Object.prototype.toString.call(this) === '[object Boolean]'
}

然后这样调用它们:

import { isArray, isFunction, isString, isObject } from 'helper'

'hello world'::isString() //true
[1, 2, 3, 4]::isObject() // false
(123123)::isNumber() // true

看起来,任意 js 数据类型,都具备了类型检测方法一般。这种模式,我称之为 helper,像原型方法一样编写,用 :: 调用。

helper 增强了 js 语言,以无副作用或者少副作用的方式,提供给所有数据类型有用的自定义方法。它的魔力,不知上面所示,我们再来看看下面的例子:

//jqueryHelper.js
import $ from 'jquery'
import { isString } from './helper'

let helper = {}
export default helper

Object.keys($.fn).forEach(key => {
    helper[key] = function(...args) {
        if (this::isString()) {
            return $(String(this))[key](...args)
        }
        return $(this)[key](...args)
    }
})

我们把所有 $.fn 方法导出为 helper 模式,于是可以像下面那样使用:

import { appendTo, attr } from 'jqueryHelper'

//这个做法过时了
$('body').html('<h1>hello world</h1>')

//酷酷的
'<h1>hello world</h1>'::appendTo('body')

//原生dom api 冗长啰嗦
document.getElementsByTagName('script')[0].getAttrubite('type')

//酷酷的
'script'::attr('type')

jquery 简化了 dom 操作,:: 简化了命名空间,代码更整洁,更声明式。对于 underscore 库,也可以做相同的 helper 转换,可用方法更丰富了。

你以为这样就完了?

函数式风格,终究还是要在函数下手,看看下面这种:

//柯里化
export let currying = function(first) {
    return (...args) => {
        this(first, ...args)
    }
}

//反柯里化
export let uncurrying = function() {
    return (context, ...args) => {
        this.apply(context, ...args)
    }
}

//管道流式拼接
export let pipe = function(next = noop) {
    if (this::isFunction()) {
        return (...args) => {
            return next(this(...args))
        }
    } else if (this::isArray()) {
        return this.reduce((prev, cur) => prev::pipe(cur))
    }

}

//promise 流式拼接
export let then = function(next) {
    if (this::isFunction) {
        return (...args) => {
            return Promise.resolve(this(...args)).then(next)
        }
    } else if (this::isArray()) {
        return this.reduce((prev, cur) => prev::then(cur))
    }
}

然后,我们的函数就可以像这样拼接起来了:

//直接将数据模型的反应函数与渲染函数绑定起来,数据发生改变,视图就做出反应
let onAction = [::this.model.onAction, ::this.render]::pipe()

//::this.model.onAction === this.model.onAction.bind(this.model)

//事件代理,事件类型与代理 selectory 存放在 key 里,事件回调存放在 value 里
let events = {
            'change : #new-todo'(e) {
                //onAdd 为 helper,this 值为 e.target,从中获取到新的 todo title
                //onAdd 返回 {type: 'add',title: value }
                //onAction 接受一个 action 对象,根据 action 的 type 做不同反应
                //类似于 reactjs 的 flux 模式
                e.target::onAdd::pipe(onAction)()

                //e.target::onAdd === onAdd.bind(e.target)
            },
            'change : #todo-list .edit'(e) {
                //事件绑定变得干净
                e.target::onEdited::pipe(onAction)()
            },
            'keyup : #new-todo'(e) {
                if (e.keyCode === ENTER_KEY) {
                    //可维护性强,增加业务逻辑,只是 pipe 多一个 helper 纯函数
                    e.target::onAdd::pipe(onAction)()
                }
            },
            'dblclick : #todo-list label'(e) {
                e.target::onEditing()
            },
            'keyup : #todo-list .edit'(e) {
                let keyCode = e.keyCode
                if (keyCode === ENTER_KEY || keyCode === ESCAPE_KEY) {
                    e.target::onEdited()
                }
            },
            'change : #todo-list .toggle'(e) {
                e.target::onToggle::pipe(onAction)()
            },
            'click : #todo-list .destroy'(e) {
                e.target::onRemove::pipe(onAction)()
            },
            'click : #clear-completed'(e) {
                onAction({
                    type: 'clear'
                })
            },
            'change : #toggle-all'(e) {
                onAction({
                    type: 'toggleAll',
                    completed: e.target.checked
                })
            }
        }

就这样,通过函数式 helper ,我们的代码更清晰。

逻辑分组

在[范式01]中,代码根据性质做了分组与分类

  • component 组件化的视图
  • helper 函数式纯逻辑
  • directive 将数据渲染到视图的自定义指令
  • method 响应 dom 操作,从视图中获取数据,返回数据对象的纯函数
  • model 数据模型类,响应 method 返回的数据对象
  • app 出口模块类,协调上面四个代码类型,拼接业务逻辑。

尾声

以上就是小试牛刀的[范式01],其代码仓库地址是:https://github.com/Lucifier129/esnext-framework/tree/patten1

在 examples 文件夹里可以找到 todos-vanillajs 与 todos-jquery 查看示例代码。

欢迎大家一起来维护这个项目,寻找更好的前端编程范式。

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions