2024/10/16

推播服務 DIY - 使用 node.js 實作


 

 

 

在製作 Prototype 產品時,例如電商網站的促銷活動、新聞網站的即時通知等,推播是一項必要且方便的技術,以往只能使用 email 作為推播的手段,現在有了 Web Push 功能,推播就更方便了。


Web Push 支援程度

 

Web Push 技術已行之有年,但各家瀏覽器支援的時間卻不一定,Safari on iOS 在 2018 開始支援後,推播技術也算是達到了真正的跨平台技術。以下是最早支援 Web Push 的瀏覽器版本。

 


Minimal version Supported year
Safari on iOS 11.3 iPhone XR (2018)
Chrome Android 40 2012
Firefox Android 44 2011
Chrome / Edge 40 / 17 2015
Firefox 44 2016

 

雖然知道現在多數的瀏覽器都支援,在實作上,仍然需要先進行檢查,因為很難保證不會有使用者仍然使用 IE 瀏覽器。

 

先以建立 Express.js 服務作為開始吧!這裡要謝謝 Whien 寫的這篇「建立 Service Worker Web Push Notification — (Web Notification 實作紀錄)」 文章,本篇文章的程式碼大多參考自該篇文章。檔案目錄結構如下所示:

 

├── server
│   └── server.js
└── js
    ├── main.js
    └── sw.js

 

server.js 的程式碼如下:

 

const path = require('path');
const express = require('express');
const app = express();
app.use('/js', express.static(path.resolve(__dirname, '../js')));
app.get('/', (req, res) => {
  res.send(`
    <html>
      <head></head>
      <body>
        <h1>Push Notification</h1>
        <script src='js/main.js'></script>
      </body>
    </html>
  `);
});app.listen(8998);

 

 

檢查 Web Push 機能

 

在實作 Web Push 前,要先確認瀏覽器是否支援這項技術。瀏覽器的 Web Push 標準是以 "PushManager" 和 "ServiceWorker" 兩個 API 架構而成。 

 

  • PushManager:瀏覽器提供的推播管理 API,負責處理推播訊息的訂閱和接收
  • ServiceWorker:瀏覽器提供的 Service Worker API,允許在背景執行 JavaScript 程式碼,即使網頁關閉也能接收推播訊息

 

ServiceWorker 具體實作內容交由開發者自行決定。由於 serviceWorker 比 PushManager 還要晚推出,所以要各別檢查 (又或是檢查 serviceWorker 即可)。


使用以下程式碼作為 main.js 程式碼的起始:


if ('serviceWorker' in navigator && 'PushManager' in window) {
  console.log('Service Worker and Push is supported')
} else {
  console.log('Push is not supported')
}

 

 

註冊 ServiceWorker

 

Service Worker 是在背景執行的 JavaScript 檔案,負責接收和處理推播訊息。必須先向瀏覽器註冊 Service Worker 才能使用 Web Push。以下程式碼示範如何註冊 Service Worker:

 

if ('serviceWorker' in navigator && 'PushManager' in window) {
//...
  navigator.serviceWorker
    .register('/js/sw.js')
    .then(swReg => {
      console.log('Service Worker is registered', swReg);
    })
    .catch(err => console.log('Service Worker register error.', err));
//...

 

 

  • navigator.serviceWorker.register('/js/sw.js') 會向瀏覽器註冊位於 /js/sw.js 的 Service Worker 檔案。
  • .then(swReg => { ... }) 會在 Service Worker 註冊成功後執行,swReg 參數包含 Service Worker 的註冊資訊。 
  • .catch(err => console.log('Service Worker register error.', err)) 會在 Service Worker 註冊失敗時執行,err 參數包含錯誤資訊。

 

 

建立 Web Push 金鑰

 

Web Push 規範中對資料的加密有嚴格的要求,這部份可以透過 node.js 裡的 web-push 函式庫產生 VAPID 金鑰:

 


npx web-push generate-vapid-keys

=======================================

Public Key:
BNuUKzbcqU288QpgHA4PMSAfw8I________________XYLHBZCuPH27DMlLJMhXTlr84fpHtKs3g

Private Key:
ywMS_d68vai________________lrNSOz3IjxU22ks-IFhGc

=======================================

 

 

接著把 Public Key 在 main.js 最上層放入變數:


const applicationServerPublicKey = "BNuUKzbcqU288QpgHA4PMSAfw8I________________XYLHBZCuPH27DMlLJMhXTlr84fpHtKs3g"

 

 

 

檢查使用者是否已訂閱

 

接下來在訂閱推播訊息之前,我們需要先檢查使用者是否已經訂閱,避免重複訂閱。程式碼變化後如下所示:

 


navigator.serviceWorker
  .register('/js/sw.js')
  .then(swReg => {
    console.log('Service Worker is registered', swReg);
    let swRegistration = swReg;
    return Promise.resolve(swRegistration)
  })
  .then(swRegistration => {
    inititalUI(swRegistration)
  })


return Promise.resolve(swRegistration)swRegistration 包裝成一個 Promise 物件,並返回。 這樣可以將 Service Worker 註冊的結果傳遞給後續的 .then() 方法,以便進行訂閱等操作。如此使用 Promise 可以把註冊、訂閱程式碼邏輯拆開,方便日後程式碼的維護。


initialUI 可以實作顯示訂閱的按鈕,範例程式碼如下所示:


function inititalUI(swRegistration) {
  const pushButton = document.createElement('button');
  pushButton.textContent = '啟用推播';
  document.body.appendChild(pushButton);

  swRegistration.pushManager.getSubscription().then(subscription => {
    updatePushButton(subscription, pushButton);

    pushButton.addEventListener('click', () => {
      if (subscription) {
        unsubscribeUser(subscription, pushButton, swRegistration);
      } else {
        // 檢查通知權限狀態
        if (Notification.permission === 'denied') {
          // 如果已封鎖,引導使用者前往瀏覽器設定頁面
          alert('您已封鎖通知權限,請前往瀏覽器設定頁面開啟通知。');
          // (Optional)  您可以根據瀏覽器類型,提供更具體的引導方式
          // window.open('chrome://settings/content/notifications'); // Chrome 
          // window.open('about:preferences#privacy'); // Firefox
        } else {
          subscribeUser(swRegistration, pushButton);
        }
      }
    });
  });
}

function updatePushButton(subscription, pushButton) {
  if (subscription) {
    console.log('使用者已訂閱.');
    pushButton.textContent = '取消推播';
  } else {
    console.log('使用者未訂閱.');
    pushButton.textContent = '啟用推播';
  }
}

 

  • initialUI 所建立的「啟用推播」按鈕按下後,實際上只會詢問使用者一次 (瀏覽器預設為「詢問」), 按下允許 / 封鎖後,就不會再從瀏覽器跳出,要使用者自行進瀏覽器變更設定。
  • swRegistration.pushManager.getSubscription() 方法會返回一個 Promise,其中包含使用者的訂閱資訊。如果使用者尚未訂閱,則 Promise 的值為 null。


啟用推播和取消推播的程式碼如下:

 


function urlB64ToUint8Array(base64String) {
  const padding = '='.repeat((4 - base64String.length % 4) % 4);
  const base64 = (base64String + padding)
    .replace(/\-/g, '+')
    .replace(/_/g, '/');const rawData = window.atob(base64);
  const outputArray = new Uint8Array(rawData.length);for (let i = 0; i < rawData.length; ++i) {
    outputArray[i] = rawData.charCodeAt(i);
  }
  return outputArray;
}

function subscribeUser(swRegistration) {
  const applicationServerKey = urlB64ToUint8Array(applicationServerPublicKey);
  swRegistration.pushManager
    .subscribe({
      userVisibleOnly: true,
      applicationServerKey
    })
    .then(subscription => {
      console.log('User is subscribe.');
      updatePushButton(subscription, pushButton);
    })
    .catch(err => console.log('Failed to subscribe the user: ', err));
}

function unsubscribeUser(subscription, pushButton, swRegistration) {
  subscription.unsubscribe()
    .then(() => {
      console.log('使用者已取消訂閱.');
      updatePushButton(null, pushButton);

      // (Optional) 向伺服器發送取消訂閱請求
      // ...
    })
    .catch(err => console.log('取消訂閱失敗: ', err));
}

 

  • urlB64ToUint8Array(base64String) 函式:將 Base64 編碼的字串轉換為 Uint8Array 格式。這是 Web Push 訂閱所需的格式。
  • subscribeUser(swRegistration) 函式:訂閱使用者的推播通知。
  • userVisibleOnly: true :確保所有推播通知都顯示給使用者。
  • unsubscribeUser(subscription, pushButton, swRegistration) 函式:取消訂閱使用者的推播通知。



實作 Service Worker


Chrome / Edge 的 DevTools 的「應用程式」頁籤有提供 Service Worker 測試推播功能,不用送 request 到後端,直接觸發 API 呼叫 push 事件,以下程式碼示範如何在 Service Worker 中處理 push 事件,並顯示推播通知:


const SW = '[Service Worker]';

self.addEventListener('push', event => {  // 監聽 push 事件
  const eventData = event.data.text();  // 提取推播訊息
  console.log(`${SW} Push Received.`);
  console.log(`${SW} Push had this data: ${eventData}`);

  const title = '推送好消息';  // 設定通知標題
  const options = {  // 設定通知選項
    body: eventData,  // 設定通知內容
    // icon: '',  // 設定通知圖示
    // badge: ''  // 設定通知徽章
  };

  // 使用 showNotification() 方法顯示推播通知
  event.waitUntil(
    new Promise(resolve => { 
      setTimeout(() => {
        self.registration.showNotification(title, options).then(resolve); 
      }, 5000);  // 延遲 5 秒顯示通知
    })
  );
});


這段程式碼會監聽 push 事件,並從事件資料中提取推播訊息。 然後,它會使用 showNotification() 方法顯示推播通知,並且通知的標題和內容中顯示提取到的訊息。 


  • 監聽 push 事件:self.addEventListener('push', event => { ... });
  • 提取推播訊息:const eventData = event.data.text();
  • 設定通知標題和選項:const title = '推送好消息'; 和 const options = { ... };
  • 顯示推播通知:self.registration.showNotification(title, options);
  • 延遲顯示通知:setTimeout(() => { ... }, 5000);
  • 使用 waitUntil() 方法確保通知成功顯示:event.waitUntil( ... );


 

要注意的地方是,某些瀏覽器到現在 2024 年,Service Worker 都還不支援 ECMAScript modules 設計,所以 Servcie Worker 實作上建議盡量使用簡單的方式設計。



總結

 

呼~終於完成了!這篇文章帶大家用瀏覽器的內建功能,完整體驗了一次 Web Push 的流程,也實際看到了推播通知的效果。相當的簡單方便!

從檢查瀏覽器支援性、註冊 Service Worker、產生 Web Push 金鑰,再到訂閱和取消訂閱推播訊息,最後實作 Service Worker API 進行推播通知的顯示。希望這些步驟都能幫助大家理解 Web Push 的運作方式。

當然,這只是 Web Push 的單機入門介紹,之後如何與後端伺服器整合,發送客製化的推播訊息等相關資訊,我會在之後的文章中陸續跟大家分享,敬請期待!

最後,要再次感謝 Whien 的文章「建立 Service Worker Web Push Notification — (Web Notification 實作紀錄)」提供的程式碼範例和靈感。 也歡迎大家參考 Whien 的文章,了解更多 Web Push 的實作細節。

希望這篇文章對大家有所幫助,也歡迎大家留言分享你的學習心得和問題喔!

 

 

See also

2024/03/11

突破限制:在64位元Windows上使用JScript的兩種方法

 

作者:吳祐賓

 


 

最近我的程式莫名出現了:EOleSysError: 類別未登錄, ProgID: "MSScriptControl.ScriptControl" 的錯誤訊息。一查才知道原來是 Windows 沒有提供 Microsoft Script Control 64 位元版本,但許多程式會用到 JScript。遇到這難題該怎麼處理呢? 

 


 

 

使用外掛 Tablacus Script Control 64

 

外國網友 Gaku 提供了好用的 OCX 套件:Tablacus Script Control 64。現有的程式幾乎不用修改就可以使用。缺點是必須要另外安裝。截取官方範例如下:

 

var SC = new ActiveXObject("ScriptControl");
SC.Language = "JScript";
SC.AddObject("WScript", WScript);
SC.AddCode('function fnx(a,b,c){ WScript.Echo(a + ":" + b + ":" + c) }');
SC.Run("fnx", 1, "data2", "data3");

 

使用直譯式工具 FastScript


FastScript 是 FastReport 公司旗下的直譯式跨語言開發套件,可在不開啟程式編譯器的場合下做簡易的程式開發。使用起來也相當簡單。缺點就是 JScript 語言支援度比微軟老舊的 JScript 版本更為受限。

 

procedure TForm2.Button1Click(Sender: TObject);
var
  LfsResult: Variant;
begin
  fsScript1.Clear;
  fsScript1.Parent := fsGlobalUnit;
  fsScript1.SyntaxType := 'JScript';
  fsScript1.Lines.Text := 'function jMethod(a, b){return a+b}';
  if fsScript1.Compile then
  begin
    LfsResult := fsScript1.CallFunction('jMethod',VarArrayOf([1, 1]));
    ShowMessage(LfsResult);
  end;
end;

 

總結

 

兩種解決方案各有優缺點,使用者可根據自身需求進行選擇:

 

若需要使用現有的程式,且對相容性要求較高,採用 Tablacus Script Control 64 外掛可以很快進入狀況

 

若需要快速開發簡易的 JScript 程式,或對相容性要求不高,則可以使用 FastScript 直譯式工具

 

其他注意事項

    使用 Tablacus Script Control 64 外掛時,需注意其版本與 Windows 系統的版本是否相符。
    使用 FastScript 直譯式工具時,需注意其對 JScript 語言的支持程度。



本文介紹了兩種解決 Windows 64 位元系統下使用 JScript 的方案,希望能對相關使用者有所幫助。

 

 和你分享

 

See also

 


2024/03/07

DevExtreme DataGrid 新手必讀!淺入淺出教學系列

 

作者:吳祐賓

 


 DataGrid,DevExtreme 最重要的元件之一,也是 WinForm 界夢幻逸品 cxGrid 的 Web 化元件。若能精通它,所有元件都能百分百理解!

 

對於初學者而言,DataGrid 可能有些陌生,但它的用途非常廣泛。這篇文章將帶你一窺 DataGrid 的神奇之處,從最基礎的 Array 顯示開始,一路探索至增刪改的高階操作!

 

要進入增刪改階段,可能會遇到一些困難,但不用擔心,我們會一步步解決這些難題。

 

首先,讓我們從新增階段開始。在這篇文章中,將重點放在 DataGrid 屬性 dataSource 和 store 上。dataSource 是一個大容器,可以根據需求的大小進行調整。你可以使用平舖的 Array,也可以使用更複雜的 DataSource 類別實作。不管你遇到什麼樣的需求,DataSource 都能應對自如!

 

如果你覺得講得太難,別擔心!這裡也提供了 DevExtreme 官方文件的 Data Layer Overview,讓你更容易理解。留言下方告訴我,你覺得更容易理解的是哪一種方法!

 

 

本章目標:快速建立可以 CRUD 的 DataGrid 頁面

 

使用簡單的 array 就可以成為 DataGrid 的資料來源,官方很貼心的提供展示用的人事資料(連結在此)。將上一章 "WinForm 設計師的 Web 開發秘訣:DevExtreme 教學" 建立的HelloWorld 頁面,顯示內容的地方置換為 DataGrid,並在 import 區域導入上述的人事資料及 DataGrid 依賴的套件檔,完成後的程式碼應如下內容。


import React from 'react';
import './hello.scss';

import { employees } from './emp';
import {
  DataGrid
} from 'devextreme-react/data-grid';

export default () => (
  <React.Fragment>
    <h2 className={'content-block'}>Hello</h2>
    <DataGrid
      className={'dx-card wide-card'}
      dataSource={employees}
      keyExpr="ID">
    </DataGrid>
  </React.Fragment>
);


 

 

調整顯示欄位


由於欄位數量太多,畫面顯得相當擁擠。所以接下來調整要顯示的欄位內容。


使用 Column 建立自定欄位 


這裡限制僅顯示 FirstName, Position, BirthDate, HireDate 四個欄位。簡單指定欄位時只要填入 <Column dataField={FieldName}></Column> 即可,需要指定型別時再加入 dataType 等屬性,如下表程式碼粗體文字內容。DataGrid allowColumnReordering 屬性則是允許使用者自行調整欄位順序。


...
import { DataGrid, Column } from 'devextreme-react/data-grid';
...
<DataGrid
  className={'dx-card wide-card'}
  dataSource={employees}
  keyExpr="ID"
  allowColumnReordering={true}>
    <Column dataField="FirstName"></Column>
    <Column dataField="Position"></Column>
    <Column
        dataField="BirthDate"
        dataType="date">
    </Column>
    <Column
        dataField="HireDate"
        dataType="date">
    </Column>

</DataGrid>


開啟編輯功能


加入 Editing 標籤,並指定顯示方式為 popup 氣泡顯示模式,並加入 allowUpdating, allowDeleting, allowAdding 及設定為 true。如此就完成第一部份「帶有 CRUD 的 DataGrid」的內容。


import { DataGrid, Column, Editing } from 'devextreme-react/data-grid';
...
<DataGrid>
  <Column>...</Column>
  ...
  <Editing
      mode="popup"
      allowUpdating={true}
      allowDeleting={true}
      allowAdding={true}
  />

</DataGrid>





總結


這篇文章說明 DevExtreme DataGrid 是目前商業套件中最強大的表格工具,面對新手或簡單功能的場合下,在設計上依然容易學習及使用


無論你是新手還是有經驗的開發者,這篇文章都會帶給你全新的視角和啟發。DataGrid 這個元件越研究越有意思,讓我們一起持續消化中!

 

喜歡這篇文章的話,記得分享給更多人哦!

 

 

See also


2024/03/04

WinForm 設計師的 Web 開發秘訣:DevExtreme 教學

 

作者:吳祐賓

 


許多 WinForm 設計者都被傳統開發方式所制約,要在短時間內寫出符合桌面、平板、手機通用的畫面實在是非常困難。坊間有許多解決方案,以後端 (Back-End) 設計為主的有:ASP.NET 的 Web Form;Delphi 的 IntraWeb、UniGUI 等他們都是以傳統 WinForm 設計概念所發展的框架,佈局和 UI 元件都已經配置好,設計者只要會操作即可。

 

後端設計很好,但前端仍然要碰

 

後端玩過一陣後,還是覺得以後端為主的 Web 應用程式很沉重,不論是設計還是操作上都是。除此之外,前後端混雜交錯的網頁 (MVC 裡的 V 最明顯) 複雜的程度很高,能接手的大多已是專家級,能交付任務的人選並不多。WinForm 轉 Web 的設計師往往是蠟燭兩頭燒,有沒有更好的作法?

 

前端框架學完才發現只有骨架

 

WinForm 設計者已經習慣邏輯設計,全心學習前端後,我的情況是發現:三大框架 Angular、VUE、React 都只有設計框架,佈局、元件還得另學 HTML、CSS。後台要的格式就那樣,每次都要手刻也太辛苦。而且前端 CSS 佈局難度其實比邏輯更加困難,如果有人可以把 UI 調整一致就能讓我專注在邏輯上了。

 

入門?就從 DevExtreme 開始

 

Bootstrap、EasyUI 都有推 Augular, VUE, React 元件,佈局也沒問題,然而 DevExtreme 還提供了命令列工具,新增頁面、調整配色都可以使用 DevExtreme-CLI 命令列工具完成,這種感覺和操作 Visual Studio Code 的 File > New File 快沒什麼兩樣了。但在這之前請先安裝好 DevExtreme-CLI,如果還沒安裝,請先參閱:【翻譯】建立 DevExtreme React 應用程式 

 

以 DevExtreme + React 為例,建立 Web 應用程式專案

 

使用如下的 DevExtreme-CLI 指令建立 my-hello-world 專案,並以預設值:TypeScript、Outer toolbar Layout 作為開始。建立完成後進入該目錄。

 

devextreme new react-app my-hello-world
cd my-hello-world



建立 HelloWorld 分頁

專案剛建立會有幾張頁面,以方便我們參考。在使用 Visual Studio Code 開啟目錄之前,我們先使用 DevExtreme-CLI 建立一張新頁面,使用指令:

 

devextreme add view HelloWorld

 

接下來啟動 Visual Studio Code, 開啟 my-hello-world 目錄,可以在 Pages 路徑看到剛才建立的 HelloWorld 資料夾,裡面有 HelloWorld.cssHelloWorld.tsx 檔案,副檔名 tsx 表示為 支援 TypeScript 的設計檔案。畫面應如下顯示:

 


主菜 -- 建立 Button

 

接在 import 區的最末句,輸入:


import { Button } from 'devextreme-react';

 

把 "Put your content here" 內容置換為 Button 標籤,並設定按下後會跳出 Hello World 的提示訊息:

 

<Button text='Eden Button' onClick={()=>alert("Hello World!")}></Button>

 

此時程式畫面應如下所示:

 


啟動 Hello World 服務,來看成果吧!

 

開啟 Visual Studio Code 的終端機頁面,輸入:

 

npm run start

 

啟動網站後等它一段時間,系統會自動開啟瀏覽器,並開啟我們的 my-hello-world 網站。接下來點擊選單中的 HelloWorld 項目就會看到剛剛我們所建立的頁面,裡面會看到一個按鈕,按下後就會看到 Hello World 的提示訊息。此時畫面效果應該如下所示:


總結

 

習慣 WinForm 體貼的 IDE 工具開發者在切換到指令模式可能剛開始還不習慣。把上述的指令多敲幾次後就會習慣使用 DevExtreme-CLI 的開發模式。

和完全從無到有的鍵盤輸入,DevExtreme-CLI 縮短了很多重複建立環境的時間。下一回我會介紹選單內容和標題的修改的教學。

 

和你分享

 

See also

 

SharpDevelop 使用 NuGet 套件管理器,以 Newtonsoft.Json 為例

 

作者:吳祐賓

 

 

在 Delphi 裡寫了轉 Json 超好用的 TDBXJSONToolsHelper 單元後,在 C# 開發工具裡 SharpDevelop 卻沒看到類似的 Json 分析工具,覺得很奇怪,一查才知道 C# 裡的 Json 分析有兩款套件:

 

  • Newtonsoft.Json (Dot Net 2 - 4.5.2)
  • System.Text.Json (Dot Net 4.7.2 以上)

 

使用 SharpDevelop 有幾個困難,首先是它只支援到 Dot Net 4.5.2,所以只能使用 Newtonsoft.Json。Newtonsoft.Json 也很好,但它卻沒有內建在 Dot Net 4.5 中。所以必須自己另外安裝其套件。最後是 SharpDevelop 並沒有設置 NuGet 套件管理工具,再來是 SharpDevelop 要如何導入這些套件。底下會一步一步帶你做完所有流程。

 

NuGet 套件管理器安裝

 

雖然 SharpDevelop 並沒有像 Visual Studio 內建 NuGet 套件管理器,不過微軟有主動對開發生態系有釋出 NuGet 獨立執行檔,讓任何 Dot Net 工程師都可以享受 NuGet 套件的便利。你可以到 https://www.nuget.org 的下載頁面取得獨立執行檔 -- nuget.exe

 

 

Newtonsoft.Json 套件使用 NuGet 下載

 

使用 "命令提示字元" 進入 NuGet 檔案目錄,並使用以下指令進行安裝。預設會安裝最新版本。安裝後可以在 NuGet 所在路徑看到 Newtonsoft.Json 目錄出現。


nuget install Newtonsoft.Json

 

 

  

SharpDevelop 導入 Newtonsoft.Json 方法

 

使用 SharpDevelop 開啟你的專案,在 Project 管理視窗點擊滑鼠右鍵,路徑 Add Reference > .NET Assembly Browser > Browse... > Newtonsoft.Json lib 目錄 > Newtonsoft.Json.dll。之後就可以在你的專案使用。

 

 

 

範例程式碼

 

使用一個簡單的 Json 字串並使用 Newtonsoft.Json 套件載入,最後顯示屬性內容 "hello"

 

    void Button1Click(object sender, EventArgs e)
    {
      string json = @"{""name"":""hello""}";
      
      var obj = JsonConvert.DeserializeObject<dynamic>(json);
      var name = obj["name"];
      
      MessageBox.Show("Hello, " + name);
    }

 

總結

微軟為了 Dot NET 生態圈所打造的 NuGet 套件管理器著實好用,就算是 SharpDevelop 也能享受到它的福利,使 Dot NET 開發者不用再為套件一個一個去拜訪其官網,只需要一個 NuGet 就可以整合在一起,這真的太好用!

 

和你分享

 

See also

 

2024/02/17

【新手必看】CSS Grid 排版主畫面,從零到一教學!

作者:吳祐賓





在我的【Delphi in Depth DataSnap 網站應用程式全端開發】一書裡提到,主畫面的製作會使用 東西南北中(EWSNC)的版面配置。這是由於網頁前端沒有像 WinForm framework 般有內建制式的版面配置,若要實現類 WinForm 版面配置就要借助 Layout 概念元件來實現,在 EasyUI 中採用【Layout 布局】,在 ExtJS 則採用 【Border Layout】。

EasyUI Layout 布局

ExtJS Border Layout



傷腦筋,使用多組三方元件時 CSS 就必打架

EasyUI 和 ExtJS 他們都屬於 JavaScript 應用程式框架,他們的差異在於:

  • EasyUI:jQuery 為核心的可佈局元件組,也可讓 Angular, VUE, React 等框架使用
  • ExtJS:MVVM 為設計概念的 JS 框架,從佈局到小元件的使用採一條龍式的完整框架

不論是哪一種,只要使用完整的三方元件解決方案, 他們大多會控制到最底層的 CSS 顯示效果,進而造成彼此間的 CSS 或多或少互相干擾的情形。所以可能的話還是要自己試著寫寫看 CSS Layout,來減少干擾的可能。

自己做 Layout 如何?

Layout 要自己做而不用元件,這很有挑戰性,目標先定在把東西南北中建立起來,至於可隱藏、自由調整寬度等功能就先忽略。

之後就來看看僅用 HTML 和 CSS 如何建立 Border Layout。

HTML

東西南北中佈局,從字面上來看可以知道會有五個格子,HTML 可以寫成以下內容,實際呈現時可以發現它們會從上到下排列。

  <div class="container">
    <header></header>
    <nav></nav>
    <main></main>
    <aside>西</aside>
    <footer></footer>
  </div>



"header" 標籤顧名思義就是佈局最頂端的位置,通常會設立標題在此處。"nav" 標籤用於導覽及連結,常見的是 Tree 元件。"main" 為中間的主要顯示區塊。"aside" 放側邊欄或額外內容。"footer" 放在佈局最底端的內容,通常用於版權宣告。



CSS

東西南北中的佈局方式相當制式,類似於表格,所以可以使用 CSS Grid 排版法進行處理。由於呈現"東西中"及"南北中"交錯,所以 Grid 設為 3 x 3 的方式進行排版。"container" 為圖層最底端,決定在它之上的圖層要怎麼排版。按底下的 CSS 設定完成後應該會像下圖顯示的那般好看。

.container {
  display: grid;
  grid-template-columns: 16rem 1fr 16rem;
  grid-template-rows: auto 1fr auto;
  grid-template-areas:
    "header header header"
    "nav content sidebar"
    "footer footer footer";
  min-height: 100dvh;
}

header {
  grid-area: header;
  padding: 2rem;
  background-color: #C5E1A5;
}

nav {
  grid-area: nav;
  padding: 2rem;
  background-color: #90CAF9;
}

main {
  grid-area: content;
  padding: 2rem;
  background-color: #F48FB1;
}

aside {
  grid-area: sidebar;
  padding: 2rem;
  background-color: #FFAB91;
}

footer {
  grid-area: footer;
  padding: 2rem;
  background-color: #FFF59D;
}  




"container" 在 "中" 的區塊都用 1fr 將所有剩餘空間給它。關鍵在 "grid-template-areas" 區塊排版內容,這裡使用硬編碼將 3 x 3 的區塊都寫上去,雖然囉嗦了點,但一看就知道整個版型大概的模樣。其它標籤定義 "grid-area" 區塊名稱,grid-template-areas 就是按照 grid-area 進行排版的。

加入 RWD 吧!

既然都自己刻 CSS 了,自動適應畫面也順便一起加上去,就加入手機大小的 RWD 處理。因為手機畫面屬於寬度較短,高度較長,所以就使用由上到下排列的方式進行排版。由於有五個區塊,且資訊顯示的權重也不一樣,所以高度的呈現也有所調整。最後使用 "grid-template-areas" 決定手機版面顯示區塊的順序。畫面應如下顯示。

@media (max-width: 1024px) {
  .container {
    grid-template-columns: 1fr;
    grid-template-rows: auto minmax(5rem, auto) 1fr minmax(5rem, auto) auto;
    grid-template-areas:
      "header"
      "nav"
      "content"
      "sidebar"
      "footer";
  }
}





"grid-template-rows" 對於"東"、"西"兩個區塊使用 minmax(5rem, auto) 限制它最小高度為 5rem,最大高度為 auto。

總結

本篇文章主要講述使用三方廠商的排版解決方案時,可能遇到因為 CSS 設定底層顯示方式不同而產生的衝突問題。若是底層沒有很複雜,自己寫排版畫面或許可以減少三方廠商元件的衝突問題。至於如何開始,這篇的教學內容相信可以讓你很快速地從 0 到 1 排出心目中理想的畫面。

和你分享

See also


2024/01/19

如何在 React 18 使用 React Router v6.4 建立路由,超簡單

作者:吳祐賓





React Router 目的在實現"客戶端路由" (Client Site Routing)。官方文件是這麼介紹的:

在傳統網站(Server Site),瀏覽器從網站服務端下載文件,解析 CSS 和 JavaScript 資源,之後呈現服務端所要顯示的 HTML 內容。當使用者點擊網頁內的超連結時,它會在頁面(同一個或新頁面)上重新啟動此流程。

客戶端路由可以實現在您的頁面透過點擊超連結更新 URL 時,不需要再從網站服務端再次下載另一個文件。您的頁面可以立即顯示一些新的 UI,並使用 fetch 實現 AJAX,以新資訊來刷新頁面。來讓使用者有更好的操作體驗

React Router 和 React Redux 幾乎是 React 開發者必須會學會的函式庫,本篇文章會逐步帶你邊做邊學如何在 React 18 使用 React Router v6 建立路由,超簡單


 

前置作業:安裝 pnpm 管理 npm 套件

因為 pnpm 官方是這麼說的:

使用 npm 時,若您有 100 件專案都使用了同個依附套件,磁碟中就會有 100 份該套件的副本。 反之,有了 pnpm,該依附套件會被儲存在一個由內容定址的儲存區,因此:

    如果您同時需要該依附套件的不同版本,只有存在差異的檔案才會被加入儲存區。 例如,如果此依賴套件有 100 個檔案,而新的 版本僅變更其中一個檔案,則 pnpm update 將只會新增一個 新的檔案到儲存庫中,而不會為了單一檔案的變更而複製整個依賴套件。
    所有依賴套件的檔案將被儲存在磁碟中的單一位置。 當依賴套件被 安裝時,依賴套件的檔案會被硬鏈結至該位置,不會消耗 額外的磁碟空間。 這將允許您在不同專案之間共享相同版本的依賴套件。

由於上述原因,您將節省大量的磁碟空間,這將與您的 專案和依賴套件的數量成正比,並且將大幅提升安裝的速度!

所以我現在也改用 pnpm 來管理 npm 套件:

npm install -g pnpm



Step 1. 建立新的 React 應用程式

使用 pnpm 建立 React 應用程式:

pnpm create vite my-react-router --template react


移動到建立的目錄中

cd my-react-router


安裝專案所需套件

pnpm install




Step 2. 建立呈現的頁面

在資料夾 src 中建立 pages 資料夾

接著在資料夾 src/pages 建立 Home.jsx

const Home = () => <div>Home</div>
export default Home


接著在資料夾 src/pages 建立 Content.jsx

const Content = () => <div>Content</div>
export default Content


接著在資料夾 src/pages 建立 About.jsx

const About = () => <div>About Me</div>
export default About



Step 3. 安裝本次主角: React Router npm 套件

使用官方教學的安裝指令進行安裝:

pnpm install react-router-dom localforage match-sorter sort-by

 

Step 4. 註冊 Router

這裡註冊 Router 的意思是指建立一個路由分配器,建立時也一併新建路徑及對應頁面,打開 src/main.jsx,修改後文件應該會如下表所示。



src/main.jsx
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.jsx'
import './index.css'
import {
  createBrowserRouter,
  RouterProvider,
} from "react-router-dom";
import Home from "./pages/Home"
import Content from "./pages/Content"
import About from "./pages/About"

const router = createBrowserRouter([
  {
    path: "/",
    element: <App />,
    children: [
      {
        path: "Home",
        element: <Home />,
      },
      {
        path: "content",
        element: <Content />,
      },
      {
        path: "about",
        element: <About />,
      },
    ],
  },
]);

ReactDOM.createRoot(document.getElementById('root')).render(
  <React.StrictMode>
    <RouterProvider router={router} />
  </React.StrictMode>,
)


因為我計畫在 Root Layout 裡切換分頁,所以在 root 裡加入 children 內容。



Step 5. 製作導覽功能

開啟 src/App.jsx ,並加入導覽介面,將以下程式碼置換原模板內容

 

src/App.jsx
import './App.css'
import { Routes, Route, NavLink, Outlet } from "react-router-dom";
import Home from './pages/Home'
import Content from './pages/Content';
import About from './pages/About';

function Layout() {
  return (
    <>
      <header>
        <h1>My Super Cool App</h1>
        <NavLink to="/">首頁</NavLink>
        <NavLink to="/content">內容</NavLink>
        <NavLink to="/about">關於</NavLink>
      </header>
      <main>
        <Outlet />
      </main>
      <footer>©️ Eden 2024</footer>
    </>
  );
}

function App() {
  return (
    <>
      <Routes>
        <Route element={<Layout />}>
          <Route path="/" element={<Home />} />
          <Route path="/content" element={<Content />} />
          <Route path="/about" element={<About />} />
        </Route>
      </Routes>
    </>
  )
}

export default App


在同一頁進行切換頁面的場合下,會需要在 App 裡增加 Layout 層級,並使用  Router Outlet  來顯示  Route  內容。


Step 6. 啟動瀏覽器執行 React 應用程式

使用以下指令啟動 Web 服務:

pnpm run dev

 

按照預設值,您可以透過以下網址進入 React 應用程式

http://localhost:5173/

 

畫面應該長成這樣:


總結

本篇介紹 pnpm 套件管理器的好處,以及 Vite 建立 React 應用程式的方法,和 React Router v6.4 的使用。在 Router v6.4 中,由於添加  Remix Data APIs,所以原作者開始推薦使用 RouterProvider 來替代舊版 <BrowserRouter> 的使用,所以這篇文章的寫法可能會和你之前所學過的不太一樣。目前 BrowserRouter 仍然可以相容使用,不過身為前端工程師的我們,還是得接受前端技術會不斷進化的現實。學吧!一起加油!

和你分享


See also