全栈技术:React全生态实战<下篇>-演道网
PHP技术大全
每天精彩分享不间断
如前文所说,我们通过react-dom/server的renderToString方法从后端渲染了一模一样的内容给浏览器。另外我们还把初始化image state的工作放到了后端,通过window对象传给前端。最后我们看看新加的setImage action
export function setImage() {
return dispatch => {
let url = '';
if(__DEVCLIENT__) {
url = '/api/fetch';
} else {
url = `http://localhost:${config.port}/api/fetch`;
}
return request.get(url)
.then(response => {
const imgsArrangeArr = [];
const imageDatas = response.data.map((imageData) => {
imageData.imageURL = 'images/' + imageData.fileName;
imgsArrangeArr.push({
pos: {
left: 0,
top: 0
},
rotate: 0,
isInverse: false,
isCenter: false
});
return imageData;
});
return {
imageDatas: imageDatas,
imgsArrangeArr: imgsArrangeArr
};
})
.then(images => {
dispatch({type: types.INIT_IMAGE, images});
})
.catch(err => {
console.log('SSR render error' + err.stack);
});
}
}
我们在webpack的配置文件中已经用DefinePlugin插件定义了DEVCLIENT这个变量了,可以根据它判断是后端渲染还是前端,从而决定调用api的url。
我们看最终页面的dom结构:这里的data-react-checksum就表示服务端渲染成功
react-router
复杂的网站不可能只有一张页面,这就需要react-router来做路由。同样,我们的图片画廊也不应该仅仅只能展示图片,还应该提供编辑图片的页面。
首先这里需要把图片信息保存在数据库中,并且在后端要修改图片尺寸,这些都是纯nodejs的知识,这里就不细说了。我们再加一个upload组件,并修改前端入口代码:
ReactDOM.render(
{routes}
,
document.getElementById('app')
);
对于routes,代码如下:
export default () => {
return (
);
}
需要注意的是由于我们采用了服务端渲染,后台的入口也要加上react-router,保证与前端入口一致。
import React from 'react';
import {renderToString} from 'react-dom/server';
import { createMemoryHistory, match, RouterContext } from 'react-router';
import { Provider } from 'react-redux';
import createRoutes from 'routes';
import configureStore from 'stores/configureStore';
import {setImage} from 'actions/image';
import header from './meta';
export default function render(req, res) {
const history = createMemoryHistory();
const store = configureStore({}, history);
const routes = createRoutes();
match({routes, location: req.url}, (err, redirect, props) => {
if (err) {
res.status(500).json(err);
} else if (redirect) {
res.redirect(302, redirect.pathname + redirect.search);
} else if (props) {
new Promise(resolve => {
return resolve(store.dispatch(setImage()));
}).then(() => {
const initialState = store.getState();
const componentHTML = renderToString(
);
return res.status(200).send(`
${header.title.toString()}
${header.meta.toString()}
${header.link.toString()}
${componentHTML}
`);
}).catch((err) => {
console.log('Error happened ' + err.stack);
res.status(500).json(err);
});
} else {
res.sendStatus(404);
}
});
}
unit test
最后,我们还需要写单元测试。由于前端组件化的实现,前端的单元测试再也不像以前那样沦为鸡肋。这也是react为广大前端开发带来的福音。想当初笔者要测试页面还只能用TestNG。
这里的单元测试分为两部分,对后端的测试:用supertest模拟http请求来测试接口。前端的测试:用redux-mock-store,sinon,react-addons-test-utils,enzyme等工具来模拟组件渲染。
不过最开始还是先要实现测试自动化。添加karma.conf.js。代码如下:
var path = require('path');
var webpack = require('webpack');
module.exports = function(config) {
config.set({
// Start these browsers, currently available:
// - Chrome
// - ChromeCanary
// - Firefox
// - Opera (has to be installed with `npm install karma-opera-launcher`)
// - Safari (only Mac; has to be installed with `npm install karma-safari-launcher`)
// - PhantomJS
// - IE (only Windows; has to be installed with `npm install karma-ie-launcher`)
browsers: ['jsdom'],
frameworks: ['mocha', 'sinon'],
// Point karma at the tests.webpack.js
files: [
'webpack/tests.webpack.js'
],
// Run karma through preprocessor plugins
preprocessors: {
'webpack/tests.webpack.js': [ 'webpack', 'sourcemap' ]
},
// Continuous Integration mode
// if true, it capture browsers, run tests and exit
singleRun: true,
// How long will Karma wait for a message from a browser before disconnecting
// from it (in ms).
browserNoActivityTimeout: 30000,
webpack: {
devtool: 'inline-source-map',
context: path.join(__dirname, 'client'),
externals: {
'cheerio': 'window',
'react/addons': true, // important!!
'react/lib/ExecutionEnvironment': true,
'react/lib/ReactContext': true
},
module: {
loaders: [
{
test: /\.js$|\.jsx$/,
loader: 'babel-loader',
// Reason why we put this here instead of babelrc
// https://github.com/gaearon/react-transform-hmr/issues/5#issuecomment-142313637
query: {
'presets': ['es2015', 'react', 'stage-0'],
'plugins': [
'transform-react-remove-prop-types',
'transform-react-constant-elements',
'transform-react-inline-elements'
]
},
include: [path.join(__dirname, 'client'), path.join(__dirname, 'tests', 'client')],
exclude: path.join(__dirname, '/node_modules/')
},
{
test: /\.(png|jpg|gif|woff|woff2|eot|ttf|ico)$/,
loader: 'url-loader',
query: {
name: '[hash].[ext]',
limit: 8192
}
},
{
test: /\.(mp4|ogg|svg)$/,
loader: 'file-loader'
},
{
test: /\.json$/,
loader: 'json-loader'
},
{
test: /\.css$/,
loader: 'style-loader!css-loader!postcss-loader'
},
{
test: /\.sass/,
loader: 'style-loader!css-loader!postcss-loader!sass-loader?outputStyle=expanded&indentedSyntax'
},
{
test: /\.scss/,
loader: 'style-loader!css-loader!postcss-loader!sass-loader?outputStyle=expanded'
},
{
test: /\.less/,
loader: 'style-loader!css-loader!postcss-loader!less-loader'
},
{
test: /\.styl/,
loader: 'style-loader!css-loader!postcss-loader!stylus-loader'
}
]
},
resolve: {
extensions: ['', '.js', '.jsx'],
modulesDirectories: [
'client', 'tests', 'node_modules'
]
},
node: {
fs: 'empty'
},
watch: true
},
webpackMiddleware: {
// webpack-dev-middleware configuration
noInfo: true
},
webpackServer: {
noInfo: true // Do not spam the console when running in karma
},
plugins: [
'karma-jsdom-launcher',
'karma-mocha',
'karma-sinon',
'karma-mocha-reporter',
'karma-sourcemap-loader',
'karma-webpack'
],
// test results reporter to use
// possible values: 'dots', 'progress', 'junit', 'growl', 'coverage',
// 'mocha' (added in plugins)
reporters: ['mocha'],
// level of logging
// possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
logLevel: config.LOG_INFO
});
};
然后再test.webpack.js中指定前端测试的脚本文件。
var context = require.context('../tests/client', true, /-test.js$/);
context.keys().forEach(context);
好了,我们尝试写一个前端测试用例,来展示下怎么测试dom的渲染。
import expect from 'expect';
import React from 'react';
import TestUtils from 'react-addons-test-utils';
import {Upload} from '../../../client/containers/upload';
import {ImageEditor} from '../../../client/components/imageEditor';
function setup(imageDatas = []) {
const props = {
imageDatas
};
const output = TestUtils.renderIntoDocument(
);
return {
props,
output
};
}
describe('component test', () => {
describe('test upload component', () => {
it('should render ImageEditors', () => {
const optionalProps = [{
fileName: '1.jpg',
title: 'xx',
desc: 'xxx',
imageURL: 'xxxx'
}];
const { output } = setup(optionalProps);
const imageEditorComponents = TestUtils.scryRenderedComponentsWithType(output, ImageEditor);
expect(imageEditorComponents.length).toEqual(1);
});
});
});
接下来写后端测试用例。后端用例麻烦的地方在于要先连数据库,导入写初始文件。还需要把express的配置也引进来,因为后端路由的配置都在这里做的。代码如下:
/**
* Created by rick on 2016/10/28.
*/
import request from 'supertest';
import mongoose from 'mongoose';
import expect from 'expect';
import sinon from 'sinon';
import app from '../../server';
import {Image} from '../../server/db';
import config from '../../config';
describe('test apis', () => {
const db = `mongodb://${config.mongoUrl}/gallery_test`;
before((done) => {
mongoose.connect(db, (err) => {
if (err) {
console.log(`===> Error connecting to ${db}`);
console.log(`Reason: ${err}`);
} else {
console.log(`===> Succeeded in connecting to ${db}`);
Image.find({}, (err, images) => {
if(err || !images || images.length === 0) {
const imageDatas = require('../../server/data/imageDatas.json');
imageDatas.map(image => {
const img = new Image(image);
img.save();
});
}
});
setTimeout(done, 5000);
}
});
});
after(() => {
// drop all collections
mongoose.connection.collections['images'].drop(() => {
console.log('image collection dropped');
});
});
describe('test image api', () => {
beforeEach(() => {
// To avoid printing too much log by backend code in console.
sinon.stub(console, 'log', () => {});
});
// GET /api/fetch
it('fetch data success', (done) => {
const url = '/api/fetch';
request(app)
.get(url)
.then(response => {
console.log.restore();
expect(response.status).toEqual(200);
expect(response.body.length).toEqual(16);
expect(response.body[0].fileName).toEqual('1.jpg');
})
.then(done)
.catch(done);
});
});
});
最后,在package.json中添加测试命令
"test": "npm run test:karma && npm run test:api",
"test:karma": "NODE_ENV=test karma start",
"test:api": "npm run build:dev && NODE_ENV=test mocha ./tests/server/*-test.js --compilers js:babel-core/register --timeout 0",
总结
react的生态系统十分庞大也很有趣,这里也只是展示了其中一些最常用的中间件。但是内容就已经很多了,本文不少细节都一笔带过,肯定有很多叙述不清的地方,因为细节是在太多了。所以人家说一入前端深似海。不过代码我已经挂到github上了https://github.com/ErosZZH/gallery。欢迎大家批评指正,共同进步!最后感谢Mater Liu老师提供的精彩教程。
微信扫描二维码,关注PHP技术大全!
让知识地带无限蔓延!
转载自演道,想查看更及时的互联网产品技术热点文章请点击http://go2live.cn