Dojo Build 進階

建立包

一個包就是一部分代碼,它用於表示一部分功能。能夠按需異步、並行加載包。與不使用任何代碼拆分技術的應用程序相比,合理分包的應用程序能夠顯著提升響應速度,須要請求的字節數更少,加載的時間更短。在處理大型應用程序時,這一點尤爲重要,由於這類應用程序的大部分表現層邏輯在初始化時是不須要加載的。css

Dojo 嘗試使用路由和 outlet 智能地作出選擇,自動將代碼拆分爲更小的包。一般各個包內的代碼都是緊密相關的。這是構建系統內置的功能,可直接使用。可是,對於有特殊分包需求的用戶,Dojo 還容許在 .dojorc 配置文件中顯示定義包。html

默認狀況下,Dojo 應用程序只建立一個應用程序包。可是 @dojo/cli-build-app 提供了不少配置選項,這些選項可將應用程序拆分爲較小的、可逐步加載的包。node

使用路由自動分包

默認狀況下,Dojo 會基於應用程序的路由自動建立包。要作到這一點須要遵循如下幾條規則。webpack

  1. src/routes.ts 必須默認導出路由配置信息
  2. 部件所屬的模塊必須默認導出部件
  3. Outletrender 函數必須使用內聯函數
src/routes.ts
export default [
    {
        path: 'home',
        outlet: 'home',
        defaultRoute: true
    },
    {
        path: 'about',
        outlet: 'about'
    },
    {
        path: 'profile',
        outlet: 'profile'
    }
];
src/App.ts
export default class App extends WidgetBase {
    protected render() {
        return (
            <div classes={[css.root]}>
                <Menu />
                <div>
                    <Outlet key="home" id="home" renderer={() => <Home />} />
                    <Outlet key="about" id="about" renderer={() => <About />} />
                    <Outlet key="profile" id="profile" renderer={() => <Profile username="Dojo User" />} />
                </div>
            </div>
        );
    }
}

將會爲應用程序的每一個頂級路由生成單獨的包。在本例中,會生成一個應用程序的主包以及 src/Homesrc/Aboutsrc/Profile 三個包。git

使用 @dojo/cli-create-app 新建一個應用程序,而後運行 npm run build,就可看到自動分包的實際效果。Dojo 將自動爲示例應用程序中的全部路由建立包。github

手動分包

能夠在 .dojorc 配置文件中手動分包,這就爲應用程序提供了一種聲明式代碼拆分的手段。當自動根據路由分包沒法知足需求時,這對於將應用程序拆分爲更小的包是極其有用的。web

bundles 功能是 build app 命令的一部分。配置由由一組包名和緊隨其後的文件列表或匹配符組成。正則表達式

例如,如下配置將 AboutProfile 合在一個包中,並命名爲 additional.[hash].js。在 w() 中使用的部件模塊將被自動轉換爲在父部件中延遲加載的本地註冊項。npm

.dojorc
{
    "build-app": {
        "bundles": {
            "additional": ["src/widgets/About", "src/widgets/Profile"]
        }
    }
}

若是咱們想分地區建立國際化模塊,咱們應該使用通配符以確保將每一個語言目錄下的全部文件都會包含在內。json

.dojorc
{
    "build-app": {
        "bundles": {
            "fr": ["src/**/nls/fr/**"],
            "de": ["src/**/nls/de/**"]
        }
    }
}

在這種狀況下,Dojo 將建立名爲 fr.[hash].js 的包,和名爲 de.[hash].js 的包。想了解更新信息,請參閱國際化參考指南中的使用消息包

分包注意事項

有時,根據構建工具自動分包或者在 .dojorc 中手動定義的包中會重複包含被多個包共享的資源。有一些是沒法避免的。一個避免重複的法則是嘗試將共享代碼移到應用程序依賴樹的最外圍。換句話說,就是儘量減小共享代碼之間的依賴。若是大量的代碼在包之間共享(例如,公共的部件),請考慮將這些資源放在一個包中。

靜態資源

不少靜態資源,如在模塊中導入的 CSS 和圖片,在構建過程當中會被自動內聯。可是,有時還須要爲網站圖標(favicon)或視頻文件等靜態資源提供服務。

靜態資源存放在項目根目錄下的 assets/ 文件夾中。在構建時,這些資源會被複制到應用程序構建版本的 assets/ 文件夾中。

構建也會解析 src/index.html 中引用的 CSS、JavaScript 和圖片資源等,對這些資源名進行哈希處理,並在輸出文件夾中包含這些資源。能夠將 favicon 存放在 src 文件夾中,而後在 src/index.html 中引用。構建會自動對 favicon 文件名進行哈希處理,並會將文件複製到輸出文件夾中,而後重命名爲 favicon.[hash].ico

漸進式 web 應用程序

漸進式 web 應用程序(PWA)由一系列技術和模式組成,主要用於改善用戶體驗,幫助建立更可靠和可用的應用程序。尤爲是移動用戶,他們會發現應用程序能更好的集成到他們的設備中,就跟本地安裝的應用程序同樣。

漸進式 web 應用程序主要由兩種技術組成:Service Worker 和 Manifest。Dojo 的構建命令經過 .dojorcpwa 對象支持這兩種技術。

Manifest

Manifest 在一個 JSON 文件中描述一個應用程序,並提供了一些詳細信息,所以能夠直接從萬維網安裝到設備的主屏幕上。

.dojorc
{
    "build-app": {
        "pwa": {
            "manifest": {
                "name": "Todo MVC",
                "description": "A simple to-do application created with Dojo",
                "icons": [
                    { "src": "./favicon-16x16.png", "sizes": "16x16", "type": "image/png" },
                    { "src": "./favicon-32x32.png", "sizes": "32x32", "type": "image/png" },
                    { "src": "./favicon-48x48.png", "sizes": "48x48", "type": "image/png" },
                    { "src": "./favicon-256x256.png", "sizes": "256x256", "type": "image/png" }
                ]
            }
        }
    }
}

當提供了 manifest 信息時,dojo build 將在應用程序的 index.html 中注入必需的 <meta> 標籤。

  • mobile-web-app-capable="yes": 告知 Android 上的 Chrome 能夠將此應用程序添加到用戶的主界面上。
  • apple-mobile-web-app-capable="yes": 告知 iOS 設備能夠將此應用程序添加到用戶的主界面上。
  • apple-mobile-web-app-status-bar-style="default": 告知 iOS 設備,狀態欄使用默認外觀。
  • apple-touch-icon="{{icon}}": 至關於 Manifest 中的 icons,由於 iOS 當前沒有從 Manifest 中讀取 icons,因此須要爲 icons 數組中每張圖片單獨注入一個 meta 標籤。

Service worker

Service worder 是一種 web worker,可以攔截網絡請求、緩存和提供資源。Dojo 的 build 命令可以自動構建功能全面的 service worker,它會在啓動時激活,而後使用配置文件完成預緩存和自定義路由處理。

例如,咱們編寫一個配置文件來建立一個簡單的 service worker,它會緩存除了 admin 包以外的全部應用程序包,也會緩存應用程序最近訪問的圖像和文章。

.dojorc
{
    "build-app": {
        "pwa": {
            "serviceWorker": {
                "cachePrefix": "my-app",
                "excludeBundles": ["admin"],
                "routes": [
                    {
                        "urlPattern": ".*\\.(png|jpg|gif|svg)",
                        "strategy": "cacheFirst",
                        "cacheName": "my-app-images",
                        "expiration": { "maxEntries": 10, "maxAgeSeconds": 604800 }
                    },
                    {
                        "urlPattern": "http://my-app-url.com/api/articles",
                        "strategy": "cacheFirst",
                        "expiration": { "maxEntries": 25, "maxAgeSeconds": 86400 }
                    }
                ]
            }
        }
    }
}

ServiceWorker 配置

在底層,@dojo/webpack-contrib 中的 ServicerWorkerPlugin 用於生成 service worker,它的全部選項都是有效的 pwa.serviceWorker 屬性。

屬性 類型 可選 描述
bundles string[] 須要進行預緩存的一組包。默認是全部包。
cachePrefix string 在運行時進行預緩存使用的前綴。
clientsClaim boolean Service worker 是否要在開始激活時控制客戶端。默認爲 false
excludeBundles string[] 要從預緩存中排除的一組包。默認爲 []
importScripts string[] 須要在 service worker 中加載的一組腳本的路徑。
precache object 描述預緩存配置選項的對象(見下文)。
routes object[] 一組描述要在運行時緩存的配置對象(見下文)。
skipWaiting boolean Service worker 是否要跳過「等待」生命週期。

預緩存

precache 選項使用如下選項控制預緩存行爲:

屬性 類型 可選 描述
baseDir string 匹配 include 時使用的根目錄。
ignore string[] 一組通配符模式的字符串,當生成預緩存項時用於匹配須要忽略的文件。默認爲 [ 'node_modules/**/*' ]
include string or string[] 一個或者一組通配符模式的字符串,用於匹配 precache 應該包含的文件。默認是構建管道中的全部文件。
index string 若是請求以 / 結尾的 URL 失敗,則應該查找的 index 文件名。默認爲 'index.html'
maxCacheSize number 往預緩存中添加的每個文件不該超過的最大字節數。默認爲 2097152 (2 MB)。
strict boolean 若是爲 true,則 include 模式匹配到一個不存在的文件夾時,構建就會失敗。默認爲 true
symlinks boolean 當生成預緩存時是否容許軟鏈接(symlinks)。默認爲 true

運行時緩存

除了預緩存以外,還能夠爲特定路由提供緩存策略,以肯定它們是否能夠緩存以及如何緩存。routes 選項是一組包含如下屬性的對象:

屬性 類型 可選 描述
urlPattern string 用於匹配特定路由的模式字符串(會被轉換爲正則表達式)。
strategy string 緩存策略(見下文)。
options object 一個描述附加選項的對象。每一個選項的詳情以下。
cacheName string 路由使用的緩存名稱。注意 cachePrefix 不會 添加到緩存名前。默認爲主運行時緩存(${cachePrefix}-runtime-${domain})。
cacheableResponse object 使用 HTTP 狀態碼或者報頭(Header)信息來決定是否能夠緩存響應的內容。此對象有兩個可選屬性:statusesheadersstatuses 是一組對緩存生效的狀態碼。headers 是一組 HTTP 的 header 和 value 鍵值對;至少要與一個報頭匹配,響應纔會被視爲有效。當 strategy 的值是 'cacheFirst' 時,默認爲 { statuses: [ 200 ] };當 strategy 的值爲 networkFirst 或者 staleWhileRevalidate 時,默認爲 { statuses: [0, 200] }
expiration object 控制如何讓緩存失效。此對象有兩個可選屬性。maxEntries 是任什麼時候間能夠緩存的響應個數。一旦超過此最大值,就會刪除最舊的條目。maxAgeSeconds 是一個響應能緩存的最長時間(以秒爲單位),超過此時長就會被刪除。
networkTimeoutSeconds number networkFirst 策略一塊兒使用,指定當網絡請求的響應多久沒有返回時就從緩存中獲取資源,單位爲秒。

目前支持四種路由策略:

  • networkFirst 嘗試經過網絡加載資源,若是請求失敗或超時才從緩存中獲取資源。對於頻繁更改或者可能頻繁更改(即沒有版本控制)的資源,這是一個頗有用的策略。
  • cacheFirst 優先從緩存中加載資源,若是緩存中不存在,則經過網絡獲取。這對於不多更改或者能緩存很長一段時間的資源(受版本控制的資源)來講是最好的策略。
  • networkOnly 強制始終經過網絡獲取資源,對於無需離線處理的資源是頗有用的策略。
  • staleWhileRevalidate 同時從緩存和網絡中請求資源。網絡成功響應後都會更新緩存。此策略最適用於不須要持續更新的資源,好比用戶信息。可是,當獲取第三方資源時沒有發送 CORS 報頭,就沒法讀取響應的內容或驗證狀態碼。所以,可能會緩存錯誤的響應。在這種狀況下,networkFirst 策略可能更適合。

構建時渲染

構建時渲染(Build-time rendering,簡稱 BTR)在構建過程當中將一個路由渲染爲一個 HTML,並將在初始視圖中顯示的、關鍵的 CSS 和資源嵌入到頁面中。Dojo 能預渲染路由使用的初始 HTML,並直接注入到頁面中,這樣會帶來不少與服務器端渲染(SSR)相同的好處,如性能提高、搜索引擎優化且沒有引入 SSR 的複雜性。

使用 BTR

首先確保 index.html 中包含一個擁有 id 屬性的 DOM 節點。Dojo 的虛擬 DOM 會使用這個節點來比較和渲染應用程序的 HTML。BTR 須要此設置,這樣它就能渲染在構建階段生成的 HTML。這將會爲路由建立一個響應很是快的初始渲染。

index.html
<!DOCTYPE html>
<html lang="en-us">
    <head>
        <title>sample-app</title>
        <meta name="theme-color" content="#222127" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
    </head>
    <body>
        <div id="app"></div>
    </body>
</html>

而後將應用程序掛載到指定的 DOM 節點上:

main.ts
const r = renderer(() => w(App, {}));
const domNode = document.getElementById('app') as HTMLElement;
r.mount({ registry, domNode });

而後更新項目的 .dojorc 配置文件,設置根 DOM 節點的 id 和在構建時要渲染的路由。

.dojorc
{
    "build-app": {
        "build-time-render": {
            "root": "app",
            "paths": [
                "#home",
                {
                    "path": "#comments/9999",
                    "match": ["#comments/.*"]
                }
            ]
        }
    }
}

此配置描述了兩個路由。一個是 home 路由,一個是較複雜的 comments 路由。comments 是一個比較複雜的路由,須要傳入參數。match 參數用於確保在構建時爲此路由生成的 HTML 能夠應用到與此正則表達式匹配的任何路由上。

BTR 在構建時爲每一個渲染路徑(path)生成一份屏幕快照,存在 ./output/info/screenshots 文件夾中。

History 管理器

構建時渲染支持使用 @dojo/framework/routing/history/HashHistory@dojo/framework/routing/history/StateHistory history 管理器的應用程序。當使用 HashHistory 時,確保全部的路徑都是以 # 字符開頭。

build-time-render 功能標記

運行時渲染公開了一個 build-time-render 功能標記,可用於跳過在構建時不能執行的功能。這樣在建立一個初始渲染時,就能夠避免對外部系統調用 fetch,而是提供靜態數據。

if (!has('build-time-render')) {
    const response = await fetch(/* remote JSON */);
    return response.json();
} else {
    return Promise.resolve({
        /* predefined Object */
    });
}

Dojo Blocks

Dojo 提供了一個 block 系統,在構建階段的渲染過程當中會執行 Node.js 代碼。執行的結果會被寫入到緩存中,而後在瀏覽器運行階段會以相同的方式、透明的使用這些緩存。這就爲使用一些瀏覽器中沒法實現或者性能不佳的操做開闢了新的機會。

例如,Dojo 的 block 模塊能夠讀取一組 markdown 文件,將其轉換爲 VNode,並使它們能夠在應用程序中渲染,全部這些均可在構建時執行。而後 Dojo block 模塊的構建結果會緩存在應用程序的包中,以便在運行時在瀏覽器中使用。

Dojo block 模塊的用法與在 Dojo 部件中使用其它 meta 的用法相似。所以無需大量的配置或其餘編寫模式。

例如,block 模塊讀取一個文本文件,而後將內容返回給應用程序。

src/blocks/read-file.ts
import * as fs from 'fs';
import { resolve } from 'path';

export default (path: string) => {
    path = resolve(__dirname, path);
    return fs.readFileSync(path, 'utf8');
};
src/widgets/MyBlockWidget.tsx
import Block from '@dojo/framework/core/meta/Block';
import WidgetBase from '@dojo/framework/core/WidgetBase';
import { v } from '@dojo/framework/core/vdom';

import readFile from '../blocks/read-file';

export default class MyBlockWidget extends WidgetBase {
    protected render() {
        const message = this.meta(Block).run(readFile)('../content/hello-dojo-blocks.txt');
        return v('div', [message]);
    }
}

這個部件會在構建階段運行 src/blocks/read-file.ts 模塊來讀取指定文件的內容。而後將內容做爲文本節點,用做部件 VDOM 輸出的子節點。

按條件選取代碼

構建工具的靜態代碼分析工具可以從它建立的包中移除無用的代碼。命名的條件塊是使用 dojo 框架的 has 模塊定義的,而且能夠在 .dojorc 中靜態設置爲 true 或 false,而後在構建階段移除。

main.ts
import has from '@dojo/framework/has';

if (has('production')) {
    console.log('Starting in production');
} else {
    console.log('Starting in dev mode');
}

export const mode = has('production') ? 'dist' : 'dev';
.dojorc
{
    "build-app": {
        "features": {
            "production": true
        }
    }
}

上述的 production 功能將構建生產版本dist 模式)設置爲 true。構建系統使用 @dojo/framework/has 將代碼標記爲沒法訪問,並在構建時移除這些無用的代碼。

好比,上述代碼將重寫爲:

static-build-loader 輸出
import has from '@dojo/framework/has';

if (true) {
    console.log('Starting in production');
} else {
    console.log('Starting in dev mode');
}

export const mode = true ? 'dist' : 'dev';

而後,構建工具的無用分支移除工具將移除無用的代碼。

Uglify 輸出
console.log('Starting in production');
export const mode = 'dist';

任何沒有被靜態斷言的功能都不會被重寫。這就容許在運行時來肯定是否存在這個功能。

已提供的功能

構建系統已提供如下功能(feature),用於幫助識別特定的環境或操做模式。

功能標記 描述
debug 提供了一種爲代碼建立代碼路徑的方法,該代碼路徑僅在調試或者提供更強的診斷時有用,在爲 生產 構建時是不須要的。默認爲 true,但在構建生產版本時應該靜態地配置爲 false
host-browser 肯定當前環境下 global 上下文中是否包含 windowdocument 對象,所以一般能夠安全地假設代碼運行在瀏覽器環境下。
host-node 嘗試檢測當前環境是否是 node 環境。
build-time-render 在構建期間渲染時由 BTR 系統靜態定義。

外部依賴

一般不能被打包的非模塊化庫或者獨立的應用程序,若是須要引入到 dojo 應用程序中,則能夠經過提供一個 requiredefine 實現,並在項目的 .dojorc 文件中作一些配置。

要配置外部依賴項,則須要爲 build-app 配置對象設置 externals 屬性。externals 是一個對象,包含如下兩個屬性:

  • outputPath: 一個可選屬性,指定一個將文件複製到何處的輸出路徑。
  • dependencies: 一個必填的數組,定義哪些模塊應該經過外部加載器加載,以及在構建時應該包含哪些文件。每一個記錄能夠是如下兩種類型之一:

    • 一個字符串,表示應該使用外部加載器加載此路徑及其全部子路徑。
    • 一個對象,爲須要複製到構建版應用程序的依賴提供附加配置項。此對象具備如下屬性:
屬性 類型 可選 描述
from string 相對於項目根目錄的路徑,指定位於何處的文件夾或目錄要複製到已構建應用程序中。
to string 一個路徑,表示將 from 路徑下的依賴複製到何處的目標路徑。默認狀況下,依賴會被複制到 ${externalsOutputPath}/${to};若是沒有設置 to,依賴會被複制到 ${externalsOutputPath}/${from}。若是路徑中包含 . 字符或者路徑表示的是一個文件夾,則須要以正斜槓結尾。
name string 在應用程序代碼中引用的模塊 id 或者全局變量名。
inject string, string[], or boolean 此屬性表示這個依賴定義的(或者包含的),要在頁面中加載的腳本或樣式文件。若是 inject 的值爲 true,那麼就會在頁面中加載 tofrom 指定位置的文件。若是依賴的是文件夾,則 inject 能夠被設置爲一個或者一組字符串,來定義一個或多個要注入的文件。inject 中的每一個路徑都應該是相對於 ${externalsOutputPath}/${to}${externalsOutputPath}/${from}(具體取決因而否指定了 to)。
type 'root' or 'umd' or 'amd' or 'commonjs' or 'commonjs2' 強制模塊用指定的方法解析。若是是 AMD 風格,則必須使用 umdamd。若是是 node 風格則必須使用 commonjs,而且值爲 root 時以全局的方式訪問對象。

例如,如下配置會將 src/legacy/layer.js 注入到應用程序頁面中;注入定義了 MyGlobal 全局變量的文件;聲明模塊 ab 爲外部依賴,且要委託給外部層;而後複製 node_modules/legacy-dep 下的文件,並將其中的幾個文件注入到頁面中。全部文件都將被複制到 externals 文件夾中,也可使用 externals 配置中的 outputPath 屬性來從新指定文件夾。

{
    "build-app": {
        "externals": {
            "dependencies": [
                "a",
                "b",
                {
                    "from": "node_modules/GlobalLibrary.js",
                    "to": "GlobalLibrary.js",
                    "name": "MyGlobal",
                    "inject": true
                },
                { "from": "src/legacy/layer.js", "to": "legacy/layer.js", "inject": true },
                {
                    "from": "node_modules/legacy-dep",
                    "to": "legacy-dep/",
                    "inject": ["moduleA/layer.js", "moduleA/layer.css", "moduleB/layer.js"]
                }
            ]
        }
    }
}

externals 中包含的依賴項的類型會被安裝到 node_modules/@types 中,這跟其它依賴項是同樣的。

由於這些文件位於主構建(main build)以外,因此在生產構建中不會執行版本控制或哈希處理(在 inject 中指定資源的連接除外)。能夠在 to 屬性中指定版本號,將依賴複製到對應版本的文件夾下,這樣就能避免緩存不一樣版本的文件。

脫離 Dojo 構建管道

Dojo 的構建管道爲項目提供了一個端到端的工具鏈,可是,在極少數狀況下,可能須要自定義工具鏈。只要將項目脫離 Dojo 的構建管道,就能夠自定義工具鏈。

將項目脫離構建管道,是一個不可逆的、單向過程,它會導出 Webpack、Intern 以及 dojo 命令使用的其餘項目的底層配置文件。若是提供的生成工具沒法提供所需的功能或特性,推薦的方法是 fork 選定的構建命令,而後往工具中添加額外的功能。Dojo 的 CLI 本質上是專門按模塊化設計的,考慮到了這個用例。

要將一個項目脫離出 dojo 構建管道,請使用 dojo eject 命令,它將提示你確實已明白過程是不可逆的。這個導出過程將全部已安裝的 dojo 命令中導出的配置信息存到 config 文件夾中。這個過程也會安裝一些項目須要的附加依賴。

如今項目已是一個 webpack 項目。能夠經過修改 config/build-app/base.config.js 來更改構建配置。

而後,能夠經過運行 webpack 的構建命令並提供配置項來觸發一個構建。此外,使用 webpack 的 env 標記(例如 --env.mode=dev)來指定模式,默認爲 dist。

./node_modules/.bin/webpack --config=config/build-app/ejected.config.js --env.mode=[mode]
相關文章
相關標籤/搜索