我的 API 驅動網站如何幫助我環遊世界
已發表: 2022-03-10(這是一個贊助帖子。)最近,我決定重建我的個人網站,因為它已經有六年曆史了,而且看起來——禮貌地說——有點“過時”。 目標是包括一些關於我自己的信息、一個博客區域、我最近的副項目列表以及即將發生的事件。
當我不時地做客戶工作時,我不想處理一件事——數據庫! 以前,我為所有想要我的人建立了 WordPress 網站。 編程部分對我來說通常很有趣,但是發布、將數據庫移動到不同的環境以及實際發布,總是很煩人。 廉價的託管服務提供商只提供糟糕的 Web 界面來設置 MySQL 數據庫,而通過 FTP 訪問上傳文件一直是最糟糕的部分。 我不想為我的個人網站處理這個問題。
所以我對重新設計的要求是:
- 基於 JavaScript 和前端技術的最新技術堆棧。
- 一種內容管理解決方案,可從任何地方編輯內容。
- 一個性能良好的網站,結果很快。
在本文中,我想向您展示我構建的內容以及我的網站如何令人驚訝地成為我的日常伴侶。
定義內容模型
在網絡上發布東西似乎很容易。 選擇一個內容管理系統 ( CMS ),它為每個需要的頁面提供所見即所得的編輯器(您所見即所得),並且所有編輯器都可以輕鬆管理內容。 就是這樣,對吧?
在建立了幾個客戶網站之後,從小咖啡館到成長中的初創公司,我發現神聖的 WYSIWYG 編輯器並不總是我們都在尋找的靈丹妙藥。 這些界面旨在使構建網站變得容易,但重點是:
建立網站並不容易
要在不經常破壞網站的情況下構建和編輯網站的內容,您必須對 HTML 有深入的了解,並且至少了解一點 CSS。 這不是你可以從你的編輯那裡得到的。
我見過用所見即所得編輯器構建的可怕的複雜佈局,當一切都因係統太脆弱而崩潰時,我無法開始命名所有情況。 這些情況會導致爭吵和不適,各方都在為不可避免的事情互相指責。 我總是試圖避免這些情況,並為編輯創造舒適、穩定的環境,以避免憤怒的電子郵件尖叫,“救命! 一切都被打破。”
結構化內容為您省去了一些麻煩
我很快就了解到,當我將所有需要的網站內容分成幾個塊時,人們很少會破壞事物,每個塊相互關聯而不考慮任何表示。 在 WordPress 中,這可以使用自定義帖子類型來實現。 每個自定義帖子類型都可以包含多個屬性,這些屬性具有自己易於掌握的文本字段。 我把思考的概念完全埋沒在頁面中。
我的工作是連接內容片段並從這些內容塊中構建網頁。 這意味著編輯只能在他們的網站上做很少的視覺變化。 他們對內容負責,而且只對內容負責。 視覺上的改變必須由我來完成——不是每個人都能為網站設計樣式,我們可以避免脆弱的環境。 這個概念感覺像是一個很好的權衡,通常很受歡迎。
後來,我發現我正在做的是定義一個內容模型。 Rachel Lovinger 在她的優秀文章“內容建模:一項大師技能”中定義瞭如下內容模型:
“內容模型記錄了給定項目將擁有的所有不同類型的內容。 它包含每種內容類型元素及其相互關係的詳細定義。”
從內容建模開始對大多數客戶來說都很好,除了一個。
“Stefan,我沒有定義你的數據庫模式!”
這個項目的想法是建立一個龐大的網站,該網站應該通過提供大量內容來創造大量自然流量——在多個不同頁面和位置顯示的所有變體中。 我召開了一次會議,討論我們處理這個項目的策略。
我想定義所有應該包含的頁面和內容模型。 無論客戶想到什麼小部件或側邊欄,我都希望它被明確定義。 我的目標是創建一個可靠的內容結構,以便為編輯器提供易於使用的界面,並提供可重用的數據以任何可以想到的格式顯示。
事實證明,這個項目的想法不是很清楚,我無法得到所有問題的答案。 項目負責人不明白我們應該從適當的內容建模(而不是設計和開發)開始。 對他來說,這只是一大堆頁面。 重複的內容和巨大的文本區域以添加大量文本,這似乎不是問題。 在他看來,我關於結構的問題是技術性的,他們不應該擔心。 長話短說,我沒有做這個項目。
重要的是,內容建模與數據庫無關。
這是關於使您的內容易於訪問和麵向未來。 如果您不能在項目啟動時定義對內容的需求,那麼以後重用它將非常困難,如果不是不可能的話。
正確的內容建模是當前和未來網站的關鍵。
內容豐富:無頭 CMS
很明顯,我也想為我的網站遵循一個好的內容建模。 然而,還有一件事。 我不想處理存儲層來構建我的新網站,所以我決定使用 Contentful,一個無頭 CMS,我目前正在研究它(完全免責聲明!)。 “無頭”意味著該服務提供了一個 Web 界面來管理雲中的內容,它提供了一個 API,它將以 JSON 格式返回我的數據。 選擇這個 CMS 幫助我立即提高工作效率,因為我在幾分鐘內就有了可用的 API,而且我不必處理任何基礎設施設置。 Contentful 還提供了一個免費計劃,非常適合小型項目,例如我的個人網站。
獲取所有博客文章的示例查詢如下所示:
<a href="https://cdn.contentful.com/spaces/space_id/entries?access_token=access_token&content_type=post">https://cdn.contentful.com/spaces/space_id/entries?access_token=access_token&content_type=post</a>
簡短版本的響應如下所示:
{ "sys": { "type": "Array" }, "total": 7, "skip": 0, "limit": 100, "items": [ { "sys": { "space": {...}, "id": "455OEfg1KUskygWUiKwmkc", "type": "Entry", "createdAt": "2016-07-29T11:53:52.596Z", "updatedAt": "2016-11-09T21:07:19.118Z", "revision": 12, "contentType": {...}, "locale": "en-US" }, "fields": { "title": "How to React to Changing Environments Using matchMedia", "excerpt": "...", "slug": "how-to-react-to-changing-environments-using-match-media", "author": [...], "body": "...", "date": "2014-12-26T00:00+02:00", "comments": true, "externalUrl": "https://4waisenkinder.de/blog/2014/12/26/handle-environment-changes-via-window-dot-matchmedia/" }, {...}, {...}, {...}, {...}, {...}, {...} ] } }
Contentful 最重要的一點是它非常擅長內容建模,這是我所需要的。 使用提供的 Web 界面,我可以快速定義所有需要的內容片段。 Contentful 中特定內容模型的定義稱為內容類型。 這裡要指出的一件好事是對內容項之間的關係進行建模的能力。 例如,我可以輕鬆地將作者與博客文章聯繫起來。 這可以產生結構化的數據樹,非常適合在各種用例中重用。
因此,我設置了我的內容模型,沒有考慮我將來可能想要構建的任何頁面。
下一步是弄清楚我想用這些數據做什麼。 我問了一個我認識的設計師,他想出了一個網站的索引頁面,結構如下。
使用 Node.js 渲染 HTML 頁面
現在到了棘手的部分。 到目前為止,我不必處理存儲和數據庫,這對我來說是一項了不起的成就。 那麼,當我只有一個可用的 API 時,如何構建我的網站?
我的第一種方法是自己動手的方法。 我開始編寫一個簡單的 Node.js 腳本,該腳本將檢索數據並從中呈現一些 HTML。
預先渲染所有 HTML 文件滿足了我的主要要求之一。 可以非常快速地提供靜態 HTML。
那麼,讓我們看看我使用的腳本。
'use strict'; const contentful = require('contentful'); const template = require('lodash.template'); const fs = require('fs'); // create contentful client with particular credentials const client = contentful.createClient({ space: 'your_space_id', accessToken: 'your_token' }); // cache templates to not read // them over and over again const TEMPLATES = { index : template(fs.readFileSync(`${__dirname}/templates/index.html`)) }; // fetch all the data Promise.all([ // get posts client.getEntries({content_type: 'content_type_post_id'}), // get events client.getEntries({content_type: 'content_type_event_id'}), // get projects client.getEntries({content_type: 'content_type_project_id'}), // get talk client.getEntries({content_type: 'content_type_talk_id'}), // get specific person client.getEntries({'sys.id': 'person_id'}) ]) .then(([posts, events, projects, talks, persons]) => { const renderedHTML = TEMPLATES.index({ posts, events, projects, talks, person : persons.items[0] }) fs.writeFileSync(`${__dirname}/build/index.html`, renderedHTML); console.log('Rendered HTML'); }) .catch(console.error);
<!doctype html> <html lang="en"> <head> <!-- ... --> </head> <body> <!-- ... --> <h2>Posts</h2> <ul> <% posts.items.forEach( function( talk ) { %> <li><%- talk.fields.title %> <% }) %> </ul> <!-- ... --> </body> </html>
這工作得很好。 我可以以一種完全靈活的方式構建我想要的網站,做出關於文件結構和功能的所有決定。 使用完全不同的數據集渲染不同的頁麵類型完全沒有問題。 每個與 HTML 渲染附帶的現有 CMS 的規則和結構作鬥爭的人都知道,完全的自由可能是一件好事。 尤其是當數據模型隨著時間的推移變得更加複雜時,包括許多關係——靈活性會得到回報。
在這個 Node.js 腳本中,創建了一個 Contentful SDK 客戶端,並使用客戶端方法getEntries
獲取所有數據。 客戶端提供的所有方法都是 Promise 驅動的,這樣可以很容易地避免深度嵌套的回調。 對於模板,我決定使用 lodash 的模板引擎。 最後,對於文件讀取和寫入,Node.js 提供了原生fs
模塊,然後用於讀取模板並寫入呈現的 HTML。
但是,這種方法有一個缺點。 這是非常簡單的。 即使這種方法完全靈活,也感覺像是在重新發明輪子。 我正在構建的基本上是一個靜態站點生成器,並且已經有很多。 是時候重新開始了。
尋找真正的靜態站點生成器
著名的靜態站點生成器,例如 Jekyll 或 Middleman,通常處理將呈現為 HTML 的 Markdown 文件。 編輯使用這些,網站是使用 CLI 命令構建的。 不過,這種方法未能滿足我最初的要求之一。 我希望無論身在何處都能夠編輯該站點,而不是依賴於我私人計算機上的文件。
我的第一個想法是使用 API 呈現這些 Markdown 文件。 雖然這會奏效,但感覺不太對勁。 與我最初的解決方案相比,渲染 Markdown 文件以稍後轉換為 HTML 仍然是兩個步驟,並沒有帶來很大的好處。
幸運的是,有 Contentful 集成,例如 Metalsmith 和 Middleman。 我決定為這個項目選擇 Metalsmith,因為它是用 Node.js 編寫的,而且我不想引入 Ruby 依賴項。
Metalsmith 轉換源文件夾中的文件並將它們呈現在目標文件夾中。 這些文件不一定是 Markdown 文件。 你也可以用它來轉譯 Sass 或優化你的圖像。 沒有限制,而且非常靈活。
使用 Contentful 集成,我能夠定義一些源文件作為配置文件,然後可以從 API 獲取所需的一切。
--- title: Blog contentful: content_type: content_type_id entry_filename_pattern: ${ fields.slug } entry_template: article.html order: '-fields.date' filter: include: 5 layout: blog.html description: >- Recent articles by Stefan Judis. ---
此示例配置使用父blog.html
文件呈現博客文章區域,包括 API 請求的響應,但還使用article.html
模板呈現多個子頁面。 子頁面的文件名通過entry_filename_pattern
定義。
如您所見,有了這樣的東西,我可以輕鬆地構建我的頁面。 此設置完美地確保所有頁面都依賴於 API。
將服務與您的項目聯繫起來
唯一缺少的部分是將站點與 CMS 服務連接起來,並在編輯任何內容時使其重新呈現。 這個問題的解決方案——webhooks,如果你使用像 GitHub 這樣的服務,你可能已經熟悉了。
Webhook 是軟件作為服務向先前定義的端點發出的請求,通知您發生了某些事情。 例如,當有人在您的一個存儲庫中打開拉取請求時,GitHub 可以回复您。 關於內容管理,我們可以在這裡應用相同的原則。 每當內容髮生問題時,ping 端點並讓特定環境對其做出反應。 在我們的例子中,這意味著使用 metalsmith 重新渲染 HTML。
為了接受 webhook,我還使用了 JavaScript 解決方案。 我選擇的託管服務提供商 (Uberspace) 可以在服務器端安裝 Node.js 和使用 JavaScript。
const http = require('http'); const exec = require('child_process').exec; const server = http.createServer((req, res) => { res.setHeader('Content-Type', 'text/plain'); // check for secret header // to not open up this endpoint for everybody if (req.headers.secret === 'YOUR_SECRET') { res.end('ok'); // wait for the CDN to // invalidate the data setTimeout(() => { // execute command exec('npm start', { cwd: __dirname }, (error) => { if (error) { return console.log(error); } console.log('Rebuilt success'); }); }, 1000 * 120 ); } else { res.end('Not allowed'); } }); console.log('Started server at 8000'); server.listen(8000);
此腳本在端口 8000 上啟動一個簡單的 HTTP 服務器。它檢查傳入請求的正確標頭,以確保它是來自 Contentful 的 webhook。 如果請求被確認為 webhook,則執行預定義的命令npm start
以重新渲染所有 HTML 頁面。 您可能想知道為什麼會有超時。 這需要暫停操作片刻,直到雲中的數據失效,因為存儲的數據是從 CDN 提供的。
根據您的環境,此 HTTP 服務器可能無法通過 Internet 訪問。 我的站點使用 apache 服務器提供服務,因此我需要添加一個內部重寫規則以使正在運行的節點服務器可以訪問 Internet。
# add node endpoint to enable webhooks RewriteRule ^rerender/(.*) https://localhost:8000/$1 [P]
API優先和結構化數據:永遠的好朋友
此時,我能夠在雲中管理我的所有數據,並且我的網站會在更改後做出相應的反應。
到處重複
在路上是我生活的重要組成部分,因此有必要隨時掌握信息,例如給定場地的位置或我預訂的酒店——通常存儲在谷歌電子表格中。 現在,這些信息散佈在電子表格、幾封電子郵件、我的日曆以及我的網站上。
我不得不承認,我在日常流程中創建了很多數據重複。
結構化數據的時刻
我夢想有一個單一的事實來源(最好是在我的手機上),以快速了解即將發生的事件,還可以獲得有關酒店和場地的更多信息。 我的網站上列出的事件目前還沒有所有信息,但是在 Contentful 中向內容類型添加新字段確實很容易。 因此,我將所需的字段添加到“事件”內容類型。
將這些信息放入我的網站 CMS 從來都不是我的意圖,因為它不應該在線顯示,但是通過 API 訪問它讓我意識到我現在可以用這些數據做完全不同的事情。
使用 JavaScript 構建本機應用程序
多年來,構建移動應用程序一直是一個話題,並且有幾種方法可以解決這個問題。 漸進式 Web 應用程序 (PWA) 是當今特別熱門的話題。 使用 Service Worker 和 Web App Manifest,可以構建完整的類似應用程序的體驗,從主屏幕圖標到使用 Web 技術管理的離線行為。
有一個缺點要提。 漸進式 Web 應用程序正在興起,但還沒有完全實現。 例如,現在 Safari 不支持 Service Worker,到目前為止,Apple 方面只是“正在考慮”。 這對我來說是一個交易破壞者,因為我也想在 iPhone 上擁有一個支持離線的應用程序。
所以我尋找替代品。 我的一個朋友真的很喜歡 NativeScript,並且一直在告訴我這個相當新的技術。 NativeScript 是一個開源框架,用於使用 JavaScript 構建真正的原生移動應用程序,因此我決定嘗試一下。
了解 NativeScript
NativeScript 的設置需要一些時間,因為您必須安裝很多東西來為原生移動環境進行開發。 當您第一次使用npm install nativescript -g
安裝 NativeScript 命令行工具時,將指導您完成安裝過程。
然後,您可以使用腳手架命令來設置新項目: tns create MyNewApp
然而,這不是我所做的。 我在掃描文檔時發現了一個用 NativeScript 構建的示例雜貨管理應用程序。 所以我拿了這個應用程序,挖掘代碼,一步一步地修改它,讓它適合我的需要。
我不想深入研究這個過程,但是用我想要的所有信息建立一個查找列表,並沒有花費很長時間。
NativeScript 與 Angular 2 配合得非常好,這次我不想嘗試,因為發現 NativeScript 本身感覺足夠大。 在 NativeScript 中,您必須編寫“視圖”。 每個視圖都包含一個定義基本佈局和可選 JavaScript 和 CSS 的 XML 文件。 所有這些都在每個視圖的一個文件夾中定義。
可以使用這樣的 XML 模板來呈現一個簡單的列表:
<!-- call JavaScript function when ready --> <Page loaded="loaded"> <ActionBar title="All Travels" /> <!-- make it scrollable when going too big --> <ScrollView> <!-- iterate over the entries in context --> <ListView items="{{ entries }}"> <ListView.itemTemplate> <Label text="{{ fields.name }}" textWrap="true" class="headline"/> </ListView.itemTemplate> </ListView> </ScrollView> </Page>
這裡發生的第一件事是定義一個頁面元素。 在這個頁面內部,我定義了一個ActionBar
來賦予它經典的 Android 外觀以及一個合適的標題。 有時為本地環境構建東西可能有點棘手。 例如,要實現有效的滾動行為,您必須使用“ScrollView”。 最後一件事是,只需使用ListView
遍歷我的事件。 總的來說,感覺很簡單!
但是視圖中使用的這些條目是從哪裡來的呢? 事實證明,有一個共享上下文對象可用於此目的。 在讀取視圖的 XML 時,您可能已經註意到該頁面具有已loaded
的屬性集。 通過設置這個屬性,我告訴視圖在頁面加載時調用特定的 JavaScript 函數。
這個 JavaScript 函數在依賴的 JS 文件中定義。 它可以通過簡單地使用exports.something
出來訪問。 要添加數據綁定,我們所要做的就是為頁面屬性bindingContext
設置一個新的 Observable。 NativeScript 中的 Observable 會發出propertyChange
事件,這些事件是對 'iews 內的數據更改做出反應所需的,但您不必擔心這一點,因為它開箱即用。
const context = new Observable({ entries: null}); const fetchModule = require('fetch'); // export loaded to be called from // List.xml when everything is loaded exports.loaded = (args) => { const page = args.object; page.bindingContext = context; fetchModule.fetch( `https://cdn.contentful.com/spaces/${config.space}/entries?access_token=${config.cda.token}&content_type=event&order=fields.start`, { method: "GET", headers: { 'Content-Type': 'application/json' } } ) .then(response => response.json()) .then(response => context.set('entries', response.items)); }
最後一件事是獲取數據並將其設置到上下文中。 這可以通過使用 NativeScript fetch
模塊來完成。 在這裡,您可以看到結果。
因此,如您所見——使用 NativeScript 構建一個簡單的列表並不難。 後來我用另一個視圖擴展了該應用程序,並增加了在谷歌地圖和網絡視圖中打開給定地址以查看活動網站的附加功能。
這裡要指出的一件事是,NativeScript 仍然很新,這意味著在 npm 上找到的插件通常在 GitHub 上沒有很多下載或 star。 起初這讓我很惱火,但我使用了幾個原生組件(nativescript-floatingactionbutton、nativescript-advanced-webview 和 nativescript-pulltorefresh),它們幫助我獲得了原生體驗,並且一切都運行良好。
你可以在這裡看到改進的結果:
我在這個應用程序中添加的功能越多,我就越喜歡它,並且使用它的次數也越多。 最好的部分是,我可以擺脫數據重複,在一個地方管理所有數據,並且足夠靈活,可以在各種用例中顯示它。
頁面是昨天:結構化內容萬歲!
構建這個應用程序再次向我展示了以頁面格式保存數據的原則已成為過去。 我們不知道我們的數據會去哪裡——我們必須為無限數量的用例做好準備。
回想起來,我取得的成就是:
- 在雲中擁有內容管理系統
- 無需處理數據庫維護
- 一個完整的 JavaScript 技術棧
- 擁有一個高效的靜態網站
- 有一個 Android 應用程序可以隨時隨地訪問我的內容
而最重要的部分:
讓我的內容結構化和易於訪問幫助我改善了我的日常生活。
這個用例現在對你來說可能看起來微不足道,但是當你想到你每天構建的產品時——在不同平台上你的內容總是有更多用例。 今天,我們承認移動設備終於超越了老式的桌面環境,但汽車、手錶甚至冰箱等平台已經在等待它們的聚光燈。 我什至無法想到將會出現的用例。
所以,讓我們試著做好準備,把結構化內容放在中間,因為最後它不是關於數據庫模式——而是關於為未來構建的。
關於 SmashingMag 的進一步閱讀:
- 使用 Node.js 進行網頁抓取
- Sails.js 航行:Node.js 的 MVC 風格框架
- 40 個旅行圖標來美化您的設計
- Webpack 詳細介紹