AngularでSSRをしてみる
SPAの敵はファイルサイズの大きいJSや初期化にかかる時間です。
そのなかでファイルサイズはAoTやuglifyで小さくしていくようにしています。
初期化にかかる時間を抑えるには、サーバーサイドレンダリングが1つの解になります。
Angularでは現在@angular/universalというSSRを実現するプロジェクトがあります。
今回はこれを使ってみます。
利用するには
angular-universalを利用するのもいいですが、ここは素早く利用できるuniversal-starterを使ってみましょう
まずはuniversal-starterをcloneしてきます。
% git clone git@github.com:angular/universal-starter
Cloning into 'universal-starter'...
remote: Counting objects: 2070, done.
remote: Compressing objects: 100% (23/23), done.
remote: Total 2070 (delta 10), reused 0 (delta 0), pack-reused 2047
Receiving objects: 100% (2070/2070), 370.34 KiB | 437.00 KiB/s, done.
Resolving deltas: 100% (1272/1272), done.
Checking connectivity... done.
レポジトリのディレクトリに移動し、npm installを実行します
cd universal-starter && npm install
起動してみる
% npm start
> universal-starter@2.0.0 prestart /Users/teyosh/Test/advent/universal-starter
> npm run build
> universal-starter@2.0.0 prebuild /Users/teyosh/Test/advent/universal-starter
> npm run clean:dist
> universal-starter@2.0.0 clean:dist /Users/teyosh/Test/advent/universal-starter
> rimraf dist
> universal-starter@2.0.0 build /Users/teyosh/Test/advent/universal-starter
> webpack --progress
[0] Hash: f3ecdceb6ff8b863cd64d0ac0448d61acda74259 r Version: webpack 2.1.0-beta.27
Child
Hash: f3ecdceb6ff8b863cd64
Version: webpack 2.1.0-beta.27
Time: 12533ms
Asset Size Chunks Chunk Names
0.bundle.js 2.23 kB 0 [emitted]
main.bundle.js 2.99 MB 1 [emitted] main
0.bundle.js.map 2.35 kB 0 [emitted]
main.bundle.js.map 2.95 MB 1 [emitted] main
[279] ./src async 160 bytes {1} [built]
[446] ./src/+app/+lazy async ^\.\/lazy\.module.*$ 160 bytes {1} [built]
+ 449 hidden modules
Child
Hash: d0ac0448d61acda74259
Version: webpack 2.1.0-beta.27
Time: 12512ms
Asset Size Chunks Chunk Names
0.index.js 2.24 kB 0 [emitted]
index.js 2.66 MB 1 [emitted] main
0.index.js.map 2.35 kB 0 [emitted]
index.js.map 2.56 MB 1 [emitted] main
[260] ./src async 160 bytes {1} [built]
[406] ./src/+app/+lazy async ^\.\/lazy\.module.*$ 160 bytes {1} [built]
+ 429 hidden modules
> universal-starter@2.0.0 start /Users/teyosh/Test/advent/universal-starter
> npm run server
> universal-starter@2.0.0 server /Users/teyosh/Test/advent/universal-starter
> nodemon dist/server/index.js
[nodemon] 1.11.0
[nodemon] to restart at any time, enter `rs`
[nodemon] watching: /Users/teyosh/Test/advent/universal-starter/dist/**/* src/index.html
[nodemon] starting `node dist/server/index.js`
Listening on: http://localhost:3000
ブラウザで確認
/data.json Cache Miss
GET /data.json 200 3.359 ms - 62
GET /home 200 111.378 ms - -
こうなります。
これだけではSSRかどうかはわからないですね
index.html
こちらがソースのindexになります
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<title>Angular 2 Universal Starter</title>
<meta name="description" content="Angular 2 Universal">
<meta name="keywords" content="Angular 2,Universal">
<meta name="author" content="PatrickJS">
<meta name="viewport" content="width=device-width,minimum-scale=1">
<link rel="icon" href="data:;base64,iVBORw0KGgo=">
<link rel="prerender" href="http://localhost:3000/lazy">
<link rel="preload" href="/assets/logo.png">
<base href="/">
</head>
<body>
<app>
Loading Universal ...
</app>
<script async src="/main.bundle.js"></script>
</body>
</html>
で、こっちがブラウザが取得したindexになります
<!DOCTYPE html><html><head>
<meta charset="UTF-8">
<title>Angular 2 Universal Starter</title>
<meta name="description" content="Angular 2 Universal">
<meta name="keywords" content="Angular 2,Universal">
<meta name="author" content="PatrickJS">
<meta name="viewport" content="width=device-width,minimum-scale=1">
<link rel="icon" href="data:;base64,iVBORw0KGgo=">
<link rel="prerender" href="http://localhost:3000/lazy">
<link rel="preload" href="/assets/logo.png">
<base href="/">
<style>*[_ngcontent-a72c-1] { padding:0; margin:0; font-family: 'Droid Sans', sans-serif; }
#universal[_ngcontent-a72c-1] { text-align:center; font-weight:bold; padding:15px 0; }
nav[_ngcontent-a72c-1] { background:#158126; min-height:40px; border-bottom:5px #046923 solid; }
nav[_ngcontent-a72c-1] a[_ngcontent-a72c-1] { font-weight:bold; text-decoration:none; color:#fff; padding:20px; display:inline-block; }
nav[_ngcontent-a72c-1] a[_ngcontent-a72c-1]:hover { background:#00AF36; }
.hero-universal[_ngcontent-a72c-1] { min-height:500px; display:block; padding:20px; background: url('/assets/logo.png') no-repeat center center; }
.inner-hero[_ngcontent-a72c-1] { background: rgba(255, 255, 255, 0.75); border:5px #ccc solid; padding:25px; }
.router-link-active[_ngcontent-a72c-1] { background-color: #00AF36; }
main[_ngcontent-a72c-1] { padding:20px 0; }
pre[_ngcontent-a72c-1] { font-size:12px; }</style><style>blockquote[_ngcontent-a72c-3] {
border-left:5px #158126 solid;
background:#fff;
padding:20px 20px 20px 40px;
}
blockquote[_ngcontent-a72c-3]::before {
left: 1em;
}</style></head>
<body>
<app _nghost-a72c-1="">
<h3 _ngcontent-a72c-1="" id="universal">Angular2 Universal</h3>
<nav _ngcontent-a72c-1="">
<a _ngcontent-a72c-1="" routerLink="home" routerLinkActive="router-link-active" href="/home" class="router-link-active">Home</a>
<a _ngcontent-a72c-1="" routerLink="about" routerLinkActive="router-link-active" href="/about">About</a>
<a _ngcontent-a72c-1="" routerLink="todo" routerLinkActive="router-link-active" href="/todo">Todo</a>
<a _ngcontent-a72c-1="" routerLink="lazy" routerLinkActive="router-link-active" href="/lazy">Lazy</a>
</nav>
<div _ngcontent-a72c-1="" class="hero-universal">
<div _ngcontent-a72c-1="" class="inner-hero">
<div _ngcontent-a72c-1="">
<span _ngcontent-a72c-1="" xLarge="" style="font-size:x-large;">Universal JavaScript ftw!</span>
</div>
Two-way binding: <input _ngcontent-a72c-1="" type="text" value="ftw">
<br _ngcontent-a72c-1="">
<br _ngcontent-a72c-1="">
<strong _ngcontent-a72c-1="">Router-outlet:</strong>
<main _ngcontent-a72c-1="">
<router-outlet _ngcontent-a72c-1=""></router-outlet><home _nghost-a72c-3=""><div _ngcontent-a72c-3="" class="home">
Home component
<strong _ngcontent-a72c-3="">Async data call return value:</strong>
<pre _ngcontent-a72c-3="">{
"data": "This fake data came from the db on the server."
}</pre>
<blockquote _ngcontent-a72c-3="">This fake data came from the db on the server.</blockquote>
</div>
</home>
</main>
</div>
</div>
</app>
<script async="" src="/main.bundle.js"></script>
<universal-script><script>
try {window.UNIVERSAL_CACHE = ({"APP_ID":"a72c","CacheService":"{\"/data.json\":{\"data\":\"This fake data came from the db on the server.\"}}"}) || {};} catch(e) { console.warn("Angular Universal: There was a problem parsing data from the server")}
</script></universal-script></body></html>
<app></app>
の中身が生成されてブラウザに送られているのが分かるかと思います。
初期化の処理が省かれるのはもちろん、SEO対策もできる一石二鳥です。
AoT
もちろんAoTにも対応しています。
% npm run build:prod:ngc
> universal-starter@2.0.0 build:prod:ngc /Users/teyosh/Test/advent/universal-starter
> npm run clean:ngc && npm run ngc && npm run clean:dist && npm run build:prod
> universal-starter@2.0.0 clean:ngc /Users/teyosh/Test/advent/universal-starter
> rimraf **/*.ngfactory.ts **/*.css.shim.ts
> universal-starter@2.0.0 ngc /Users/teyosh/Test/advent/universal-starter
> ngc -p tsconfig.aot.json
> universal-starter@2.0.0 clean:dist /Users/teyosh/Test/advent/universal-starter
> rimraf dist
> universal-starter@2.0.0 build:prod /Users/teyosh/Test/advent/universal-starter
> webpack --config webpack.prod.config.ts
Hash: b1b2d9e2afd25d442e1f48118b7ce828e0579d81
Version: webpack 2.1.0-beta.27
Child
Hash: b1b2d9e2afd25d442e1f
Version: webpack 2.1.0-beta.27
Time: 27017ms
Asset Size Chunks Chunk Names
72fdbbaf74c66f7684c5.js 5.62 kB 0 [emitted]
main.bundle.js 616 kB 1 [emitted] main
72fdbbaf74c66f7684c5.js.map 32.3 kB 0 [emitted]
main.bundle.js.map 4.53 MB 1 [emitted] main
stats.json 2.61 MB [emitted]
[26] ./empty.js 191 bytes {1} [built]
[216] ./src async 160 bytes {1} [built]
+ 351 hidden modules
Child
Hash: 48118b7ce828e0579d81
Version: webpack 2.1.0-beta.27
Time: 27005ms
Asset Size Chunks Chunk Names
0.bundle.js 8.94 kB 0 [emitted]
index.js 1.66 MB 1 [emitted] main
0.bundle.js.map 32.4 kB 0 [emitted]
index.js.map 6.26 MB 1 [emitted] main
[273] ./src async 160 bytes {1} [built]
+ 435 hidden modules
ファイルサイズは結構変わります。
AoTしていない場合
% ls -lh client
total 11704
-rw-r--r-- 1 teyosh staff 12K 12 19 14:50 0.bundle.js
-rw-r--r-- 1 teyosh staff 20K 12 19 14:50 0.bundle.js.map
-rw-r--r-- 1 teyosh staff 2.2K 12 19 14:50 1.bundle.js
-rw-r--r-- 1 teyosh staff 2.3K 12 19 14:50 1.bundle.js.map
-rw-r--r-- 1 teyosh staff 2.9M 12 19 14:50 main.bundle.js
-rw-r--r-- 1 teyosh staff 2.8M 12 19 14:50 main.bundle.js.map
% ls -lh server
total 10296
-rw-r--r-- 1 teyosh staff 12K 12 19 14:49 0.index.js
-rw-r--r-- 1 teyosh staff 20K 12 19 14:49 0.index.js.map
-rw-r--r-- 1 teyosh staff 2.2K 12 19 14:49 1.index.js
-rw-r--r-- 1 teyosh staff 2.3K 12 19 14:49 1.index.js.map
-rw-r--r-- 1 teyosh staff 2.5M 12 19 14:49 index.js
-rw-r--r-- 1 teyosh staff 2.4M 12 19 14:50 index.js.map
AoTした場合
% ls -lh client
total 15240
-rw-r--r-- 1 teyosh staff 5.5K 12 19 14:58 72fdbbaf74c66f7684c5.js
-rw-r--r-- 1 teyosh staff 31K 12 19 14:58 72fdbbaf74c66f7684c5.js.map
-rw-r--r-- 1 teyosh staff 602K 12 19 14:58 main.bundle.js
-rw-r--r-- 1 teyosh staff 4.3M 12 19 14:58 main.bundle.js.map
-rw-r--r-- 1 teyosh staff 2.5M 12 19 14:58 stats.json
% ls -lh server
total 15560
-rw-r--r-- 1 teyosh staff 8.7K 12 19 14:58 0.bundle.js
-rw-r--r-- 1 teyosh staff 32K 12 19 14:58 0.bundle.js.map
-rw-r--r-- 1 teyosh staff 1.6M 12 19 14:58 index.js
-rw-r--r-- 1 teyosh staff 6.0M 12 19 14:58 index.js.map
まだまだ、開発中で不安定ですが、SEO対策や初期化で悩んでいる場合は使ってみても良いかもしれないですね!!