服务端渲染(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. Linux环境下Redis集群实践

    环境:centos 7 一.编译及安装redis源码 源码地址:redis版本发布列表 cd redis-3.2.8 sudo make && make install 二.创建节点 ...

  2. 数据库 的几种链接 join

    直接demo,懒的同学可以看看效果 两个表的数据 join和inner join一样 full join报错,可有大神知道原因?

  3. oracle 恢复table删除数据 恢复package(使用闪回)

    好久没写东西了,今天写一篇凑个数吧,来公司一年多了,感觉自己到了一个小瓶颈期了. 以前每天很多新东西,都是忙着学,感觉没时间写博客总结一下,大部分都是写笔记,现在又是没东西可以写,每天干着95%都是重 ...

  4. 端口转发 Port Forwarding (一)

    0x00First 最近发现一些好用的端口转发工具和技巧,计划认真梳理一下 SSH.NC.LCX.EW.FRP 0x01 SSH隧道端口转发 目前利用SSH隧道(SSH tunneling)进行端口转 ...

  5. 升讯威微信营销系统开发实践:订阅号和服务号深入分析( 完整开源于 Github)

    GitHub:https://github.com/iccb1013/Sheng.WeixinConstruction因为个人精力时间有限,不会再对现有代码进行更新维护,不过微信接口比较稳定,经测试至 ...

  6. Git使用详细教程(3):git add, git commit详解

    在使用git之前,我们首先要初始化一个git管理的仓库,这里以博客(blog)为例 git init blog 我们进入目录,执行git status查看git状态,可以看到一个新的git管理的项目目 ...

  7. JavaScript 数组方法

    数组方法: 1.Array.join([param]) 方法:将数组中所有的元素都转换为字符串并连接起来,通过字符 param 连接,默认使用逗号,返回最后生成的字符串 2.Array.reverse ...

  8. 一条SQL语句在MySQL中如何执行的

    本篇文章会分析一个 sql 语句在 MySQL 中的执行流程,包括 sql 的查询在 MySQL 内部会怎么流转,sql 语句的更新是怎么完成的. 在分析之前我会先带着你看看 MySQL 的基础架构, ...

  9. 如何实现一个基于 jupyter 的 microservices

    零.背景: 现有基于 Node.js 的项目,但需要整合 Data Science 同事的基于 python(jupyter) 的代码部分,以实现额外的数据分析功能.于是设想实现一个 microser ...

  10. 【PHP篇】数组

    1.简介:数组存储方式是键值对 2.声明:$数组变量名=array(2,3,9,3,“546”,“yy”=>”hhhh”,100=>”uu100”): 3.下标注意:可为“字符串”或者整数 ...