closertb / closertb.github.io

浏览issue 或 我的网站,即可查看我的所有博客
https://closertb.site
32 stars 0 forks source link

初探SSR,React + Koa + Dva-Core #35

Open closertb opened 5 years ago

closertb commented 5 years ago

过去一年半一直在用React + Dva + Antd写中后台项目,从最初的6小时一个页面,到现在的两小时一套页面,其中的秘诀就是不断总结与熟悉,写一些适合业务的轮子,比如:antd-doddle。今年随着业务稳定,有机会去尝试一些自己感兴趣的方向,比如前端工程化、SSR、 小程序;最近由于苹果对App上架流程的调整,部门需要写一个官网。虽然说一个上午就写出来了,但从官网的角度,以及对成品不断追求的态度,现在这个官网太Low了, 无Seo,无移动端适配,无首屏渲染。所以最近开始接触SSR,试图用一个更加专业的方案去重新打造这个官网。

方案筛选

其实没啥筛选的,页面框架React是铁打的。而React的服务端渲染,市面上一般就有完全自己搭和选择NextJs

image

路由的兼容

如果对SPA和SSR了解的话,就知道:SPA一般我们用Hash路由HashRoutr(#/home),而SSR在浏览器端则采用传统路由,即浏览器路由BrowserRouter(/home),但只有去尝试SSR后我才知道,还有一种路由被称之为静态路由StaticRouter(/home),看起来和BrowserRouter相似,之所以称之为静态的,就是它没有前进,后退,跳转这些路由操作。具体可参考React-Router的相关介绍。这三种路由分别对应三种入口:

const history = createHistory(); const app = createApp({ history }); app.start();

const App = () => (

);

render(, document.getElementById('app'));


 - 服务端StaticRouter
```js
import { StaticRouter as Router } from 'react-router-dom';
import createHistory from 'history/createMemoryHistory';
import createApp from './model/createApp';
import Layout from './Layout';

export default function CreateDom({ location, context }) {
  const history = createHistory(location);
  const app = createApp({ history });
  app.start();
  return {
    app,
    render: () => (
      <Provider store={app._store}>
        <Router location={location} context={context} history={history}>
          <Layout location={location} context={context} history={history} />
        </Router>
      </Provider>)
  };
}

// ssr渲染,浏览器端渲染入口 const history = createHistory();

const app = createApp({ history, initialState: window.states && JSON.parse(window.states), }); app.start(); delete window.states; const App = () => (

);

ReactDOM.hydrate(, document.getElementById('app'));

从上面三种代码也可以看出,在reactDom的渲染方式上我们也分别对应三种:
 - render: spa常用;
 - renderToString:服务端渲染专用,用于将React对象渲染成Dom字符串;
 - hydrate:服务端渲染专用,用于延用已存在的dom节点  

### 数据流的管理  
 在上面的三段代码中,都看到了两个共同的模块,一个Layout,一个createApp,分别对应页面与数据流。自己搭框架,其实难点就在怎么公用数据流管理,让差异最小化。由于自己中后台写的太多,对Dva这一套情有独钟,所以怎么绕,最后都把眼神聚焦到了这里。所以最后数据流的管理,还是选择了Dva-core。createApp源码:
 ```js
 import { create } from 'dva-core';
import hook from '@doddle/dva';
import * as models from './model';

export default function createApp(opts) {
  const app = create(opts);
  app._history = opts.history;
  hook({ app }); // 扩展对象, 增加listen, update, loading等插件
  Object.keys(models).forEach(key => app.model(models[key]));
  return app;
}

代码非常简短,没有做什么差异化的兼容处理,只做了dva数据对象的初始化;扩展了这个数据对象;加了一个history属性,目的是listen插件需要;model对象的加载。

SPA渲染与SSR渲染数据流处理的差异就在首屏。通常我们在做SPA时,将获取页面初始状态的操作都放在页面监听中(dva model的subsciption),而不是最初的componentDidMount这个钩子里。但在服务端做首屏渲染时,这种方案就不可取,没有history变化这一说,所以需要采用其他方案。最早写React的人都知道,曾今还有个方法叫getInitialState,但后面这个方法被弃用。在NextJs中也存在一个这样一个方法,其目的就是做服务端渲染的首屏数据获取。我在自己的设计中也沿用了这个思想,具体是:

这样做还有一个好处就是,BrowerRouter由于是初次进这个页面,所以listen监听不会生效,所以不会存在重复获取初始状态这个问题。以上就是数据流方案的整体思路,也是整个SSR中比较重点的。

服务端代码实现

SSR渲染和纯前端渲染最大的区别就是,你需要写一个服务器。而Node给我们提供了这样的能力,让我们可以用js语言写后端服务。之所以从众多的后端框架中选择了Koa,是因为前段时间刚好对Koa有一个比较全面的了解。Koa经典的洋葱模型,将服务实现插件化,非常易于扩展,Async Await的插件语法,也非常符合时代的潮流。后端服务主要在功能上要实现:

const path = require('path');
const Koa = require('koa');
const staticSource = require('koa-static');
const router = require('./router');

const app = new Koa();
const staticPath = '../public';

app.use(staticSource(path.join(__dirname, staticPath)));

app.use(router.routes())
  .use(router.allowedMethods());

// router.js
const Router = require('koa-router');
const stateMiddleaWare = require('./stateMiddleaWare');
const ssrMiddleware = require('./ssrMiddleware');

const router = new Router();
router.get('/states/:key.js', stateMiddleaWare); // 提供同构的初始状态对象
router.get('/:url', ssrMiddleware); // 提供页面的服务端渲染

重点还是在服务端渲染这一块,在我的项目里,这部分是由ssrMiddleware中间件来完成的,源码也比较简单,如果认真读且理解了前面讲的,那这一部分的源码就比较好理解了。大体上讲,做了三件事:

写在最后

由于公司项目,不便提供源码,如果你感兴趣,可以去fork我的示例项目SSrTemplate, 分支ssr, 也可通过下面两种方式下载:

hzfvictory commented 4 years ago

大佬我用的dva-core 为啥store老是公用,我设成回调函数了,还是不行

closertb commented 4 years ago

大佬我用的dva-core 为啥store老是公用,我设成回调函数了,还是不行

你store 是那种概念的,model 中的初始state? 比如:

export default ({
  namespace: 'index',
  state: {
    total: 0,
    num: Math.floor(Math.random() * 1000),
  },
  // ...
}

上面这种写法肯定是公用的,因为其实质就是一个静态导出,这是语法决定的。

如果不是,show your code

hzfvictory commented 4 years ago

就是上面的那种用法,跟直接的客户端的用法是一样的,我看打包输出的是全局的store,所以肯定是所有用户公用了,但是我看你源码中也是这么写的,在你里面没有发现这个问题。

export default {
  namespace: 'menuTree',
  state: {
    routes: []
  },
  effects: {
    * reset(payload, {call, put, select, update}) {
      const {routes} = yield select(state => state.menuTree);
      routes.push(111111)
      yield put({
        type: 'save',
        payload: {
          routes: [...routes]
        },
      });
    },
  },
  reducers: {
    save(state, {payload}) {
      return {...state, ...payload};
    },
  },
};

经你这么点播,我把model原有的对象形式,换成换成函数的形式,然后在导出没问题了。