如何避免經(jīng)常重寫你的Javascript代碼

原文信息

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

原文作者:Victor Perron

原文創(chuàng)作時(shí)間:September 28, 2016

翻譯作者:無法畢業(yè)的fulvaz

Contact:fulvaz@foxmail.com

譯者注:
這篇文章非常有趣,作者介紹了利用復(fù)古的的圖形界面設(shè)計(jì)模式實(shí)現(xiàn)組件間解耦。但說起來古老,實(shí)際上說明了flux做了什么事情,以及為什么要有flux。


正文

利用過去的編程范式可以避免重寫你的JavaScript應(yīng)用。

太多時(shí)間花在了重寫UI上面

我們會出于不同的原因去重寫UI。

通常來說,我們會把原因歸結(jié)到工具上 ---- 這很合理。

框架,庫,包管理,guidelines,甚至是語言和元語言本身都變化地非常快,所以現(xiàn)在很難找到一個做應(yīng)用的普遍適用標(biāo)準(zhǔn)。

更別提測試和測試的方法,那可以另外寫一篇文章進(jìn)行討論。硬要進(jìn)行測試,也只是靠眼睛去看他是否可以正常工作。

大多數(shù)情況,UI都會連同產(chǎn)品一起發(fā)布,用戶會花錢買,然后在一個重寫了數(shù)據(jù)庫的關(guān)鍵更新后,用戶會花錢再買一次。

整個行業(yè)的公司都會招新人畢業(yè)生,然后叫他們用最新的工具,在最短的時(shí)間組裝出Web UI,最后,把UI作為產(chǎn)品的一部分賣掉。

最大的問題在于,沒人會去考慮UI的長期支持(LTS)和維護(hù) --- 這里的工作量差不多是要建一個生態(tài)系統(tǒng)。

我們可以做出改變。我們可以建立一個標(biāo)準(zhǔn)。我們可以專注于寫Web UI時(shí)出現(xiàn)頻率高的問題,然后解決這些問題。而且我們可以用古老的設(shè)計(jì)模式和編碼策略去解決這些問題。

三個錯誤

下面看一個簡單的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}>
    )
  }
})

特別簡單的一個例子,你可以看出上面代碼有什么問題嗎?

不恰當(dāng)?shù)膶?dǎo)入和數(shù)據(jù)流

當(dāng)應(yīng)用的邏輯分散在你的組件和控制器中時(shí),你也只能毫無辦法地使用隱式全局變量(hidden globals)去組織你的代碼。我用還是使用上面的例子,然后用其他方式重寫這段代碼。

首先,代碼中先導(dǎo)入了ApiClient

import {ApiClient} from '../api_client'

這段代碼將你的組件和數(shù)據(jù)源耦合了起來。這不是一種好的實(shí)踐。這種設(shè)計(jì)至少有3個問題。

  • 修改ApiClient會導(dǎo)致FooComponent跟著也要修改
  • 測試FooComponent需要一個mock的ApiClient:比如一個HTTP后端
  • 如果(在頁面內(nèi))同時(shí)有兩個FooComponent,那這個頁面會想服務(wù)器提交兩次請求。

但這些都不是主要問題。

主要問題是:這個API client還沒有初始化。如果它需要一個base URL或者是一個token呢?

你的組件就需要去給這個API client提供參數(shù)。意思是你需要給這個組件提供一些選項(xiàng)的參數(shù),然而這樣就違反了關(guān)注點(diǎn)分離原則。

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

這段代碼中至少有兩個非必要的參數(shù)(baseUrltoken)。這兩個參數(shù)需要在測試的時(shí)候mock。

你有見過組件需要傳URL才能工作嗎?組件只需要數(shù)據(jù)。

這個組件依賴不可見的全局變量

其次,這段代碼還依賴了全局的bar_component去處理點(diǎn)擊事件和重置cocounts。 這種寫法非常不好。

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

imports里面沒任何東西可以提醒我們,這個組件依賴著在window對象的隱式全局變量bar_component.

另外,問題并不僅是因?yàn)樗侨值模@種寫法還會引起其他的bug。比如說,bar_componenthandleClick()函數(shù)的作用域中被定義了。(譯注:那么bar_component重置的Cocount就是另外一個Cocount了)

下面列出了不同等級的麻煩:

  • Level 1:你不能在沒有BarComponent的情況下測試FooComponent.

  • Level 10:BarComponent并不僅是一個依賴,它需要進(jìn)行實(shí)例化

  • Level 9001:這個實(shí)例需要保存在一個全局范圍內(nèi)。而不能通過顯式聲明,top-level, automatically-retrievable,或者是導(dǎo)入等方式來使用這個實(shí)例。

譯注1:
此處top-level意思是通過查詢依賴鏈的根部然后使用實(shí)例。

譯注2:automatically-retrievable:自動查詢依賴,RPM的自動查詢依賴

對AngularJS用戶:遇到這種情況的最常見原因是在FooComponent內(nèi)注入BarComponent或者ApiService

當(dāng)然,這些依賴都是可以mock的。(用angular特定的方式) 即便如此,他們依然是需要在定義在某處的全局變量。

他們間產(chǎn)生了耦合。

數(shù)據(jù)流不明確

一個非常常見的問題:代碼各處都是數(shù)據(jù)查詢(XHR,JSONP)

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

這段代碼中,除了我們剛才的耦合問題,還有數(shù)據(jù)流不清晰的問題,即你沒法清晰地看出HTTP請求在哪,什么時(shí)候發(fā)出。

更糟糕的是,你部分組件可能依賴于未更新的請求(API可能會改變),而你的應(yīng)用的其他部分依賴更新過的API請求。

如果通過XHR獲得的(JSON內(nèi)的)comment屬性發(fā)生了改變,你就要在組件內(nèi)部修改才能修正你的組件,這看起來并不太對。

這堆錯誤的實(shí)踐累加起來,最終就會導(dǎo)致不久后的重寫。你需要的是嚴(yán)格的關(guān)注點(diǎn)分離。實(shí)現(xiàn)的方法是提前計(jì)劃好,盡可能準(zhǔn)確地預(yù)估你的應(yīng)用是做什么的,數(shù)據(jù)流是怎么樣的。

然而,這里還有另一個范式。

"main loop"

下面從介紹一個古老的設(shè)計(jì)模式開始。

回憶你寫代碼最開始要做什么:打開一個編輯器,創(chuàng)建一個main()循環(huán),然后在某處輸出『Hello World』

這看起來很簡單,但在游戲和桌面軟件領(lǐng)域,main循環(huán)是相關(guān)邏輯的骨架。

在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;
}

非常復(fù)古的代碼,對吧?

分析:WinMain函數(shù)使用幾個參數(shù)創(chuàng)建了應(yīng)用的window。其中WndProc回調(diào)函數(shù)負(fù)責(zé)處理各種事件:用戶事件,重繪事件等等

一個main循環(huán),一個事件循環(huán)。

就是這樣。生存了20年,擁有數(shù)百萬的應(yīng)用
的Windows生態(tài)系統(tǒng),只是基于簡單的main函數(shù)和事件循環(huán)。

SDL API里

SDL API主要用來設(shè)計(jì)游戲。經(jīng)常被當(dāng)做輕量級的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;
}

不同的生態(tài)系統(tǒng),但是東西還是那套東西。一個main入口把事情初始化了,一個事件循環(huán)負(fù)責(zé)處理用戶輸入和其他東西。

OpenGL應(yīng)用極度簡單,你可在這這里找到例子。

不敢相信對吧,就是main循環(huán),初始化。這就是我們今天要說的:幾乎所有類型的UI都是基于一個事件循環(huán),然后應(yīng)用的不同部分觸發(fā)不同的事件。

那么,為什么大部分前端應(yīng)用不使用同樣的方法呢?

因?yàn)闆]人告訴過我們可以這么用。我們習(xí)慣用了jQuery,然后慢慢開始組建Angular組件,訪問不知道定義在哪的全局變量。

Javascript應(yīng)用:Main loop model

我們已經(jīng)知道了非常簡單的Windows API例子、對游戲開發(fā)友好的OpenGL和SDL庫。

在某種程度上說,一個web界面就是一個簡單的圖形應(yīng)用。不同的是它用的是更現(xiàn)代的工具。

如果我們將我們的應(yīng)用寫成這樣子

// 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}>
    )
  }
})

我們來認(rèn)真看看上述代碼,特別是main.js

我們獲得了解耦和的組件,F(xiàn)ooComponent和BarComponent都不需要知道數(shù)據(jù)請求的細(xì)節(jié)。

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

他們所需要的是,請求自一個hardcoded fixture的cocount, 關(guān)鍵的是,bar對外暴露一個非常簡單的setCocount()API函數(shù)。任何應(yīng)用邏輯都可以在外部使用它。

事件在代碼中廣播(嫌low使用event bus也可以),不同的組件可以捕獲事件然后進(jìn)行處理。foo顯式發(fā)送點(diǎn)擊事件,在應(yīng)用中監(jiān)聽這個事件,就可以實(shí)現(xiàn)通過點(diǎn)擊foo然后重置bar的count。

這樣,組件就真正地解除了耦合。他們可以隨意重用,而不需要知道組件內(nèi)部是如何實(shí)現(xiàn)的。

只有main循環(huán)里面需要寫與app相關(guān)的業(yè)務(wù)邏輯。

注:這里沒有使用全局變量,我們在main循環(huán)中利用了閉包。

總結(jié)

文中說的幾個原則其實(shí)非常簡單。在你的應(yīng)用中使用這種簡單的代碼結(jié)構(gòu),然后繼續(xù)使用這個代碼結(jié)構(gòu)。

盡管如此,我們還是看到了許多錯誤的設(shè)計(jì)模式。(或者根本沒有設(shè)計(jì)模式)他們甚至連改善設(shè)計(jì)模式的想法都沒有。

在Polyconseil公司,我們使用了就像上文中說的,經(jīng)歷時(shí)間洗禮的方法。我們使用了make,而不是GruntGulp,broccoli,詳見

我們下次會討論其他的編程范式,路由,還有一堆我們在Polyconseil和其他地方遇到的常見陷阱。

PS:你可能發(fā)發(fā)現(xiàn)這些解除耦合的方法很像:Flux

- EOF -

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,501評論 6 544
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,673評論 3 429
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,610評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,939評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 72,668評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 56,004評論 1 329
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,001評論 3 449
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 43,173評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,705評論 1 336
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 41,426評論 3 359
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,656評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,139評論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,833評論 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,247評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,580評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,371評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,621評論 2 380

推薦閱讀更多精彩內(nèi)容