整合營銷服務商

          電腦端+手機端+微信端=數據同步管理

          免費咨詢熱線:

          NodeJS的客戶端瀏覽器和304緩存及OnceIO

          NodeJS的客戶端瀏覽器和304緩存及OnceIO緩存控制

          nceIO是OnceDoc企業私有內容(網盤)管理系統的底層Web框架,它可以實現模板文件、靜態文件的全緩存,運行起來完全不需要I/O操作,并且支持客戶端緩存優化,GZIP壓縮等(只壓縮一次),擁有非常好的性能,為您節約服務器成本。它的模塊化功能,可以讓你的Web進行分布式存儲,在一個擴展包里即可包含前端、后端和數據庫定義,只需通過添加/刪除目錄的方式就可實現功能刪減,實現真正的模塊化擴展。目前OnceIO已經開源,這里是介紹如何使用的一系列文章。

          客戶端緩存

          緩存定義

          這里討論的緩存是指 web 緩存:一個 web 資源(如 html 頁面、圖片、文件等)在服務器和客戶端(瀏覽器)之間的副本。緩存會根據進來的請求保存請求輸出的內容的副本;然后,如果下一個請求是相同的 URL,且網頁在這段時間內沒有更新,瀏覽器就不會再次下載網頁,而是直接使用本地緩存的網頁副本。

          緩存的作用主要有:

          • 節約帶寬。

          • 減少延遲。

          • 降低服務器壓力。

          客戶端(瀏覽器)的緩存機制

          所有的緩存都有一套規則來幫助它們決定什么情況下使用緩存中的副本,什么情況下向源服務器再次發送請求。這些規則有的在協議(如 HTTP 協議 1.0 和 1.1)中有定義,有的則是由緩存的管理員(如 DBA、瀏覽器的用戶、代理服務器管理員或者應用開發者)設置。

          對于瀏覽器端的緩存,這些規則是在 HTTP 協議頭和 html 頁面的 meta 標簽中定義的。它們從新鮮度和校驗值兩個維度來決定瀏覽器是否可以直接使用緩存中的副本。

          新鮮度(過期機制):也就是緩存副本有效期。一個緩存副本必須滿足以下條件,瀏覽器才會認為它是有效的:

          1. 含有完整的過期時間控制頭信息(HTTP協議報頭),并且仍在有效期內;

          2. 瀏覽器已經使用過這個緩存副本,并且在一個會話中已經檢查過新鮮度;

          滿足以上兩個情況的一種,瀏覽器會直接從緩存中獲取副本并渲染。

          校驗值(驗證機制):服務器返回資源的時候有時在控制頭信息帶上這個資源的實體標簽 ETag(Entity Tag),它可以用來作為瀏覽器再次請求過程的校驗標識。如過發現校驗標識不匹配,說明資源已經被修改或過期,瀏覽器需求重新獲取資源內容。

          常用的與緩存有關的 HTTP 消息報頭

          消息報頭類型作用規則
          Status Code200 OK普通表明服務器成功返回網頁不適用
          304 Not Modified普通表明當前資源的內容(自上次訪問以來或根據請求的條件)沒有修改過,服務器不返回網頁內容不適用
          Cache-Controlmax-age=315360000響應指明緩沖副本的有效時長,單位為秒新鮮度
          ExpiresThu, 31 Dec 2037 23:55:55 GMT響應告訴瀏覽器在過期時間前可以使用副本新鮮度
          Last-ModifiedSun, 23 Oct 2016 06:36:08 GMT響應告訴瀏覽器當前資源的最近一次修改時間新鮮度
          If-Modified-SinceSun, 23 Oct 2016 06:36:08 GMT請求如果瀏覽器第一次請求時響應中 Last-Modified 非空,第二次請求同一資源時,會把它作為該項的值發給服務器新鮮度
          ETag978534響應告訴瀏覽器當前資源在服務器的唯一標識符(生成規則由服務器決定)校驗值
          If-None-Match978534請求如果瀏覽器第一次請求時響應中 ETag 非空,第二次請求同一資源時,會把它作為該項的值發給服務器校驗值

          以訪問網站 http://oncedoc.com/ 為例,網站的 shader.css 文件的 HTTP 頭信息為:

          客戶端緩存生效的常見流程

          服務器收到請求時,在 200 OK 響應中回送該資源的 Last-Modified 和 ETag,客戶端將該資源保存在緩存中,并記錄這兩個屬性。當客戶端再次發送相同的請求時,會在請求中攜帶 If-Modified-Since 和 If-None-Match 兩個消息報頭。兩個報頭的值分別是上次請求收到的響應中 Last-Modified 和 ETag 的值。服務器通過這兩個頭判斷本地資源未發生變化,客戶端不需要重新下載,返回 304 響應。以訪問 oncedoc.com 為例,客戶端緩存生效流程如下:

          用戶操作行為與緩存

          用戶在使用瀏覽器的時的各種操作,如輸入地址后回車,按F5刷新等,對緩存有可能會造成影響。

          用戶操作Expires/Cache-ControllLast-Modified/ETag
          地址欄回車有效有效
          頁面鏈接跳轉有效有效
          新開窗口有效有效
          前進后退有效有效
          F5 刷新無效有效
          Ctrl+F5 強制刷新無效無效

          當用戶在按 F5 進行刷新時,瀏覽器會忽略 Expires/Cache-Control 的設置,再次向服務器發送請求,而 Last-Modified/Etag 仍然是有效的,服務器會根據情況判斷返回 304 還是 200 ;而當用戶使用 Ctrl+F5 進行強制刷新的時候,所有的緩存機制都將失效,;瀏覽器將重新從服務器下載資源并返回 200。

          在服務器端設置客戶端緩存機制

          瀏覽器端緩存

          運行服務器,訪問 localhost:8054/img,打開瀏覽器開發者工具中的 Network 欄,地址欄回車,Network 顯示:

          此時瀏覽器直接從本地獲取圖片資源,瀏覽器和服務器之間并沒有進行I/O操作。瀏覽器沒有問服務器端是否有更新,而直接從本地緩存中獲取資源。

          res.cache(0)

          有時侯,我們可能需要禁用瀏覽器端的緩存機制,然后讓瀏覽器發送一次請求詢問是否有更新(比如ajax操作)。可以用添加一個 cache-control的header: res.cache(0),即0秒后立即失效(不緩存),示例代碼如下:

          app.use(function(req, res) {

          res.cache(0)

          req.filter.next()

          })

          app.get('/img', function(req, res) {

          res.render('img.html')

          })

          此時瀏覽器與服務器之間會進行一次 I/O,如果本地緩文件的修改時間(IF-Modify-since)與服務器端的一致,即沒有修改,則OnceIO會發出 304 響應(如圖所示),告訴瀏覽器從本地緩存中獲取資源;如果服務器端文件有更新,OnceIO則會發出 200 響應,并將更新資源重新發給瀏覽器。

          此時服務器端通過304告訴瀏覽器從本地緩存中獲取資源。

          通過res.cache接口,您可以根據您應用的Release周期(周、月)來設置資源文件緩存的最大緩存時間,來優化您的應用,比如一周后過期:

          res.cache(7*24*3600)

          下一節我們將介紹OnceIO的服務器端的模板和靜態文件緩存和gzip壓縮機制,和其一次讀取,永久使用的實現原理。

          OnceIO項目: https://github.com/OnceDoc/onceio

          文將通過一個快速演示深入研究 HTTP103狀態代碼

          每天?分享?最新?軟件?開發?,Devops,敏捷?,測試?以及?項目?管理?最新?,最熱門?的?文章?,每天?花?3分鐘?學習?何樂而不為?,希望?大家?點贊?,加?關注?,你的?支持?是我?最大?的?動力?。



          最近,谷歌 Chrome 103發布了一系列新功能。其中一個值得注意的特點是引入了 HTTP狀態碼103。本文將通過一個快速演示深入研究 HTTP103狀態代碼。

          HTTP 103

          從 Mozilla Developer Network 的網絡文檔來看,HTTP 103早期提示是信息響應狀態代碼,主要用于鏈接頭,允許用戶代理在服務器還在準備響應時開始預載資源。

          以下是 RFC 鏈接以獲得更多詳細信息。


          HTTP103可以通過使用 link rel=preload 配置 HTTP 頭字段來優化頁面速度。

          它是如何工作的?

          通常,當瀏覽器發送一個請求時,服務器會在不到一秒鐘的時間內接收并處理該請求,然后發送一個 HTTP200OK 響應,如下所示。


          然而,使用 HTTP103早期提示,還有提高頁面呈現速度的空間。

          一旦服務器使用 HTTP 103功能進行了更新,當瀏覽器發送一個請求時,如果服務器知道內容需要 style.css、 script.js 等資源,那么它將使用 HTTP 103早期提示響應向瀏覽器提示(響應)以預加載內容,如下所示。

          然后,一旦服務器處理完整的響應,它將向瀏覽器發送普通的 HTTP200OK。

          當瀏覽器預先加載內容時,這個過程將有助于提高頁面呈現速度。

          如上所述,此功能需要對服務器進行更新。如需更新 Apache HTTP Server,請點擊這里進行配置。

          早期提示僅適用于 HTTP/2和 HTTP/3。

          它只支持200、301和304響應返回代碼。

          此外,它工作在具有預連接或預加載重載類型的響應鏈接頭上。

          演示


          為了演示 HTTP103早期提示,我在 AWS 上部署了一個帶有 Ubuntu 映像的 EC2實例。我用 HTTP/2和 SSL 安裝了 Apache HTTP Server。

          這是我的 conf 文件內容。

          H2Push on
          H2EarlyHints on

          下面是演示頁面的 curl 輸出:

          正常 HTTP 200 OK

          讓我們在 conf 文件中配置 H2PushResource 并重新加載服務器。

          H2Push on
          H2EarlyHints on
          <Location /index.html>
              H2PushResource /main.css
          </Location>

          使用 sudosystemctl 重新啟動 apache2命令重新啟動 apacheserver。

          下面是啟用 HTTP103EarlyHint 特性后的 curl 輸出。

          HTTP 103早期提示

          如上所述,服務器的第一個響應是 HTTP/2103,將 main.css 預加載到瀏覽器,然后服務器將用 HTTP 200作出響應。

          下面是服務器響應時間部分。

          服務器響應時間

          最后的想法

          正如您了解到的,HTTP103早期提示通過提示瀏覽器預加載資源來幫助優化頁面呈現時間。它還解決了這里概述的 HTTP/2服務器推送的主要問題。Cloudflare 還致力于利用機器學習使早期提示更加智能。我們祈禱吧。

          者:Qiuyi

          證明即程序,結論公式即程序類型。
          —— 柯里-霍華德對應 [1]

          背景

          我們每天的編碼都會使用到類型系統,本篇文章希望能夠簡單地介紹原理到實踐,讓讀者能更好的使用類型系統編寫出類型安全并簡潔的代碼。

          本篇文章預期讀者是擁有 TypeScript 基礎的同學。

          CodeShare - 安全的 any 互操作

          眾所周知,any 是一個危險的類型,可以關閉所有類型檢查。 但是實際的瀏覽器程序中不可能完全避免 any 類型進入類型系統,對我們的類型推理產生影響。比如

          • `JSON.parse()`[2]
          • `Reflect.get()`[3]
          • `Response.json()`[4]

          對于 any 的處理,最佳方法是先把他變成 TypeScript 的頂層類型 unknown,這樣它就不能在類型系統中隨意傳播了,必須要求程序員主動進行類型轉換才能在其他地方使用。

          分享一個代碼片段,這個代碼片段嘗試將 window 上的掛載的一個全局方法獲取出來,假如存在,就轉換成安全的類型后再放出去;假如不存在,就換成一段 fallback 邏輯并展示警告信息。

          export type I18NMethod=(key: string, options: unknown, fallbackText: string)=> string;
          
          function isI18nFunction(input: unknown): input is I18NMethod {
            return typeof input==='function';
          }
          
          function makeI18nMethod(): I18NMethod {
            let hasWarnShown=false;
          
            return function (key: string, options: unknown, fallbackText: string) {
              if (Reflect.has(window, '$i18n')) {
                // $i18n是一個掛載到 window 對象上的全局方法
                const globalI18n: unknown=Reflect.get(window, '$i18n');
                if (isI18nFunction(globalI18n)) {
                  return globalI18n(key, options, fallbackText);
                }
              }
              showWarnOnce();
              return fallbackText;
            };
          
            function showWarnOnce() {
              if (hasWarnShown===false) {
                hasWarnShown=true; // 只展示一次警告
                console.warn('Cannot Fetch i18n Text: window.$18n is not a valid function');
              }
            }
          }
          
          export const $i18n=makeI18nMethod();
          
          // usecase
          $i18n("hello-text-key", {}, "你好");
          

          13 行獲取了一個 any 類型的對象,第一步是將其轉換為 unknown 類型。

          假如 14 行不調用 isI18nFunction 轉換類型,而是直接返回 globalI18n,ts 將報錯:Type 'unknown' is not assignable to type 'string',從而要求開發者必須編寫類型轉換。

          本文中所有 TypeScript 示例代碼都可以復制粘貼放進 TypeScript Playground[5] 運行。

          非常推薦讀者這樣做,可以看到編譯器真實的類型推斷過程。

          這里我采用了 typescript 的 is 語法來進行一個運行時類型檢測,通過后進行類型轉換。從而使得運行時類型更安全。

          類型系統基礎原理

          CodeShare 中提到通過將 any 轉換成了頂層類型 unknown,從而確保了類型安全

          要理解這個操作需要回答四個問題:

          1. 為什么直接用 any 不安全?
          2. 頂層類型是什么?
          3. 為什么頂層類型是安全的?
          4. unknwon 為什么是頂層類型?

          要回答這些問題,我們需要理解類型系統為什么把一些類型轉換當作安全的(可以隱式轉換),另一些類型轉換當作不安全的(需要用 as 強制類型轉換)。換句話說,需要了解類型系統的推導原理。

          子類型

          類型系統的推導原理是子類型系統,所以我們首先來看什么是子類型。

          子類型(subtype) :如果在期望類型 T 的實例的任何地方,都可以安全地使用類型 S 的實例,那么稱類型 S 是類型 T 的子類型,反之則稱為父類型。

          假設一個函數接受一個 Shape 的參數,如果此時能安全地傳入 Rect,那么 Rect 就是 Shape 的子類型。

          TypeScript 使用了結構子類型 (Structural Type System) 來實現子類型系統:如果 A 類型擁有 B 類型全部相同的結構,A 就是 B 的子類型。

          以下示例演示 typescript 的基礎子類型推導。注意本篇文章全部使用 class 表示類型,是因為這里是為了簡化代碼說明子類型原理,而非解釋狹義的類型定義語法(type 或 interface)。

          class Employee{
              public base=4000;
          }
          
          class Programmer extends Employee {
              public base=5000;
          }
          
          class Designer {
              public base=5000;
          }
          
          class Advertiser{
              public bonus=6000;
          }
          
          function getSalary(who: Employee): number{
              return who.base;
          }
          
          // OK,類型一致
          getSalary(new Employee())
          
          // Ok,Programmer 是 Employee 的子類型,編譯器可以安全的做隱式類型轉換 Programmer -> Employee
          getSalary(new Programmer())
          
          // Ok,Designer 雖然沒有聲明是 Employee 的子類型,但是由于結構子類型的定義,Designer 是安全的
          getSalary(new Designer())
          
          // Error Advertiser 不是 Employee 的子類型,這里不能做隱式類型轉換
          getSalary(new Advertiser())
          
          // OK,我們可以強制轉換。但是這樣不安全。
          getSalary(new Advertiser() as unknown as Employee)
          

          any 類型

          any 實際上是一個 TypeScript 的特例,是作為關閉“繞過類型檢查”的標志,用來和 JavaScript 互操作。如果非要從類型系統的角度看,any 既是任何類型的子類型,又是任何類型的父類型。因為太特殊了,一般不把 any 作為頂層或底層類型看待。

          let aAny: any=1;
          let aNumber: number=1;
          
          aAny=aNumber; // OK
          aNumber=aAny; // OK
          

          any 既是任何類型的子類型,又是任何類型的父類型。

          any 類型會讓 TS 關閉所有類型檢查,非常不安全。

          頂層類型

          當一個類型是其他所有可能的類型的父類型,則稱之為頂層類型。

          回顧一下子類型的定義:如果在期望類型 T 的實例的任何地方,都可以安全地使用類型 S 的實例,那么稱類型 S 是類型 T 的子類型。

          換句話說,頂層類型就是在聲明使用頂層類型的地方,可以安全地傳入其他任意類型。從這個推理出發,我們可以發現頂層類型是:unknown

          let aUnknown: unknown=1;
          let aNumber: number=1;
          
          aUnknown=aNumber; // OK,number 可以賦給 unknown,因為 number 是 unknown 的子類型
          aNumber=aUnknown; // Error: unknown 不是 number 的子類型
          

          unknown 頂層類型的特性演示

          從定義我們知道,頂層類型不是任何類型的子類型,所以使用在任何聲明非頂層類型使用的地方,都必須經過強制類型轉換。

          類型轉換

          在子類型示例中我們寫了一段強制類型轉換的代碼:

          // Advertiser -> unknown -> Employee
          getSalary(new Advertiser() as unknown as Employee)
          

          這里的 as unknown 其實是必須的,并不是寫著玩。讀者可以嘗試在 TypeScript Playground 中嘗試刪除中間的 as unknown,編譯器會直接報錯:

          Conversion of type 'Advertiser' to type 'Employee' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first.
            Property 'base' is missing in type 'Advertiser' but required in type 'Employee'.
          

          這是因為 TypeScript 只允許父子類型之間進行類型轉換。換句話說,只允許將類型向上轉換為父類型,或者將類型向下轉換為子類型。而 unknown 作為頂層類型,就可以在任何地方承擔轉換的“中間態”。

          作為一個類型系統而言,TypeScript 這個設計是合理且安全的,不是 Bug。

          • 子類型到父類型轉換:稱為向上轉換,是安全的,可以隱式轉換;
          • 父類型到子類型轉換:稱為向下轉換,是不安全的,需要主動聲明才能轉換;
          • 非父子類型間類型轉換:非法行為。

          總結

          • 子類型(subtype) :如果在期望類型 T 的實例的任何地方,都可以安全地使用類型 S 的實例,那么稱類型 S 是類型 T 的子類型。
          • 只有父子類型之間才能進行類型轉換。
          • 為什么 any 類型不安全?
            因為 any 既是任何類型的子類型,又是任何類型的父類型,可以繞過所有 TS 類型檢查。
          • 什么是頂層類型?
            當一個類型是其他所有類型的父類型,則稱之為頂層類型。
          • 為什么頂層類型安全?
            因為頂層類型不是任何類型的子類型,在接收其他類型地方,必須經過手動強制類型轉換。強制類型轉換需要開發者主動聲明,讓開發者告訴編譯器:我已經做好了所有檢測,可以進行轉換。
          • 為什么 unknown 是頂層類型?
            任何類型的值都可以賦給 unknown,但是 unknown 類型的值不能賦給其他類型(any 除外)。

          編寫類型安全代碼

          類型編程最大的應用就是用來對代碼進行靜態檢查,減少潛在的 bug。

          TypeScript 設置

          對于 TS 來說,非常建議開啟兩個選項,新項目最好一開始就打開:

          • strictNullChecks 選項讓 null 和 undefined 成為單元類型。
          • strictFunctionTypes 確保函數中返回值類型是協變的,而參數類型是逆變的,這樣函數子類型更安全。(協變和逆變的概念見本文“類型可變性”章節)

          基本類型偏執

          基本類型 number string boolean不好的點在于:這些類型攜帶的可讀性信息不足,并且對使用者暴露了太多細節。

          比如一個防抖函數:

          declare function debounce<Args extends unknown[], Output>(
              wait: number, fn: (...args: Args)=> Output
          ): (...args: Args)=> Output;
          
          // useCase
          const debouncedLog=debounce(500, (input:string)=> console.log(input))
          

          這里的問題是:

          • 500 是指什么?500 秒還是 500 毫秒?
          • wait 傳入 -1 會發生什么?

          對于有具體意義的概念不愿意建模,而是用基本類型表示,這種問題稱為基本類型偏執。(出處:《重構,改善既有代碼的設計》[6]

          我們新增一個簡單的 Millseconds 類型來解決問題:

          declare function debounce<Args extends unknown[], Output>(
              wait: Millseconds, fn: (...args: Args)=> Output
          ): (...args: Args)=> Output;
          
          class Millseconds {
            constructor(readonly value: number){
                if(this.value < 0){
                    throw new Error('Millseconds Value Cannot Smaller Than 0');
                }
            }
          }
          
          // useCase:
          const debouncedLog=debounce(new Millseconds(500), (input: string)=> console.log(input))
          

          這樣我們的可讀性就好了很多,無論是誰都能直接讀出來我們在設置一個 500 毫秒等待時間的防抖函數。

          優化:模擬名義子類型

          然而這里還有一個問題,由于 TypeScript 是一個基于結構子類型的類型系統,只要結構類型相同就可以在這里順利傳入。

          declare function debounce<Args extends unknown[], Output>(
              wait: Millseconds, fn: (...args: Args)=> Output
          ): (...args: Args)=> Output;
          
          class Millseconds {
            constructor(readonly value: number){
                if(this.value < 0){
                    throw new Error('Millseconds Value Cannot Smaller Than 0');
                }
            }
          }
          
          class Seconds {
            constructor(readonly value: number){}
          }
          
          const debouncedLog=debounce(new Seconds(500), (input: string)=> console.log(input))
          

          在本文其實一直在用一個操作來模擬名義子類型,用一個 unique symbol 來強制類型結構獨一無二,無法仿造。

          declare const msSym: unique symbol;
          class Millseconds {
            private [msSym]=null;
          
            constructor(readonly value: number){
                if(this.value < 0){
                    throw new Error('Millseconds Value Cannot Smaller Than 0');
                }
            }
          }
          

          優化:字面量檢測

          然而這里還有一個問題:雖然報錯信息好了很多,但是傳入小于 0 的數還是只能在運行階段報錯。

          就算寫new Millseconds(-1)這種明顯的錯誤,類型系統依然躺平裝死。為了解決這個問題,我們可以用 TS 新增的 string literal 特性(需要 TS 大于 4.5)來搞一點點字面量體操:

          // <N extends number> 要求 N 是 number 的子類型
          // 第一個判斷條件:`number extends N ?` 意思是如果 number 是 N 的子類型,就進入分支 1,否則進入分支 2
          // 第一個條件分支:如果 number 是 N 的子類型,則類型是 N,又已知 N 是 number 的子類型,那么 N=number
          // 第二個條件分支:如果`${N}` 的字符串字面量是 `-${string}` 的子類型,返回空類型,否則返回N
          type AssertPositive<N extends number>=number extends N ?
               N :
              `${N}` extends `-${string}` ? never : N;
          
          class Millseconds<N extends number> {
            constructor(public readonly value: AssertPositive<N>){
                if(this.value < 0){
                    throw new Error('Value Cannot Smaller Than 0');
                }
            }
          }
          
          new Millseconds(0); // OK
          new Millseconds(1); // OK
          new Millseconds(-1); // Error
          

          實施類型約束

          基本類型偏執模式的思路可以用到其他地方。假設我們需要有一個定時器,指定一個未來的絕對時間,在那個時間執行操作:

          declare function setTimer(absoluteTime: Date, callback: ()=> void): void;
          
          setTimer(new Date("2024-01-01T00:00:00"), console.log.bind(null, 'Happy New Year!');
          

          這里可讀性還是不錯的,很容易讀出來這里是要在 24 年元旦節祝你新年快樂。但是這里使用 Date 無法表明要一個未來的時間。

          和基本類型偏執一樣,我們可以套一個用于檢測約束的類型來優化:

          declare function setTimer(absoluteTime: FutureDate, callback: ()=> void): void;
          
          class FutureDate {
            constructor(public readonly date: string){
                const targetDate=new Date(date);
                if(!isNaN(targetDate.getTime()) || targetDate.valueOf() < new Date().valueOf()){
                    throw new Error('Error: Must provide a future date')
                }
            }
          }
          
          setTimer(new FutureDate("2024-01-01T00:00:00"), console.log.bind(null, 'Happy New Year!'))
          

          這種類型檢測的模式可以套用在很多類型信息不具體的地方。

          用運行時信息輔助類型系統

          安全的 any 互操作例子中,已經演示了怎么用運行時的數據來幫助類型系統更加健壯。在第 4 行調用 typeof 來獲取變量運行時類型名稱,根據運行時類型來進行強制類型轉換(TypeScript 的 is 返回值是一種類型向下轉換)。

          運用類似的思路,可以依據運行時信息編寫讓類型轉換更安全的代碼,從而健壯我們的類型推導。

          這里舉個例子,swift 語言中有一個經典的 Optional 類型設計。

          let number: Int?=Optional.some(42);
          
          if number==nil {
            print('number is nil')
          } else {
            print('The value is {number}')
          }
          

          TypeScript 3.7 已經用和類型 T | undefined 實現了類似的語法Optional Chain[7]。假設 TS 中沒有實現這個語法,我們需要手動寫一個 Optional 類型,如下代碼所示。

          class Optional<T> {
              private assigned=false;
          
              constructor(public value: T | undefined) {
                  if (value !==undefined) {
                      this.assigned=true;
                  }
              }
          
              hasValue() { return this.assigned }
          
              setValue(value?: T){
                 if (value !==undefined) {
                      this.assigned=true;
                      this.value=value;
                  }
              }
          
              getValue(): T {
                  if (this.assigned) {
                      return this.value as T
                  }
                  throw new Error('OptionalError: Value has not be assigned')
              }
          }
          
          const maybeNumber=new Optional<number>(1);
          
          // unboxing check
          if(maybeNumber.hasValue()){
            // `T | undefined` -> `T`
            const mustbeNumber: number=maybeNumber.getValue();
          }
          

          其中第 20 行通過判斷一個附加信息(this.assigned)后進行類型轉換,安全地將 undefined 排除出和類型 T | undefined

          深入類型系統原理

          如果你并不滿足于了解最基本的類型系統原理,那就可以看一下以下內容。

          類型可變性

          現在我們知道了基礎的子類型原理。假設我們現在有一個 Programmer 是 Employee 的子類型(class Programmer extends Employee),考慮這幾個問題:

          • 'A' | 'B''A' | 'B' | 'C' 的子類型關系如何?
          • Programmer[]Employee[] 的子類型關系如何?
          • 對于范型結構 List<Programmer>List<Employee> 的子類型關系如何?
          • ()=> Programmer()=> Employee 的子類型關系如何?
          • (input:Programmer)=> void(input: Employee)=> void 的子類型關系如何?

          在做這些證明之前,還是需要先明確子類型的定義:
          子類型(subtype) :如果在期望類型 T 的實例的任何地方,都可以安全地使用類型 S 的實例,那么稱類型 S 是類型 T 的子類型。

          • 對于和類型而言,父類型比子類型復雜度更高。換句話說,'A' | 'B''A' | 'B' | 'C' 的子類型。
            證明: 假設一個函數要求參數是 'A' | 'B' | 'C',那么我們傳入 'A' | 'B' 始終是合法的,反之則不行。所以 'A' | 'B''A' | 'B' | 'C' 的子類型。
          • 數組子類型關系和原類型子類型關系一致。
          declare const employeeSym: unique symbol;
          
          class Employee {
          	[employeeSym]: void
          }
          
          declare const programmerSym: unique symbol;
          
          class Programmer extends Employee {
          	[programmerSym]: void
          }
          
          const employees: Employee[]=[new Programmer()]; // OK
          const programmers: Programmer[]=[new Employee()]; // Error
          
          • 范型子類型關系和原類型子類型關系一致。
          declare const employeeSym: unique symbol;
          
          class Employee {
          	[employeeSym]: void
          }
          
          declare const programmerSym: unique symbol;
          
          class Programmer extends Employee {
          	[programmerSym]: void
          }
          
          class List<T> {
            constructor(public readonly list: T[]){};
          }
          
          let eList:List<Employee>=new List([new Employee()])
          let pList:List<Programmer>=new List([new Programmer()])
          
          eList=pList; // OK
          pList=eList;  // Error
          
          • 返回值子類型關系和原類型子類型關系一致。
          declare const employeeSym: unique symbol;
          
          class Employee {
          	[employeeSym]: void
          }
          
          declare const programmerSym: unique symbol;
          
          class Programmer extends Employee {
          	[programmerSym]: void
          }
          
          function getEmployee(getter: ()=> Employee) {
            return getter()
          }
          getEmployee(()=> new Employee()) // OK
          getEmployee(()=> new Programmer()) // OK
          
          function getProgrammer(getter: ()=> Programmer) {
            return getter()
          }
          getProgrammer(()=> new Programmer()) // OK
          getProgrammer(()=> new Employee()) // Error
          
          • 參數子類型關系和原類型子類型關系相反。
          declare const employeeSym: unique symbol;
          
          class Employee {
          	[employeeSym]: void
          }
          
          declare const programmerSym: unique symbol;
          
          class Programmer extends Employee {
          	[programmerSym]: void
          }
          
          function useEmployee(setter: (e: Employee)=> void) {
            return setter(new Employee())
          }
          function useProgrammer(setter: (e: Programmer)=> void) {
            return setter(new Programmer())
          }
          
          const employeeUser=(e: Employee)=> e;
          const programmerUser=(e: Programmer)=> e;
          
          useEmployee(employeeUser) // OK
          useEmployee(programmerUser) // Error
          useProgrammer(employeeUser) // OK
          useProgrammer(programmerUser) // OK
          

          協變性:如果一個類型保留其底層類型的子類型關系,就稱該類型具有協變性。

          逆變性:如果一個類型顛倒了其底層類型的子類型關系,則稱該類型具有逆變性。

          從數學角度理解類型

          類型:類型是對數據做的一種分類,定義了能夠對數據執行的操作、數據的意義。編譯器和運行時會檢查類型,以確保數據的完整性,實施訪問限制,以及按照開發人員的意圖來解釋數據。

          從數學上來看,類型就是一個集合

          • number 類型,代表一個 64 位浮點數可以表示的所有數字的一個集合。
          • string 類型,代表一個無限的集合,所有字符串數據都在此集合中。

          函數代表從一個集合到另外一個集合的映射。比如此函數類型定義:

          type typeA='a' | 'b' | 'c' | 'd'
          type typeB='m' | 'n' | 'p' | 'q'
          
          type a2b=(a: typeA)=> typeB;
          

          a2b 函數可以表示從 A 集合到 B 集合到一個映射。

          有多個函數參數的情況下,一個函數代表參數的積類型到返回值類型的一個映射。積類型的概念在本文后面介紹。

          說完了類型,再來看看類型系統的定義。類型系統是一組規則,從職責上來看,一個具有類型系統的編程語言代表:

          • 可以用類型表示語言中的所有元素所在的集合,比如變量、函數、類、模塊等;
          • 可以對類型進行邏輯運算推導,從而靜態代碼檢查等功能。

          名義子類型和結構子類型

          子類型的概念比較抽象,沒有指定具體實現方式,不同編程語言對子類型的實現不盡相同,但是一般可以分為兩種類型:名義子類型結構子類型

          名義子類型 Nominal Type System

          名義子類型意味著當且僅當顯式說明的情況下,兩個類型才具有父子類型關系。采用這種實現的語言有 C++ Java C# 等。

          // Java Compiler: https://www.jdoodle.com/online-java-compiler/
          class Employee{
           public int base=4000;
          }
          
          class Programmer extends Employee{
           public int base=5000;
          }
          
          class Advertiser {
           public int base=6000;
          }
          
          class Business {
             public static int getSalary(Employee who){
                 return who.base;
             }
          }
          
          public class Main {
            public static void main(String[] args){
                Business.getSalary(new Employee()); // output: 4000
                Business.getSalary(new Programmer()); // output: 5000
          
                // Incompatible Types Error: Advertiser cannot be converted to Employee
                Business.getSalary(new Advertiser());
            }
          }
          

          這是一段 Java 代碼來演示名義子類型的特性。Employee Programmer Advertiser 都包含一個 base 字段,Business.getSalary 方法指定了接受一個 Employee 類型參數,并返回他的 base 字段。

          因為名義子類型的要求,即使 Advertiser 的結構和 Employee 一模一樣,看起來 getSalary 方法也可以正常運行,也不允許輸入。

          結構子類型 Structural Type System

          結構子類型意味著,A 類型只要具有 B 類型的全部相同結構,就可以認為 A 是 B 的子類型,而不用顯式說明子類型關系。典型采用結構子類型的語言有 TypeScript 和 Scala。

          // TS Compiler: https://www.typescriptlang.org/play?ts=4.8.4
          class Employee{
              public base=4000;
          }
          
          class Programmer extends Employee{
              public base=5000;
          }
          
          class Advertiser {
              public base=6000;
              public bonus=1000;
          }
          
          function getSalary(who: Employee): number{
              return who.base;
          }
          
          getSalary(new Employee()) // 4000
          getSalary(new Programmer()) // 5000
          getSalary(new Advertiser()) // 6000
          

          這是一段用 TypeScript 模仿上述 Java 示例寫的代碼。Employee Programmer Advertiser 都包含一個 base 字段,getSalary 方法指定了接受一個 Employee 類型參數。

          和名義類型系統的差別是 getSalary(new Advertiser()) 可以正常運行,因為 Advertiser 包含全部 Employee 的相同結構,而不用顯式聲明 Advertiser 和 Employee 的關系。

          名義 vs 結構

          實際上,當在名義子類型語言中,聲明為父子類型的類型也要求有相同的結構。所以可以認為名義子類型比結構子類型的推導更嚴格,是結構子類型推導的一個子集。

          結構子類型可以表達為:

          A is a subtype of B
            when A is structurally identical to B
          

          名義子類型就表達為:

          A is a subtype of B
            when A is structurally identical to B
                and A is declared to be a subtype of B
          

          一般來說,使用結構子類型可以使類型系統更靈活;反之,名義子類型的使得類型檢查更嚴格。具體差別還是要看不同語言的實現細節。

          其他特殊類型和用法

          除了頂層類型和 any 類型之外,還有其他的特殊類型。

          底層類型

          當一個類型是其他所有可能的類型的子類型,則稱之為底層類型。換句話說,底層類型就是在聲明使用任何類型的地方,都可以安全地傳入的類型

          在 TypeScript 這種結構子類型系統的語言中,一個類型如果要是所有類型的子類型,那么就必須包含所有類型的結構。不可能創建出來一個變量滿足這種要求,所以底層類型只有一個: never

          declare let aNever: never; // 由于不可能創建一個 Never 變量,所以這里使用了 declare
          let aNumber: number=1;
          
          aNumber=aNever; // OK, never 是底層類型,所以是 number 的子類型
          aNever=aNumber; // Error: number 不是 never 的子類型
          

          單元類型

          單元類型:只有一個值的類型。對于這種類型的變量,檢查其值是沒有意義的,它只能是那一個值。

          對于 TypeScript (嚴格模式)來說,單元類型有三個:void null undefined。

          當函數的結果沒有意義時,我們會使用單元類型,一般來說我們都會用 void。為什么不用 null 和 undefined?因為 TypeScript 語言層面上限制 void 值只能從不返回的函數中產生,可以用來確保函數沒有任何返回語句。

          const log(message:string): void{
            console.log(message);
          }
          

          自己實現一個單元類型比較簡單,就是寫一個單例模式:

          declare const unitSymbol: unique symbol;
          
          class Unit {
           	[unitSymbol]: unknown;// 模擬名義子類型
          
            static readonly unit: Unit=new Unit();  // 唯一單例
          
            private constructor(){} // 私有化構造器保證沒有其他 instance
          }
          
          function getUnit(): Unit {
              return Unit.unit; // 只能返回唯一的單例 Unit.unit
          }
          
          getUnit()
          

          空類型

          空類型:沒有值的類型。

          對于 TypeScript 來說,空類型只有一個:never。

          一般我們只在函數不返回的情況下使用空類型作為返回值,比如拋出錯誤:

          function raise(message:string): never{
            throw new Error(message);
          }
          

          另外還有無限循環函數也可以返回空類型(一般在圖形學程序中比較多):

          function mainLoop(): never {
              while(true) {
                  /** ... */
              }
          }
          

          當你寫單例模式不要單例,就產生了一個空類型。但自制空類型一般沒有什么意義,一個編程語言中也往往只要一個空類型,為了好讀還是用 never 比較合適。為了演示,自制空類型代碼如下:

          declare const unitSymbol: unique symbol;
          
          class Void {
           	[unitSymbol]: unknown;// 模擬名義子類型
          
            private constructor(){} // 私有化構造器保證沒有 instance
          }
          
          function raise(message:string): Void { // 不返回的函數可以返回自制的空類型
            throw new Error(message);
          }
          

          類型組合復雜度

          大部分語言類型組合按復雜度一般有兩種:

          • 和類型 Sum Type
            代數上可以表達為 AB=A + B。即 AB 的復雜度是 A 的復雜度和 B 的復雜度之和。在 TypeScript 中,和類型就是聯合類型:
          type A='A1' | 'A2' | 'A3';
          type B='B1' | 'B2';
          
          type AB=A | B; // AB 可能值有 5 個=type A 3 個 + type B 2 個
          
          • 積類型 Product Type
            代數上可以表達為 AB=A * B。即 AB 的復雜度是 A 的復雜度和 B 的復雜度之乘積。在 TypeScript,積類型包括元祖、對象等等。
          type A='A1' | 'A2' | 'A3';
          type B='B1' | 'B2';
          
          type ABTuple=[A, B]; // 可能值有 6 個=type A 3 個 * type B 2 個
          type ABObject={ a: A, b: B }; // 可能值有 6 個=type A 3 個 * type B 2 個
          

          還有一種類型組合比較罕見,一般只在結構子類型系統中存在:

          • 交叉類型 Intersection Type
            交叉類型并沒有增加類型復雜度,而是根據兩個輸入類型 A B 的結構創建一個類型 C,其中 C 既是 A 的子類型,也是 B 的子類型。TypeScript 中交叉類型實現是 '&' 類型。
          type A={ a: boolean }
          type B={ b: number }
          
          type C=A & B; // C 既是 A 的子類型,也是 B 的子類型
          

          參考資料

          [1] 柯里-霍華德對應: https://zh.wikipedia.org/wiki/%E6%9F%AF%E9%87%8C-%E9%9C%8D%E5%8D%8E%E5%BE%B7%E5%90%8C%E6%9E%84

          [2] JSON.parse(): https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse

          [3] Reflect.get(): https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect/get

          [4] Response.json(): https://developer.mozilla.org/en-US/docs/Web/API/Response/json

          [5] TypeScript Playground: https://www.typescriptlang.org/play?ts=4.8.4

          [6] 《重構,改善既有代碼的設計》: https://weread.qq.com/web/bookDetail/2ed32e60811e3a304g014c02

          [7] Optional Chain: https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-7.html

          [8] Nominal And Structural Typing: https://www.eclipse.org/n4js/features/nominal-and-structural-typing.html#_nominal_and_structural_typing

          [9] product / sum / union / intersection types: https://www.jianshu.com/p/72c89e660559

          [10] 《編程與類型系統》: https://weread.qq.com/web/bookDetail/d9532b107221fcb0d95a94b


          關注「字節前端 ByteFE」公眾號,追更不迷路!


          主站蜘蛛池模板: 丝袜无码一区二区三区| 亚洲国产精品一区二区三区久久| 日本夜爽爽一区二区三区| 无码人妻精品一区二区三区夜夜嗨 | 色一情一乱一区二区三区啪啪高| 国产午夜精品一区理论片| 精品视频在线观看你懂的一区| 久久精品日韩一区国产二区| 亚洲国产成人精品无码一区二区| 无码人妻精品一区二区| 中文字幕一精品亚洲无线一区| 一区二区三区内射美女毛片| 国产精品一区二区资源| 日韩一区二区免费视频| 欧美日本精品一区二区三区 | 亚洲高清偷拍一区二区三区| 国产一区二区福利久久| 国产精品乱码一区二区三| 无码一区二区波多野结衣播放搜索 | 无码人妻少妇色欲AV一区二区 | 国产伦精品一区二区三区视频金莲| 国产微拍精品一区二区| 在线视频国产一区| 国产一区内射最近更新| 午夜福利国产一区二区| 中文字幕精品无码一区二区| 伊人色综合一区二区三区| 久久99精品国产一区二区三区| 久久一区不卡中文字幕| 97av麻豆蜜桃一区二区| 精产国品一区二区三产区| 欧洲亚洲综合一区二区三区| 日本精品一区二区三区在线视频一 | 国产吧一区在线视频| 国产成人精品一区二三区熟女| 精品国产一区二区三区在线| 波霸影院一区二区| 成人区精品人妻一区二区不卡| 日韩免费无码视频一区二区三区 | 无码日本电影一区二区网站| 国产一区在线视频观看|