nodejh 5 years ago
parent
commit
0036bdf79d
9 changed files with 512 additions and 1 deletions
  1. 2
    1
      .gitignore
  2. 38
    0
      lib/block.js
  3. 23
    0
      lib/dvaServerSync.js
  4. 26
    0
      lib/index.js
  5. 95
    0
      lib/preSSRService.js
  6. 201
    0
      lib/render.js
  7. 59
    0
      lib/runtimeSSRMiddle.js
  8. 12
    0
      lib/ssrModel.js
  9. 56
    0
      lib/utils.js

+ 2
- 1
.gitignore View File

@@ -1,3 +1,4 @@
1 1
 node_modules
2 2
 .DS_Store
3
-npm-debug.log
3
+npm-debug.log
4
+package-lock.json

+ 38
- 0
lib/block.js View File

@@ -0,0 +1,38 @@
1
+"use strict";
2
+
3
+Object.defineProperty(exports, "__esModule", {
4
+  value: true
5
+});
6
+const container = new Map();
7
+const callbacks = new Map();
8
+
9
+exports.default = {
10
+  lock: id => {
11
+    let count = container.get(id) || 0;
12
+    container.set(id, ++count);
13
+  },
14
+  release: function (id) {
15
+    let count = container.get(id) || 0;
16
+    if (count) {
17
+      container.set(id, --count);
18
+    }
19
+    this.check();
20
+  },
21
+  check: () => {
22
+    for (let [id, callback] of callbacks) {
23
+      const count = container.get(id);
24
+      if (count === 0) {
25
+        container.delete(id);
26
+        callback();
27
+      }
28
+    }
29
+  },
30
+  wait: function (id, callback) {
31
+    callbacks.set(id, function () {
32
+      callbacks.delete(id);
33
+      callback();
34
+    });
35
+    this.check();
36
+  }
37
+};
38
+module.exports = exports["default"];

+ 23
- 0
lib/dvaServerSync.js View File

@@ -0,0 +1,23 @@
1
+"use strict";
2
+
3
+Object.defineProperty(exports, "__esModule", {
4
+  value: true
5
+});
6
+exports.default = sync;
7
+function sync(key, filter, block) {
8
+  return {
9
+    onEffect: function (effect, { put }, model, actionType) {
10
+      const temp = [];
11
+      return function* (...args) {
12
+        if (filter(args[0])) {
13
+          block.lock(key);
14
+        }
15
+        yield effect(...args);
16
+        if (filter(args[0])) {
17
+          block.release(key);
18
+        }
19
+      };
20
+    }
21
+  };
22
+}
23
+module.exports = exports["default"];

+ 26
- 0
lib/index.js View File

@@ -0,0 +1,26 @@
1
+'use strict';
2
+
3
+Object.defineProperty(exports, "__esModule", {
4
+  value: true
5
+});
6
+
7
+var _runtimeSSRMiddle = require('./runtimeSSRMiddle');
8
+
9
+var _runtimeSSRMiddle2 = _interopRequireDefault(_runtimeSSRMiddle);
10
+
11
+var _preSSRService = require('./preSSRService');
12
+
13
+var _preSSRService2 = _interopRequireDefault(_preSSRService);
14
+
15
+var _render = require('./render');
16
+
17
+var _render2 = _interopRequireDefault(_render);
18
+
19
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
20
+
21
+exports.default = {
22
+  runtimeSSRMiddle: _runtimeSSRMiddle2.default,
23
+  preSSRService: _preSSRService2.default,
24
+  render: _render2.default
25
+};
26
+module.exports = exports['default'];

+ 95
- 0
lib/preSSRService.js View File

@@ -0,0 +1,95 @@
1
+'use strict';
2
+
3
+Object.defineProperty(exports, "__esModule", {
4
+  value: true
5
+});
6
+exports.RenderService = undefined;
7
+exports.default = preSSRService;
8
+
9
+var _render = require('./render');
10
+
11
+var _render2 = _interopRequireDefault(_render);
12
+
13
+var _utils = require('./utils');
14
+
15
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
16
+
17
+function _asyncToGenerator(fn) { return function () { var gen = fn.apply(this, arguments); return new Promise(function (resolve, reject) { function step(key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { return Promise.resolve(value).then(function (value) { step("next", value); }, function (err) { step("throw", err); }); } } return step("next"); }); }; }
18
+
19
+function getPathsFromRoutes(routes) {
20
+  const paths = [];
21
+  (0, _utils.searchRoutes)(routes, route => {
22
+    paths.push(route.props.path);
23
+  });
24
+  return paths;
25
+}
26
+
27
+class RenderService {
28
+  constructor({ url, interval, renderOptions }) {
29
+    this.url = url;
30
+    this.timeout = interval;
31
+    this.renderOptions = renderOptions;
32
+  }
33
+
34
+  render() {
35
+    var _this = this;
36
+
37
+    return _asyncToGenerator(function* () {
38
+      yield (0, _render2.default)(_this.renderOptions);
39
+      _this.timer = setTimeout(_this.run.bind(_this), _this.timeout);
40
+    })();
41
+  }
42
+
43
+  run() {
44
+    this.render();
45
+  }
46
+
47
+  stop() {
48
+    if (this.timer) {
49
+      clearTimeout(this.timer);
50
+    }
51
+  }
52
+}
53
+
54
+exports.RenderService = RenderService;
55
+function preSSRService({
56
+  renderFullPage, createApp, initialState, interval = 10000, onRenderSuccess, routes, timeout = 6000, verbose = true
57
+}) {
58
+  const paths = getPathsFromRoutes(routes);
59
+  paths.forEach(path => {
60
+    new RenderService({
61
+      url: path,
62
+      interval,
63
+      renderOptions: {
64
+        routes,
65
+        url: path,
66
+        renderFullPage,
67
+        createApp,
68
+        initialState,
69
+        onRenderSuccess,
70
+        timeout,
71
+        env: {
72
+          platform: 'pc'
73
+        },
74
+        verbose
75
+      }
76
+    }).run();
77
+    new RenderService({
78
+      url: path,
79
+      interval,
80
+      renderOptions: {
81
+        routes,
82
+        url: path,
83
+        renderFullPage,
84
+        createApp,
85
+        initialState,
86
+        onRenderSuccess,
87
+        timeout,
88
+        env: {
89
+          platform: 'mobile'
90
+        },
91
+        verbose
92
+      }
93
+    }).run();
94
+  });
95
+}

+ 201
- 0
lib/render.js View File

@@ -0,0 +1,201 @@
1
+'use strict';
2
+
3
+Object.defineProperty(exports, "__esModule", {
4
+  value: true
5
+});
6
+
7
+let renderFragment = (() => {
8
+  var _ref = _asyncToGenerator(function* (createApp, routes, url, initialState, timeout, verbose) {
9
+    const history = (0, _history.createMemoryHistory)();
10
+    history.push(url);
11
+    const context = {};
12
+    const app = createApp({
13
+      history,
14
+      initialState
15
+    });
16
+    if (!existSSRModel(app)) {
17
+      app.model(_ssrModel2.default);
18
+    }
19
+    app.router(function (options) {
20
+      return _react2.default.createElement(
21
+        _reactRouter.StaticRouter,
22
+        { location: url, context: options.context },
23
+        _react2.default.createElement(
24
+          'div',
25
+          null,
26
+          routes
27
+        )
28
+      );
29
+    });
30
+    const asyncActions = getAsyncActions(app);
31
+    const branch = (0, _utils.findRouteByUrl)(routes, url);
32
+    if (branch.length === 0) {
33
+      return {};
34
+    }
35
+    const sync = findSync(branch);
36
+    if (!sync && asyncActions && asyncActions.length > 0) {
37
+      const id = (0, _uid2.default)(10);
38
+      app.use((0, _dvaServerSync2.default)(id, function (action) {
39
+        if (asyncActions.indexOf(action.type) > -1) {
40
+          return true;
41
+        }
42
+        return false;
43
+      }, _block2.default));
44
+      const appDOM = app.start()({
45
+        context
46
+      });
47
+      if (verbose) {
48
+        console.time(`${url}: async wait time`);
49
+      }
50
+      const result = yield new Promise(function (resolve, reject) {
51
+        const timer = setTimeout(function () {
52
+          reject(new Error('render timeout'));
53
+        }, timeout);
54
+        _block2.default.wait(id, function () {
55
+          if (verbose) {
56
+            console.timeEnd(`${url}: async wait time`);
57
+          }
58
+          clearTimeout(timer);
59
+          const curState = appDOM.props.store.getState();
60
+          const html = (0, _server.renderToStaticMarkup)(appDOM);
61
+          resolve({ html, state: curState, context });
62
+        });
63
+      });
64
+      return result;
65
+    }
66
+    const appDOM = app.start()({
67
+      context
68
+    });
69
+    const html = (0, _server.renderToStaticMarkup)(appDOM);
70
+    const curState = appDOM.props.store.getState();
71
+    return { html, state: curState, context };
72
+  });
73
+
74
+  return function renderFragment(_x, _x2, _x3, _x4, _x5, _x6) {
75
+    return _ref.apply(this, arguments);
76
+  };
77
+})();
78
+
79
+var _react = require('react');
80
+
81
+var _react2 = _interopRequireDefault(_react);
82
+
83
+var _uid = require('uid');
84
+
85
+var _uid2 = _interopRequireDefault(_uid);
86
+
87
+var _reactRouter = require('react-router');
88
+
89
+var _history = require('history');
90
+
91
+var _lodash = require('lodash.merge');
92
+
93
+var _lodash2 = _interopRequireDefault(_lodash);
94
+
95
+var _server = require('react-dom/server');
96
+
97
+var _ssrModel = require('./ssrModel');
98
+
99
+var _ssrModel2 = _interopRequireDefault(_ssrModel);
100
+
101
+var _utils = require('./utils');
102
+
103
+var _dvaServerSync = require('./dvaServerSync');
104
+
105
+var _dvaServerSync2 = _interopRequireDefault(_dvaServerSync);
106
+
107
+var _block = require('./block');
108
+
109
+var _block2 = _interopRequireDefault(_block);
110
+
111
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
112
+
113
+function _asyncToGenerator(fn) { return function () { var gen = fn.apply(this, arguments); return new Promise(function (resolve, reject) { function step(key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { return Promise.resolve(value).then(function (value) { step("next", value); }, function (err) { step("throw", err); }); } } return step("next"); }); }; }
114
+
115
+function existSSRModel(app) {
116
+  try {
117
+    let model = null;
118
+    app._models.forEach(m => {
119
+      if (m.namespace === 'ssr') {
120
+        model = m;
121
+      }
122
+    });
123
+    return !!model;
124
+  } catch (e) {
125
+    return false;
126
+  }
127
+}
128
+
129
+function getAsyncActions(app) {
130
+  try {
131
+    let actions = [];
132
+    app._models.forEach(model => {
133
+      if (model.effects) {
134
+        actions = actions.concat(Object.keys(model.effects));
135
+      }
136
+    });
137
+    return actions;
138
+  } catch (e) {
139
+    return [];
140
+  }
141
+}
142
+
143
+function findSync(branch) {
144
+  let sync = false;
145
+  branch.forEach(b => {
146
+    sync = !!b.props.sync;
147
+  });
148
+  return sync;
149
+}
150
+
151
+exports.default = (() => {
152
+  var _ref2 = _asyncToGenerator(function* ({
153
+    url, env, routes, renderFullPage, createApp, initialState, onRenderSuccess, timeout = 6000, verbose = true
154
+  }) {
155
+    try {
156
+      if (verbose) {
157
+        console.log(`[${url}]`);
158
+        console.time(`${url}: render time`);
159
+      }
160
+      const state = (0, _lodash2.default)({}, initialState || {}, {
161
+        ssr: {
162
+          env
163
+        }
164
+      });
165
+      const fragment = yield renderFragment(createApp, routes, url, state, timeout, verbose);
166
+      if (verbose) {
167
+        console.timeEnd(`${url}: render time`);
168
+      }
169
+      const context = fragment.context;
170
+      if (!context) {
171
+        return { code: 404, url, env };
172
+      } else if (context.url) {
173
+        return {
174
+          code: 302, url, env, redirect: context.url
175
+        };
176
+      }
177
+      const html = yield renderFullPage(fragment);
178
+      if (onRenderSuccess) {
179
+        yield onRenderSuccess({
180
+          html, url, env, state: fragment.state
181
+        });
182
+      }
183
+      return {
184
+        code: 200, url, env, html
185
+      };
186
+    } catch (e) {
187
+      console.error(e);
188
+      return {
189
+        code: 500, url, env, error: e
190
+      };
191
+    }
192
+  });
193
+
194
+  function render(_x7) {
195
+    return _ref2.apply(this, arguments);
196
+  }
197
+
198
+  return render;
199
+})();
200
+
201
+module.exports = exports['default'];

+ 59
- 0
lib/runtimeSSRMiddle.js View File

@@ -0,0 +1,59 @@
1
+'use strict';
2
+
3
+Object.defineProperty(exports, "__esModule", {
4
+  value: true
5
+});
6
+exports.default = runtimeSSRMiddle;
7
+
8
+var _mobileDetect = require('mobile-detect');
9
+
10
+var _mobileDetect2 = _interopRequireDefault(_mobileDetect);
11
+
12
+var _render = require('./render');
13
+
14
+var _render2 = _interopRequireDefault(_render);
15
+
16
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
17
+
18
+function _asyncToGenerator(fn) { return function () { var gen = fn.apply(this, arguments); return new Promise(function (resolve, reject) { function step(key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { return Promise.resolve(value).then(function (value) { step("next", value); }, function (err) { step("throw", err); }); } } return step("next"); }); }; }
19
+
20
+function runtimeSSRMiddle({
21
+  routes, renderFullPage, createApp, initialState, onRenderSuccess, timeout = 6000, verbose = true
22
+}) {
23
+  return (() => {
24
+    var _ref = _asyncToGenerator(function* (req, res, next) {
25
+      const isMobile = !!new _mobileDetect2.default(req.headers['user-agent']).mobile();
26
+      const result = yield (0, _render2.default)({
27
+        url: req.url,
28
+        env: { platform: isMobile ? 'mobile' : 'pc' },
29
+        routes,
30
+        renderFullPage,
31
+        createApp,
32
+        initialState,
33
+        onRenderSuccess,
34
+        timeout,
35
+        verbose
36
+      });
37
+      switch (result.code) {
38
+        case 200:
39
+          return res.end(result.html);
40
+        case 302:
41
+          return res.redirect(302, result.redirect);
42
+        case 404:
43
+          next();
44
+          break;
45
+        case 500:
46
+          next(result.error);
47
+          break;
48
+        default:
49
+          next();
50
+          break;
51
+      }
52
+    });
53
+
54
+    return function (_x, _x2, _x3) {
55
+      return _ref.apply(this, arguments);
56
+    };
57
+  })();
58
+}
59
+module.exports = exports['default'];

+ 12
- 0
lib/ssrModel.js View File

@@ -0,0 +1,12 @@
1
+'use strict';
2
+
3
+Object.defineProperty(exports, "__esModule", {
4
+  value: true
5
+});
6
+exports.default = {
7
+  namespace: 'ssr',
8
+  state: {
9
+    env: null
10
+  }
11
+};
12
+module.exports = exports['default'];

+ 56
- 0
lib/utils.js View File

@@ -0,0 +1,56 @@
1
+'use strict';
2
+
3
+Object.defineProperty(exports, "__esModule", {
4
+  value: true
5
+});
6
+exports.searchRoutes = searchRoutes;
7
+exports.findRouteByUrl = findRouteByUrl;
8
+
9
+var _reactRouter = require('react-router');
10
+
11
+var _react = require('react');
12
+
13
+var _react2 = _interopRequireDefault(_react);
14
+
15
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
16
+
17
+function isArray(element) {
18
+  return Object.prototype.toString.call(element) === '[object Array]';
19
+}
20
+
21
+function searchRoutes(r, callback) {
22
+  function searchPaths(routes) {
23
+    if (isArray(routes)) {
24
+      routes.forEach(route => {
25
+        searchPaths(route);
26
+      });
27
+    }
28
+    if (typeof routes === 'object' && routes.props && routes.props.children) {
29
+      _react2.default.Children.forEach(routes.props.children, route => {
30
+        searchPaths(route);
31
+      });
32
+    }
33
+    if (typeof routes === 'object' && routes.props && routes.props.path) {
34
+      callback(routes);
35
+    }
36
+  }
37
+  searchPaths(r);
38
+}
39
+
40
+function findRouteByUrl(routes, url) {
41
+  const rtn = [];
42
+  const queryIndex = url.indexOf('?');
43
+  if (queryIndex > -1) {
44
+    url = url.slice(0, queryIndex);
45
+  }
46
+  if (url.length > 1 && url[url.length - 1] === '/') {
47
+    url = url.slice(0, -1);
48
+  }
49
+  searchRoutes(routes, route => {
50
+    const match = (0, _reactRouter.matchPath)(url, route.props);
51
+    if (match) {
52
+      rtn.push(route);
53
+    }
54
+  });
55
+  return rtn;
56
+}