概要
本文以個人閱讀實踐經驗歸納前端架構構建過程,以Step by Step方式說明創建一個前端項目的過程。並會對每個階段所使用的技術進行可替代分析,如Express替換Hapi或者Koa的優缺點分析。本文僅供參考。
流程
1. Package.json
首先,我們需要創建package.json文件。對設計初期已知的引用包和依賴包進行管理,使用ES6的,需要設置babel。其次編寫腳本命令。一般文件形式如下:
{ "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相關的工作。
Express創建服務端
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創建服務端
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創建服務端
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'); });
三者間優缺點比較
優點 | 缺點 | |
Express | 龐大的社區,相對成熟。極易方便創建服務端,創建路由方面代碼復用率高 | 基於callback機制,不可以組合使用,也不能捕獲異常 |
Koa | 相比Express,移除Route和View,中間件的使用移植和編寫都比較方便,擁抱ES6, 借助Promise和generator而非callback,能夠捕獲異常和組合使用 |
以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'])
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組件

這里采用的是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