向 JAMstack 站點添加動態和異步功能
已發表: 2022-03-10這是否意味著 JAMstack 站點無法處理動態交互? 當然不!
JAMstack 站點非常適合創建高度動態的異步交互。 通過對我們對代碼的思考方式進行一些小的調整,我們可以僅使用靜態資產創建有趣的、身臨其境的交互!
使用 JAMstack 構建的網站越來越常見,即可以作為靜態 HTML 文件提供服務的網站,這些文件由 JavaScript、標記和 API 構建而成。 公司喜歡 JAMstack,因為它降低了基礎設施成本、加快交付速度並降低了性能和安全性改進的障礙,因為交付靜態資產消除了擴展服務器或保持數據庫高可用性的需要(這也意味著沒有服務器或數據庫可以被黑客攻擊)。 開發人員喜歡 JAMstack,因為它降低了在 Internet 上運行網站的複雜性:無需管理或部署服務器; 我們可以編寫前端代碼,它就像魔術一樣上線。
(在這種情況下,“魔術”是自動靜態部署,許多公司都可以免費使用,包括我工作的 Netlify。)
但是如果你花很多時間與開發人員談論 JAMstack,那麼 JAMstack 是否可以處理嚴重的 Web 應用程序的問題就會出現。 畢竟,JAMstack 站點是靜態站點,對吧? 靜態網站的功能不是超級有限嗎?
這是一個非常普遍的誤解,在本文中,我們將深入探討誤解的來源,查看 JAMstack 的功能,並通過幾個使用 JAMstack 構建嚴重 Web 應用程序的示例。
JAMstack 基礎知識
Phil Hawksworth 解釋了 JAMStack 的實際含義,何時在您的項目中使用它有意義,以及它如何影響工具和前端架構。 閱讀相關文章 →
是什麼讓 JAMstack 網站“靜態”?
今天的 Web 瀏覽器加載 HTML、CSS 和 JavaScript 文件,就像它們在 90 年代所做的一樣。
JAMstack 站點的核心是一個包含 HTML、CSS 和 JavaScript 文件的文件夾。
這些是“靜態資產”,這意味著我們不需要中間步驟來生成它們(例如,像 WordPress 這樣的 PHP 項目需要一個服務器來為每個請求生成HTML)。
這就是 JAMstack 的真正威力:它不需要任何專門的基礎架構即可工作。 您可以在本地計算機上運行 JAMstack 站點,將其放在您首選的內容交付網絡 (CDN) 上,使用 GitHub Pages 等服務託管它——您甚至可以將文件夾拖放到您最喜歡的 FTP 客戶端中以上傳它共享主機。
靜態資產不一定意味著靜態體驗
因為 JAMstack 站點是由靜態文件組成的,所以很容易假設這些站點上的體驗是靜態的。 但事實並非如此!
JavaScript 能夠做很多動態的事情。 畢竟,在我們完成構建步驟之後,現代 JavaScript 框架是靜態文件——並且有數百個由它們提供支持的令人難以置信的動態網站體驗示例。
有一個普遍的誤解,認為“靜態”意味著不靈活或固定。 但是,在“靜態站點”的上下文中,“靜態”的真正含義是瀏覽器不需要任何幫助來傳遞它們的內容——它們可以在本地使用它們,而無需服務器首先處理處理步驟。
或者,換一種說法:
“靜態資產”不代表靜態應用; 這意味著不需要服務器。
“
JAMstack 能做到嗎?
如果有人詢問有關構建新應用程序的問題,通常會看到有關 JAMstack 方法的建議,例如 Gatsby、Eleventy、Nuxt 和其他類似工具。 同樣常見的反對意見是:“靜態站點生成器不能做 _______”,其中 _______ 是動態的。
但是——正如我們在上一節中提到的——JAMstack 站點可以處理動態內容和交互!
這是我反复聽到人們聲稱 JAMstack 無法處理它絕對可以處理的事情的不完整列表:
- 異步加載數據
- 處理處理文件,例如處理圖像
- 讀取和寫入數據庫
- 處理用戶身份驗證並保護登錄後的內容
在以下部分中,我們將了解如何在 JAMstack 站點上實現這些工作流中的每一個。
如果您迫不及待地想看看動態 JAMstack 的運行情況,您可以先查看演示,然後再回來了解它們的工作原理。
關於演示的說明:
這些演示是在沒有任何框架的情況下編寫的。 它們只是 HTML、CSS 和標準 JavaScript。 它們在構建時考慮了現代瀏覽器(例如 Chrome、Firefox、Safari、Edge),並利用了 JavaScript 模塊、HTML 模板和 Fetch API 等新功能。 沒有添加任何 polyfill,因此如果您使用的是不受支持的瀏覽器,演示可能會失敗。
從第三方 API 異步加載數據
“如果我在構建靜態文件後需要獲取新數據怎麼辦?”
在 JAMstack 中,我們可以利用眾多異步請求庫,包括內置的 Fetch API,隨時使用 JavaScript 加載數據。
演示:從 JAMstack 站點搜索第三方 API
需要異步加載的常見場景是當我們需要的內容取決於用戶輸入時。 例如,如果我們為Rick & Morty API構建一個搜索頁面,在有人輸入搜索詞之前我們不知道要顯示什麼內容。
為了解決這個問題,我們需要:
- 創建一個表單,人們可以在其中輸入搜索詞,
- 收聽表單提交,
- 從表單提交中獲取搜索詞,
- 使用搜索詞向 Rick & Morty API 發送異步請求,
- 在頁面上顯示請求結果。
首先,我們需要創建一個表單和一個包含搜索結果的空元素,如下所示:
<form> <label for="name">Find characters by name</label> <input type="text" name="name" required /> <button type="submit">Search</button> </form> <ul></ul>
接下來,我們需要編寫一個處理表單提交的函數。 該功能將:
- 防止默認的表單提交行為
- 從表單輸入中獲取搜索詞
- 使用 Fetch API 通過搜索詞向 Rick & Morty API 發送請求
- 調用在頁面上顯示搜索結果的輔助函數
我們還需要在表單上為調用我們的處理函數的提交事件添加一個事件監聽器。
下面是這段代碼的樣子:
<script type="module"> import showResults from './show-results.js'; const form = document.querySelector('form'); const handleSubmit = async event => { event.preventDefault(); // get the search term from the form input const name = form.elements['name'].value; // send a request to the Rick & Morty API based on the user input const characters = await fetch( `https://rickandmortyapi.com/api/character/?name=${name}`, ) .then(response => response.json()) .catch(error => console.error(error)); // add the search results to the DOM showResults(characters.results); }; form.addEventListener('submit', handleSubmit); </script>
注意:為了專注於動態 JAMstack 行為,我們不會討論如何編寫 showResults 等實用函數。 不過,該代碼已被徹底註釋,因此請查看源代碼以了解其工作原理!
有了這段代碼,我們可以在瀏覽器中加載我們的網站,我們將看到沒有結果顯示的空表單:
如果我們輸入一個角色名稱(例如“rick”)並單擊“search”,我們會看到名稱中包含“rick”的角色列表:
嘿! 那個靜態站點只是動態加載數據嗎? 聖水桶!
您可以在現場演示中親自嘗試,或查看完整的源代碼以獲取更多詳細信息。
在用戶設備外處理昂貴的計算任務
在許多應用程序中,我們需要做一些資源密集型的事情,例如處理圖像。 雖然其中一些類型的操作僅使用客戶端 JavaScript 是可能的,但讓用戶的設備完成所有這些工作並不一定是個好主意。 如果他們使用的是低功率設備或試圖延長最後 5% 的電池壽命,那麼讓他們的設備完成大量工作可能會讓他們感到沮喪。
那麼這是否意味著 JAMstack 應用程序不走運? 一點也不!
JAMstack 中的“A”代表 API。 這意味著我們可以將這項工作發送到 API,並避免將用戶的計算機風扇旋轉到“懸停”設置。
“但是等等,”你可能會說。 “如果我們的應用程序需要進行自定義工作,而這項工作需要 API,那不就意味著我們正在構建一個服務器嗎?”
由於無服務器功能的強大功能,我們不必這樣做!
無服務器函數(也稱為“lambda 函數”)是一種不需要任何服務器樣板的 API。 我們開始編寫一個普通的舊 JavaScript 函數,所有部署、擴展、路由等工作都卸載到我們選擇的無服務器提供商。
使用無服務器功能並不意味著沒有服務器。 它只是意味著我們不需要考慮服務器。
“
無服務器函數是我們 JAMstack 的花生醬:它們無需我們處理服務器代碼或 devops 就可以解鎖整個世界的高性能、動態功能。
演示:將圖像轉換為灰度
假設我們有一個應用程序需要:
- 從 URL 下載圖像
- 將該圖像轉換為灰度
- 將轉換後的圖像上傳到 GitHub 存儲庫
據我所知,沒有辦法完全在瀏覽器中進行這樣的圖像轉換——即使有,這也是一項相當耗費資源的事情,所以我們可能不想把這種負載放在我們的用戶身上' 設備。
相反,我們可以將要轉換的 URL 提交到無服務器函數,這將為我們完成繁重的工作並將 URL 發送迴轉換後的圖像。
對於我們的無服務器函數,我們將使用 Netlify 函數。 在我們網站的代碼中,我們在根級別添加一個名為“functions”的文件夾,並在其中創建一個名為“convert-image.js”的新文件。 然後我們編寫所謂的處理程序,它接收並 - 正如您可能已經猜到的那樣 -處理對我們的無服務器函數的請求。
要轉換圖像,它看起來像這樣:
exports.handler = async event => { // only try to handle POST requests if (event.httpMethod !== 'POST') { return { statusCode: 404, body: '404 Not Found' }; } try { // get the image URL from the POST submission const { imageURL } = JSON.parse(event.body); // use a temporary directory to avoid intermediate file cruft // see https://www.npmjs.com/package/tmp const tmpDir = tmp.dirSync(); const convertedPath = await convertToGrayscale(imageURL, tmpDir); // upload the processed image to GitHub const response = await uploadToGitHub(convertedPath, tmpDir.name); return { statusCode: 200, body: JSON.stringify({ url: response.data.content.download_url, }), }; } catch (error) { return { statusCode: 500, body: JSON.stringify(error.message), }; } };
此函數執行以下操作:
- 檢查以確保請求是使用 HTTP POST 方法發送的
- 從 POST 正文中獲取圖像 URL
- 創建一個臨時目錄用於存儲將在函數執行完成後清理的文件
- 調用將圖像轉換為灰度的輔助函數
- 調用幫助函數將轉換後的圖像上傳到 GitHub
- 返回帶有 HTTP 200 狀態代碼和新上傳圖片 URL 的響應對象
注意:我們不會討論用於圖像轉換或上傳到 GitHub 的輔助函數是如何工作的,但源代碼有很好的註釋,因此您可以看到它是如何工作的。
接下來,我們需要添加一個表單,用於提交 URL 進行處理,以及一個顯示前後的位置:
<form action="/.netlify/functions/convert-image" method="POST" > <label for="imageURL">URL of an image to convert</label> <input type="url" name="imageURL" required /> <button type="submit">Convert</button> </form> <div></div>
最後,我們需要在表單中添加一個事件監聽器,以便我們可以將 URL 發送到我們的無服務器函數進行處理:
<script type="module"> import showResults from './show-results.js'; const form = document.querySelector('form'); form.addEventListener('submit', event => { event.preventDefault(); // get the image URL from the form const imageURL = form.elements['imageURL'].value; // send the image off for processing const promise = fetch('/.netlify/functions/convert-image', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ imageURL }), }) .then(result => result.json()) .catch(error => console.error(error)); // do the work to show the result on the page showResults(imageURL, promise); }); </script>
在將站點(連同其新的“functions”文件夾)部署到 Netlify 和/或在 CLI 中啟動 Netlify Dev 後,我們可以在瀏覽器中看到表單:
如果我們在表單中添加一個圖像 URL 並單擊“轉換”,我們將在轉換過程中看到“正在處理...”片刻,然後我們將看到原始圖像及其新創建的灰度副本:
天哪! 我們的 JAMstack 站點剛剛處理了一些非常重要的業務,我們不必考慮服務器一次或耗盡用戶的電池!
使用數據庫存儲和檢索條目
在許多應用程序中,我們不可避免地需要保存用戶輸入的能力。 這意味著我們需要一個數據庫。
你可能會想,“就是這樣,對吧? 夾具起來了? 當然,JAMstack 站點——你告訴我們的只是文件夾中的文件集合——無法連接到數據庫!”
相反。
正如我們在上一節中看到的,無服務器功能使我們能夠做各種強大的事情,而無需創建自己的服務器。
同樣,我們可以使用數據庫即服務 (DBaaS) 工具(例如 Fauna)來讀取和寫入數據庫,而無需自己設置或託管它。
DBaaS 工具極大地簡化了為網站設置數據庫的過程:創建新數據庫就像定義我們要存儲的數據類型一樣簡單。 這些工具會自動生成所有代碼來管理創建、讀取、更新和刪除 (CRUD) 操作,並通過 API 供我們使用,因此我們不必實際管理數據庫; 我們只是使用它。
演示:創建請願頁面
如果我們想創建一個小應用程序來收集請願書的數字簽名,我們需要建立一個數據庫來存儲這些簽名並允許頁面讀取它們以進行顯示。
對於這個演示,我們將使用 Fauna 作為我們的 DBaaS 提供者。 我們不會深入探討 Fauna 是如何工作的,但為了演示設置數據庫所需的少量工作,讓我們列出每個步驟並單擊以獲取現成的數據庫:
- 在 https://fauna.com 創建一個 Fauna 帳戶
- 點擊“新建數據庫”
- 為數據庫命名(例如“dynamic-jamstack-demos”)
- 點擊“創建”
- 在下一頁的左側菜單中單擊“安全”
- 點擊“新密鑰”
- 將角色下拉菜單更改為“服務器”
- 為密鑰添加名稱(例如“Dynamic JAMstack Demos”)
- 將密鑰存儲在安全的地方以供應用程序使用
- 點擊“保存”
- 點擊左側菜單中的“GraphQL”
- 點擊“導入架構”
- 上傳一個名為
db-schema.gql
的文件,其中包含以下代碼:
type Signature { name: String! } type Query { signatures: [Signature!]! }
一旦我們上傳了模式,我們的數據庫就可以使用了。 (嚴重地。)
十三個步驟很多,但是通過這十三個步驟,我們只得到了一個數據庫、一個 GraphQL API、容量自動管理、擴展、部署、安全性等等——所有這些都由數據庫專家處理。 免費。 什麼時候活著!
為了嘗試一下,左側菜單中的“GraphQL”選項為我們提供了一個 GraphQL 瀏覽器,其中包含有關可用查詢和突變的文檔,這些查詢和突變允許我們執行 CRUD 操作。
注意:我們不會在這篇文章中詳細介紹 GraphQL 查詢和突變,但是如果你想了解它的工作原理,Eve Porcello 寫了一篇關於發送 GraphQL 查詢和突變的精彩介紹。
準備好數據庫後,我們可以創建一個在數據庫中存儲新簽名的無服務器函數:
const qs = require('querystring'); const graphql = require('./util/graphql'); exports.handler = async event => { try { // get the signature from the POST data const { signature } = qs.parse(event.body); const ADD_SIGNATURE = ` mutation($signature: String!) { createSignature(data: { name: $signature }) { _id } } `; // store the signature in the database await graphql(ADD_SIGNATURE, { signature }); // send people back to the petition page return { statusCode: 302, headers: { Location: '/03-store-data/', }, // body is unused in 3xx codes, but required in all function responses body: 'redirecting...', }; } catch (error) { return { statusCode: 500, body: JSON.stringify(error.message), }; } };
此函數執行以下操作:
- 從表單
POST
數據中獲取簽名值 - 調用將簽名存儲在數據庫中的輔助函數
- 定義要寫入數據庫的 GraphQL 突變
- 使用 GraphQL 輔助函數發送突變
- 重定向回提交數據的頁面
接下來,我們需要一個無服務器函數來讀取數據庫中的所有簽名,這樣我們就可以顯示有多少人支持我們的請願:
const graphql = require('./util/graphql'); exports.handler = async () => { const { signatures } = await graphql(` query { signatures { data { name } } } `); return { statusCode: 200, body: JSON.stringify(signatures.data), }; };
這個函數發送一個查詢並返回它。
關於敏感密鑰和 JAMstack 應用程序的重要說明:
關於這個應用程序需要注意的一點是,我們使用無服務器函數來進行這些調用,因為我們需要將私有服務器密鑰傳遞給 Fauna,以證明我們具有對該數據庫的讀寫訪問權限。 我們不能將此密鑰放入客戶端代碼中,因為這意味著任何人都可以在源代碼中找到它並使用它對我們的數據庫執行 CRUD 操作。 無服務器功能對於在 JAMstack 應用程序中保持私鑰私有至關重要。
一旦我們設置好我們的無服務器函數,我們可以添加一個表單來提交添加簽名的函數,一個顯示現有簽名的元素,以及一些 JS 來調用該函數以獲取簽名並將它們放入我們的顯示中元素:
<form action="/.netlify/functions/add-signature" method="POST"> <label for="signature">Your name</label> <input type="text" name="signature" required /> <button type="submit">Sign</button> </form> <ul class="signatures"></ul> <script> fetch('/.netlify/functions/get-signatures') .then(res => res.json()) .then(names => { const signatures = document.querySelector('.signatures'); names.forEach(({ name }) => { const li = document.createElement('li'); li.innerText = name; signatures.appendChild(li); }); }); </script>
如果我們在瀏覽器中加載它,我們會看到我們的請願書下面有簽名:
然後,如果我們添加我們的簽名......
......並提交它,我們會看到我們的名字附加到列表的底部:
熱狗! 我們剛剛編寫了一個完整的數據庫驅動的 JAMstack 應用程序,其中包含大約 75 行代碼和 7 行數據庫模式!
使用用戶身份驗證保護內容
“好吧,這次你肯定被卡住了,”你可能在想。 “JAMstack 站點無法處理用戶身份驗證。 這到底是怎麼回事,甚至?!”
我會告訴你它是如何工作的,我的朋友:使用我們值得信賴的無服務器功能和 OAuth。
OAuth 是一種廣泛採用的標準,允許人們為應用程序提供對其帳戶信息的有限訪問權限,而不是共享他們的密碼。 如果您曾經使用其他服務登錄過服務(例如,“使用您的 Google 帳戶登錄”),那麼您之前使用過 OAuth。
注意:我們不會深入探討 OAuth 的工作原理,但 Aaron Parecki 寫了一篇詳盡的 OAuth 概述,其中涵蓋了細節和工作流程。
在 JAMstack 應用程序中,我們可以利用 OAuth 和它為我們提供的 JSON Web 令牌 (JWT) 來識別用戶、保護內容並只允許登錄用戶查看它。
演示:需要登錄才能查看受保護的內容
如果我們需要構建一個只向登錄用戶顯示內容的站點,我們需要一些東西:
- 管理用戶和登錄流程的身份提供者
- 用於管理登錄和註銷的 UI 元素
- 一種無服務器函數,使用 JWT 檢查登錄用戶並返回受保護的內容(如果提供)
對於本示例,我們將使用 Netlify Identity,它為我們提供了非常愉快的開發人員添加身份驗證體驗,並提供了一個用於管理登錄和註銷操作的插入式小部件。
要啟用它:
- 訪問您的 Netlify 儀表板
- 從您的站點列表中選擇需要身份驗證的站點
- 點擊頂部導航中的“身份”
- 點擊“啟用身份”按鈕
我們可以通過添加顯示已註銷內容的標記並添加一個元素以在登錄後顯示受保護的內容來將 Netlify Identity 添加到我們的站點:
<div class="content logged-out"> <h1>Super Secret Stuff!</h1> <p> only my bestest friends can see this content</p> <button class="login">log in / sign up to be my best friend</button> </div> <div class="content logged-in"> <div class="secret-stuff"></div> <button class="logout">log out</button> </div>
這個標記依賴 CSS 來根據用戶是否登錄來顯示內容。 但是,我們不能依靠它來實際保護內容——任何人都可以查看源代碼並竊取我們的秘密!
相反,我們創建了一個包含受保護內容的空 div,但我們需要向無服務器函數發出請求才能實際獲取該內容。 我們很快就會深入研究它是如何工作的。
接下來,我們需要添加代碼以使我們的登錄按鈕工作,加載受保護的內容,並將其顯示在屏幕上:
<script src="https://identity.netlify.com/v1/netlify-identity-widget.js"></script> <script> const login = document.querySelector('.login'); login.addEventListener('click', () => { netlifyIdentity.open(); }); const logout = document.querySelector('.logout'); logout.addEventListener('click', () => { netlifyIdentity.logout(); }); netlifyIdentity.on('logout', () => { document.querySelector('body').classList.remove('authenticated'); }); netlifyIdentity.on('login', async () => { document.querySelector('body').classList.add('authenticated'); const token = await netlifyIdentity.currentUser().jwt(); const response = await fetch('/.netlify/functions/get-secret-content', { headers: { Authorization: `Bearer ${token}`, }, }).then(res => res.text()); document.querySelector('.secret-stuff').innerHTML = response; }); </script>
下面是這段代碼的作用:
- 加載 Netlify Identity 小部件,這是一個幫助庫,用於創建登錄模式,使用 Netlify Identity 處理 OAuth 工作流程,並讓我們的應用程序訪問登錄用戶的信息
- 向登錄按鈕添加一個事件偵聽器,觸發 Netlify Identity 登錄模式打開
- 向調用 Netlify Identity 註銷方法的註銷按鈕添加事件偵聽器
- 添加註銷的事件處理程序以在註銷時刪除經過身份驗證的類,該類隱藏已登錄的內容並顯示已註銷的內容
- 添加用於登錄的事件處理程序:
- 添加經過身份驗證的類以顯示已登錄的內容並隱藏已註銷的內容
- 獲取登錄用戶的 JWT
- 調用無服務器函數來加載受保護的內容,在 Authorization 標頭中發送 JWT
- 將秘密內容放在 secret-stuff div 中,以便登錄用戶可以看到它
現在我們在該代碼中調用的無服務器函數不存在。 讓我們使用以下代碼創建它:
exports.handler = async (_event, context) => { try { const { user } = context.clientContext; if (!user) throw new Error('Not Authorized'); return { statusCode: 200, headers: { 'Content-Type': 'text/html', }, body: `
你被邀請了,${user.user_metadata.full_name}!
如果你能讀到這意味著我們是最好的朋友。
以下是我生日派對的秘密細節:
`, }; } 捕捉(錯誤){ 返回 { 狀態碼:401, 正文:'未授權', }; } };
jason.af/派對
此函數執行以下操作:
- 在無服務器函數的上下文參數中檢查用戶
- 如果找不到用戶,則拋出錯誤
- 在確保登錄用戶請求後返回秘密內容
Netlify Functions 將檢測授權標頭中的 Netlify Identity JWT,並自動將該信息放入上下文中——這意味著我們可以檢查有效的 JWT,而無需編寫代碼來驗證 JWT!
當我們在瀏覽器中加載此頁面時,我們將首先看到已註銷的頁面:
如果我們單擊按鈕登錄,我們將看到 Netlify Identity 小部件:
登錄(或註冊)後,我們可以看到受保護的內容:
哇! 我們剛剛向 JAMstack 應用程序添加了用戶登錄和受保護的內容!
接下來做什麼
JAMstack 不僅僅是“靜態站點”——我們可以響應用戶交互、存儲數據、處理用戶身份驗證,以及我們想要在現代網站上做的任何其他事情。 所有這些都無需配置、配置或部署服務器!
你想用 JAMstack 構建什麼? 有什麼你仍然不相信 JAMstack 可以處理的嗎? 我很想听聽——在 Twitter 或評論中聯繫我!