降落首屏时间,Webpack写一个简短的Blog网站

下降首屏时间,“直出”是个什么概念?

降落首屏时间,Webpack写一个简短的Blog网站。2015/12/31 · HTML5 · 2
评论 ·
首屏

初稿出处: VaJoy Larn   

早几年前端还地处刀耕火种、JQuery独树一帜的时期,前后端代码的耦合度很高,一个web页面文件的代码可能是那般的:

亚洲必赢官网 1

亚洲必赢官网 2

那意味着后端的工程师往往得承受一部分改动HTML、编写脚本的工作,而前者开发者也得询问页面上设有的服务端代码含义。

有时候某处页面逻辑的改动,鉴于代码的混搭,可能都不确定相应请后端仍旧前者来改变(固然他们都能处理)。

亚洲必赢官网 3

前端框架热潮

有句俗话说的好——“人啊,假如擅于开口‘关自家屁事’和‘关你屁事’那俩句,可以节约人生中的半数以上时光”。

乘胜那两年被 angular
牵头带起的种种前端MV*框架的流行,后端可以毋须再于静态页面用度感情,只须求专心开发数据接口供前端选用即可。得益于此,前后端终于可以告慰地互相道一声“关我屁事”或“关你屁事”了。

以 avalon
为例,前端只须要在页面加载时发送个ajax请求取得数据绑定到vm,然后做view层渲染即可:

var vm = avalon.define({ $id: “wrap”, list: [] });
fetch(‘data/list.php’) //向后端接口发出请求 .then(res => res.json())
.then(json => { vm.list = json; //数据注入vm avalon.scan();
//渲染view层 });

1
2
3
4
5
6
7
8
9
10
11
var vm = avalon.define({
    $id: "wrap",
    list: []
});
 
fetch(‘data/list.php’)   //向后端接口发出请求
    .then(res => res.json())
    .then(json => {
        vm.list = json; //数据注入vm
        avalon.scan();  //渲染view层
    });

静态页面的代码也由前端一手精通,原本服务端的代码换成了 avalaon
的专用属性与插值表达式:

ul ms-controller=”wrap”> li ms-repeat=”list”>{el.name}li>
ul>

1
2
3
ul ms-controller="wrap">
    li ms-repeat="list">{el.name}li>
ul>

内外端代码隔离的方式大大升级了品种的可维护性和支出成效,已经改成一种web开发的主流格局。它解放了后端程序员的双手,也将更加多的控制权转移给前端人员(当然前端也因而需求多读书一些框架知识)。

亚洲必赢官网 4

弊端

左右端隔离的方式纵然给支付推动了有益,但对照关系融洽的旧方式,页面首屏的多寡须求在加载的时候向服务端发去请求才能得到,多了请求等候的岁月(RTT)。

那象征用户访问页面的时候,那段“等待后端再次回到数据”的时延会处在白屏状态,假若用户网速差,那么这段首屏等候时间会是很不好的经验。

自然拉到数据后,还得做 view
层渲染(客户端引擎的拍卖依然很快的,忽略渲染的时刻),这又凭借于框架本身,即框架要先被下载下来才能处理这个视图渲染操作。那么好东西,一个
angular.min.js 就高达了 120
多KB,用着渣信号的用户得多等上一两秒来下载它。

这么看来,单纯前后端隔离的形式存在首屏时间较长的题材,除非未来平均网速达到上G/s,不然都是糟糕好的心得。

除此以外利用前端框架的页面也不便民SEO,其实应当说不便于国内那么些渣搜索引擎的SEO,谷歌(Google)现已能从内存中去抓数据(客户端渲染后的DOM数据)。

so 怎么办?相信广大朋友猜到了——用 node 来助阵。

亚洲必赢官网 5

直出和同构

亚洲必赢官网 ,直出大约其实就是“服务端渲染并出口”,跟起头大家提及的前后端关系融洽的开发方式为主接近,只是后端语言大家换成了
node 。

09年始发冒头的 node
现在成了当红炸子鸡,包罗阿里、腾讯在内的各大集团都广泛地把 node
用到品种上,前后端整而为一,假设 node
的特征适用于您的档次,那么甘之如饴呢。

俺们在那边也提及了一个“同构”的概念,即上下端(那里的“后端”指的是直出端,数据接口不必然由node开发)运用相同套代码方案,方便维护。

眼下 node 在服务端有着许多主流抑或非主流的框架,包罗express、koa、thinkjs 等,可以较快上手,利用种种中间件得以拓展连忙开发。

除此以外诸如 ejs、jade
那样的渲染模板能让大家轻松地把首屏内容(数据或渲染好的DOM树)注入页面中。

诸如此类用户访问到的便是现已包涵首屏内容的页面,大大下降了等候时间,提高了心得。

亚洲必赢官网 6

示例

在那边大家以 koa + ejs + React
的服务端渲染为例,来探望一个简易的“直出”方案是怎样完成的。该示例也可以在我的github内外载到。

项目标目录结构如下:

+—data //模拟数据接口,放了一个.json文件 +—dist
//文件打造后(gulp/webpack)存放处 | +—css | | +—common | | —page
| +—js | | +—component | | —page | —views | +—common | —home
+—modules //一些活动封装的通用业务模块 +—routes //路由布置 —src
//未打造的文书夹 +—css | +—common | +—component | —page +—js |
+—component //React组件 | —page //页面入口文件 —views //ejs模板
+—common —home

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
+—data   //模拟数据接口,放了一个.json文件
+—dist  //文件构建后(gulp/webpack)存放处
|   +—css
|   |   +—common
|   |   —page
|   +—js
|   |   +—component
|   |   —page
|   —views
|       +—common
|       —home
+—modules  //一些自行封装的通用业务模块
+—routes  //路由配置
—src  //未构建的文件夹
    +—css
    |   +—common
    |   +—component
    |   —page
    +—js
    |   +—component //React组件
    |   —page //页面入口文件
    —views  //ejs模板
        +—common
        —home

1. node 端 jsx 解析处理

node 端是不会自己辨认 React 的 jsx
语法的,故我们要求在类型文件中引入 node-jsx ,即便明天可以安装 babel-cli
(并加上预设)利用 babel-node 命令替代
node,但后者用起来总会出难题,故暂时仍旧选拔 node-jsx 方案:

//app.js require(‘node-jsx’).install({ //让node端能解析jsx extension:
‘.js’ }); var fs = require(‘fs’), koa = require(‘koa’), compress =
require(‘koa-compress’), render = require(‘koa-ejs’), mime =
require(‘mime-types’), r_home = require(‘./routes/home’), limit =
require(‘koa-better-ratelimit’), getData = require(‘./modules/getData’);
var app = koa(); app.use(limit({ duration: 1000*10 , max: 500,
accessLimited : “您的请求太过频仍,请稍后重试”}) ); app.use(compress({
threshold: 50, flush: require(‘zlib’).Z_SYNC_FLUSH })); render(app, {
//ejs渲染配置 root: ‘./dist/views’, layout: false , viewExt: ‘ejs’,
cache: false, debug: true }); getData(app); //首页路由 r_home(app);
app.use(function*(next){ var p = this.path; this.type = mime.lookup(p);
this.body = fs.createReadStream(‘.’+p); }); app.listen(3300);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
//app.js
require(‘node-jsx’).install({  //让node端能解析jsx
    extension: ‘.js’
});
 
var fs = require(‘fs’),
    koa = require(‘koa’),
    compress = require(‘koa-compress’),
    render = require(‘koa-ejs’),
    mime = require(‘mime-types’),
    r_home = require(‘./routes/home’),
    limit = require(‘koa-better-ratelimit’),
    getData = require(‘./modules/getData’);
 
var app = koa();
 
app.use(limit({ duration: 1000*10 ,
    max: 500, accessLimited : "您的请求太过频繁,请稍后重试"})
);
app.use(compress({
    threshold: 50,
    flush: require(‘zlib’).Z_SYNC_FLUSH
}));
 
render(app, {  //ejs渲染配置
    root: ‘./dist/views’,
    layout: false ,
    viewExt: ‘ejs’,
    cache: false,
    debug: true
});
 
getData(app);
 
//首页路由
r_home(app);
 
app.use(function*(next){
    var p = this.path;
    this.type = mime.lookup(p);
    this.body = fs.createReadStream(‘.’+p);
});
 
app.listen(3300);

2. 首页路由(’./routes/home’)配置

var router = require(‘koa-router’), getHost =
require(‘../modules/getHost’), apiRouter = new router(); var React =
require(‘react/lib/ReactElement’), ReactDOMServer =
require(‘react-dom/server’); var List =
React.createFactory(require(‘../dist/js/component/List’));
module.exports = function (app) { var data =
this.getDataSync(‘../data/names.json’), //取首屏数据 json =
JSON.parse(data); var lis = json.map(function(item, i){ return (
<li>{item.name}</li> ) }), props = {color: ‘red’};
apiRouter.get(‘/’, function *() { //首页 yield
this.render(‘home/index’, { title: “serverRender”, syncData: { names:
json, //将取到的首屏数据注入ejs模板 props: props }, reactHtml:
ReactDOMServer.renderToString(List(props, lis)), dirpath: getHost(this)
}); }); app.use(apiRouter.routes()); };

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
var router = require(‘koa-router’),
    getHost = require(‘../modules/getHost’),
    apiRouter = new router();
 
var React = require(‘react/lib/ReactElement’),
    ReactDOMServer = require(‘react-dom/server’);
var List = React.createFactory(require(‘../dist/js/component/List’));
 
module.exports = function (app) {
 
    var data = this.getDataSync(‘../data/names.json’),  //取首屏数据
        json = JSON.parse(data);
 
    var lis = json.map(function(item, i){
       return (
           <li>{item.name}</li>
       )
    }),
        props = {color: ‘red’};
 
    apiRouter.get(‘/’, function *() {  //首页
        yield this.render(‘home/index’, {
            title: "serverRender",
            syncData: {
                names: json,  //将取到的首屏数据注入ejs模板
                props: props
            },
            reactHtml:  ReactDOMServer.renderToString(List(props, lis)),
            dirpath: getHost(this)
        });
    });
 
    app.use(apiRouter.routes());
 
};

只顾那里大家运用了 ReactDOMServer.renderToString 来渲染 React 组件为纯
HTML 字符串,注意 List(props, lis) ,大家还盛传了 props 和 children。

其在 ejs 模板中的应用为:

div class=”wrap” id=”wrap”>-reactHtml%>div>

1
div class="wrap" id="wrap">-reactHtml%>div>

就好像此简单地形成了服务端渲染的拍卖,但还有一处难点,要是组件中绑定了风云,客户端不会感知。

因而在客户端我们也急需再做一次与服务端一致的渲染操作,鉴于服务端生成的DOM会被打上
data-react-id 标志,故在客户端渲染的话,react
会通过该标志位的争论统一来防止冗余的render,并绑定上相应的风云。

这也是大家把所要注入组件中的数据(syncData)传入 ejs
的原由,大家将把它当做客户端的一个全局变量来利用,方便客户端挂载组件的时候用上:

ejs上注入直出多少:

script> syncData = JSON.parse(”); script>

1
2
3
  script>
    syncData = JSON.parse(”);
  script>

页面入口文件(js/page/home.js)挂载组件:

import React from ‘react’; import ReactDOM from ‘react-dom’; var List =
require(‘../component/List’); var lis =
syncData.names.map(function(item, i){ return (
<li>{item.name}</li> ) }); ReactDOM.render( <List
{…syncData.props}> {lis} </List>,
document.getElementById(‘wrap’) );

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import React from ‘react’;
import ReactDOM from ‘react-dom’;
var List = require(‘../component/List’);
 
var lis = syncData.names.map(function(item, i){  
    return (
        <li>{item.name}</li>
    )
});
ReactDOM.render(
    <List {…syncData.props}>
        {lis}
    </List>,
    document.getElementById(‘wrap’)
);

3. 帮助工具

为了玩鲜,在一部分模块里写了 es2015 的语法,然后使用 babel
来做转换处理,在 gulp 和 webpack 中都有应用到,具体可参照它们的安顿。

其它是因为服务端对 es2015 的性状襄助不完整,配合 babel-core/register
或者利用 babel-node
命令都存在兼容难题,故针对具有要求在服务端引入到的模块(比如React组件),在koa运行前先做gulp处理转为es5(这些打造模块仅在服务端会用到,客户端走webpack直接引用未更换模块即可)。

ejs文件中样式或脚本的内联处理我动用了团结付出的
gulp-embed
,有趣味的恋人可以玩一玩。

4. issue

说实话 React
的服务端渲染处理一体化开发是没难题的,就是支付体验不够好,首要缘由依然各市点对
es2015 协理不做到导致的。

虽说在服务端运行前,大家在gulp中应用babel对相关模块进行更换,但像 export
default XXX

那样的语法转换后或者不可以被服务端援救,只好降级写为 module.exports =
XXX
。但这么写,在其余模块就没办法 import XXX from ‘X’ 了(改为
require(‘X’)代替)
,可想而知不爽快。只可以期待后续 node(其实应该说V8)
再迭代一些本子能更好地襄助 es2015 的特征。

其余如若 React 组件涉及列表项,常规大家会加上 key
的props特性来提高渲染功能,但就算前后端传入相同的key值,末了 React
渲染出来的 key 值是不雷同的,会招致客户端挂载组件时再做一次渲染处理。

对此那一点我个人指出是,就算是静态的列表,那么统一都不加 key
,倘诺是动态的,那么就加吧,客户端再渲染一次感觉也没多大点事。(或者您有更好方案请留言哈~)

5. 其它

偶尔服务端引入的模块里面,有些东西是仅仅需求在客户端采取到的,大家以那些示例中的组件component/List为例,里面的体裁文件

require(‘css/component/List’);

1
require(‘css/component/List’);

不该在服务端执行的时候使用到,但鉴于同构,前后端用的一套东西,这么些怎么化解吧?其实很好办,通过
window
对象来判断即可(只要没有啥中间件给你在服务端也加了window接口)

var isNode = typeof window === ‘undefined’; if(!isNode){
require(‘css/component/List’); }

1
2
3
4
5
var isNode = typeof window === ‘undefined’;
 
if(!isNode){
    require(‘css/component/List’);
}

不过请小心,那里我透过 webpack
把组件的体制也打包进了客户端的页面入口文件,其实不服帖。因为经过直出,页面在响应的时候就早已把组件的DOM树都先出示出来了,但以此时候是还未曾取到样式的(样式打包到进口脚本了),须要等到进口脚本加载的时候才能看到科学的体裁,那个进度会有一个眨眼的进度,是种不痛快的体验。

之所以走直出的话,提议把首屏的体裁抽离出来内联到头顶去。

1 赞 2 收藏 2
评论

亚洲必赢官网 7

React是现阶段前端社区最风靡的UI库之一,它的基于组件化的开发格局极大地进步了前端开发体验,React通过拆分一个大的应用至一个个小的组件,来驱动大家的代码尤其的可被收录,以及得到更好的可维护性,等等还有其他众多的优点…

一、前言

用到的技术:React Node Webpack material-ui mongo

通过React,
大家普通会支付一个单页应用(SPA),单页应用在浏览器端会比传统的网页有更好的用户体验,浏览器一般会获得一个body为空的html,然后加载script指定的js,
当所有js加载完成后,开首实施js, 最终再渲染到dom中,
在这几个历程中,一般用户只好等待,什么都做不了,假使用户在一个高效的网络中,高配置的设施中,以上先要加载所有的js然后再履行的进度可能不是何等大题材,可是有众多情景是大家的网速一般,设备也恐怕不是最好的,在那种境况下的单页应用可能对用户来说是个很差的用户体验,用户可能还没经验到浏览器端SPA的好处时,就早已偏离网站了,这样的话你的网站做的再好也不会有太多的浏览量。

何以必要服务端渲染?什么情状下开展服务端渲染?小编觉得,当大家渴求渲染时间尽可能快、页面响应速度快时(优点),才会使用服务器渲染,并且应该“按需”对页面举办渲染
——“首次加载/首屏”。即服务端渲染的优势在于:由中间层( node端
)为客户端请求初叶数据、并由node渲染页面。那客户端渲染和服务端渲染有怎么样差距?服务端渲染究竟快在何地啊?

github地址:https://github.com/shenjiajun53/HiBlog
欣赏请给个star!!!
推介多个工具:https://app.astralapp.com/dashboard
这一个网站可以用来治本github上的star

不过大家总不可能重返以前的一个页面一个页面的历史观支付吧,现代化的UI库都提供了服务端渲染(SSR)的功用,使得大家开发的SPA应用也能完善的运转在服务端,大大加速了首屏渲染的光阴,那样的话用户既能更快的看出网页的内容,与此同时,浏览器同时加载须求的js,加载完后把富有的dom事件,及各类互动添加到页面中,最终如故以一个SPA的花样运行,这样的话大家既进步了首屏渲染的岁月,又能博得SPA的客户端用户体验,对于SEO也是个必须的职能。

二、原因与思路

另一个叫 Octotree 是Chrome应用,可以见到github上的连串协会,跟IDE一样

OK,大家大体明白了SSR的必要性,下边大家就可以在一个React
App中来落实服务端渲染的效率,BTW,
既然我们早已处于一个各州是async/await的条件中,那里的服务端大家拔取koa2来落到实处我们的服务端渲染。

客户端渲染路线:1. 请求一个html -> 2. 服务端再次回到一个html -> 3.
浏览器下载html里面的js/css文件 -> 4. 等候js文件下载已毕 -> 5.
等待js加载并开始化达成 -> 6.
js代码终于可以运行,由js代码向后端请求数据( ajax/fetch ) -> 7.
守候后端数据重返 -> 8. react-dom( 客户端
)从无到总体地,把多少渲染为响应页面

自家是一名Android开发,固然在互连网公司,可是感觉温馨的做事根本就是软件行业的行事。
思考对服务端,前端都不打听真是挺无知的。
事先看同事有和好的博客站,百度了下,发现要做那种个人网站真是很简短,只不过那根本就是个静态网站,太辣鸡,所以我说了算自己撸个博客网站。

开端化一个无独有偶的单页应用SPA

服务端渲染路线:2. 伸手一个html -> 2. 服务端请求数据( 内网请求快 )
-> 3. 服务器早先渲染(服务端品质好,较快) -> 4.
服务端重回已经有不易内容的页面 -> 5. 客户端请求js/css文件 -> 6.
等待js文件下载完毕 -> 7. 守候js加载并发轫化已毕 -> 8. react-dom(
客户端 )把剩余部分渲染完结( 内容小,渲染快 )

主页

率先大家先不管服务端渲染的事物,大家先创制一个基于React和React-Router的SPA,等大家把一个完好的SPA创立好后,再插手SSR的意义来最大化升高app的质量。

证实:对同一个零部件,服务端渲染“可视的”一部分(
render/componentWillMount部分代码 
),为有限支撑组件有一揽子的生命周期及事件处理,客户端需求重新渲染。即:服务端渲染,实际上也是急需客户端举办再度地、但支付很小的二次渲染。

亚洲必赢官网 8

首先进入app入口 App.js:

时间耗时比较:

home_page.png

import ReactDOM from 'react-dom';
import { BrowserRouter as Router, Route } from 'react-router-dom';

const Home = () => <div>Home</div>;
const Hello = () => <div>Hello</div>;

const App = () => {
 return (
  <Router>
   <Route exact path="/" component={Home} />
   <Route exact path="/hello" component={Hello} />
  </Router>
 )
}

ReactDOM.render(<App/>, document.getElementById('app'))

1.
数据请求:由服务端请求数据而不是客户端请求数据,那是“快”的一个根本缘由。服务端在内网举行呼吁,数据响应速度快。客户端在差距互联网环境进行数量请求,且外网http请求支付大,导致时间差(主要原因)。

注册页

地方大家为路由/ 和
/hello创制了2个只是渲染一些文字到页面的零部件。但当大家的类型变得更其大,组件越多,最后大家打包出来的js可能会变得很大,甚至变得不可控,所以啊大家第一步须要优化的是代码拆分(code-splitting),幸运的是通过webpack
dynamic
import和
react-loadable,大家得以很不难形成这点。

2.
手续:服务端是先请求数据然后渲染“可视”部分,而客户端是等待js代码下载、加载成功再请求数据、渲染。即:服务端渲染不用等待js代码下载达成再请求数据,并会回来一个一度有内容的页面。

亚洲必赢官网 9

用React-Loadable来时间代码拆分

  1. 渲染品质:服务端品质比客户端高,渲染速度快( 估量,该项数据不详 )。

register_page.png

运用之前,先安装 react-loadable:

4.
渲染内容:服务端渲染会把”可视“部分先渲染,然后提交客户端再作一些渲染。而客户端渲染,则是从无到有,要求经验完全的渲染步骤。    

上边就是技巧选型啦:

npm install react-loadable
# or
yarn add react-loadable

亚洲必赢官网 10  

前端:

要用就用潮的,多个知名框架Angular,React,Vue
Angular听说1和2全然不协作,最头痛那种对开发人员不承担的框架了,不考虑
Vue的语法更像正常的前端开发,React默许匡助es6,风格跟Android更像,尤其跟Android的DataBinding很像。
另一方面es6很像Java,我一定使用es6开支的,我更爱好面向对象。Vue当然也得以用es6,但自我无意鼓捣了,更体贴的ReactNative可比Weex火多了。哈哈,所往日端我就分选React了。
当真的前端开发应该更欣赏Vue吧,文档都有普通话的。

接下来在你的 javascript中:

三、注意事项与题材

后端:

虽说自己是做Android的,可是回想两年前先是次接触JavaEE的时候正是被恶意到了,不考虑
python没有花括号,新浪上都调侃写python要用游标卡尺,不考虑
PHP开发比Java更快,而且现在快跟Java平分秋色了呢,可以设想。
Node技术照旧挺新的,各样作品资料会比较少,坑也可能比较多。但自己真是不开心再学一门语言了,所以哈哈,就选Node了。

//...
import Loadable from 'react-loadable';
//...

const AsyncHello = Loadable({
 loading: <div>loading...</div>,
 //把你的Hello组件写到单独的文件中
 //然后使用webpack的 dynamic import
 loader: () => import('./Hello'), 
})

//然后在你的路由中使用loadable包装过的组件:
<Route exact path="/hello" component={AsyncHello} />

0.
类型看重什么?答:node端:express、react-dom/server、webpack。前端:React、mobx(一个更好的redux)、React-router、webpack

运行

1.git clone
2.装置mongo,然后在装置目录下新建文件夹,比如“D:\data\db”,然后打开“D:\MongoDB\Server\3.4\bin\mongod.exe”开启服务,会有个命令行窗口保持运行
3.指出设置一个mongo可视化工具,Robomongo,默认连接localhost:27017,能一而再上就表明好了
4.进来项目目录下,执行命令

npm install

设置相关依赖库
5.执行

node server.js

打开服务。
要么,比如自己用的Webstorm,可以点击左下角npm按钮,点击start-dev-serpervisor或者点击start-dev-nodemon,都可以

亚洲必赢官网 11

webstorm.png

6.开拓浏览器,输入 localhost:5006

很粗略吗,大家只需求importreact-loadable
然后传一些option进去就行了,其中的loading选料是当动态加载Hello组件所需的js时,渲染loading组件,给用户一种加载中的感觉,体验也会比如何都不加好。

1.
前端/node端共用那部分代码?答:node端/前端有个其他进口文件,server.js/client.js,通过react-router的路由配置文件routes.js作中间层

下边起首:

本身对上下端大约零基础,从前有看过ReactNative,顺便看了下React。
为此写的代码在内行眼里也许很丢脸,哈哈

好了,现在如果大家走访首页的话,唯有Home组件依赖的js才会被加载,然后点击某个链接进入hello页面的话,会先渲染loading组件,并还要异步加载hello组件珍重的js,加载完后,替换掉loading来渲染hello组件。通过按照路由拆分代码到不相同的代码块,大家的SPA已经有了很大的优化,cheers。更叼的是react-loadable一样支撑SSR,所以你可以在自由地方采用react-loadable,不管是运作在前者照旧服务端,要让react-loadable在服务端正常运转的话大家须求做一些附加的布置,本文前边会讲到,先不急。‍

// routes.js
module.exports = (
  <Route path="/" component={ IComponent } >
    <Route path="/todo" component={ AComponent }>
    </Route>
  </Route>
)

第一步:

设置node,新建个公文夹,HiBlog
命令行:

cd HiBlog

初始化:

npm init

生成package.json文件
然后安装种种须要的包,比如express–node框架,webpack–打包工具

npm i express  --save;npm i webpack --save

然后那四个就下载好了,在node_module文件夹下,引用也自动添加到package.json文件里了
或者package.json文件一贯复制的外人的,里面有内容了,

npm i

把package里所有引用都下载到node_module下。
跟Android的依赖很像啊

我的module引用

{
  "name": "HiBlog",
  "version": "0.1.0",
  "private": true,
  "engines": {
    "node": "^7.4.0",
    "npm": ">= 3.x.x"
  },
  "devDependencies": {
    "concurrently": "^3.1.0",
    "nodemon": "^1.11.0",                                     //node监听工具,改动代码自动重启服务
    "supervisor": "^0.12.0",                                   //跟上面一样的,用一个就行,反正我都加了
  },
  "dependencies": {
    "antd": "^2.6.4",                                         //蚂蚁金服的UI框架,项目里没用,哈哈
    "babel-core": "^6.22.1",
    "babel-loader": "^6.2.10",
    "babel-preset-es2015": "^6.22.0",
    "babel-preset-react": "^6.22.0",
    "body-parser": "^1.16.0",
    "config-lite": "^1.5.0",
    "connect-flash": "^0.1.1",
    "connect-mongo": "^1.3.2",
    "cookie-parser": "^1.4.3",
    "css-loader": "^0.26.1",
    "ejs": "^2.5.5",
    "express": "^4.14.1",
    "express-formidable": "^1.0.0",
    "express-session": "^1.15.0",
    "express-winston": "^2.1.3",
    "jsx-loader": "^0.13.2",
    "marked": "^0.3.6",
    "material-ui": "^0.16.7",                                      //UI框架,项目里主要用的就是这个
    "moment": "^2.17.1",
    "mongoose": "^4.8.1",                                     //数据库框架
    "multer": "^1.3.0",
    "objectid-to-timestamp": "^1.3.0",
    "react": "^15.4.2",
    "react-dom": "^15.4.2",
    "react-router": "^3.0.2",
    "react-tap-event-plugin": "^2.0.1",
    "roboto": "^0.8.2",
    "sha1": "^1.1.1",
    "style-loader": "^0.13.1",
    "webpack": "^1.14.0",
    "winston": "^2.3.1"
  },
  "scripts": {
    "start": "node server.js",                                          //启动服务
    "start-supervisor": "supervisor --harmony server",        //启动服务    代码有变动就重启
    "start-nodemon": "nodemon server.js",                    //启动服务       代码有变动就重启
    "build": "webpack --watch",                                   //打包React代码, --watch代表代码有变动就重新打包
    "start-dev": "concurrently \"npm run start\" \"npm run build\"",
    "start-dev-supervisor": "concurrently \"npm run start-supervisor\" \"npm run build\"",
    "start-dev-nodemon": "concurrently \"npm run start-nodemon\" \"npm run build\""
  }
}

看下我的目录结构

亚洲必赢官网 12

2017-02-18.png

看下我的github页面,左边就是自家在最上边介绍的github插件,工欲善其事必先利其器。

新建文件webpack.config.js,webpack工具会识别到那几个文件里的本子,然后把React代码打包,很紧要,不然不管是React的JSX或者ES6,浏览器都读不懂。

新建文件夹views,里面是前者代码
新建文件夹routes,里面放路由代码,因为大家用的React,所以这几个网站就是所谓的单页网站,One
Page Website。看views上面唯有一个index.html。
以此HTML就是个空壳,真正的网页都是有js代码生成的,不管是哪位页面,都寄予那么些HTML。所以那里和常规网页不通,网页不是由服务器渲染的一个个HTML。网站的劳作方式更像APP,通过api落成网页内容变动。前后端分离才是人性化的开发格局,不是吧?

文件夹config,里面放一些装置

最要害的server.js

/**
 * Created by shenjj on 2017/1/23.
 */
let express = require('express');
let app = express();
let path = require('path');
let ejs = require('ejs');
let bodyParser = require("body-parser");
let MongoUtil = require("./server/lib/MongoUtil");
let session = require('express-session');
let MongoStore = require('connect-mongo')(session);
let cookieParser=require("cookie-parser");
let config = require('config-lite');
let flash = require('connect-flash');

let homeRouter = require('./routes/HomePageRouter');
let userRouter = require('./routes/userInfo');
// let signRouter = require("./server/signUp").signUp;
// let DealSignUp = require("./server/DealSignUp");
let RouterManager = require("./routes/RouterManager");

// 设置模板目录
app.set('views', './views');

// 设置模板引擎为 ejs
app.set('view engine', 'html');
app.engine('html', ejs.renderFile);

// app.use配置
app.use('/output', express.static(path.join(__dirname, '/output')));
//app.use('/views', express.static(path.join(__dirname, '/views')));
app.use('/uploadFiles', express.static(path.join(__dirname, '/uploadFiles')));

...
...

app.use(bodyParser.json());
app.use(bodyParser.urlencoded({extended: true}));

// app.use('/', homeRouter);
app.use('/users', userRouter);

let routerManager = new RouterManager(app);
routerManager.startRouters();


app.get('*', function (request, response) {
    response.sendFile(path.resolve(__dirname, 'views', 'index.html'))
});

app.listen(process.env.PORT || 5006);

console.log("url=" + process.env.PORT);

此间开端就是的确node代码里
安装模版目录在views下边,其实就是那么些index.html
那是模版引擎ejs,用来渲染成html,(有三个主流引擎ejs和jade,ejs和html一毛一样,所以我引进那个,jade尽管省代码,不过跟python一样,太随便了)
其它我不须要用ejs的成效,所以我一向设置成HTML了。

app.use(‘/output’, express.static(path.join(__dirname, ‘/output’)));
app.use(‘/uploadFiles’, express.static(path.join(__dirname,
‘/uploadFiles’)));
用来指定打包后的js代码,在output目录下,这几个是在wenpack.config里安装的
uploadFiles是js代码里引用的图纸,空目录,里面的图形是用户上传的。

RouterManager是我写的Router管理类,从作风也能观望我就是相比习惯Java,如故面向对象舒服,即使代码量大一些。
routerManater把app传进去

终极是端口号,默许5006

RouterManager里:

...
        this.app.use("/", new HomePageRouter().getRouter());
        this.app.use("/api", new UserRouter().getRouter());
        this.app.use("/api", new BlogRouter().getRouter());
...

HomePageRouter里:

        router.get('/', function (req, res) {
            console.log("homepage on render");
            res.render('index');
            // res.render('');
            // res.redirect("/SignUp");
        });

故此就是,直接在浏览器输入localhost:5006,会跳转到主页
即使输入localhost:5006/api/xxx,会调起userRouter或者blogRouter里的接口,看那一个匹配了

到此处大家已经创办好一个中坚的React
SPA,加上代码拆分,大家的app已经有了不易的性质,可是大家还是能进一步极致的优化app的习性,下边大家由此扩张SSR的功能来更加升高加载速度,顺便解决一下SPA中的SEO难点。

2.
代码是由上下端共享,那怎样分平台地操作不一致代码?答:通过webpack。对共享代码,举行差别平台的,webpack(babel)编译,通过在webpack.config.js中进入

上面是部分前端代码

class App extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            hasLogin: false,
            user: null
        }
    }

    componentWillMount() {
        let url = "/api/getUserInfo";
        fetch(url, {
            method: "post",
            credentials: 'include'     //很重要,设置session,cookie可用
        }).then(
            (response) => {
                return response.json();
            }
        ).then(
            (json) => {
                console.log(JSON.stringify(json));
                if (json.result) {
                    this.setState({
                        hasLogin: json.result.hasLogin,
                        user: json.result.user
                    });
                }
                // console.log("state=" + this.state.hasLogin);
            }
        ).catch(
            (ex) => {
                console.error('parsing failed', ex);
            });
    }

    render() {
        console.log('app render');
        // console.log('chileren=' + this.props.children.name);
        return (
            <MuiThemeProvider>
                <div>

                    <TopBar hasLogin={this.state.hasLogin} user={this.state.user}/>
                    {React.cloneElement(this.props.children, {user: this.state.user})}
                </div>
            </MuiThemeProvider>
        );
    }
}

render(
    <Router history={browserHistory}>
        <Route path="/" component={App}>
            <IndexRoute component={Home}/>
            <Route path="SignUp" component={SignUp}/>
            <Route path="SignIn" component={SignIn}/>
            <Route path="UserCenter" component={UserCenter}/>
            <Route path="MyFollow" component={MyFollow}/>
            <Route path="WriteBlog" component={WriteBlog}/>
            <Route path="BlogDetail/:blogId" component={BlogDetail}/>
            <Route path="Settings" component={Settings}/>
            <Route path="Favorites" component={Favorites}/>
            <Route path="MyBlogs" component={MyBlogs}/>
        </Route>
    </Router>
    ,
    document.getElementById('root')
)

页面跳转完全由前端控制,用Rooter类,APP是漫天网页的父布局。在APP渲染已毕后拿走user音讯,this.props.children是现阶段页面,可以clone一个ReactComponent,并设置props。

进入服务端渲染(SSR)成效

 // webpack.client.config.js
plugins: [
   new webpack.DefinePlugin({
     '__isServer__': false,
     '__isClient__': true
   })
 ]
// webpack.server.config.js
plugins: [
   new webpack.DefinePlugin({
     '__isServer__': true,
     '__isClient__': false
   })
 ]
// xxx.js
if( __isServer__ ) {
  ...
}else { ... }

首先大家先搭建一个最简易的koa web服务器:

亚洲必赢官网 13

npm install koa koa-router
  1. 组件的生命周期是何许的吗?答:component威尔Mount( node端 ) ->
    render( node端 ) -> 客户端生命周期和从前一样

下一场在koa的输入文件app.js中:

5.
拉取数据后什么处理啊?答:先在node端依据数据渲染好,再把数量随页面再次来到至前端,再由React依据数量进行渲染核查(
若前后端渲染结果不雷同将报错
)。应该在component威尔Mount让组件进行地面的多少同步

const Koa = require('koa');
const Router = require('koa-router');

const app = new Koa();
const router = new Router();
router.get('*', async (ctx) => {
 ctx.body = `
   <!DOCTYPE html>
    <html lang="en">
    <head>
     <meta charset="UTF-8">
     <title>React SSR</title>
    </head>
    <body>
     <div id="app"></div>
     <script type="text/javascript" src="/bundle.js"></script>
    </body>
   </html>
  `;
});

app.use(router.routes());
app.listen(3000, '0.0.0.0');
// 组件.js
componentWillMount() {
  if( __isClient__ ) {
     this.todoStore.todos = window.initTodos; 
  }
}  
// node端返回
`
<!doctype html>
<html lang="utf-8">
    <head>
    <script> window.initTodo = ${...}</script>
    </head>
    <body> ... </body>
    <script src="/static/vendor.js"></script>
    <script src="/static/client.bundle.js"></script>

上面*路由代表擅自的url进来大家都默许渲染这几个html,包蕴html中封装出来的js,你也得以用有些服务端模板引擎(如:nunjucks)来一向渲染html文件,在webpack打包时通过html-webpack-plugin来机关插入打包出来的js/css资源路径。

6.
前端/node端“入口文件”通过webpack营造有哪些不一致?答:前端是为精晓析JSX与es6代码(蕴涵mobx的es6
decorator),node端除了以上,还亟需参加babel-plugin-transform-runtime,是为了在node良好地运转es7
async / awatit

OK, 大家的粗略koa server好了,接下去大家初叶编制React
SSR的进口文件AppSSR.js,那里大家须要利用StaticRouter来代替此前的BrowserRouter,因为在服务端,路由是静态的,用BrowserRouter的话是不起功用的,前边还会做一些陈设来驱动react-loadable运行在服务端。

  1. 怎么保管node端可以先请求数据然后再渲染?答:es7的async /
    await语法  

唤醒:
你可以把全部node端的代码用ES6/JSX风格编写,而不是一些commonjs,部分JSX,
但那样的话你必要用webpack把所有服务端的代码编译成commonjs风格,才能使得它运行在node环境中,那里的话咱们把React
SSR的代码单独抽出去,然后在寻常的node代码里去require它。因为可能在一个存世的档次中,以前都是commonjs的风格,把往日的node代码四回性转成ES6的话开销有点大,可是足以中期一步步的再迁移过去

8.
前端的react-router路由与node端路由什么合作?node怎样知道该路由是渲染哪个数据吧?答:前端是以前的react-router配置,node端是react-router的match/RouterContext//
共享文件routes.js

OK, 现在在你的 AppSRR.js中:

const routes = (
  <Route path="/" component={ IComponent } >
    <Route path="/todo" component={ AComponent }>
    </Route>
  </Route>
)
// 前端入口文件client.js
render(
  <Router routes={ routes } history={ browserHistory } />,
  ele
)
// node端入口文件server.js
let app = express();
app.get('/todo', (req, res) => {

  match({ routes: routes, location: req.url }, async (err, redirect, props) => {
     // match会帮我们找到要渲染的组件链,注:上面一行使用了async语法,因此可以在render之前使用await运行拉取数据的代码
     let html = renderToString(<RouterContext {...props} />)
     res.send( indexPage(html) )
  }
}) 
// node端返回   
let indexPage = (html)=>{
  return `
  <!doctype html>
    <html lang="utf-8">
      <head>
        <script>
        </script>
      </head>
      <body>
        <section id="hzpapp" >${html}</section>
      </body>
      <script src="/static/vendor.js"></script>
      <script src="/static/client.bundle.js"></script>
    </html>
}
import React from 'react';
//使用静态 static router
import { StaticRouter } from 'react-router-dom';
import ReactDOMServer from 'react-dom/server';
import Loadable from 'react-loadable';
//下面这个是需要让react-loadable在服务端可运行需要的,下面会讲到
import { getBundles } from 'react-loadable/webpack';
import stats from '../build/react-loadable.json';

//这里吧react-router的路由设置抽出去,使得在浏览器跟服务端可以共用
//下面也会讲到...
import AppRoutes from 'src/AppRoutes';

//这里我们创建一个简单的class,暴露一些方法出去,然后在koa路由里去调用来实现服务端渲染
class SSR {
 //koa 路由里会调用这个方法
 render(url, data) {
  let modules = [];
  const context = {};
  const html = ReactDOMServer.renderToString(
   <Loadable.Capture report={moduleName => modules.push(moduleName)}>
    <StaticRouter location={url} context={context}>
     <AppRoutes initialData={data} />
    </StaticRouter>
   </Loadable.Capture>
  );
  //获取服务端已经渲染好的组件数组
  let bundles = getBundles(stats, modules);
  return {
   html,
   scripts: this.generateBundleScripts(bundles),
  };
 }
 //把SSR过的组件都转成script标签扔到html里
 generateBundleScripts(bundles) {
  return bundles.filter(bundle => bundle.file.endsWith('.js')).map(bundle => {
   return `<script type="text/javascript" src="${bundle.file}"></script>\n`;
  });
 }

 static preloadAll() {
  return Loadable.preloadAll();
 }
}

export default SSR;
  1. client.js中是或不是仍可以继承选用webpack的require.ensure ?
    答:能够。但闪白明显,且node端重回html后会有报错,在加载脚本后该错误能忽视。 

  2. 若自己动用的是mobx,该怎样实例化store ?
    答:每一个node请求,都应该回到一个新的独立的store实例,而不是每个node请求共用一个store实例(小编易犯)。

当编译这些文件的时候,在webpack配置里使用target: "node"
externals,并且在您的包装前端app的webpack配置中,须求进入react-loadable的插件,app的打包需求在ssr打包以前运行,不然拿不到react-loadable必要的各组件信息,先来看app的打包:

亚洲必赢官网 14        

//webpack.config.dev.js, app bundle
const ReactLoadablePlugin = require('react-loadable/webpack')
 .ReactLoadablePlugin;

module.exports = {
 //...
 plugins: [
  //...
  new ReactLoadablePlugin({ filename: './build/react-loadable.json', }),
 ]
}

本demo地址( 前端库React+mobx+ReactRouter
):

在.babelrc中加入loadable plugin:

如上就是本文的整体内容,希望对我们的就学抱有协助,也可望大家多多帮衬脚本之家。  

{
 "plugins": [
   "syntax-dynamic-import",
   "react-loadable/babel",
   ["import-inspector", {
    "serverSideRequirePath": true
   }]
  ]
}

您可能感兴趣的篇章:

  • 详解React
    在服务端渲染的落到实处
  • 详解react服务端渲染(同构)的法子
  • 详解React项目标服务端渲染改造(koa2+webpack3.11)

地方的布署会让react-loadable接头怎么组件最终在服务端被渲染了,然后直接插入到html
script标签中,并在前者开头化时把SSR过的组件考虑在内,幸免双重加载,下边是SSR的打包:

//webpack.ssr.js
const nodeExternals = require('webpack-node-externals');

module.exports = {
 //...
 target: 'node',
 output: {
  path: 'build/node',
  filename: 'ssr.js',
  libraryExport: 'default',
  libraryTarget: 'commonjs2',
 },
 //避免把node_modules里的库都打包进去,此ssr js会直接运行在node端,
 //所以不需要打包进最终的文件中,运行时会自动从node_modules里加载
 externals: [nodeExternals()],
 //...
}

然后在koa app.js, require它,并且调用SSR的点子:

//...koa app.js
//build出来的ssr.js
const SSR = require('./build/node/ssr');
//preload all components on server side, 服务端没有动态加载各个组件,提前先加载好
SSR.preloadAll();

//实例化一个SSR对象
const s = new SSR();

router.get('*', async (ctx) => {
 //根据路由,渲染不同的页面组件
 const rendered = s.render(ctx.url);

 const html = `
  <!DOCTYPE html>
   <html lang="en">
   <head>
    <meta charset="UTF-8">
   </head>
   <body>
    <div id="app">${rendered.html}</div>
    <script type="text/javascript" src="/runtime.js"></script>
    ${rendered.scripts.join()}
    <script type="text/javascript" src="/app.js"></script>
   </body>
  </html>
 `;
 ctx.body = html;
});
//...

如上是个简易的兑现React SSR到koa web server,
为了使react-loadable知情哪些组件在服务端渲染了,rendered里面的scripts数组里面含有了SSR过的组件组成的逐条script标签,里面调用了SSR#generateBundleScripts()办法,在插入时索要确保那么些script标签在runtime.js之后((通过
CommonsChunkPlugin来抽出来)),并且在app
bundle以前(也就是开头化的时候理应早就知晓前边的怎么组件已经渲染过了)。更加多react-loadable服务端援救,参考那里.

地点我们还把react-router的路由都单身抽出去了,使得它可以运行在浏览器跟服务端,以下是AppRoutes组件:

//AppRoutes.js
import Loadable from 'react-loadable';
//...

const AsyncHello = Loadable({
 loading: <div>loading...</div>,
 loader: () => import('./Hello'), 
})

function AppRoutes(props) {
 <Switch>
  <Route exact path="/hello" component={AsyncHello} />
  <Route path="/" component={Home} />
 </Switch> 
}

export default AppRoutes

//然后在 App.js 入口中
import AppRoutes from './AppRoutes';
// ...
export default () => {
 return (
  <Router>
   <AppRoutes/>
  </Router>
 )
}

服务端渲染的始发状态

近日截止,大家曾经创建了一个React
SPA,并且能在浏览器端跟服务端共同运行,社区号称universal app或者
isomophic app。可是我们现在的app还有一个遗留难点,一般的话大家app的数目或者状态都急需通过远端的api来异步获取,得到数量后大家才能开首渲染组件,服务端SSR也是一致,我们要动态的取得开头数据,然后才能扔给React去做SSR,然后在浏览器端大家还要初叶化就能共同获取那几个SSR时的开端化数据,幸免浏览器端伊始化时又再一次获得了一次。

下边大家简要从github获取一些档次的音信作为页面初叶化的数目,
在koa的app.js中:

//...
const fetch = require('isomorphic-fetch');

router.get('*', async (ctx) => {
 //fetch branch info from github
 const api = 'https://api.github.com/repos/jasonboy/wechat-jssdk/branches';
 const data = await fetch(api).then(res => res.json());

 //传入初始化数据
 const rendered = s.render(ctx.url, data);

 const html = `
  <!DOCTYPE html>
   <html lang="en">
   <head>
    <meta charset="UTF-8">
   </head>
   <body>
    <div id="app">${rendered.html}</div>

    <script type="text/javascript">window.__INITIAL_DATA__ = ${JSON.stringify(data)}</script>

    <script type="text/javascript" src="/runtime.js"></script>
    ${rendered.scripts.join()}
    <script type="text/javascript" src="/app.js"></script>
   </body>
  </html>
 `;
 ctx.body = html;
});

下一场在你的Hello组件中,你需要checkwindow内部(或者在App入口中联合判断,然后经过props传到子组件中)是还是不是存在window.__INITIAL_DATA__,有的话一向用来作为初阶数据,没有的话大家在componentDidMount生命周期函数中再去来多少:

export default class Hello extends React.Component {
 constructor(props) {
  super(props);

  this.state = {
   //这里直接判断window,如果是父组件传入的话,通过props判断
   github: window.__INITIAL_DATA__ || [],
  };
 }

 componentDidMount() {
  //判断没有数据的话,再去请求数据
  //请求数据的方法也可以抽出去,以让浏览器及服务端能统一调用,避免重复写
  if (this.state.github.length <= 0) {
   fetch('https://api.github.com/repos/jasonboy/wechat-jssdk/branches')
    .then(res => res.json())
    .then(data => {
     this.setState({ github: data });
    });
  }
 }

 render() {
  return (
   <div>
    <ul>
     {this.state.github.map(b => {
      return <li key={b.name}>{b.name}</li>;
     })}
    </ul>
   </div>
  );
 }
}

好了,现在如果页面被服务端渲染过的话,浏览器会获得具备渲染过的html,
包含开端化数据,然后通过那一个SSR的情节极度加载的js,再组成一个全体的SPA,就像一个惯常的SPA一样,可是大家取得了更好的性质,更好的SEO。

React-v16 更新

在React的新颖版v16中,SSR的API做了不少的优化,并且提供了新的基于流的API来更好的升级品质,通过streaming
api,
服务端可以边渲染边把后面渲染好的html发到浏览器,浏览器端也足以提前早先渲染页面而不是等服务端所有组件都渲染达成后才能发轫浏览器端的起先化,提高了质量也暴跌了服务端资源的损耗。还有一个在浏览器端需求专注的是内需动用ReactDOM.hydrate()来替代从前的ReactDOM.render(),越多的更新参考medium文章whats-new-with-server-side-rendering-in-react-16.

要翻开完整的demo,
参考koa-web-kit,
koa-web-kit是一个现代化的按照React/Koa的全栈开发框架,包括React
SSR支持,可以一贯用来测试服务端渲染的机能

结论

好了,以上就是React-SSR +
Koa的简约实践,通过SSR,大家既提高了品质,又很好的满意了SEO的需求,Best
of the Both Worlds。

English
Version

以上就是本文的全体内容,希望对大家的求学抱有协助,也意在我们多多支持脚本之家。

你或许感兴趣的文章:

  • 详解React项目标服务端渲染改造(koa2+webpack3.11)
网站地图xml地图