如何避免经常重写你的Javascript代码

原文信息

原文: Code your JS app like it's 86

原文作者:Victor Perron

原文创作时间:September 28, 2016

翻译作者:无法毕业的fulvaz

Contact:fulvaz@foxmail.com

译者注:
这篇文章非常有趣,作者介绍了利用复古的的图形界面设计模式实现组件间解耦。但说起来古老,实际上说明了flux做了什么事情,以及为什么要有flux。


正文

利用过去的编程范式可以避免重写你的JavaScript应用。

太多时间花在了重写UI上面

我们会出于不同的原因去重写UI。

通常来说,我们会把原因归结到工具上 ---- 这很合理。

框架,库,包管理,guidelines,甚至是语言和元语言本身都变化地非常快,所以现在很难找到一个做应用的普遍适用标准。

更别提测试和测试的方法,那可以另外写一篇文章进行讨论。硬要进行测试,也只是靠眼睛去看他是否可以正常工作。

大多数情况,UI都会连同产品一起发布,用户会花钱买,然后在一个重写了数据库的关键更新后,用户会花钱再买一次。

整个行业的公司都会招新人毕业生,然后叫他们用最新的工具,在最短的时间组装出Web UI,最后,把UI作为产品的一部分卖掉。

最大的问题在于,没人会去考虑UI的长期支持(LTS)和维护 --- 这里的工作量差不多是要建一个生态系统。

我们可以做出改变。我们可以建立一个标准。我们可以专注于写Web UI时出现频率高的问题,然后解决这些问题。而且我们可以用古老的设计模式和编码策略去解决这些问题。

三个错误

下面看一个简单的React component例子。

// FooComponent.js

import react from 'react'
import {ApiClient} from '../api_client'

var FooComponent = React.createClass({
  componentDidMount: function () {
    ApiClient.getTitle().then((data) => this.setState({title: data}))
  },
  handleClick: function (e) {
    e.preventDefault();
    bar_component.resetCoconuts()
  },
  render: function() {
    return (
      <div className="title">{this.state.title}/>
      <div className="button" onClick={this.handleClick}>
    )
  }
})

特别简单的一个例子,你可以看出上面代码有什么问题吗?

不恰当的导入和数据流

当应用的逻辑分散在你的组件和控制器中时,你也只能毫无办法地使用隐式全局变量(hidden globals)去组织你的代码。我用还是使用上面的例子,然后用其他方式重写这段代码。

首先,代码中先导入了ApiClient

import {ApiClient} from '../api_client'

这段代码将你的组件和数据源耦合了起来。这不是一种好的实践。这种设计至少有3个问题。

  • 修改ApiClient会导致FooComponent跟着也要修改
  • 测试FooComponent需要一个mock的ApiClient:比如一个HTTP后端
  • 如果(在页面内)同时有两个FooComponent,那这个页面会想服务器提交两次请求。

但这些都不是主要问题。

主要问题是:这个API client还没有初始化。如果它需要一个base URL或者是一个token呢?

你的组件就需要去给这个API client提供参数。意思是你需要给这个组件提供一些选项的参数,然而这样就违反了关注点分离原则。

const myComponent = FooComponent.bootstrap('#anchor', {
    baseUrl: "https://xxxx",
    token: "MY_TOKEN",
    actualOption: xxxx,
})

这段代码中至少有两个非必要的参数(baseUrltoken)。这两个参数需要在测试的时候mock。

你有见过组件需要传URL才能工作吗?组件只需要数据。

这个组件依赖不可见的全局变量

其次,这段代码还依赖了全局的bar_component去处理点击事件和重置cocounts。 这种写法非常不好。

handleClick: function (e) {
  e.preventDefault();
  bar_component.resetCoconuts()
},

imports里面没任何东西可以提醒我们,这个组件依赖着在window对象的隐式全局变量bar_component.

另外,问题并不仅是因为它是全局的,这种写法还会引起其他的bug。比如说,bar_componenthandleClick()函数的作用域中被定义了。(译注:那么bar_component重置的Cocount就是另外一个Cocount了)

下面列出了不同等级的麻烦:

  • Level 1:你不能在没有BarComponent的情况下测试FooComponent.

  • Level 10:BarComponent并不仅是一个依赖,它需要进行实例化

  • Level 9001:这个实例需要保存在一个全局范围内。而不能通过显式声明,top-level, automatically-retrievable,或者是导入等方式来使用这个实例。

译注1:
此处top-level意思是通过查询依赖链的根部然后使用实例。

译注2:automatically-retrievable:自动查询依赖,RPM的自动查询依赖

对AngularJS用户:遇到这种情况的最常见原因是在FooComponent内注入BarComponent或者ApiService

当然,这些依赖都是可以mock的。(用angular特定的方式) 即便如此,他们依然是需要在定义在某处的全局变量。

他们间产生了耦合。

数据流不明确

一个非常常见的问题:代码各处都是数据查询(XHR,JSONP)

var FooComponent = React.createClass({
  componentDidMount: function () {
    ApiClient.getTitle().then((data) => this.setState({title: data.comment}))
  },
  // […]
})

这段代码中,除了我们刚才的耦合问题,还有数据流不清晰的问题,即你没法清晰地看出HTTP请求在哪,什么时候发出。

更糟糕的是,你部分组件可能依赖于未更新的请求(API可能会改变),而你的应用的其他部分依赖更新过的API请求。

如果通过XHR获得的(JSON内的)comment属性发生了改变,你就要在组件内部修改才能修正你的组件,这看起来并不太对。

这堆错误的实践累加起来,最终就会导致不久后的重写。你需要的是严格的关注点分离。实现的方法是提前计划好,尽可能准确地预估你的应用是做什么的,数据流是怎么样的。

然而,这里还有另一个范式。

"main loop"

下面从介绍一个古老的设计模式开始。

回忆你写代码最开始要做什么:打开一个编辑器,创建一个main()循环,然后在某处输出『Hello World』

这看起来很简单,但在游戏和桌面软件领域,main循环是相关逻辑的骨架。

在Windows API中

下面的简单代码来自Windows API例子:

LRESULT APIENTRY WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    PAINTSTRUCT ps;
    HDC hdc;

    switch (message)
    {
        case WM_PAINT:
            hdc = BeginPaint(hwnd, &ps);
            TextOut(hdc, 0, 0, "Hello, Windows!", 15);
            EndPaint(hwnd, &ps);
            return 0L;
        // Process other messages.
    }
}

int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
                     LPSTR lpCmdLine, int nCmdShow)
{
    HWND hwnd;

    hwnd = CreateWindowEx(
        // parameters
    );

    ShowWindow(hwnd, SW_SHOW);
    UpdateWindow(hwnd);

    return msg.wParam;
}

非常复古的代码,对吧?

分析:WinMain函数使用几个参数创建了应用的window。其中WndProc回调函数负责处理各种事件:用户事件,重绘事件等等

一个main循环,一个事件循环。

就是这样。生存了20年,拥有数百万的应用
的Windows生态系统,只是基于简单的main函数和事件循环。

SDL API里

SDL API主要用来设计游戏。经常被当做轻量级的OpenGL。

下面是一个简单的app

#include <SDL2/SDL.h>
#include <iostream>


int main()
{
    SDL_Window* handle(0);
    SDL_Event events;
    bool end(false);

    if(SDL_Init(SDL_INIT_VIDEO) < 0)
    {
        SDL_Quit();
        return -1;
    }

    handle = SDL_CreateWindow("Test SDL 2.0", SDL_WINDOWPOS_CENTERED,
                              SDL_WINDOWPOS_CENTERED, 800, 600,
                              SDL_WINDOW_SHOWN);
    if(handle == 0)
    {
        SDL_Quit();
        return -1;
    }

    while(!end)
    {
        SDL_WaitEvent(&events);
        if(events.window.event == SDL_WINDOWEVENT_CLOSE)
            end = true;
    }

    SDL_DestroyWindow(handle);
    SDL_Quit();
    return 0;
}

不同的生态系统,但是东西还是那套东西。一个main入口把事情初始化了,一个事件循环负责处理用户输入和其他东西。

OpenGL应用极度简单,你可在这这里找到例子。

不敢相信对吧,就是main循环,初始化。这就是我们今天要说的:几乎所有类型的UI都是基于一个事件循环,然后应用的不同部分触发不同的事件。

那么,为什么大部分前端应用不使用同样的方法呢?

因为没人告诉过我们可以这么用。我们习惯用了jQuery,然后慢慢开始组建Angular组件,访问不知道定义在哪的全局变量。

Javascript应用:Main loop model

我们已经知道了非常简单的Windows API例子、对游戏开发友好的OpenGL和SDL库。

在某种程度上说,一个web界面就是一个简单的图形应用。不同的是它用的是更现代的工具。

如果我们将我们的应用写成这样子

// main.js

import {ApiClient} from './api_client'
import {FooComponent} from './components/foo'
import {BarComponent} from './components/bar'

// Init the components
FooComponent.bootstrap($('#foo_component'), options)
const bar = new BarComponent(document.getElementByID('#bar_component')))

// Get the data
const api = ApiClient.authenticate(getTokenFromStorage())

api.fetchCoconuts(function (coconuts) {
  // Handle-based data passing
  bar.setCoconuts(coconuts)
  // Event-based data passing
  document.dispatchEvent(new Event('data_is_fetched', coconuts))
})

api.fetchTitle(function (data) {
  foo.setTitle(data.title)
})

// Event loop
document.addEventListener('foo:click', function () {
  alert('Foo component was clicked')
  bar.resetCoconuts()
})
// FooComponent.js

import react from 'react'

var FooComponent = React.createClass({
  handleClick: function (e) {
    e.preventDefault();
    // Notify other layers (simplistic, but works)
    document.dispatchEvent(new Event('foo:click'))
  },
  setTitle: function (str) {
    this.setState({title: str}))
  },
  render: function() {
    return (
      <div className="title">{this.state.title}/>
      <div className="button" onClick={this.handleClick}>
    )
  }
})

我们来认真看看上述代码,特别是main.js

我们获得了解耦和的组件,FooComponent和BarComponent都不需要知道数据请求的细节。

他们不需要知道,也不想知道。

他们所需要的是,请求自一个hardcoded fixture的cocount, 关键的是,bar对外暴露一个非常简单的setCocount()API函数。任何应用逻辑都可以在外部使用它。

事件在代码中广播(嫌low使用event bus也可以),不同的组件可以捕获事件然后进行处理。foo显式发送点击事件,在应用中监听这个事件,就可以实现通过点击foo然后重置bar的count。

这样,组件就真正地解除了耦合。他们可以随意重用,而不需要知道组件内部是如何实现的。

只有main循环里面需要写与app相关的业务逻辑。

注:这里没有使用全局变量,我们在main循环中利用了闭包。

总结

文中说的几个原则其实非常简单。在你的应用中使用这种简单的代码结构,然后继续使用这个代码结构。

尽管如此,我们还是看到了许多错误的设计模式。(或者根本没有设计模式)他们甚至连改善设计模式的想法都没有。

在Polyconseil公司,我们使用了就像上文中说的,经历时间洗礼的方法。我们使用了make,而不是GruntGulp,broccoli,详见

我们下次会讨论其他的编程范式,路由,还有一堆我们在Polyconseil和其他地方遇到的常见陷阱。

PS:你可能发发现这些解除耦合的方法很像:Flux

- EOF -

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 196,264评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,549评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,389评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,616评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,461评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,351评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,776评论 3 387
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,414评论 0 255
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,722评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,760评论 2 314
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,537评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,381评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,787评论 3 300
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,030评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,304评论 1 252
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,734评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,943评论 2 336

推荐阅读更多精彩内容