關於React前端構建的通常過程 - 理論篇

  

概要

本文以我的閱讀實踐經驗概括前端架構構建過程,以Step by Step方式說明建立一個前端項目的過程。並會對每一個階段所使用的技術進行可替代分析,如Express替換Hapi或者Koa的優缺點分析。本文僅供參考。css

流程

1. Package.json

首先,咱們須要建立package.json文件。對設計初期已知的引用包和依賴包進行管理,使用ES6的,須要設置babel。其次編寫腳本命令。通常文件形式以下:html

{
  "name": "practice",
  "description": "Ryan Project",
  "version": "1.0.0",
  "main": "server.js",
  "scripts": {
    "start": "node server.js",
    "watch": "nodemon server.js"
  },
  "babel": {
    "presets": [
      "es2015",
      "react"
    ]
  },
  "dependencies": {
    "alt": "^0.17.8",
    "async": "^1.5.0",
    "body-parser": "^1.14.1",
    "colors": "^1.1.2",
    "compression": "^1.6.0",
    "express": "^4.13.3",
    "history": "^1.13.0",
    "mongoose": "^4.2.5",
    "morgan": "^1.6.1",
    "react": "latest",
    "react-dom": "latest",
    "react-highcharts": "^10.0.0",
    "react-router": "^1.0.0",
    "request": "^2.65.0",
    "serve-favicon": "^2.3.0",
    "socket.io": "^1.3.7",
    "swig": "^1.4.2",
    "underscore": "^1.8.3",
    "xml2js": "^0.4.15"
  },
  "devDependencies": {
    "babel-core": "^6.1.19",
    "babel-preset-es2015": "^6.1.18",
    "babel-preset-react": "^6.1.18",
    "babel-register": "^6.3.13",
    "babelify": "^7.2.0",
    "bower": "^1.6.5",
    "browserify": "^12.0.1",
    "gulp": "^3.9.0",
    "gulp-autoprefixer": "^3.1.0",
    "gulp-concat": "^2.6.0",
    "gulp-cssmin": "^0.1.7",
    "gulp-if": "^2.0.0",
    "gulp-less": "^3.0.3",
    "gulp-plumber": "^1.0.1",
    "gulp-sourcemaps": "^1.6.0",
    "gulp-uglify": "^1.4.2",
    "gulp-util": "^3.0.7",
    "optimize-js": "^1.0.0",
    "vinyl-buffer": "^1.0.0",
    "vinyl-source-stream": "^1.1.0",
    "watchify": "^3.6.0"
  },
  "license": "MIT"
}

輸入完成後,運行npm install,將package.json中的包安裝到項目目錄中,存放於對應node_modules文件夾前端

2. Server.js

即服務端,可使用Express、Koa、Hapi等方式去建立服務端,設置服務端口。也能夠設置socket相關的工做。node

Express建立服務端react

var express = require('express'); 
var app = express();

//建立路由
app.get('/', function(req, res) {
  res.send('Hello world');
});

//建立REST API
var router = express.Router();
router.route('/items/:id')
.get(function(req, res, next) {
  res.send('Get id: ' + req.params.id); })
.put(function(req, res, next) {
  res.send('Put id: ' + req.params.id); })
.delete(function(req, res, next) {   
  res.send('Delete id: ' + req.params.id); });
app.use('/api', router);
var server = app.listen(3000, function() {
  console.log('Express is listening to http://localhost:3000');
});

Koa建立服務端jquery

var koa = require('koa');
var app = koa();
//建立路由
app.use(function *() {
  this.body = 'Hello world';
});
//建立REST API
app.use(route.get('/api/items', function*() { this.body = 'Get'; }));
app.use(route.post('/api/items', function*() { this.body = 'Post'; }));
app.use(route.put('/api/items/:id', function*(id) { this.body = 'Put id: ' + id; }));
app.use(route.delete('/api/items/:id', function*(id) { this.body = 'Delete id: ' + id; }));
var server = app.listen(3000, function() { console.log('Koa is listening to http://localhost:3000'); });

Hapi建立服務端webpack

var Hapi = require('hapi');
var server = new Hapi.Server(3000);
server.route({ 
  method: 'GET',
  path: '/',
  handler: function(request, reply) {
    reply('Hello world'); } });
server.route([
  { method: 'GET', path: '/api/items', handler: function(request, reply) { reply('Get item id'); } },
  { method: 'GET', path: '/api/items/{id}', handler: function(request, reply) { reply('Get item id: ' + request.params.id); } },
  { method: 'POST', path: '/api/items', handler: function(request, reply) { reply('Post item'); } },
  { method: 'PUT', path: '/api/items/{id}', handler: function(request, reply) { reply('Put item id: ' + request.params.id); } },
  { method: 'DELETE', path: '/api/items/{id}', handler: function(request, reply) { reply('Delete item id: ' + request.params.id); } },
  { method: 'GET', path: '/', handler: function(request, reply) { reply('Hello world'); } } ]);
server.start(
function() { console.log('Hapi is listening to http://localhost:3000'); });

三者間優缺點比較ios

  優勢 缺點
Express 龐大的社區,相對成熟。極易方便建立服務端,建立路由方面代碼複用率高 基於callback機制,不能夠組合使用,也不能捕獲異常
Koa

相比Express,移除Route和View,中間件的使用移植和編寫都比較方便,擁抱ES6,web

藉助Promise和generator而非callback,可以捕獲異常和組合使用mongodb

以Express同樣,須要routers中間件處理不一樣的選擇
Hapi  基於配置而非代碼的框架,對於大型項目的一致性和可重用性比較有用。 爲大型項目定製,致使在小項目中,常見的過於形式化的代碼。相關的開源資料也比較少

 

3. 工程化工具

首先,咱們須要先設計好咱們項目的目錄結構,以便使用工程化工做進行壓縮打包等操做。

簡單舉例以下項目的結構

--/public
--/css
--/js
--/fonts
--/img
--/app
    --/actions
    --/components
    --/stores
    --/stylesheets
        --main.less
    --alt.js
    --route.js
    --main.js

其次,須要webpack或者browserify工具,打包壓縮一系列的腳本文件。使用babel轉換ES6語法,由於絕大部分的瀏覽器還不支持ES6,因此須要轉換爲ES5。最後,建立gulpfile.js文件,使用gulp建立系列的工程指令,如綁定vendor文件、引用sourcemap、使用相似uglify、gulp-cssmin等輔助壓縮文件。

以下是簡易的gulpfile.js文件的配置

var gulp = require('gulp');
var gutil = require('gulp-util');
var gulpif = require('gulp-if'); //conditionally run a task
var autoprefixer = require('gulp-autoprefixer'); //Prefix CSS
var cssmin = require('gulp-cssmin');
var less = require('gulp-less'); //Less for Gulp
var concat = require('gulp-concat');
var plumber = require('gulp-plumber'); //Prevent pipe breaking caused by errors from gulp plugins
var buffer = require('vinyl-buffer'); //convert streaming vinyl files to use buffers
var source = require('vinyl-source-stream'); //Use conventional text streams at the start of your gulp or vinyl pipelines
var babelify = require('babelify');
var browserify = require('browserify');
var watchify = require('watchify');
var uglify = require('gulp-uglify'); //Minify files with UglifyJS.
var sourcemaps = require('gulp-sourcemaps');

var production = process.env.NODE_ENV === 'production'

var dependencies = [
    'alt',
    'react',
    'react-dom',
    'react-router',
    'underscore'
]
/*
 |--------------------------------------------------------------------------
 | Combine all JS libraries into a single file for fewer HTTP requests.
 |--------------------------------------------------------------------------
 */
gulp.task('vendor', function () {
    return gulp.src([
        'bower_components/jquery/dist/jquery.js',
        'bower_components/bootstrap/dist/js/bootstrap.js',
        'bower_components/magnific-popup/dist/jquery.magnific-popup.js',
        'bower_components/toastr/toastr.js'
    ]).pipe(concat('vendor.js'))
    .pipe(gulpif(production, uglify({mangle: false})))
    .pipe(gulp.dest('public/js'))
})
/*
 |--------------------------------------------------------------------------
 | Compile third-party dependencies separately for faster performance.
 |--------------------------------------------------------------------------
 */
gulp.task('browserify-vendor', function(){
    return browserify()
    .require(dependencies)
    .bundle()
    .pipe(source('vendor.bundle.js'))
    .pipe(buffer())
    .pipe(gulpif(production, uglify({mangle: false})))
    .pipe(gulp.dest('public/js'))
})

/*
 |--------------------------------------------------------------------------
 | Compile only project files, excluding all third-party dependencies.
 |--------------------------------------------------------------------------
 */
gulp.task('browserify',['browserify-vendor'], function(){
    return browserify({entries:'app/main.js', debug: true})
    .external(dependencies)
    .transform(babelify, {presets: ['es2015','react']})
    .bundle()
    .pipe(source('bundle.js'))
    .pipe(buffer())
    .pipe(soucemaps.init({loadMaps: true}))
    .pipe(gulpif(production, uglify({mangle: false})))
    .pipe(sourcemaps.write('.'))
    .pipe(gulp.dest('public/js'))
})

/*
 |--------------------------------------------------------------------------
 | Same as browserify task, but will also watch for changes and re-compile.
 |--------------------------------------------------------------------------
 */
gulp.task('browserify-watch', ['browserify-vendor'], function(){
    var bundler = watchify(browserify({ entries:'app/main.js', debug: true}), watchify.args)
    bundler.external(dependencies)
    bundler.transform(babelify, {presets: ['es2015', 'react']})
    bundler.on('update', rebundle)
    return rebundle()

    function rebundle() {
        var start = Date.now()
        return bundler.bundle()
        .on('error', function(err){
            gutil.log(gutil.colors.red(err.toString()))
        })
        .on('end', function() {
            gutil.log(gutil.colors.green(`Finished rebundling in ${(Date.now() - start)} ms`))
        })
        .pipe(source('bundle.js'))
        .pipe(buffer())
        .pipe(sourcemaps.init({loadMaps: true}))
        .pipe(sourcemaps.write('.'))
        .pipe(gulp.dest('public/js'))
    }
})

gulp.task('styles', function(){
    return gulp.src('app/stylesheets/main.less')
    .pipe(plumber())
    .pipe(less())
    .pipe(autoprefixer())
    .pipe(gulpif(production, cssmin()))
    .pipe(gulp.dest('public/css'))
})

gulp.task('watch', function(){
    gulp.watch('app/stylesheets/**/*.less', ['styles'])
})

gulp.task('default', ['styles','vendor','browserify-watch','watch'])
gulp.task('build', ['styles', 'vendor', 'browserify'])
View Code

Gulp Task所作的操做以下說明:

Gulp Task

說明

Vendor

將全部第三方的js類庫合併到一個文件

Browserify-vendor

將package.json中dependencies的依賴模塊buffer化,以提供性能

Browserify

編譯和綁定只與app相關的文件(無依賴項),並引用sourcemap對應、uglify壓縮、buffer優化、babel轉化ES6

Browserify-watch

利用watchify監測bundle.js文件的變化,並從新編譯

Styles

編譯less樣式文件,自動添加前綴

Watch

監測Less文件,發生變化從新編譯

Default

運行以上全部任務,且進程掛起監控watch

Build

運行以上全部任務,退出

4. 其餘包管理(可無)

bower包管理工具的引入。因爲NPM主要運用於Node.js項目的內部依賴包管理,安裝的模塊位於項目根目錄下的node_modules文件夾內。而且採用嵌套的依賴關係樹,即子依賴包各自有本身的依賴關係樹,並不會形成他們之間的衝突。可是這種狀況在純前端的包管理就不那麼友好了,好比你使用多個jquery版本。在使用方面npm主要用於管理相似grunt,gulp, ESlint,CoffeScript等npm模塊。而bower管理純前端css/js的包,好比jquery, bootstrap

使用步驟

1. 建立bower.json文件,將依賴包添加進(做用跟package.json相似)

{
    "name": "practice",
    "dependencies": {
        "jquery": "^2.1.4",
        "bootstrap": "^3.3.5",
        "magnific-popup": "^1.0.0",
        "toastr": "^2.1.1"
    }
}

2. 運行

npm install bower -g

bower install

5. 渲染部件 

在渲染部分,React提供了客戶端、服務端的渲染方式。具體區別以下:

1. 客戶端渲染:

  能夠直接在瀏覽器運行ReactJS,這是通用的比較簡單的方式,網上也有不少例子。http://reactjs.org。服務端只建立初始化的html,裝載你的組件UI,提供接口和數據。前端作路由與渲染的工做。缺點就是用戶等待時間長。

2. 服務端渲染:

  html從後端生成,包含全部你的組件腳本UI以及數據。能夠理解爲生成一個靜態的結果集頁面。響應快,體驗好。主要運用於提升主屏性能和SEO。服務端渲染,須要消耗CPU,但能夠藉助緩存實現優化。React中,經過renderToStaticMarkup方法實現。而且,你還須要保留對應的State以及所須要的數據。

例子援引以下開源項目,有興趣的朋友能夠去了解下。

http://sahatyalkabov.com/create-a-character-voting-app-using-react-nodejs-mongodb-and-socketio/ 

以React-Router爲例(客戶端)

1. 建立app/component/App.js

首先建立組件的容器app,this.props.children用於渲染其餘組件

import React, {Component} from 'react'

class App extends Component {
    render() {
        return (
            <div>
                {this.props.children}
            </div>
        );
    }
}

export default App

2. 建立app/routes.js

以下點,指定路由/和/add,對應Home和AddCharacter組件

import React from 'react'
import {Route} from 'react-router'
import App from './components/App'
import Home from './components/Home'
import AddCharacter from './components/AddCharacter';

export default (
    <Route component ={App} >
        <Route path= '/' component={Home} />
        <Route path= '/add' component={AddCharacter} />
    </Route>
)

3.建立main.js

將Router組合的組件渲染到id爲app的div裏。

import React from 'react'
import Router from 'react-router'
import ReactDOM from 'react-dom'
import { createHistory } from 'history'; // you need to install this package
import routers from './routes'

let history = createHistory();
ReactDOM.render(<Router history={history}>
        {routers}
        </Router>, document.getElementById('app')) 

5. app/components/添加home組件

Import React from ‘react’
Class Home extends React.Component{
    Render(){
        Return (
            <div className =’home’>
                Hello </div>)
    } }
Export default Home

6. 組件 

app/component/添加AddCharacter組件

View Code

這裏採用的是alt(基於Flux)第三方庫,因此還須要添加Actions和Store,以及alt.js文件。這裏不一一列舉,能夠查看上面的源碼地址。

Tip: 也可使用react-redux來構建咱們本身的app組件,redux能更好的管理react的state。 

7. 數據庫

建立數據庫數據,若是你是單頁應用,那麼建議使用mongoDB。具體實現再也不一一描述,能夠上網搜索相關內容

8. API 

若是是基於mongoose的話,則只須要利用上面的Express、Koa或者Hapi建立API,訪問mongoose數據.

若是是大型項目,有本身獨立的後端語言,如C#或者Java。則能夠基於微服務框架建立服務API。使用axios或者superagent等庫訪問數據。

參考文獻

http://sahatyalkabov.com/create-a-character-voting-app-using-react-nodejs-mongodb-and-socketio/

http://stackoverflow.com/questions/27290354/reactjs-server-side-rendering-vs-client-side-rendering

http://stackoverflow.com/questions/18641899/what-is-the-difference-between-bower-and-npm

https://ifelse.io/2015/08/27/server-side-rendering-with-react-and-react-router/

https://www.airpair.com/node.js/posts/nodejs-framework-comparison-express-koa-hapi

相關文章
相關標籤/搜索