Ractive.js - Web 應用程序變得簡單

已發表: 2022-03-11

在當今 JavaScript 框架和庫迅速增長的環境中,選擇您想要開發的框架和庫可能是一個相當大的挑戰。 畢竟,一旦你走上了使用特定框架的道路,將代碼遷移到使用不同的框架是一項非常重要的任務,你可能永遠沒有時間或預算來執行。

那麼,為什麼選擇 Ractive.js?

與其他生成惰性 HTML 的工具不同,Ractive 將模板轉換為默認交互應用程序的藍圖。 儘管人們可以肯定地說 Ractive 的貢獻是進化而不是革命,但它的價值仍然很重要。

Ractive 之所以如此有用,是因為它提供了強大的功能,而且以一種對開發人員來說非常簡單易用的方式來實現。 此外,它相當優雅、快速、不顯眼且體積小。

在本文中,我們將介紹構建一個簡單的 Ractive 搜索應用程序的過程,展示 Ractive 的一些關鍵功能以及它幫助簡化 Web 應用程序和開發的方式。

Ractive.js 和網絡應用

什麼是 Ractive.js?

Ractive 最初是為了以更優雅的方式解決數據綁定問題而創建的。 為此,它採用模板並將它們轉換為 DOM 的輕量級虛擬表示,以便當數據發生變化時,真實的 DOM 會智能且有效地更新。

但很快就發現,Ractive 採用的方法和基礎設施也可以用來更有效地做其他事情。 例如,它可以自動處理諸如重用事件處理程序之類的事情,並在不再需要它們時自動解除綁定。 事件委託變得不必要了。 與數據綁定一樣,這種方法可以防止代碼隨著應用程序的增長而變得笨拙。

開箱即用地提供了雙向綁定、動畫和 SVG 支持等關鍵功能,並且可以通過插件輕鬆添加自定義功能。

一些工具和框架會迫使你學習新的詞彙並以特定的方式構建你的應用程序,而 Ractive 對你有用,而不是相反。 它還與其他庫很好地集成。

我們的示例應用程序

我們的示例應用程序將用於根據技能搜索 Toptal 開發人員數據庫。 我們的應用程序將有兩個視圖:

  • 搜索:內嵌搜索框的技能列表
  • 結果:技能視圖,包括開發人員列表

對於每個開發者,我們將顯示他們的姓名、照片、簡短描述和技能列表(每個技能將鏈接到相應的技能視圖)。

(注意:本文末尾提供了應用程序的在線工作實例和源代碼存儲庫的鏈接。)

為了保持我們對 Ractive 框架的主要關注,我們將採用一些通常應該在生產中進行的簡化:

  • 默認主題。 我們將使用帶有默認主題的 Bootstrap 進行樣式設置,而不是自定義主題以適合您的應用程序樣式。
  • 依賴關係。 我們將添加我們的依賴項作為定義全局變量的單獨腳本(而不是使用 ES6 模塊、CommonJS 或 AMD 以及適當的加載器進行開發,並使用構建步驟進行生產)。
  • 靜態數據。 我們將使用我通過抓取 Toptal 網站上的公共頁面準備的靜態數據。
  • 沒有客戶端路由。 這意味著當我們在視圖之間切換時,URL 將保持不變。 您絕對不應該對 SPA 這樣做,儘管對於一些小型交互式組件可能沒問題。 Ractive 沒有內置的路由器實現,但它可以與 3rd 方路由器一起使用,如本例所示。
  • 在 HTML 中的 script 標籤內定義的模板。 這不一定是一個壞主意,尤其是對於小型應用程序,它有一些優點(它很簡單,您可以將這些客戶端模板與服務器端模板一起處理,例如用於國際化)。 但是對於更大的應用程序,您可以從預編譯(也就是將模板預解析為內部 JS 表示)中受益。

讓我們開始使用 Web 應用程序

好的,話雖如此,讓我們開始構建應用程序。 我們將以迭代的方式進行,逐個添加較小的特徵,並在遇到它們時探索概念。

讓我們首先創建一個包含兩個文件的文件夾: index.htmlscript.js 。 我們的應用程序將非常簡單,並且將使用file://協議來避免啟動開發服務器的需要(儘管如果您願意,您也可以這樣做)。

搜索頁面

我們將從實現搜索頁面開始,用戶可以在其中選擇一項技能,以便在 Toptal 數據庫中找到匹配的開發人員。

HTML 骨架

讓我們從這個簡單的 HTML 頁面開始:

 <html> <head> <title>Toptal Search</title> <!-- LOAD BOOTSTRAP FROM THE CDN --> <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css"> </head> <body> <!-- SOME BASIC STATIC CONTENT --> <div class="container"> <h1>Toptal Search</h1> </div> <!-- LOAD THE JAVASCRIPT LIBRARIES WE NEED --> <script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.9.3/lodash.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/ractive/0.7.3/ractive.min.js"></script> <!-- LOAD THE DATA --> <script src="https://rawgit.com/emirotin/toptal-blog-ractive/master/data.js"></script> <!-- LOAD OUR SCRIPT --> <script src="script.js"></script> </body> </html>

如您所見,這是一個簡單的 HTML5 文檔。 它從 CDN、Lodash(一個很棒的數據操作庫)和 Ractive.js 加載 Bootstrap。

Ractive 不需要將整個頁面都用於 SPA,因此我們可以擁有一些靜態內容。 在我們的例子中,它由一個容器元素和頁面標題組成。

最後,我們加載我為演示準備的數據,以及包含我們程序的腳本。

好的,現在我們的 HTML 框架已經到位,讓我們開始添加一些真正的功能。

技能清單

我特別喜歡 Ractive 的一件事是它如何教你思考你想要實現的最終表示(HTML),然後讓你專注於編寫必要的代碼來實現它。

所以首先,讓我們建立一個技能列表作為我們的初始視圖。 這樣做只需要:

  • 添加將顯示技能列表的 HTML 元素。
  • 將一小段模板代碼添加到我們的 HTML 中。
  • 編寫一些簡短的 JavaScript 來為模板提供數據,以在我們添加的 HTML 元素中呈現它。

HTML 的 mods 包括以下內容:

 <div class="container"> <h1>Toptal Search</h1> <div></div> <!-- THIS IS THE NEW HTML ELEMENT --> </div> <!-- THIS IS THE SMALL SNIPPET OF TEMPLATE CODE --> <script type="text/html"> <div class="row"> {{#each skills}} <span class="col-xs-3"> <a href="#" class="label label-primary">{{this}}</a> </span> {{/each}} </div> </script>

Ractive 沒有特殊的約定來指定 HTML 元素來接收要顯示的數據,但最簡單的方法是為元素添加一個 ID。 為此,我通常使用“根”ID。 我們很快就會看到它在 Ractive 初始化時是如何使用的。 對於那些好奇的人,還有其他方法可以指定根元素。

帶有type="text/html"的略顯尷尬的 script 元素是一個巧妙的技巧,它可以讓瀏覽器加載一大段 HTML,而無需對其進行解析或渲染,因為瀏覽器會忽略未知類型的腳本。 腳本的內容是一個類似 Mustache/Handlebars 的模板(儘管 Ractive 確實有一些擴展)。

我們首先編寫模板代碼,假設我們可以訪問技能數組。 我們使用{{#each}} mustache 指令來定義迭代。 在指令內部,可以通過this訪問當前元素。 同樣,我們假設技能變量包含一個字符串數組,因此我們只需使用插值鬍鬚{{this}}對其進行渲染。

好的,這就是 HTML。 但是 JavaScript 呢? 這就是將數據提供給模板的“魔法”發生的地方:

 (function () { var skills = DB.skills, developers = DB.developers; var app = new Ractive({ el: '#root', template: '#tpl-app', data: { skills: _.keys(DB.skills) } }); }());

令人印象深刻,不是嗎? 僅用這 10 行代碼,我們就能夠:

  1. 從“數據庫”中“檢索”數據。
  2. 實例化一個新的 Ractive 應用。
  3. 告訴它在元素內部渲染.
  4. 告訴它使用腳本元素獲取模板(還有其他方法可以這樣做)。
  5. 傳遞初始數據(我們將看到如何在運行時更改它),或者如果您習慣於 Angular 術語,則傳遞“範圍”。
  6. 使用 lodash keys方法獲取我們在“DB”中用作對象鍵的技能名稱。

所以基本上使用這個腳本,我們告訴框架要做什麼,而不是如何去做。 模板聲明瞭如何,我個人覺得這非常神奇和自然。

希望你開始明白這個想法,所以現在讓我們在現有的基礎上實現一些更有用的東西。

技能搜索

當然,搜索頁面需要一個搜索字段。 我們希望它提供一種交互功能,當用戶在搜索框中鍵入時,技能列表會被過濾掉,只包括那些包含輸入的子字符串的技能(過濾不區分大小寫)。

與 Ractive 一樣,我們從定義模板開始(同時考慮需要哪些新的上下文變量,以及在數據管理方面會發生什麼變化):

 <div class="container"> <h1>Toptal Search</h1> <div></div> </div> <!-- THIS IS THE SMALL SNIPPET OF TEMPLATE CODE --> <script type="text/html"> <!-- HERE'S OUR SEARCH BOX --> <div class="row"> <form class="form-horizontal col-xs-6 col-xs-offset-6"> <input type="search" class="form-control" value="{{ skillFilter }}" placeholder="Type part of the skill name here"> </form> </div> <hr> <!-- NOW INSTEAD OF DISPLAYING ALL SKILLS, WE INVOKE A TO-BE-CREATED JAVASCRIPT skills() FUNCTION THAT WILL FILTER THE SKILL LIST DOWN TO THOSE THAT MATCH THE TEXT ENTERED BY THE USER --> <div class="row"> {{#each skills()}} <span class="col-xs-3"> <a href="#" class="label label-primary">{{this}}</a> </span> {{/each}} </div> </script>

沒有太多變化,但仍有相當多的東西需要學習。

首先,我們添加了一個包含搜索框的新 <div>。 很明顯,我們希望將該輸入連接到某個變量(除非您懷念 jQuery 湯的美好時光)。 Ractive 支持所謂的雙向綁定,這意味著您的 JS 代碼可以檢索一個值,而無需手動從 DOM 中讀取它。 在我們的例子中,這是通過使用插值 mustache value="{{ skillFilter }}"來完成的。 Ractive 理解我們想要將此變量綁定到輸入的 value 屬性。 所以它會為我們觀察輸入並自動更新變量。 非常簡潔,只有 1 行 HTML。

其次,正如上面代碼片段中的註釋中所解釋的,現在我們將創建一個skills() JS 函數來過濾技能列表並僅返回與用戶輸入的文本匹配的技能,而不是顯示所有技能:

 // store skill list in a variable outside of Ractive scope var skillNames = _.keys(DB.skills); var app = new Ractive({ el: '#root', template: '#tpl-app', data: { // initializing the context variable is not strictly // required, but it is generally considered good practice skillFilter: null, // Define the skills() function right in our data object. // Function is available to our template where we call it. skills: function() { // Get the skillFilter variable from the Ractive instance // (available as 'this'). // NOTE WELL: Our use of a getter here tells Ractive that // our function has a *dependency* on the skillFilter // value, so this is significant. var skillFilter = this.get('skillFilter'); if (!skillFilter) { return skillNames; } skillFilter = new RegExp(_.escapeRegExp(skillFilter), 'i') return _.filter(skillNames, function(skill) { return skill.match(skillFilter); }); } } });

雖然這很乾淨且易於實現,但您可能想知道它如何影響性能。 我的意思是,我們每次都要調用一個函數? 那麼,每次什麼? Ractive 足夠聰明,只在它們的依賴項(變量)發生變化時重新渲染模板的一部分(並從中調用任何函數)(由於使用了 setter,Ractive 知道何時發生這種情況)。

順便說一句,對於那些有興趣更進一步的人,還有一種更優雅的方法可以使用計算屬性來實現這一點,但如果你願意,我會留給你自己玩。

結果頁面

現在我們有了一個非常有用的可搜索技能列表,讓我們繼續查看結果視圖,其中將顯示匹配的開發人員列表。

在技​​能視圖之間切換

顯然有多種方法可以實現這一點。 根據是否選擇了技能,我選擇了兩種不同視圖的方法。 如果是這樣,我們會顯示匹配的開發人員列表; 如果沒有,我們會顯示技能列表和搜索框。

因此,對於初學者來說,當用戶選擇(即單擊)技能名稱時,需要隱藏技能列表,而應將技能名稱顯示為頁面標題。 相反,在選定的技能視圖上,需要有一種方法可以關閉該視圖並返回技能列表。

這是我們在這條道路上的第一步:

 <script type="text/html"> <!-- PARTIAL IS A NEW CONCEPT HERE --> {{#partial skillsList}} <div class="row"> <form class="form-horizontal col-xs-6 col-xs-offset-6"> <input type="search" class="form-control" value="{{ skillFilter }}" placeholder="Type part of the skill name here"> </form> </div> <hr> <div class="row"> {{#each skills()}} <span class="col-xs-3"> <!-- MAKE OUR SKILLS CLICKABLE, USING PROXY EVENTS --> <a href="#" class="label label-primary" on-click="select-skill:{{this}}">{{this}}</a> </span> {{/each}} </div> {{/partial}} {{#partial skillView}} <h2> <!-- DISPLAY SELECTED SKILL AS HEADING ON THE PAGE --> {{ currentSkill }} <!-- CLOSE BUTTON TAKES USER BACK TO SKILLS LIST --> <button type="button" class="close pull-right" on-click="deselect-skill">&times; CLOSE</button> </h2> {{/partial}} <!-- PARTIALS ARE NOT IN THE VIEW UNTIL WE EXPLICITLY INCLUDE THEM, SO INCLUDE THE PARTIAL RELEVANT TO THE CURRENT VIEW. --> {{#if currentSkill}} {{> skillView}} {{else}} {{> skillsList}} {{/if}} </script>

好的,這裡發生了很多事情。

首先,為了適應將其實現為兩個不同的視圖,我將我們目前擁有的所有內容(即列表視圖)移到了一個叫做局部視圖的東西上。 部分本質上是一塊模板代碼,我們將在不同的地方(很快)包含。

然後,我們想讓我們的技能可以點擊,當它被點擊時,我們想要導航到相應的技能視圖。 為此,我們使用一種稱為代理事件的東西,藉此我們對物理事件(點擊時,名稱是 Ractive 可以理解的名稱)做出反應,並將其代理到邏輯事件(選擇技能,名稱就是我們所說的) ) 傳遞參數(你可能還記得這代表這裡的技能名稱)。

(僅供參考,存在另一種方法調用語法來完成同樣的事情。)

接下來,我們(再次)假設我們將有一個名為currentSkill的變量,該變量將具有所選技能的名稱(如果有),或者如果沒有選擇任何技能則為空。 所以我們定義了另一個顯示當前技能名稱的部分,還有一個“關閉”鏈接應該取消選擇技能。

對於 JavaScript,主要添加的是訂閱 select-skill 和 deselect-skill 事件的代碼,相應地更新currentSkill (和skillFilter ):

 var app = new Ractive({ el: '#root', template: '#tpl-app', data: { skillFilter: null, currentSkill: null, // INITIALIZE currentSkill TO null // skills function remains unchanged skills: function() { var skillFilter = this.get('skillFilter'); if (!skillFilter) { return skillNames; } skillFilter = new RegExp(_.escapeRegExp(skillFilter), 'i') return _.filter(skillNames, function(skill) { return skill.match(skillFilter); }); } } }); // SUBSCRIBE TO LOGICAL EVENT select-skill app.on('select-skill', function(event, skill) { this.set({ // SET currentSkill TO THE SKILL SELECTED BY THE USER currentSkill: skill, // RESET THE SEARCH FILTER skillFilter: null }); }); // SUBSCRIBE TO LOGICAL EVENT deselect-skill app.on('deselect-skill', function(event) { this.set('currentSkill', null); // CLEAR currentSkill });

列出每種技能的開發人員

為技能準備好新視圖後,我們現在可以添加一些內容——我們為該列表擁有的實際開發人員列表。 為此,我們將此視圖的部分擴展如下:

 {{#partial skillView}} <h2> {{ currentSkill }} <button type="button" class="close pull-right" on-click="deselect-skill">&times; CLOSE</button> </h2> {{#each skillDevelopers(currentSkill)}} <div class="panel panel-default"> <div class="panel-body"> {{ this.name }} </div> </div> {{/each}} {{/partial}}

希望到此為止,您已經對這裡發生的事情有所了解:我們在skillView部分中添加了一個新的迭代部分,它迭代了我們接下來要編寫的新skillDevelopers函數的結果。 對於數組中的每個開發人員(由該skillDevelopers函數返回),我們渲染一個面板並顯示開發人員的姓名。 請注意,我可以在這裡使用隱式形式{{name}}並且 Ractive 會通過從當前上下文(在我們的例子中是由{{#each}}綁定的開發人員對象)開始搜索上下文鏈來找到正確的屬性,但是我更喜歡明確。 Ractive 文檔中提供了有關上下文和引用的更多信息。

這是skillDevelopers()函數的實現:

 skillDevelopers: function(skill) { // GET THE SKILL OBJECT FROM THE “DB” skill = skills[skill]; // SAFETY CHECK, RETURN EARLY IN CASE OF UNKNOWN SKILL NAME if (!skill) { return; } // MAP THE DEVELOPER'S IDs (SLUGS) TO THE // ACTUAL DEVELOPER DETAIL OBJECTS return _.map(skill.developers, function(slug) { return developers[slug]; }); }

擴展每個開發人員的入口

現在我們有了一個開發人員的工作列表,是時候添加更多細節了,當然還有一張漂亮的照片:

 {{#partial skillView}} <h2> {{ currentSkill }} <button type="button" class="close pull-right" on-click="deselect-skill">&times; CLOSE</button> </h2> {{#each skillDevelopers(currentSkill)}} <div class="panel panel-default"> <div class="panel-body media"> <div class="media-left"> <!-- ADD THE PHOTO --> <img class="media-object img-circle" width="64" height="64" src="{{ this.photo }}" alt="{{ this.name }}"> </div> <div class="media-body"> <!-- MAKE THE DEVELOPER'S NAME A HYPERLINK TO THEIR PROFILE --> <a class="h4 media-heading" href="{{ this.url }}" target="_blank"> {{ this.name }}</a> <!-- ADD MORE DETAILS (FROM THEIR PROFILE) --> <p>{{ this.desc }}</p> </div> </div> </div> {{/each}} {{/partial}}

從 Ractive 方面來看,這裡沒有什麼新東西,但是對 Bootstrap 功能的使用有點重。

顯示可點擊的開發人員技能列表

到目前為止,我們已經取得了不錯的進展,但仍然缺少一個功能,那就是技能開發者關係的另一面; 即,我們希望顯示每個開發人員的技能,並且我們希望這些技能中的每一個都成為一個可點擊的鏈接,將我們帶到該技能的結果視圖。

但是等等……我確定我們在技能列表中已經有了完全相同的東西。 是的,事實上,我們做到了。 它在可點擊的技能列表中,但它來自與每個開發人員的技能集不同的數組。 然而,它們非常相似,這對我來說是一個明確的信號,我們應該重用那塊 HTML。 為此,partials 是我們的朋友。

(注意:Ractive 還有一種更高級的方法來處理可重用的視圖塊,稱為組件。它們確實非常有用,但為了簡單起見,我們現在不討論它們。)

下面是我們如何使用 partials 來實現這一點(順便提一下,我們可以在不添加任何 JavaScript 代碼的情況下添加此功能!):

 <!-- MAKE THE CLICKABLE SKILL LINK INTO ITS OWN “skill” PARTIAL --> {{#partial skill}} <a href="#" class="label label-primary" on-click="select-skill:{{this}}">{{this}}</a> {{/partial}} {{#partial skillsList}} <div class="row"> <form class="form-horizontal col-xs-6 col-xs-offset-6"> <input type="search" class="form-control" value="{{ skillFilter }}" placeholder="Type part of the skill name here"> </form> </div> <hr> <div class="row"> {{#each skills()}} <!-- USE THE NEW “skill” PARTIAL --> <span class="col-xs-3">{{> skill}}</span> {{/each}} </div> {{/partial}} {{#partial skillView}} <h2> {{ currentSkill }} <button type="button" class="close pull-right" on-click="deselect-skill">&times; CLOSE</button> </h2> {{#each skillDevelopers(currentSkill)}} <div class="panel panel-default"> <div class="panel-body media"> <div class="media-left"> <img class="media-object img-circle" width="64" height="64" src="{{ this.photo }}" alt="{{ this.name }}"> </div> <div class="media-body"> <a class="h4 media-heading" href="{{ this.url }}" target="_blank">{{ this.name }}</a> <p>{{ this.desc }}</p> <p> <!-- ITERATE OVER THE DEVELOPER'S SKILLS --> {{#each this.skills}} <!-- REUSE THE NEW “skill” PARTIAL TO DISPLAY EACH DEVELOPER SKILL AS A CLICKABLE LINK --> {{> skill}}&nbsp; {{/each}} </p> </div> </div> </div> {{/each}} {{/partial}}

我已經將技能列表附加到我們從“DB”中檢索到的開發人員對像上,所以我只是稍微更改了模板:我將呈現技能標籤的行移到了部分,並在該行原來的位置使用了這個部分。

然後,當我迭代開發人員的技能時,我可以重用這個相同的新部分來將這些技能中的每一個也顯示為可點擊的鏈接。 此外,如果你記得的話,這也代理了 select-skill 事件,傳遞了相同的技能名稱。 這意味著我們可以將它包含在任何地方(具有適當的上下文),我們將獲得一個可點擊的標籤,該標籤會導致技能視圖!

Final Touch - 預加載器

好的,我們現在有一個基本的功能應用程序。 它很乾淨並且運行速度很快,但仍然需要一些時間來加載。 (在我們的例子中,這部分是由於我們使用了非串聯、非縮小的源,但在現實世界的應用程序中可能還有其他重要原因,例如加載初始數據)。

因此,作為最後一步,我將向您展示一個巧妙的技巧來添加一個預加載動畫,該動畫將在 Ractive 渲染我們的應用程序時立即刪除:

 <div class="container"> <h1>Toptal Search</h1> <div> <div class="progress"> <div class="progress-bar progress-bar-striped active"> Loading... </div> </div> </div> </div>

那麼這裡有什麼魔力呢? 其實很簡單。 我直接向我們的根元素添加了一些內容(它是 Bootstrap 動畫進度條,但也可以是動畫 GIF 或其他)。 我認為它非常聰明——當我們的腳本正在加載時,用戶會看到加載指示器(因為它不依賴於 JavaScript,它可以立即顯示)。 但是,一旦 Ractive 應用程序被初始化,Ractive 就會用渲染的模板覆蓋根元素的內容(從而擦除預加載動畫)。 這樣,我們就可以實現這個效果,只需要一段靜態的 HTML 和 0 行邏輯。 我覺得這很酷。

結論

想想我們在這裡完成了什麼,以及我們完成它的難易程度。 我們有一個非常全面的應用程序:它顯示技能列表,允許快速搜索它們(甚至支持在用戶在搜索框中鍵入時交互式更新技能列表),允許導航到特定技能並返回,以及列表每個選定技能的開發人員。 此外,我們可以單擊任何開發人員列出的任何技能,將我們帶到具有該技能的開發人員列表。 所有這一切都只需要不到 80 行 HTML 和不到 40 行 JavaScript。 在我看來,這令人印象深刻,充分說明了 Ractive 的強大、優雅和簡單。

該應用程序的工作版本可在此處在線獲得,完整的源代碼已公開並可在此處獲得。

當然,在這篇文章中,我們只是觸及了 Ractive 框架可能實現的功能的皮毛。 如果您喜歡到目前為止所看到的內容,我強烈建議您開始使用 Ractive 的 60 秒設置,並開始自己探索 Ractive 所提供的一切。