全栈技术: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