服务端渲染(SSR: Server Side Rendering)在React项目中有着广泛的应用场景

基于React虚拟DOM的特性,在浏览器端和服务端我们可以实现同构(可以使用同一份代码来实现多端的功能)

服务端渲染的优点主要由三点

1. 利于SEO

2. 提高首屏渲染速度

3. 同构直出,使用同一份(JS)代码实现,便于开发和维护

一起看看如何在实际的项目中实现服务端渲染

和以往一样,本次项目也放到了 Github 中,欢迎围观 star ~

有纯粹的 React,也有 Redux 作为状态管理

使用 webpack 监听编译文件,nodemon 监听服务器文件变动

使用 redux-saga 处理异步action,使用 express 处理页面渲染

本项目包含四个页面,四种组合,满满的干货,文字可能说不清楚,就去看代码吧!

  1. React
  2. React + SSR
  3. React + Redux
  4. React + Redux + SSR

一、React

实现一个最基本的React组件,就能搞掂第一个页面了

/**
* 消息列表
*/
class Message extends Component {
constructor(props) {
super(props); this.state = {
msgs: []
};
} componentDidMount() {
setTimeout(() => {
this.setState({
msgs: [{
id: '1',
content: '我是消息我是消息我是消息',
time: '2018-11-23 12:33:44',
userName: '王羲之'
}, {
id: '2',
content: '我是消息我是消息我是消息2',
time: '2018-11-23 12:33:45',
userName: '王博之'
}, {
id: '3',
content: '我是消息我是消息我是消息3',
time: '2018-11-23 12:33:44',
userName: '王安石'
}, {
id: '4',
content: '我是消息我是消息我是消息45',
time: '2018-11-23 12:33:45',
userName: '王明'
}]
});
}, 1000);
} // 消息已阅
msgRead(id, e) {
let msgs = this.state.msgs;
let itemIndex = msgs.findIndex(item => item.id === id); if (itemIndex !== -1) {
msgs.splice(itemIndex, 1); this.setState({
msgs
});
}
} render() {
return (
<div>
<h4>消息列表</h4>
<div className="msg-items">
{
this.state.msgs.map(item => {
return (
<div key={item.id} className="msg-item">
<p className="msg-item__header">{item.userName} - {item.time}</p>
<p className="msg-item__content">{item.content}</p>
<a href="javascript:;" className="msg-item__read" onClick={this.msgRead.bind(this, item.id)}>&times;</a>
</div>
)
})
}
</div>
</div>
)
}
} render(<Message />, document.getElementById('content'));

是不是很简单,代码比较简单就不说了

来看看页面效果

可以看到页面白屏时间比较长

这里有两个白屏

1. 加载完JS后才初始化标题

2. 进行异步请求数据,再将消息列表渲染

看起来是停顿地比较久的,那么使用服务端渲染有什么效果呢?

二. React + SSR

在讲如何实现之前,先看看最终效果

可以看到页面是直出的,没有停顿

在React 15中,实现服务端渲染主要靠的是 ReactDOMServer 的 renderToString 和 renderToStaticMarkup方法。

let ReactDOMServer = require('react-dom/server');

ReactDOMServer.renderToString(<Message preloadState={preloadState} />)

ReactDOMServer.renderToStaticMarkup(<Message preloadState={preloadState} />)

将组件直接在服务端处理为字符串,我们根据传入的初始状态值,在服务端进行组件的初始化

然后在Node环境中返回,比如在Express框架中,返回渲染一个模板文件

      res.render('messageClient/message.html', {
appHtml: appHtml,
preloadState: JSON.stringify(preloadState).replace(/</g, '\\u003c')
});

这里设置了两个变量传递给模板文件

appHtml 即为处理之后的组件字符串

preloadState 为服务器中的初始状态,浏览器的后续工作要基于这个初始状态,所以需要将此变量传递给浏览器初始化

        <div id="content">
<|- appHtml |>
</div>
<script id="preload-state">
var PRELOAD_STATE = <|- preloadState |>
</script>

express框架返回之后即为在浏览器中看到的初始页面

需要注意的是这里的ejs模板进行了自定义分隔符,因为webpack在进行编译时,HtmlWebpackPlugin 插件中自带的ejs处理器可能会和这个模板中的ejs变量冲突

在express中自定义即可

// 自定义ejs模板
app.engine('html', ejs.__express);
app.set('view engine', 'html');
ejs.delimiter = '|';

接下来,在浏览器环境的组件中(以下这个文件为公共文件,浏览器端和服务器端共用),我们要按照 PRELOAD_STATE 这个初始状态来初始化组件

class Message extends Component {
constructor(props) {
super(props); this.state = {
msg: []
}; // 根据服务器返回的初始状态来初始化
if (typeof PRELOAD_STATE !== 'undefined') {
this.state.msgs = PRELOAD_STATE;
// 清除
PRELOAD_STATE = null;
document.getElementById('preload-state').remove();
}
// 此文件为公共文件,服务端调用此组件时会传入初始的状态preloadState
else {
this.state.msgs = this.props.preloadState;
} console.log(this.state);
} componentDidMount() {
// 此处无需再发请求,由服务器处理
}
...

核心就是这些了,这就完了么?

哪有那么快,还得知道如何编译文件(JSX并不是原生支持的),服务端如何处理,浏览器端如何处理

接下来看看项目的文件结构

   

把注意力集中到红框中

直接由webpack.config.js同时编译浏览器端和服务端的JS模块

module.exports = [
clientConfig,
serverConfig
];

浏览器端的配置使用 src 下的 client目录,编译到 dist 目录中

服务端的配置使用 src 下的 server 目录,编译到 distSSR 目录中。在服务端的配置中就不需要进行css文件提取等无关的处理的,关注编译代码初始化组件状态即可

另外,服务端的配置的ibraryTarget记得使用 'commonjs2',才能为Node环境所识别

// 文件输出配置
output: {
// 输出所在目录
path: path.resolve(__dirname, '../public/static/distSSR/js/'),
filename: '[name].js',
library: 'node',
libraryTarget: 'commonjs2'
},

client和server只是入口,它们的公共部分在 common 目录中

在client中,直接渲染导入的组件

import React, {Component} from 'react';
import {render, hydrate, findDOMNode} from 'react-dom';
import Message from '../common/message'; hydrate(<Message />, document.getElementById('content'));

这里有个 render和hydrate的区别

在进行了服务端渲染之后,浏览器端使用render的话会按照状态重新初始化一遍组件,可能会有抖动的情况;使用 hydrate则只进行组件事件的初始化,组件不会从头初始化状态

建议使用hydrate方法,在React17中 使用了服务端渲染之后,render将不再支持

在 server中,导出这个组件给 express框架调用

import Message from '../common/message';

let ReactDOMServer = require('react-dom/server');

/**
* 提供给Node环境调用,传入初始状态
* @param {[type]} preloadState [description]
* @return {[type]} [description]
*/
export function init(preloadState) {
return ReactDOMServer.renderToString(<Message preloadState={preloadState} />);
};

需要注意的是,这里不能直接使用 module.exports = ... 因为webpack不支持ES6的 import 和这个混用

在 common中,处理一些浏览器端和服务器端的差异,再导出

这里的差异主要是变量的使用问题,在Node中没有window document navigator 等对象,直接使用会报错。且Node中的严格模式直接访问未定义的变量也会报错

所以需要用typeof 进行变量检测,项目中引用的第三方插件组件有使用到了这些浏览器环境对象的,要注意做好兼容,最简便的方法是在 componentDidMount 中再引入这些插件组件

另外,webpack的style-loader也依赖了这些对象,在服务器配置文件中需要将其移除

 {
test: /\.css$/,
loaders: [
// 'style-loader',
'happypack/loader?id=css'
]
}

在Express的服务器框架中,messageSSR 路由 渲染页面之前做一些异步操作获取数据

// 编译后的文件路径
let distPath = '../../public/static/distSSR/js'; module.exports = function(req, res, next) {
// 如果需要id
let id = 'req.params.id'; console.log(id); getDefaultData(id); async function getDefaultData(id) {
let appHtml = '';
let preloadState = await getData(id); console.log('preloadState', preloadState); try {
// 获取组件的值(字符串)
appHtml = require(`${distPath}/message`).init(preloadState);
} catch(e) {
console.log(e);
console.trace();
} res.render('messageClient/message.html', {
appHtml: appHtml,
preloadState: JSON.stringify(preloadState).replace(/</g, '\\u003c')
});
}
};

使用到Node来开启服务,每次改了服务器文件之后就得重启比较麻烦

使用 nodemon工具来监听文件修改自动更新服务器,添加配置文件 nodemon.json

{
"restartable": "rs",
"ignore": [
".git",
"node_modules/**/node_modules"
],
"verbose": true,
"execMap": {
"js": "node --harmony"
},
"watch": [
"server/",
"public/static/distSSR"
],
"env": {
"NODE_ENV": "development"
},
"ext": "js,json"
}

当然,对于Node环境不支持JSX这个问题,除了使用webpack进行编译之外,

还可以在Node中执行 babel-node 来即时地编译文件,不过这种方式会导致每次编译非常久(至少比webpack久)

在React16 中,ReactDOMServer 除了拥有 renderToString 和 renderToStaticMarkup这两个方法之外,

还有 renderToNodeStream  和 renderToStaticNodeStream 两个流的方法

它们不是返回一个字符串,而是返回一个可读流,一个用于发送字节流的对象的Node Stream类

渲染到流可以减少你的内容的第一个字节(TTFB)的时间,在文档的下一部分生成之前,将文档的开头至结尾发送到浏览器。 当内容从服务器流式传输时,浏览器将开始解析HTML文档

以下是使用实例,本文不展开

// using Express
import { renderToNodeStream } from "react-dom/server"
import MyPage from "./MyPage"
app.get("/", (req, res) => {
res.write("<!DOCTYPE html><html><head><title>My Page</title></head><body>");
res.write("<div id='content'>");
const stream = renderToNodeStream(<MyPage/>);
stream.pipe(res, { end: false });
stream.on('end', () => {
res.write("</div></body></html>");
res.end();
});
});

这便是在React中进行服务端渲染的流程了,说得有点泛泛,还是自己去看 项目代码 吧

三、React + Redux

React的中的数据是单向流动的,即父组件状态改变之后,可以通过props将属性传递给子组件,但子组件并不能直接修改父级的组件。

一般需要通过调用父组件传来的回调函数来间接地修改父级状态,或者使用 Context ,使用 事件发布订阅机制等。

引入了Redux进行状态管理之后,就方便一些了。不过会增加代码复杂度,另外要注意的是,React 16的新的Context特性貌似给Redux带来了不少冲击

在React项目中使用Redux,当某个处理有比较多逻辑时,遵循胖action瘦reducer,比较通用的建议时将主要逻辑放在action中,在reducer中只进行更新state的等简单的操作

一般还需要中间件来处理异步的动作(action),比较常见的有四种 redux-thunk  redux-saga  redux-promise  redux-observable ,它们的对比

这里选用了 redux-saga,它比较优雅,管理异步也很有优势

来看看项目结构

我们将 home组件拆分出几个子组件便于维护,也便于和Redux进行关联

home.js 为入口文件

使用 Provider 包装组件,传入store状态渲染组件

import React, {Component} from 'react';
import {render, findDOMNode} from 'react-dom';
import {Provider} from 'react-redux'; // 组件入口
import Home from './homeComponent/Home.jsx';
import store from './store'; /**
* 组装Redux应用
*/
class App extends Component {
render() {
return (
<Provider store={store}>
<Home />
</Provider>
)
}
} render(<App />, document.getElementById('content'));

store/index.js 中为状态创建的过程

这里为了方便,就把服务端渲染的部分也放在一起了,实际上它们的区别不是很大,仅仅是 defaultState初始状态的不同而已

import {createStore, applyMiddleware, compose} from 'redux';
import createSagaMiddleware from 'redux-saga';
// import {thunk} from 'redux-thunk'; import reducers from './reducers';
import wordListSaga from './workListSaga';
import state from './state'; const sagaMiddleware = createSagaMiddleware(); const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; let defaultState = state; // 用于SSR
// 根据服务器返回的初始状态来初始化
if (typeof PRELOAD_STATE !== 'undefined') {
defaultState = Object.assign({}, defaultState, PRELOAD_STATE);
// 清除
PRELOAD_STATE = null;
document.getElementById('preload-state').remove();
} let store = createStore(
reducers,
defaultState,
composeEnhancers(
applyMiddleware(sagaMiddleware)
)); sagaMiddleware.run(wordListSaga); export default store;

我们将一部分action(基本是异步的)交给saga处理

在workListSaga.js中,

 import {delay} from 'redux-saga';
import {put, fork, takeEvery, takeLatest, call, all, select} from 'redux-saga/effects'; import * as actionTypes from './types'; /**
* 获取用户信息
* @yield {[type]} [description]
*/
function* getUserInfoHandle() {
let state = yield select(); return yield new Promise((resolve, reject) => {
setTimeout(() => {
resolve({
sex: 'male',
age: 18,
name: '王羲之',
avatar: '/public/static/imgs/avatar.png'
});
}, 500);
});
} /**
* 获取工作列表
* @yield {[type]} [description]
*/
function* getWorkListHandle() {
let state = yield select(); return yield new Promise((resolve, reject) => {
setTimeout(() => {
resolve({
todo: [{
id: '1',
content: '跑步'
}, {
id: '2',
content: '游泳'
}], done: [{
id: '13',
content: '看书'
}, {
id: '24',
content: '写代码'
}]
});
}, 1000);
});
} /**
* 获取页面数据,action.payload中如果为回调,可以处理一些异步数据初始化之后的操作
* @param {[type]} action [description]
* @yield {[type]} [description]
*/
function* getPageInfoAsync(action) {
console.log(action); let userInfo = yield call(getUserInfoHandle); yield put({
type: actionTypes.INIT_USER_INFO,
payload: userInfo
}); let workList = yield call(getWorkListHandle); yield put({
type: actionTypes.INIT_WORK_LIST,
payload: workList
}); console.log('saga done'); typeof action.payload === 'function' && action.payload();
} /**
* 获取页面数据
* @yield {[type]} [description]
*/
export default function* getPageInfo() {
yield takeLatest(actionTypes.INIT_PAGE, getPageInfoAsync);
}

监听页面的初始化action actionTypes.INIT_PAGE ,获取数据之后再触发一个action ,转交给reducer即可

let userInfo = yield call(getUserInfoHandle);

    yield put({
type: actionTypes.INIT_USER_INFO,
payload: userInfo
});

reducer中做的事主要是更新状态,

import * as actionTypes from './types';
import defaultState from './state'; /**
* 工作列表处理
* @param {[type]} state [description]
* @param {[type]} action [description]
* @return {[type]} [description]
*/
function workListReducer(state = defaultState, action) {
switch (action.type) {
// 初始化用户信息
case actionTypes.INIT_USER_INFO:
// 返回新的状态
return Object.assign({}, state, {
userInfo: action.payload
}); // 初始化工作列表
case actionTypes.INIT_WORK_LIST:
return Object.assign({}, state, {
todo: action.payload.todo,
done: action.payload.done
}); // 添加任务
case actionTypes.ADD_WORK_TODO:
return Object.assign({}, state, {
todo: action.payload
}); // 设置任务完成
case actionTypes.SET_WORK_DONE:
return Object.assign({}, state, {
todo: action.payload.todo,
done: action.payload.done
}); default:
return state
}
}

在 action.js中可以定义一些常规的action,比如

export function addWorkTodo(todoList, content) {
let id = Math.random(); let todo = [...todoList, {
id,
content
}]; return {
type: actionTypes.ADD_WORK_TODO,
payload: todo
}
} /**
* 初始化页面信息
* 此action为redux-saga所监听,将传入saga中执行
*/
export function initPage(cb) {
console.log(122)
return {
type: actionTypes.INIT_PAGE,
payload: cb
};
}

回到刚才的 home.js入口文件,在其引入的主模块 home.jsx中,我们需要将redux的东西和这个 home.jsx绑定起来

import {connect} from 'react-redux';

// 子组件
import User from './user';
import WorkList from './workList'; import {getUrlParam} from '../util/util'
import '../../scss/home.scss'; import {
initPage
} from '../store/actions.js'; /**
* 将redux中的state通过props传给react组件
* @param {[type]} state [description]
* @return {[type]} [description]
*/
function mapStateToProps(state) {
return {
userInfo: state.userInfo,
// 假如父组件Home也需要知悉子组件WorkList的这两个状态,则可以传入这两个属性
todo: state.todo,
done: state.done
};
} /**
* 将redux中的dispatch方法通过props传给react组件
* @param {[type]} state [description]
* @return {[type]} [description]
*/
function mapDispatchToProps(dispatch, ownProps) {
return {
// 通过props传入initPage这个dispatch方法
initPage: (cb) => {
dispatch(initPage(cb));
}
};
} ... class Home extends Component {
... export default connect(mapStateToProps, mapDispatchToProps)(Home);

当然,并不是只能给store绑定一个组件

如果某个组件的状态可以被其他组件共享,或者这个组件需要访问store,按根组件一层一层通过props传入很麻烦的话,也可以直接给这个组件绑定store

比如这里的 workList.jsx 也进行了绑定,user.jsx这种只需要展示数据的组件,或者其他一些自治(状态在内部管理,和外部无关)的组件,则不需要引入redux的store,也挺麻烦的

绑定之后,我们需要在 Home组件中调用action,开始获取数据

   /**
* 初始获取数据之后的某些操作
* @return {[type]} [description]
*/
afterInit() {
console.log('afterInit');
} componentDidMount() {
console.log('componentDidMount'); // 初始化发出 INIT_PAGE 操作
this.props.initPage(() => {
this.afterInit();
});
}

这里有个小技巧,如果在获取异步数据之后要接着进行其他操作,可以传入 callback ,我们在action的payload中置入了这个 callback,方便调用

然后Home组件中的已经没有多少state了,已经交由store管理,通过mapStateToProps传入

所以可以根据props拿到这些属性

<User {...this.props.userInfo} />

或者调用传入的 reducer ,间接地派发一些action

    // 执行 ADD_WORK_TODO
this.props.addWorkTodo(this.props.todo, content.trim());

页面呈现

四、React + Redux + SSR

可以看到上图是有一些闪动的,因为数据不是一开始就存在

考虑加入SSR,先来看看最终页面效果,功能差不多,但直接出来了,看起来很美好呀~

在Redux中加入SSR, 其实跟纯粹的React组件是类似的。

官方给了一个简单的例子

都是在服务器端获取初始状态后处理组件为字符串,区别主要是React直接使用state, Redux直接使用store

浏览器中我们可以为多个页面使用同一个store,但在服务器端不行,我们需要为每一个请求创建一个store

再来看项目结构,Redux的SSR使用到了红框中的文件

服务端路由homeSSR与messageSSR类似,都是返回数据

服务端入口文件 server中的home.js 则是创建一个新的 store, 然后传入ReactDOMServer进行处理返回

import {createStore} from 'redux';
import reducers from '../store/reducers';
import App from '../common/home';
import defaultState from '../store/state'; let ReactDOMServer = require('react-dom/server'); export function init(preloadState) {
// console.log(preloadState); let defaultState = Object.assign({}, defaultState, preloadState); // 服务器需要为每个请求创建一份store,并将状态初始化为preloadState
let store = createStore(
reducers,
defaultState
); return ReactDOMServer.renderToString(<App store={store} />);
};

同样的,我们需要在common文件中处理 Node环境与浏览器环境的一些差异

比如在 home.jsx 中,加入

// 公共部分,在Node环境中无window document navigator 等对象
if (typeof window === 'undefined') {
// 设置win变量方便在其他地方判断环境
global.win = false;
global.window = {};
global.document = {};
}

另外组件加载之后也不需要发请求获取数据了

/**
* 初始获取数据之后的某些操作
* @return {[type]} [description]
*/
afterInit() {
console.log('afterInit');
} componentDidMount() {
console.log('componentDidMount'); // 初始化发出 INIT_PAGE 操作;
// 已交由服务器渲染
// this.props.initPage(() => {
this.afterInit();
// });
}

common中的home.js入口文件用于给组件管理store, 与未用SSR的文件不同(js目录下面的home.js入口)

它需要同时为浏览器端和服务器端服务,所以增加一些判断,然后导出

if (module.hot) {
module.hot.accept();
} import React, {Component} from 'react';
import {render, findDOMNode} from 'react-dom';
import Home from './homeComponent/home.jsx';
import {Provider} from 'react-redux';
import store from '../store'; class App extends Component {
render() {
// 如果为Node环境,则取由服务器返回的store值,否则使用 ../store中返回的值
let st = global.win === false ? this.props.store : store; return (
<Provider store={st}>
<Home />
</Provider>
)
}
} export default App;

浏览器端的入口文件 home.js 直接引用渲染即可

import React, {Component} from 'react';
import {render, hydrate, findDOMNode} from 'react-dom';
import App from '../common/home'; // render(<App />, document.getElementById('content'));
hydrate(<App />, document.getElementById('content'));

这便是Redux 加上 SSR之后的流程了

其实还漏了一个Express的server.js服务文件,也就一点点代码

 const express = require('express');
const path = require('path');
const app = express();
const ejs = require('ejs'); // 常规路由页面
let home = require('./routes/home');
let message = require('./routes/message'); // 用于SSR服务端渲染的页面
let homeSSR = require('./routes/homeSSR');
let messageSSR = require('./routes/messageSSR'); app.use(express.static(path.join(__dirname, '../'))); // 自定义ejs模板
app.engine('html', ejs.__express);
app.set('view engine', 'html');
ejs.delimiter = '|'; app.set('views', path.join(__dirname, '../views/')); app.get('/home', home);
app.get('/message', message); app.get('/ssr/home', homeSSR);
app.get('/ssr/message', messageSSR); let port = 12345; app.listen(port, function() {
console.log(`Server listening on ${port}`);
});

文章说得错错乱乱的,可能没那么好理解,还是去看 项目文件 自己琢磨吧,自己弄下来编译运行看看

五、其他

如果项目使用了其他服务器语言的,比如PHP Yii框架 Smarty ,把服务端渲染整起来可能没那么容易

其一是 smarty的模板语法和ejs的不太搞得来

其二是Yii框架的路由和Express的长得不太一样

在Nginx中配置Node的反向代理,配置一个 upstream ,然后在server中匹配 location ,进行代理配置

upstream connect_node {
server localhost:54321;
keepalive 64;
} ... server
{
listen 80;
... location / {
index index.php index.html index.htm;
} location ~ (home|message)\/\d+$ {
proxy_pass http://connect_node;
} ...

更多配置

想得头大,干脆就不想了,有用过Node进行中转代理实现SSR的朋友,欢迎评论区分享哈~

教你如何在React及Redux项目中进行服务端渲染的更多相关文章

  1. React 16 服务端渲染的新特性

    React 16 服务端渲染的新特性 React 16 中关于服务端渲染的新特性 快速介绍React 16 服务端渲染的新特性,包括数组.性能.流等 React 16 终于来了!

  2. Immutable.js 以及在 react+redux 项目中的实践

    来自一位美团大牛的分享,相信可以帮助到你. 原文链接:https://juejin.im/post/5948985ea0bb9f006bed7472?utm_source=tuicool&ut ...

  3. react+redux教程(六)redux服务端渲染流程

    今天,我们要讲解的是react+redux服务端渲染.个人认为,react击败angular的真正“杀手锏”就是服务端渲染.我们为什么要实现服务端渲染,主要是为了SEO. 例子 例子仍然是官方的计数器 ...

  4. 菜鸟-手把手教你把Acegi应用到实际项目中(8)-扩展UserDetailsService接口

    一个能为DaoAuthenticationProvider提供存取认证库的的类,它必须要实现UserDetailsService接口: public UserDetails loadUserByUse ...

  5. 详解react/redux的服务端渲染:页面性能与SEO

        亟待解决的疑问 为什么服务端渲染首屏渲染快?(对比客户端首屏渲染)   react客户端渲染的一大痛点就是首屏渲染速度慢问题,因为react是一个单页面应用,大多数的资源需要在首次渲染前就加载 ...

  6. 【redux】详解react/redux的服务端渲染:页面性能与SEO

        亟待解决的疑问 为什么服务端渲染首屏渲染快?(对比客户端首屏渲染)   react客户端渲染的一大痛点就是首屏渲染速度慢问题,因为react是一个单页面应用,大多数的资源需要在首次渲染前就加载 ...

  7. 【vue】如何在 Vue-cli 创建的项目中引入 iView

    根据vue项目的搭建教程,以下记录如何在Vue-cli创建的项目中引入iView. 1)iView的安装,在项目下使用 npm 安装iView cnpm install  iview  --save ...

  8. 【vue】如何在 Vue-cli 创建的项目中引入iView

    根据vue项目的搭建教程,一下记录下如何在Vue-cli创建的项目中引入iView. 1)安装iView,在项目下 cnpm install  iview  --save 2 ) 在 webpack ...

  9. React服务端渲染总结

    欢迎吐槽 : ) 本demo地址( 前端库React+mobx+ReactRouter ):https://github.com/Penggggg/react-ssr.本文为笔者自学总结,有错误的地方 ...

随机推荐

  1. oracle odbc mysql 字段不全

    主要是字段集不对,mysql的字符集默认设置为utf8,odbc才是unicode编码连接,无法转发.选择ansi连接方式即可.

  2. Flask web 开发出现错误:TypeError: Allowed methods have to be iterables of strings, for example: @app.route(..., methods=["POST"])

    没有发现普通都语法错误,然后一一比对官方教程里面的代码,发现是在“@bp.route('/<int:id>/delete', methods=('POST'))”这一行代码里面method ...

  3. ppt制作动态表格与文字

          在工作中经常与ppt打交道的小伙伴们,是不是非常想让自己做的ppt图表能够动起来,显得高大上一点呢?比如让柱形图慢慢长起来,让折线图慢慢画出来,让文字像字幕一样缓缓上升?本文将给大家整理几 ...

  4. The test form is only available for requests from the local machine

    使用浏览器测试Web服务时出现提示“The test form is only available for requests from the local machine.”的解决办法 在Web服务项 ...

  5. arp脚本

    1.什么是arp?arp可以解决什么问题? ARP:是地址解析协议 arp解决我们知道一个机器(主机或者路由器)的IP地址,需要找出其相应的硬件地址 2.编写ARP脚本,抓取对应主机的mac地址 1 ...

  6. Java虚拟机:内存分配策略

    版权声明:本文为博主原创文章,转载请注明出处,欢迎交流学习! Java中提倡的自动内存管理机制最终可以归结为自动化的解决两个问题:给对象分配内存和回收分配给对象的内存.在之前的博客中已经详细讲解了内存 ...

  7. JSTL标签库的基本教程之核心标签库(二)

    JSTL标签库的基本教程之核心标签库(二) 核心标签库 标签 描述 <c:out> 用于在JSP中显示数据,就像<%= ... > <c:set> 用于保存数据 & ...

  8. 子弹短信光鲜的背后:网易云信首席架构师分享亿级IM平台的技术实践

    本文原文内容来自InfoQ的技术分享,本次有修订.勘误和加工,感谢原作者的分享. 1.前言 自从2018年8月20日子弹短信在锤子发布会露面之后(详见<老罗最新发布了“子弹短信”这款IM,主打熟 ...

  9. linux下如何把php升级到5.6

    1: 进入终端后查看php版本 php -v 输出可能如下: PHP (cli) (built: Nov ::) Copyright (c) - The PHP Group Zend Engine v ...

  10. shell脚本中if的“-e,-d,-f”

    文件表达式-e filename 如果 filename存在,则为真-d filename 如果 filename为目录,则为真 -f filename 如果 filename为常规文件,则为真-L ...