React 簡介、Server 與 Client 共用 component render 與網頁最佳化

久違了,兩個月多沒寫文章…好一陣子忙到沒時間學東西,最近稍微擠出一點時間學了 React,React 是我一直很想學的一個 View 的 framework,不像是 Angular.js、Ember.js、Backbone.js 等等 MVC fullstack frameworks,React 只專心處理 View 的部分,他解決了網頁前端的開放上常見的問題,並且讓網頁的 render 最佳化。

React 特色

React 使用 virtual DOM 的技術,用 JavaScript 描繪出 DOM 的結構,並且去管理,每次的變動都先經過 JavaScript 計算後才會到 DOM 去更新,因為根據 Facebook 的統計分析,影響網頁 render 效能的關鍵在於 DOM 的操作,JavaScript 本身是非常快速的!

Data flow 也是 React 主打的一部分,React 只允許單向的資料流,讓 React 的 Components 更清楚的定義,可以更快速的開發。

JSX (JavaScript syntax extension),是 React 推薦的開發語言,不是必要的,但是實際使用 React 開發後,會愛上使用 JSX 開發,JSX 讓 code 更清楚易懂!

React 能帶來的好處

  1. View 之間有清楚的邏輯關係的
  2. View 的切換是簡單的(例如:沒有資料時要有另一種呈現方式)
  3. View 的 re-use

使用 React 開發的問題

如果按照 React 的 Getting Started 來學的話,就會發現到好多問題…

  1. 撰寫好的 JSX 不能直接拿來用,需要經過 react-tools 編譯過才可以在瀏覽器執行
  2. 越來越多的 build 好的 .js 一個一個 include 進來會影響網頁載入的效能
  3. React server render? (SEO 和減少空白畫面的時間)
  4. Client 和 Server 的 component 共用?

使用 React 開發的問題之解決辦法

不在使用 react-tools 來轉換 JSX 成為 JS ,改用 Gulp 來處理,把多個 build 好的 JS 合併為一個 JS 使用的是 browserify,Server render 使用 Express + express-react-views,共用的部分因為有使用了 browserify 已經處理好這問題了,只要把 component 的撰寫方式使用 module patten 即可。

要使用 Gulp 就要建立一個 gulpfile.js 檔,下面的是完整的 gulpfile 的內容,因為這邊不是 SPA 的範例,所以有分不同的 App 分別 bundle 起來,透過這樣的設定,就可以修改完 JSX 時就 build 好 bundle 好的js,只要 reload 就可以看到新的了,如果連 reload 都懶,可以搭配 livereload 即可。同時可以再搭配 React Hot Loader,讓你更新 JSX 後不會影響到目前的 state。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
var gulp = require('gulp');
var source = require('vinyl-source-stream');
var browserify = require('browserify');
var watchify = require('watchify');
var reactify = require('reactify');
var concat = require('gulp-concat');
var uglify = require('gulp-uglify');
var minifyCSS = require('gulp-minify-css');
var es = require('event-stream');
var glob = require("glob");
var rename = require("gulp-rename");
var buffer = require('vinyl-buffer');
gulp.task('browserify', function() {
return glob('./apps/*.js', function(err, files) {
var tasks = files.map(function(entry) {
var bundler = browserify({
entries: [entry],
transform: [reactify], // We want to convert JSX to normal javascript
debug: false, // Gives us sourcemapping if true
cache: {},
packageCache: {},
// fullPaths: true // Requirement of watchify
});
var watcher = watchify(bundler);
return watcher
.on('update', function(err) { // When any files update
watcher.bundle() // Create new bundle that uses the cache for high performance
.pipe(source(entry))
.pipe(buffer())
.pipe(uglify())
.pipe(gulp.dest('public/build/'));
})
.bundle() // Create the initial bundle when starting the task
.pipe(source(entry))
.pipe(buffer())
.pipe(uglify())
.pipe(gulp.dest('public/build/'));
});
return es.merge.apply(null, tasks);
});
});
gulp.task('css', function() {
gulp.watch('public/css/**/*.css', function() {
return gulp.src('public/css/**/*.css')
.pipe(minifyCSS())
.pipe(concat('main.css'))
.pipe(gulp.dest('public/build/'));
});
});
gulp.task('default', ['browserify', 'css']);

接下來看 Server 端的設定,透過在 Server 時額外埋一個props serverRender: true 來讓 HEAD 內的東西不再 render 一次,當有需要互動的時候可以在apps 建立 *.js 檔,並在 client 端在init 一次 React。(React 會很聰明的判斷 DOM 一樣時,不再重新render,並不會影響到效能!)

app.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var express = require('express');
var app = express();
app.set('view engine', 'jsx');
app.engine('jsx', require('express-react-views').createEngine());
app.get('/', function(req, res){
res.render('index', {
title: 'Hello World!',
serverRender: true,
name: 'Shiny',
scripts: [
'/build/apps/index.js'
],
styles: [
'/build/main.css'
]
});
});
views/index.jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
var React = require('react');
var DefaultLayout = require('./layouts/default.jsx');
var IndexView = React.createClass({
render: function() {
return (
<DefaultLayout title={this.props.title}>
<div>Hello {this.props.name}</div>
</DefaultLayout>
);
}
});
module.exports = IndexView;
views/layouts/default.jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
var React = require('react');
var DefaultLayout = React.createClass({
render: function() {
var scripts = this.props.scripts.map(function(src, idx) {
return <script key={idx} src={src}></script>
});
var styles = this.props.styles.map(function(src, idx) {
return <link key={idx} rel="stylesheet" href={src}/>
});
if (!this.props.serverRender) {
return (
<div>{this.props.children}</div>
);
}
return (
<html>
<head>
<meta charSet="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<meta httpEquiv="X-UA-Compatible" content="IE=edge"/>
<title>{this.props.title}</title>
{styles}
</head>
<body>
<div className='app-container'>
<div>{this.props.children}</div>
</div>
{scripts}
</body>
</html>
);
}
});
apps/index.js
1
2
3
4
var React = require('react');
var IndexView = require('../views/index.jsx');
React.render(<IndexView/>, document.querySelector('.app-container'));

透過以上的設定,就可以達到 min 過的JS、CSS、HTML,請求次數最少,達到網頁的最佳化,後續的互動交由 React 來處理 :)

完整範例:React-Server-Render-Example

參考資料:

  1. http://christianalfoni.github.io/javascript/2014/08/15/react-js-workflow.html
  2. http://stackoverflow.com/questions/24978244/using-bootstrap-3-0-with-browserify
  3. http://stackoverflow.com/questions/29237714/how-can-i-input-and-output-multiple-files-with-gulp-and-browserify
  4. http://facebook.github.io/react/
  5. http://browserify.org/
  6. http://gulpjs.com/
  7. https://github.com/reactjs/express-react-views
  8. http://livereload.com/
  9. https://gaearon.github.io/react-hot-loader/
  10. http://expressjs.com/