UNPKG

12.4 kBJavaScriptView Raw
1import React from 'react'
2import HTMLTool from './HTMLTool'
3import { renderToString } from 'react-dom/server'
4import { createMemoryHistory, RouterContext, match } from 'react-router'
5import { Provider } from 'react-redux'
6import { syncHistoryWithStore } from 'react-router-redux'
7
8import htmlInject from './inject'
9import { localeId } from 'super-project/i18n'
10
11const path = require('path')
12
13const defaultEntrypoints = require('../defaults/entrypoints')
14const getChunkmap = require('../utils/get-chunkmap')
15const getClientFilePath = require('../utils/get-client-file-path')
16const readClientFile = require('../utils/read-client-file')
17
18const error = require('debug')('SYSTEM:isomorphic:error')
19
20const injectOnceCache = {}
21
22export default class ReactIsomorphic {
23
24 createKoaMiddleware(options = {
25 routes: [],
26 configStore: () => { },
27 onServerRender: () => { },
28 inject: { /*key: value*/ } // 在html中会这样替换 <script>inject_[key]</script> => value
29 }) {
30
31 /*
32 同构中间件流程:
33
34 根据router计算出渲染页面需要的数据,并把渲染需要的数据补充到store中
35 补充服务端提供的信息数据到store中
36 把同构时候服务端预处理数据补充到store中
37
38 把react部分渲染出html片段,并插入到html中
39 html 处理:
40 向html中注入引用文件链接
41 把同构时候服务端预处理数据补充到html中
42 调整样式位置,从下到上
43 */
44
45 const { template, onServerRender, inject, configStore, routes } = options
46 const ENV = process.env.WEBPACK_BUILD_ENV
47
48 const chunkmap = getChunkmap(true)
49 let entrypoints = {}
50 let filemap = {}
51
52 const i18nType = JSON.parse(process.env.SUPER_I18N)
53 ? JSON.parse(process.env.SUPER_I18N_TYPE)
54 : undefined
55 const isI18nDefault = (i18nType === 'default')
56
57 // 针对 i18n 分包形式的项目,js/css 等资源需每次访问时实时注入
58 const assetsInjectOnce = !isI18nDefault
59
60 if (isI18nDefault) {
61 for (let l in chunkmap) {
62 const thisLocaleId = l.substr(0, 1) === '.' ? l.substr(1) : l
63 entrypoints[thisLocaleId] = chunkmap[l]['.entrypoints']
64 filemap[thisLocaleId] = chunkmap[l]['.files']
65 injectOnceCache[thisLocaleId] = {}
66 }
67 } else {
68 entrypoints = chunkmap['.entrypoints']
69 filemap = chunkmap['.files']
70 }
71
72 // 配置 html 注入内容
73 // html [只更新1次]的部分
74 const injectOnce = {
75 // js: inject.js ? inject.js.map((js) => `<script src="${js}" defer></script>`).join('') : '', // 引用js文件标签
76 // css: inject.css ? inject.css.map((css) => `<link rel="stylesheet" href="${css}">`).join('') : '', // 引用css文件标签
77 }
78
79 // koa 中间件结构
80 // 每次请求时均会执行
81 return async (ctx, next) => {
82
83 const url = ctx.path + ctx.search
84 try {
85
86 const memoryHistory = createMemoryHistory(url)
87 const store = configStore()
88 const history = syncHistoryWithStore(memoryHistory, store)
89
90 // 根据router计算出渲染页面需要的数据,并把渲染需要的数据补充到store中
91
92 const { redirectLocation, renderProps } = await asyncReactRouterMatch({ history, routes, location: url })
93
94 // 判断是否重定向页面
95
96 if (redirectLocation) return ctx.redirect(redirectLocation.pathname + redirectLocation.search)
97 if (!renderProps) return await next()
98
99 // 补充服务端提供的信息数据到store中
100
101 onServerRender && onServerRender({ koaCtx: ctx, reduxStore: store })
102
103 // 把同构时候服务端预处理数据补充到store中
104
105 await ServerRenderDataToStore(store, renderProps)
106
107 // 把同构时候服务端预处理数据补充到html中(根据页面逻辑动态修改html内容)
108
109 const htmlTool = await ServerRenderHtmlExtend(store, renderProps)
110
111 // 把react部分渲染出html片段,并插入到html中
112
113 const reactHtml = renderToString(
114 <Provider store={store} >
115 <RouterContext {...renderProps} />
116 </Provider>
117 )
118 const filterResult = filterStyle(reactHtml)
119
120 const thisInjectOnceCache = assetsInjectOnce ? injectOnceCache : injectOnceCache[localeId]
121 const thisFilemap = assetsInjectOnce ? filemap : filemap[localeId]
122 const thisEntrypoints = assetsInjectOnce ? entrypoints : entrypoints[localeId]
123
124 // console.log(chunkmap)
125 // console.log(filemap)
126 // console.log(entrypoints)
127 // console.log(localeId)
128 // console.log(thisInjectOnceCache)
129 // console.log(thisFilemap)
130 // console.log(thisEntrypoints)
131
132 // 配置 html 注入内容
133 // html [实时更新]的部分
134 const injectRealtime = {
135 htmlLang: localeId ? ` lang="${localeId}"` : '',
136 title: htmlTool.getTitle(),
137 metas: `<!--SUPER_METAS_START-->${htmlTool.getMetaHtml()}<!--SUPER_METAS_END-->`,
138 styles: (() => {
139 if (!assetsInjectOnce || typeof thisInjectOnceCache.styles === 'undefined') {
140 let r = ''
141 if (typeof thisFilemap['critical.css'] === 'string') {
142 if (ENV === 'prod')
143 r += `<style id="__super-critical-styles" type="text/css">${readClientFile('critical.css')}</style>`
144 if (ENV === 'dev')
145 r += `<link id="__super-critical-styles" media="all" rel="stylesheet" href="${getClientFilePath('critical.css')}" />`
146 }
147 thisInjectOnceCache.styles = r
148 }
149 return thisInjectOnceCache.styles + filterResult.style
150 })(),
151 react: filterResult.html,
152 scripts: (() => {
153 if (!assetsInjectOnce || typeof thisInjectOnceCache.scriptsInBody === 'undefined') {
154 let r = ''
155
156 // 优先引入 critical
157 if (Array.isArray(thisEntrypoints.critical)) {
158 thisEntrypoints.critical
159 .filter(file => path.extname(file) === '.js')
160 .forEach(file => {
161 if (ENV === 'prod')
162 r += `<script type="text/javascript">${readClientFile(true, file)}</script>`
163 if (ENV === 'dev')
164 r += `<script type="text/javascript" src="${getClientFilePath(true, file)}"></script>`
165 })
166 }
167
168 // 引入其他入口
169 // Object.keys(thisEntrypoints).filter(key => (
170 // key !== 'critical' && key !== 'polyfill'
171 // ))
172 defaultEntrypoints.forEach(key => {
173 if (Array.isArray(thisEntrypoints[key])) {
174 thisEntrypoints[key].forEach(file => {
175 if (ENV === 'prod')
176 r += `<script type="text/javascript" src="${getClientFilePath(true, file)}" defer></script>`
177 if (ENV === 'dev')
178 r += `<script type="text/javascript" src="${getClientFilePath(true, file)}" defer></script>`
179 })
180 }
181 })
182
183 thisInjectOnceCache.scriptsInBody = r
184 }
185 return `<script type="text/javascript">${htmlTool.getReduxScript(store)}</script>`
186 + thisInjectOnceCache.scriptsInBody
187 })(),
188 }
189
190 const injectResult = Object.assign({}, injectRealtime, injectOnce, inject)
191
192 // 响应给客户端
193
194 const html = htmlInject(template, injectResult)
195 ctx.body = html
196
197
198 } catch (e) {
199 // console.error('Server-Render Error Occures: %s', e.stack)
200 error('Server-Render Error Occures: %O', e.stack)
201 ctx.status = 500
202 ctx.body = e.message
203 }
204 }
205 }
206
207}
208
209// location 解构:
210// { history, routes, location }
211function asyncReactRouterMatch(location) {
212 return new Promise((resolve, reject) => {
213 match(location, (error, redirectLocation, renderProps) => {
214 if (error) {
215 return reject(error)
216 }
217
218 resolve({ redirectLocation, renderProps })
219 })
220 })
221}
222
223/**
224 * 服务端渲染时扩展redux的store方法
225 * 注:组件必须是redux包装过的组件
226 *
227 * @param {any} store
228 * @param {any} renderProps
229 * @returns
230 */
231function ServerRenderDataToStore(store, renderProps) {
232
233 const SERVER_RENDER_EVENT_NAME = 'onServerRenderStoreExtend'
234
235 let serverRenderTasks = []
236 for (let component of renderProps.components) {
237
238 // component.WrappedComponent 是redux装饰的外壳
239 if (component && component.WrappedComponent && component.WrappedComponent[SERVER_RENDER_EVENT_NAME]) {
240
241 // 预处理异步数据的
242 const tasks = component.WrappedComponent[SERVER_RENDER_EVENT_NAME]({ store })
243 if (Array.isArray(tasks)) {
244 serverRenderTasks = serverRenderTasks.concat(tasks)
245 } else if (tasks.then) {
246 serverRenderTasks.push(tasks)
247 }
248 }
249 }
250
251 return Promise.all(serverRenderTasks)
252}
253
254/**
255 * 服务端渲染时候扩展html的方法
256 * 注:组件必须是redux包装过的组件
257 *
258 * @param {any} store
259 * @param {any} renderProps
260 * @returns
261 */
262function ServerRenderHtmlExtend(store, renderProps) {
263
264 const SERVER_RENDER_EVENT_NAME = 'onServerRenderHtmlExtend'
265
266 const htmlTool = new HTMLTool()
267
268 // component.WrappedComponent 是redux装饰的外壳
269 for (let component of renderProps.components) {
270 if (component && component.WrappedComponent && component.WrappedComponent[SERVER_RENDER_EVENT_NAME]) {
271 component.WrappedComponent[SERVER_RENDER_EVENT_NAME]({ htmlTool, store })
272 }
273 }
274
275 return htmlTool
276}
277
278// TODO: move to ImportStyle npm
279// 样式处理
280// serverRender 的时候,react逻辑渲染的css代码会在html比较靠后的地方渲染出来,
281// 为了更快的展现出正常的网页样式,在服务端处理的时候用正则表达式把匹配到的css
282// 移动到html的header里,让页面展现更快。
283function filterStyle(htmlString) {
284
285 // 获取样式代码
286 let styleCollectionString = htmlString
287 .replace(/\r\n/gi, '')
288 .replace(/\n/gi, '')
289 .match(/<div id="styleCollection(.*?)>(.*?)<\/div>/gi)[0]
290
291 // 提取 css
292 let style = styleCollectionString.substr(styleCollectionString.indexOf('>') + 1, styleCollectionString.length)
293 style = style.substr(0, style.length - 6)
294
295 // 去掉 <div id="styleCollection">...</div>
296 let html = htmlString.replace(/\n/gi, '').replace(styleCollectionString, '')
297
298 return {
299 html,
300 style
301 }
302}