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

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

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,谷歌早已能从内存中去抓数据(客户端渲染后的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 Node Webpack material-ui mongo

React是目前前端社区最流行的UI库之一,它的基于组件化的开发方式极大地提升了前端开发体验,React通过拆分一个大的应用至一个个小的组件,来使得我们的代码更加的可被重用,以及获得更好的可维护性,等等还有其他很多的优点…

github地址:https://github.com/shenjiajun53/HiBlog
喜欢请给个star!!!
推荐两个工具:https://app.astralapp.com/dashboard
这个网站可以用来管理github上的star

通过React,
我们通常会开发一个单页应用,单页应用在浏览器端会比传统的网页有更好的用户体验,浏览器一般会拿到一个body为空的html,然后加载script指定的js,
当所有js加载完毕后,开始执行js, 最后再渲染到dom中,
在这个过程中,一般用户只能等待,什么都做不了,如果用户在一个高速的网络中,高配置的设备中,以上先要加载所有的js然后再执行的过程可能不是什么大问题,但是有很多情况是我们的网速一般,设备也可能不是最好的,在这种情况下的单页应用可能对用户来说是个很差的用户体验,用户可能还没体验到浏览器端SPA的好处时,就已经离开网站了,这样的话你的网站做的再好也不会有太多的浏览量。

另一个叫 Octotree 是Chrome应用,可以看到github上的项目结构,跟IDE一样

但是我们总不能回到以前的一个页面一个页面的传统开发吧,现代化的UI库都提供了服务端渲染的功能,使得我们开发的SPA应用也能完美的运行在服务端,大大加快了首屏渲染的时间,这样的话用户既能更快的看到网页的内容,与此同时,浏览器同时加载需要的js,加载完后把所有的dom事件,及各种交互添加到页面中,最后还是以一个SPA的形式运行,这样的话我们既提升了首屏渲染的时间,又能获得SPA的客户端用户体验,对于SEO也是个必须的功能。

我是一名Android开发,虽然在互联网公司,但是感觉自己的工作根本就是软件行业的工作。
想想对服务端,前端都不了解真是挺无知的。
之前看同事有自己的博客站,百度了下,发现要做那种个人网站真是很简单,只不过那根本就是个静态网站,太辣鸡,所以我决定自己撸个博客网站。

OK,我们大致了解了SSR的必要性,下面我们就可以在一个React
App中来实现服务端渲染的功能,BTW,
既然我们已经处在一个到处是async/await的环境中,这里的服务端我们使用koa2来实现我们的服务端渲染。

主页

初始化一个普通的单页应用SPA

图片 8

首先我们先不管服务端渲染的东西,我们先创建一个基于React和React-Router的SPA,等我们把一个完整的SPA创建好后,再加入SSR的功能来最大化提升app的性能。

home_page.png

首先进入app入口 App.js:

注册页

import ReactDOM from 'react-dom';import { BrowserRouter as Router, Route } from 'react-router-dom';const Home = () => Home;const Hello = () => Hello;const App = () => { return (     )}ReactDOM.render(, document.getElementById

图片 9

上面我们为路由/ 和
/hello创建了2个只是渲染一些文字到页面的组件。但当我们的项目变得越来越大,组件越来越多,最终我们打包出来的js可能会变得很大,甚至变得不可控,所以呢我们第一步需要优化的是代码拆分,幸运的是通过webpack
dynamic import 和 react-loadable,我们可以很容易做到这一点。

register_page.png

用React-Loadable来时间代码拆分

下面就是技术选型啦:

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

前端:

要用就用潮的,三个著名框架Angular,React,Vue
Angular听说1和2完全不兼容,最讨厌这种对开发人员不负责的框架了,不考虑
Vue的语法更像常规的前端开发,React默认支持es6,风格跟Android更像,尤其跟Android的DataBinding很像。
另一方面es6很像Java,我肯定使用es6开发的,我更喜欢面向对象。Vue当然也可以用es6,但我懒得鼓捣了,更重要的ReactNative可比Weex火多了。哈哈,所以前端我就选择React了。
真正的前端开发应该更喜欢Vue吧,文档都有中文的。

npm install react-loadable# oryarn add react-loadable

后端:

虽然我是做Android的,但是遥想两年前第一次接触JavaEE的时候真是被恶心到了,不考虑
python没有花括号,知乎上都嘲笑写python要用游标卡尺,不考虑
PHP开发比Java更快,而且现在快跟Java平分秋色了吧,可以考虑。
Node技术还是挺新的,各种文章资料会比较少,坑也可能比较多。但我真是不高兴再学一门语言了,所以哈哈,就选Node了。

然后在你的 javascript中:

运行

1.git clone
2.安装mongo,然后在安装目录下新建文件夹,比如“D:datadb”,然后打开“D:MongoDBServer3.4binmongod.exe”开启服务,会有个命令行窗口保持运行
3.建议安装一个mongo可视化工具,Robomongo,默认连接localhost:27017,能连接上就说明好了
4.进入项目目录下,执行命令

npm install

安装相关依赖库
5.执行

node server.js

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

图片 10

webstorm.png

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

//...import Loadable from 'react-loadable';//...const AsyncHello = Loadable({ loading: loading..., //把你的Hello组件写到单独的文件中 //然后使用webpack的 dynamic import loader: () => import//然后在你的路由中使用loadable包装过的组件:

下面开始:

我对前后端几乎零基础,之前有看过ReactNative,顺便看了下React。
所以写的代码在内行眼里可能很丢人,哈哈

很简单吧,我们只需要importreact-loadable
然后传一些option进去就行了,其中的loading选项是当动态加载Hello组件所需的js时,渲染loading组件,给用户一种加载中的感觉,体验也会比什么都不加好。

第一步:

安装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""
  }
}

看下我的目录结构

图片 11

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里的接口,看那个匹配了

好了,现在如果我们访问首页的话,只有Home组件依赖的js才会被加载,然后点击某个链接进入hello页面的话,会先渲染loading组件,并同时异步加载hello组件依赖的js,加载完后,替换掉loading来渲染hello组件。通过基于路由拆分代码到不同的代码块,我们的SPA已经有了很大的优化,cheers🍻。更叼的是react-loadable同样支持SSR,所以你可以在任意地方使用react-loadable,不管是运行在前端还是服务端,要让react-loadable在服务端正常运行的话我们需要做一些额外的配置,本文后面会讲到,先不急🏃。‍

下面是部分前端代码

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。

到这里我们已经创建好一个基本的React
SPA,加上代码拆分,我们的app已经有了不错的性能,但是我们还可以更加极致的优化app的性能,下面我们通过增加SSR的功能来进一步提升加载速度,顺便解决一下SPA中的SEO问题🎉。

加入服务端渲染功能

相关文章

发表评论

电子邮件地址不会被公开。 必填项已用*标注

*
*
Website