React是用于开发数据不断变化的大型应用程序的前端view框架,结合其余轮子例如redux
和react-router
就能够开发大型的前端应用。javascript
React开发之初就有一个特别的优点,就是先后端同构。css
什么是先后端同构呢?就是先后端均可以使用同一套代码生成页面,页面既能够由前端动态生成,也能够由后端服务器直接渲染出来html
最简单的同构应用其实并不复杂,复杂的是结合webpack,router以后的各类复杂状态不容易解决前端
htmljava
<!DOCTYPE html> <html> <head lang="en"> <meta charset="UTF-8"> <title>React同构</title> <link href="styles/main.css" rel="stylesheet" /> </head> <body> <div id="app"> <%- reactOutput %> </div> <script src="bundle.js"></script> </body> </html>
jsnode
import path from 'path'; import Express from 'express'; import AppRoot from '../app/components/AppRoot' import React from 'react'; import {renderToString} from 'react-dom/server' var app = Express(); var server; const PATH_STYLES = path.resolve(__dirname, '../client/styles'); const PATH_DIST = path.resolve(__dirname, '../../dist'); app.use('/styles', Express.static(PATH_STYLES)); app.use(Express.static(PATH_DIST)); app.get('/', (req, res) => { var reactAppContent = renderToString(<AppRoot state={{} }/>); console.log(reactAppContent); res.render(path.resolve(__dirname, '../client/index.ejs'), {reactOutput: reactAppContent}); }); server = app.listen(process.env.PORT || 3000, () => { var port = server.address().port; console.log('Server is listening at %s', port); });
你看服务端渲染的原理就是,服务端调用react的renderToString
方法,在服务器端生成文本,插入到html文本之中,输出到浏览器客户端。而后客户端检测到这些已经生成的dom,就不会从新渲染,直接使用现有的html结构。react
然而现实并非这么单纯,使用react作前端开发的应该不会不使用webpack
,React-router
,redux
等等一些提升效率,简化工做的一些辅助类库或者框架,这样的应用是否是就不太好作同构应用了?至少不会向上文这么简单吧?webpack
作固然是能够作的,但复杂度确实也大了很多git
这个webpack插件的主要做用有两点github
获取webpack打包以后的入口文件路径,包括js,css
把一些特殊的文件例如大图片、编译以后css的映射保存下来,以便在服务器端使用
webpack配置文件
import path from "path"; import webpack from "webpack"; import WebpackIsomorphicToolsPlugin from "webpack-isomorphic-tools/plugin"; import ExtractTextPlugin from "extract-text-webpack-plugin"; import isomorphicToolsConfig from "../isomorphic.tools.config"; import {client} from "../../config"; const webpackIsomorphicToolsPlugin = new WebpackIsomorphicToolsPlugin(isomorphicToolsConfig) const cssLoader = [ 'css?modules', 'sourceMap', 'importLoaders=1', 'localIdentName=[name]__[local]___[hash:base64:5]' ].join('&') const cssLoader2 = [ 'css?modules', 'sourceMap', 'importLoaders=1', 'localIdentName=[local]' ].join('&') const config = { // 项目根目录 context: path.join(__dirname, '../../'), devtool: 'cheap-module-eval-source-map', entry: [ `webpack-hot-middleware/client?reload=true&path=http://${client.host}:${client.port}/__webpack_hmr`, './client/index.js' ], output: { path: path.join(__dirname, '../../build'), filename: 'index.js', publicPath: '/build/', chunkFilename: '[name]-[chunkhash:8].js' }, resolve: { extensions: ['', '.js', '.jsx', '.json'] }, module: { preLoaders: [ { test: /\.jsx?$/, exclude: /node_modules/, loader: 'eslint-loader' } ], loaders: [ { test: /\.jsx?$/, loader: 'babel', exclude: [/node_modules/] }, { test: webpackIsomorphicToolsPlugin.regular_expression('less'), loader: ExtractTextPlugin.extract('style', `${cssLoader}!less`) }, { test: webpackIsomorphicToolsPlugin.regular_expression('css'), exclude: [/node_modules/], loader: ExtractTextPlugin.extract('style', `${cssLoader}`) }, { test: webpackIsomorphicToolsPlugin.regular_expression('css'), include: [/node_modules/], loader: ExtractTextPlugin.extract('style', `${cssLoader2}`) }, { test: webpackIsomorphicToolsPlugin.regular_expression('images'), loader: 'url?limit=10000' } ] }, plugins: [ new webpack.HotModuleReplacementPlugin(), new ExtractTextPlugin('[name].css', { allChunks: true }), webpackIsomorphicToolsPlugin ] } export default config
webpack-isomorphic-tools 配置文件
import WebpackIsomorphicToolsPlugin from 'webpack-isomorphic-tools/plugin' export default { assets: { images: { extensions: ['png', 'jpg', 'jpeg', 'gif', 'ico', 'svg'] }, css: { extensions: ['css'], filter(module, regex, options, log) { if (options.development) { return WebpackIsomorphicToolsPlugin.style_loader_filter(module, regex, options, log) } return regex.test(module.name) }, path(module, options, log) { if (options.development) { return WebpackIsomorphicToolsPlugin.style_loader_path_extractor(module, options, log); } return module.name }, parser(module, options, log) { if (options.development) { return WebpackIsomorphicToolsPlugin.css_modules_loader_parser(module, options, log); } return module.source } }, less: { extensions: ['less'], filter: function(module, regex, options, log) { if (options.development) { return webpack_isomorphic_tools_plugin.style_loader_filter(module, regex, options, log) } return regex.test(module.name) }, path: function(module, options, log) { if (options.development) { return WebpackIsomorphicToolsPlugin.style_loader_path_extractor(module, options, log); } return module.name }, parser: function(module, options, log) { if (options.development) { return WebpackIsomorphicToolsPlugin.css_modules_loader_parser(module, options, log); } return module.source } } } }
这些文件配置好以后,当再运行webpack打包命令的时候就会生成一个叫作webpack-assets.json
的文件,这个文件记录了刚才生成的如文件的路径以及css,img映射表
客户端的配置到这里就结束了,来看下服务端的配置
服务端的配置过程要复杂一些,由于须要使用到WebpackIsomorphicToolsPlugin
生成的文件,
咱们直接使用它对应的服务端功能就能够了
import path from 'path' import WebpackIsomorphicTools from 'webpack-isomorphic-tools' import co from 'co' import startDB from '../../server/model/' import isomorphicToolsConfig from '../isomorphic.tools.config' const startServer = require('./server') var basePath = path.join(__dirname, '../../') global.webpackIsomorphicTools = new WebpackIsomorphicTools(isomorphicToolsConfig) // .development(true) .server(basePath, () => { const startServer = require('./server') co(function *() { yield startDB yield startServer }) })
必定要在WebpackIsomorphicTools
初始化以后再启动服务器
文章开头咱们知道react是能够运行在服务端的,其实不光是react,react-router,redux也都是能够运行在服务器端的
既然前端咱们使用了react-router,也就是前端路由,那后端又怎么作处理呢
其实这些react-router在设计的时候已经想到了这些,设计了一个api: match
match({routes, location}, (error, redirectLocation, renderProps) => { matchResult = { error, redirectLocation, renderProps } })
match方法在服务器端解析了当前请求路由,获取了当前路由的对应的请求参数和对应的组件
知道了这些还不足以作服务端渲染啊,好比一些页面本身做为一个组件,是须要在客户端向服务
器发请求,获取数据作渲染的,那咱们怎么把渲染好数据的页面输出出来呢?
那就是须要作一个约定,就是前端单独放置一个获取数据,渲染页面的方法,由后端能够调用,这样逻辑就能够保持一份,
保持好的维护性
可是怎么实现呢?实现的过程比较简单,想法比较绕
1.调用的接口的方式必须前端通用
2.渲染页面的方式必须先后端通用
先来第一个,你们都知道前端调用接口的方式经过ajax,那后端怎么使用ajax呢?有一个库封装了服务器端的fetch
方法实现,能够用来作这个
因为ajax方法须要先后端通用,那就要求这个方法里面不能夹杂着客户端或者服务端特有的api
调用。
还有个很重要的问题,就是权限的问题,前端有时候是须要登陆以后才能够调用的接口,后端直接调用
显然是没有cookie的,怎么办呢?解决办法就是在用户第一个请求进来以后保存cookie甚至是所有的http
头信息,而后把这些信息传进fetch方法里面去
通用组件方法必须写成类的静态成员,不然后端获取不到,名称也必须统一
static getInitData (params = {}, cookie, dispatch, query = {}) { return getList({ ...params, ...query }, cookie) .then(data => dispatch({ type: constants.article.GET_LIST_VIEW_SUCCESS, data: data })) }
再看第二个问题,前端渲染页面天然就是改变state或者传入props就能够更新视图,服务器端怎么办呢?
redux是能够解决这个问题的
由于服务器端不像前端,须要在初始化以后再去更新视图,服务器端只须要先把数据准备好,而后直接一遍生成
视图就能够了,因此上图的dispatch
方法是由先后端均可以传入
渲染页面的后端方法就比较简单了
import React, { Component, PropTypes } from 'react' import { renderToString } from 'react-dom/server' import {client} from '../../config' export default class Html extends Component { get scripts () { const { javascript } = this.props.assets return Object.keys(javascript).map((script, i) => <script src={`http://${client.host}:${client.port}` + javascript[script]} key={i} /> ) } get styles () { const { assets } = this.props const { styles, assets: _assets } = assets const stylesArray = Object.keys(styles) // styles (will be present only in production with webpack extract text plugin) if (stylesArray.length !== 0) { return stylesArray.map((style, i) => <link href={`http://${client.host}:${client.port}` + assets.styles[style]} key={i} rel="stylesheet" type="text/css" /> ) } // (will be present only in development mode) // It's not mandatory but recommended to speed up loading of styles // (resolves the initial style flash (flicker) on page load in development mode) // const scssPaths = Object.keys(_assets).filter(asset => asset.includes('.css')) // return scssPaths.map((style, i) => // <style dangerouslySetInnerHTML={{ __html: _assets[style]._style }} key={i} /> // ) } render () { const { component, store } = this.props return ( <html> <head> <meta charSet="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="apple-mobile-web-app-capable" content="yes" /> <meta name="apple-mobile-web-app-status-bar-style" content="black" /> <title>前端博客</title> <link rel="icon" href="/favicon.ico" /> {this.styles} </head> <body> <div id="root" dangerouslySetInnerHTML={{ __html: renderToString(component) }} /> <script dangerouslySetInnerHTML={{ __html: `window.__INITIAL_STATE__=${JSON.stringify(store.getState())};` }} /> {this.scripts} </body> </html> ) } }
ok了,页面刷新的时候,是后端直出的,点击跳转的时候是前端渲染的
作了一个相对来讲比较完整的案例,使用了react+redux+koa+mongodb开发的,还作了个爬虫,爬取了一本小说