如何使用 Angular Static Site Generator: Scully.io

blank

如何使用 Angular Static Site Generator: Scully.io

如果說 React 有 Gatsby 和 Next,Vue.js 有 Gridsome 和 Nuxt,那麼 Angular 未來可能就是剛剛發展起來的 Scully 。 Scully 在 2019 年末左右公開,它是第一個嘗試把 JAMstack 帶進 Angular 的產品,也是第一個專門的 Angular 靜態網站生成器。 那麼 Scully 如何從 Gatsby & Next,Gridsome & Nuxt 獲取到的靈感呢?

原來, Scully 創始者 Aaron Frost 認為,Gatsby 、Next 、Gridsome 和 Nuxt 他們都具有一個同樣的缺陷。 就拿 Gatsby 來說,Gatsby 作為 SSG 卻和 React 本身的框架是兩種不同的 「paradigms」。 這樣就導致了,如果想讓專案使用 Gatsby 這個靜態網站生成器,那麼你必須得從專案最開始就配置使用它。 但是在這方面 Scully 卻是不同的:

You write your Angular app, then use Scully to pre-render it and generate static HTML. It doesn’t get in the way of your regular Angular development.
在你已有的 Angular 專案的基礎上,用 Scully 來預成成整個專案,生成一系列靜態的 HTML 檔。 Scully 不會對正常的 Angular 應用開發產生任何影響。

Scully 最美妙的地方在於,即使是一個已經寫好的專案,你也可以暫時設定 Scully 來為專案產生 static sites

Scully work with your Angular app, without any change in your code.
Scully 和 Angular 應用相容,不會對應用的原始程式碼帶來任何改變。

如果換句話評價一下 Scully 的優勢,「Scully is less opinionated」 。 由此可見,Scully 正是 Angular Community 當下最需要的。

Aaron Frost 也提到了他對 Scully 的野心。 他說,在未來 Scully 對 Angular 的支援發展穩定之後,研發團隊計畫繼續拓展到 React 市場。 由於Scully的這個優勢,它將是Gatsby & Next的有力競爭對手。

如果你對於 JAMstack 和 SSG 這些概念不太熟悉,那麼我們來簡單講講為什麼要使用 SSG 。

靜態網站生成器帶來的好處。

  • 更好的效能。 網站內容都是通過內容分發網路(CDN)獲取,不需要通過相對很慢的服務端訪問。
  • 更高的安全性。 不使用服務端訪問來獲取網站內容也意味著更少的安全隱患。
  • 更效率的開發週期。 不需要建立與維護很複雜的項目架構或 'infrastructure' 。

當然,Scully 只是剛剛發展起來,它的官方文檔也還有很多可以提升的空間。 但是這並不是說不值得去閱讀他的官方文檔。 如果你想更好的瞭解 Scully ,請前往 Scully.io

接下來的部分,我會總結一下Scully的使用方法。

Scully 的使用。

簡單的來說,專案組使用 Scully 會有以下流程:

  1. 正常的建立並開發一個 Angular 應用,包括 Services 和 Components (以及 Lazy-loading Modules )。
  2. 為 Angular 應用程式加入 Scully 。 一個名叫 scully.{projectName}.config.js 的檔會在專案根目錄下預設生成。
  3. 按實際項目的情況,定義並註冊自己需要的 plugins (包括 Router PluginsRender PluginsFile Handler Plugins ,具體的會在文章後面介紹)。
  4. scully.{projectName}.config.js 檔案中根據 ScullyConfig Interface 設定 Scully。 此步驟涉及到根據路由符合來設定你上一步裡定義並註冊的 plugins 。
  5. Build Angular 應用。 這會在 /dist/{projectName} 下生成 Angular 應用的 distribution 檔案。
  6. Run Scully 。 根據需要加上 Scully CLI options 。 這會 build Scully 並 pre-render 項目從而在指定的路徑下生成靜態網站的檔。 您也可以加上 serve option 預設建立兩個伺服器,一個是為了 Angular 應用,另一個是為了 Scully build 。

為了更好的理解,我接下來將使用一個簡單的專案 Scully Example 當作例子。 一個編碼請參考Github scully-example

blank
圖 1: Scully Example 專案簡單演示

Scully Example 網站使用的新聞訊息是從 HackerNews 官方的 Firebase 隨機挑選的。 為了方便演示 Scully ,所有訊息是存儲在 new.json 靜態檔 (為了簡化,只存儲了三個 news 的 JSON 訊息)。 該專案使用了一個名為 news 的 Lazy-loading module 。 具體檔結構如下:

blank
圖 2:專案基本專案文件結構

接著,我們就來展示如何把Scully加進去吧。

按照提到的第一步,打開你的 terminal ,輸入以下 command :

ng add @scullyio/init

這產生如下改變:

  • 將 Scully 加入到 dependencies 中。
  • 更新 src/app/app.module.ts 檔。 將 ScullyLibModule 添加到 imports 裡。
import{ScullyLibModule}from'@scullyio/ng-lib';@NgModule({declarations:[AppComponent,AboutComponent,HomeComponent],imports:[BrowserModule,AppRoutingModule,HttpClientModule,ScullyLibModule],providers:[],bootstrap:[AppComponent],})exportclassAppModule{}
  • 更新 src/polyfills.ts 檔。 新增如下代碼:
/***************************************************************************************************
* SCULLY IMPORTS
*/// tslint:disable-next-line: align
import'zone.js/dist/task-tracking';
  • 更新 package.json 檔。
  • 建立 scully.{projectName}.config.js 檔案。 對於我們的項目來說,也就是 scully.scully-example.config.js 檔。 檔案最初包含如下代碼:
exports.config={projectRoot:'./src',projectName:'scully-example',outDir:'./dist/static',routes:{},};

outDir 屬性是指 Scully build 產生預算檔的放置路徑,預設就是 ./dist/static

重點就是 routes 屬性,它用於配置那些需要額外處理的 routes ,之後定義並註冊的 plugins 都是在這裡面配置。

如果我們直接 build 我們的應用和 Scully,

ng build
npm run scully

我們會發現:

blank
圖 3:Terminal 截圖

截圖最後一行告訴了我們,我們可能需要為提到的 route 進行額外配置。 這時就涉及到了Scully Plugins 的定義了。

我們還可以去查看 /dist/static/assets/scully-routes.json 文件,我們會發現下列代碼:

[{"route":"/"},{"route":"/about"},{"route":"/news"}]

scully-routes.json 檔案會為我們列出所有 Scully 已經產生的預算的 routesScully 會自動預渲染這些 routes 是因為它們本身就只包含靜態的內容。 其中 /news 會被預成成也是因為清單裡的 news titles 資料都是來自本地靜態 assets/news.json 檔。

由於 Scully 是一個很新的 SSG,目前支援三種 Plugins:

  • Router Plugin 。 任何在網頁路徑裡含有 router-paramsroute 都需要在 Router Plugin 裡面進行配置。 Router Plugin 的作用就是告訴 Scully 如何根據 router-params 來獲得 static 檔需要的將被預渲染數據。
  • Render Plugin 。 所有在 Angular 完成渲染之後的 HTML 檔都將被提供給 Render Plugin 進行再一次的改變。
  • File Handler Plugin 。 在渲染過程中,File Handler Plugin會被 contentFolder Plugin調用。 contentFolder Plugin 用於對指定類型的檔進行解析,例如 Markdown 類型檔。

要自定義一個新的 Plugin ,Scully 為我們提供了 registerPlugin() 方法。 registerPlugin() 方法需要四個參數:

  • typerenderrouterfileHandler
  • name :p lugin 的名字, 用於配置。
  • plugin : plugin 方法本身。
  • validator :一個 validation 方法。

需要注意的是,

任何新的 Scully plugin 都需要在 .js 檔案中定義並被 exported 。 同時他們也必須在 scully.{projectName}.config.js 文件中通過 require() 而被 required。

Router Plugin

在這個專案中我們需要為 /news/:id 進行 plugin 配置。

Scully 為我們提供 router plugin 的 模版:

functionexampleRouterPlugin(route:string,config:any):Promise<HandledRoute[]>{// Must return a promise
}interfaceHandledRoute{route:string;}

根據這些所有的訊息,我們首先創建一個 plugins/newsPlugin.js 檔,在裡面定義我們的 Plugin function 和 validator。 如下列代碼所示,其實 newsPlugin 就是返回 handledRoutes ,一串 HandledRoute 類型的陣組。

const{registerPlugin,routeSplit}=require("@scullyio/scully");const{httpGetJson}=require("@scullyio/scully/utils/httpGetJson");constNewsPlugin="news";constnewsPlugin=async(route,config)=>{constlist=awaithttpGetJson(config.url);const{createPath}=routeSplit(route);consthandledRoutes=[];for(letitemoflist){handledRoutes.push({route:createPath(item.id),});}returnhandledRoutes;};constnewsPluginValidator=async(conf)=>[];registerPlugin("router","news",newsPlugin,newsPluginValidator);exports.NewsPlugin=NewsPlugin;

config 參數包含了所有我們需要用來建立 "list of urls" 的配置訊息。 這個例子中, config.url 包含的就是我們設定時提供的 url

createPath 方法通過 route 參數生成。 該方法以取得 httpGetJson() 的 JSON 資料作為參數,以及原本的 route,產生 string 類型的 url 。

至於 validatorvalidator 一般的作用是在執行 plugin function 之前對 config 裏面的數據進行一系列的驗證,將沒有通過驗證的數據對應的 error 推進 errors 陣列里。 最終返回 errors 陣列。

由於這個例子我們不需要使用任何的 validation 方法,所以 newsPluginValidator 只需要返回 [] 就好。

接下來我們只需要調用 Scully 提供的 registerPlugin() 方法,同時別忘記 export 我們新定義的 Plugin 。 請注意我們只需要 export 一串自定義的字串 'news' ,而不是 Plugin function 本身。

// ABOVE CODE...
// DO NOT FORGET TO REGISTER THE PLUGIN
registerPlugin("router","news",newsPlugin,newsPluginValidator);exports.NewsPlugin=NewsPlugin;

最後一步,我們只需要在 scully.scully-example.config.js 檔案裡設定 newsPlugin 。 這裡的 url 就是指能直接 fetch 我們的 JSON 資料的 url 。

const{NewsPlugin}=require("./plugins/newsPlugin");exports.config={projectRoot:"./src",projectName:"scully-example",outDir:"./dist/static",routes:{"/news/:id":{type:NewsPlugin,url:"http://my-json-server.typicode.com/yangjunhan/demo/news",},},};

現在我們重新 build 專案並 run Scully 。 由於我們添加了一個 route 的額外配置,我們需要加上 --scanRoutesCommand Line Options--scanRoutes options 會完整地偵測一個所有的 routes

ng build
npm run scully -- --scanRoutes

這次之後果然 Scully 查找到 /news/:id 的設定,並預成成所有本地靜態 JSON 檔對應 idroutes 下的網頁 。

blank
圖 4:Terminal 截圖

如果我們再確認一次 /dist/static/assets/scully-routes.json 檔案:

[{"route":"/"},{"route":"/about"},{"route":"/news"},{"route":"/news/23035019"},{"route":"/news/23029396"},{"route":"/news/23039355"}]

/dist/static/ 路徑下創建了名為 news 的資料夾,其中包含了一系列被 Scully 預像而生成的 HTML 檔。

blank
圖 5:Scully 生成的預渲染文件結構

以上展示如何設定自訂的 Router Plugin 。

Render Plugin

接下來我們再嘗試設定一個簡單的 Render Plugin 。

我們首先創建新的 .js 檔案, /plugins/colorPlugin.jscolorPlugin 的目標是為所有 <p> tag 加上 color: red 的 style 設定。

跟 Router Plugin 一樣的, 我們需要再次使用到 registerPlugin() 方法。 因此,我們首先還是定義我們的 Render Plugin function 和 validator 。

這是官方提供的自訂 Render Plugin Interface

functionexampleContentPlugin(HTML:string,route:HandledRoute):Promise<string>{// Must return a promise
}

遵循該 Interface,我們需要保證最終返回值必須是 Promise 。

const{registerPlugin}=require("@scullyio/scully");constColorPlugin="color";constcolorPlugin=async(html,route)=>{constsplitter="</head>";const[begin,end]=html.split(splitter);constcolorStyle="<style>p {color:red}</style>";returnPromise.resolve(</span><span class="si">${</span><span class="nx">begin</span><span class="si">}${</span><span class="nx">colorStyle</span><span class="si">}${</span><span class="nx">splitter</span><span class="si">}${</span><span class="nx">end</span><span class="si">}</span><span class="sb">);};constcolorPluginValidator=async()=>[];

colorPlugin 方法就是通過在 <head></head> 之間加入字串 <style>p {color:red}</style> ,最終用 Promise.resolve() 把重新拼湊的完整 HTML 字串包裹起來,從而把字串轉換為 Promise 類型。

同樣的,我們還是不考慮 validator 的問題。

定義好了 Plugin function 和 validator,我們接下來只需要按照註冊 Router Plugin 相似的步驟:

// ABOVE CODE...
// DON NOT FORGET REGISTER THE PLUGIN
registerPlugin("render",ColorPlugin,colorPlugin,colorPluginValidator);exports.ColorPlugin=ColorPlugin;

最後,我們在 scully.scully-example.config.js 裡 require 並配置我們的 Renderer:

const{NewsPlugin}=require("./plugins/newsPlugin");const{ColorPlugin}=require("./plugins/colorPlugin");exports.config={projectRoot:"./src",projectName:"scully-example",outDir:"./dist/static",routes:{"/news/:id":{type:NewsPlugin,url:"http://my-json-server.typicode.com/yangjunhan/demo/news",postRenderers:[ColorPlugin],},},};

注意這裡的 postRenderers/news/:id 下的屬性,這代表了 ColorPlugin 只會在這些 routes 下被使用。

當然,我們也可以通過 Scully Config Interface 下的 defaultPostRenderers: string[]; 屬性來設置全域的預設 Render Plugins 。 需要注意的是, defaultPostRenderers 會在有設置 postRenderersroutes 下被覆蓋掉。

最後,我們只需要重新 build Scully 並執行 Scully 提供的 Scully static server (預設連接埠:1668) 。 Scully serve 同時也會啟動 Angular distribution server (預設連接埠:1864) 。 兩個 servers 的連接埠都可以在 Scully Config Interface 裡重新設定。

npm run scully -- --scanRoutes
npm run scully:serve
blank
圖 6:配置 Router Plugin 和 Render Plugin 之後的專案演示

顏色果然成功變成紅色,我們的 ColorPlugin 配置成功!

現在,我們已經大概瞭解了Scully的Router Plugins和Render Plugins。 當然, Scully 目前已經為我們提供了一些現成的 Plugins。 如果想要瞭解,歡迎前往他們的官方文檔 - List of Plugins

File Handler Plugin

最後,關於 File Handler Plugin,Scully 同樣已經提供了對 asciidocMarkdown 類型檔的 fileHandler Plugins 支援。 如果你需要自定義對一些特殊文件類型支援,請參考以下 fileHandler Plugin Interface:

functionexampleFileHandlerPlugin(rawContent:string):Promise<string>{// Must return a promise
}

一般來說,File Handler Plugin 的目的就是為了「格式化」 rawContent ,例如利用正則運算式對 rawContent 進行字串匹配,並用 Table 的 HTML tags 把匹配的內容包起來。

Scully 的 Blogging

Scully 本身也提供了在 Angular 專案裡使用靜態 Blog 的支援。 Blog 本質上就是 Markdown 類型的檔,而Scully則會把這些 .md 文件的內容自動預渲染成靜態的 HTML 檔。

讓我們繼續使用上面的同一個項目作為演示。 首先,我們需要為專案添加 Blog 的支援。

ng g @scullyio/init:blog

這行 Command 會自動為專案新增名為 blog 的 lazy-loading module 。 同時,它會在專案路徑下創建一個 blog 資料夾,資料夾裡面自動含有一個預設的 Markdown 檔。

blank
圖 7:添加 Scully Blog 支援而生成的 blog 資料夾

下面就是 blog .md 檔案的預設格式:

---
title: 2020-05-06-blog
description: blog description
published: false
---

接下來我們生成一個新的 blog post ,我們叫它 「Demo Blog」。。

ng g @scullyio/init:post --name="Demo Blog"

於是,一個新的名為 demo-blog.md 的檔會在 /blog/ 資料夾里生成。

blank
圖 8:Blog Posts 指令生成的 Markdown 檔

我們將它的內容更改成以下:

---
title: Demo Blog
description: blog description
published: true
---

# Demo Blog

Scully is the best option for moving a blog to Angular!
 

注意,blog 裡面的 published 必須設為 true 內容才能被訪問。

默認 Scully Blog 支援的設定裡,Blog 是通過 /blog/:slug 路徑被訪問的。 這裡的 slug 預設是指 Blog 的 title ,在我們的展示專案裡,也就是 /blog/demo-blog

接下來,我們簡單的在 /src/app.component.html 檔案裡添加一個導航按鈕:

<li><ahref="blog/demo-blog">Demo Blog</a></li>

最後,我們只需要重新 build 我們的專案以及 Run Scully:

&& npm run scully -- --scanRoutes

再啟動我們的伺服器:

npm run scully:serve

Blog 的內容成功的載入出來啦!

blank
圖 9:Scully static server 里 Blog 的內容載入演示

需要注意的是,Blog 的內容只能在 Scully static server 裡成功顯示出來。 如果我們嘗試在一般的 Angular distribution server 裡顯示 Blog 的話。。。

blank
圖 10:Angular distribution server 里 Blog 內容無法載入

結尾

據我所知,每個 SSG 的核心其實就是看他們為消費者提供的 Plugins。 目前來說,由於Scully才剛剛起步,現成的Plugins確實還太少,但是相信不久的將來,Scully 會擁有數量可觀的現成Plugins以及使用者群。

What do you think?

Written by marketer

blank

前端架構之 JAMStack

blank

WordPress終結者:進擊的JAMStack