UNPKG

9.9 kBJavaScriptView Raw
1// TODO: All-in-one decorator for react component
2// https://github.com/cmux/koot/issues/8
3
4import React from 'react'
5import { connect } from 'react-redux'
6// import { hot } from 'react-hot-loader'
7// import PropTypes from 'prop-types'
8import hoistStatics from 'hoist-non-react-statics'
9// import { ImportStyle } from 'sp-css-import'
10
11//
12
13import { store } from '../index.js'
14
15//
16
17import {
18 fromServerProps as getRenderPropsFromServerProps,
19 fromComponentProps as getRenderPropsFromComponentProps
20} from './get-render-props'
21import {
22 append as appendStyle,
23 remove as removeStyle,
24 StyleMapContext,
25} from './styles'
26import clientUpdatePageInfo from './client-update-page-info'
27
28//
29
30// 是否已挂载了组件
31let everMounted = false
32const defaultPageInfo = {
33 title: '',
34 metas: []
35}
36
37/**
38 * 获取数据
39 * @callback callbackFetchData
40 * @param {Object} state 当前 state
41 * @param {Object} renderProps 封装的同构 props
42 * @param {Function} dispatch Redux dispatch
43 * @returns {Promise}
44 */
45
46/**
47 * 判断数据是否准备好
48 * @callback callbackCheckLoaded
49 * @param {Object} state 当前 state
50 * @param {Object} renderProps 封装的同构 props
51 * @returns {Boolean}
52 */
53
54/**
55 * 获取页面信息
56 * @callback callbackGetPageInfo
57 * @param {Object} state 当前 state
58 * @param {Object} renderProps 封装的同构 props
59 * @returns {Object}
60 */
61
62/**
63 * 高阶组件/组件装饰器:组件扩展
64 * @param {Object} options 选项
65 * @param {Boolean|Function} [options.connect] react-redux 的 connect() 的参数。如果为 true,表示使用 connect(),但不连接任何数据
66 * @param {callbackGetPageInfo} [options.pageinfo]
67 * @param {Object} [options.data] 同构数据相关
68 * @param {callbackFetchData} [options.data.fetch]
69 * @param {callbackCheckLoaded} [options.data.check]
70 * @param {Object} [options.styles] 组件 CSS 结果
71 * @returns {Function} 封装好的 React 组件
72 */
73export default (options = {}) => (WrappedComponent) => {
74
75 const {
76 connect: _connect = false,
77 pageinfo,
78 data: {
79 fetch: _dataFetch,
80 check: dataCheck,
81 } = {},
82 styles: _styles,
83 // hot: _hot = true,
84 } = options
85
86 // 样式相关
87
88 /** @type {Object} 经过 koot-css-loader 处理后的 css 文件的结果对象 */
89 const styles = (!Array.isArray(_styles) ? [_styles] : styles).filter(obj => (
90 typeof obj === 'object' && typeof obj.wrapper === 'string'
91 ))
92
93 /** @type {Boolean} 是否有上述结果对象 */
94 const hasStyles = (
95 Array.isArray(styles) &&
96 styles.length > 0
97 )
98
99 // 同构数据相关
100
101 /** @type {Boolean} 同构数据是否已经获取成功 */
102 // let isDataPreloaded = false
103
104 /** @type {Function} 获取同构数据 */
105 const dataFetch = typeof options.data === 'function' || Array.isArray(options.data)
106 ? options.data
107 : (typeof _dataFetch === 'function' || Array.isArray(_dataFetch) ? _dataFetch : undefined)
108
109 /** @type {Function} 获取同构数据的执行方法 */
110 const doFetchData = (store, renderProps) => {
111 const result = dataFetch(store.getState(), renderProps, store.dispatch)
112 // if (result === true) {
113 // isDataPreloaded = true
114 // return new Promise(resolve => resolve())
115 // }
116 if (Array.isArray(result))
117 return Promise.all(result)
118 if (result instanceof Promise)
119 return result
120 return new Promise(resolve => resolve(result))
121 }
122
123 // 页面信息相关
124
125 /**
126 * 更新页面信息
127 * @param {Object} store
128 * @param {Object} props renderProps
129 * @returns {Object} infos
130 * @returns {String} infos.title
131 * @returns {Array} infos.metas
132 */
133 const doPageinfo = (store, props) => {
134 const defaultPageInfo = {
135 title: '',
136 metas: []
137 }
138
139 if (typeof pageinfo !== 'function')
140 return defaultPageInfo
141
142 const state = store.getState()
143
144 let infos = pageinfo(state, props)
145 if (typeof infos !== 'object')
146 infos = defaultPageInfo
147
148 const {
149 title = defaultPageInfo.title,
150 metas = defaultPageInfo.metas
151 } = infos
152
153 if (state.localeId) {
154 if (!metas.some(meta => {
155 if (meta.name === 'koot-locale-id') {
156 meta.content = state.localeId
157 return true
158 }
159 return false
160 })) {
161 metas.push({
162 name: 'koot-locale-id',
163 content: state.localeId
164 })
165 }
166 }
167
168 return {
169 title,
170 metas
171 }
172 }
173
174 // 装饰组件
175
176 class KootReactComponent extends React.Component {
177 static onServerRenderHtmlExtend = ({ htmlTool, store, renderProps = {} }) => {
178 const {
179 title,
180 metas
181 } = doPageinfo(store, getRenderPropsFromServerProps(renderProps))
182 htmlTool.title = title
183 htmlTool.metas = metas
184 }
185
186 static onServerRenderStoreExtend({ store, renderProps }) {
187 if (typeof dataFetch === 'undefined')
188 return new Promise(resolve => resolve())
189 // console.log('onServerRenderStoreExtend')
190 return doFetchData(store, getRenderPropsFromServerProps(renderProps))
191 }
192
193 //
194
195 static contextType = StyleMapContext
196
197 //
198
199 clientUpdatePageInfo() {
200 if (typeof pageinfo !== 'function')
201 return
202
203 const {
204 title,
205 metas
206 } = doPageinfo(store, getRenderPropsFromComponentProps(this.props))
207 clientUpdatePageInfo(title, metas)
208 }
209
210 //
211
212 state = {
213 loaded: typeof dataCheck === 'function'
214 ? dataCheck(store.getState(), getRenderPropsFromComponentProps(this.props))
215 : undefined
216 ,
217 }
218 mounted = false
219 kootClassNames = []
220
221 //
222
223 constructor(props, context) {
224 super(props, context)
225
226 if (hasStyles) {
227 this.kootClassNames = styles.map(obj => obj.wrapper)
228 appendStyle(context, styles)
229 // console.log('----------')
230 // console.log('styles', styles)
231 // console.log('theStyles', theStyles)
232 // console.log('this.classNameWrapper', this.classNameWrapper)
233 // console.log('----------')
234 }
235 }
236
237 componentDidUpdate(prevProps) {
238 if (typeof prevProps.location === 'object' &&
239 typeof this.props.location === 'object' &&
240 prevProps.location.pathname !== this.props.location.pathname
241 )
242 this.clientUpdatePageInfo()
243 }
244
245 componentDidMount() {
246 this.mounted = true
247
248 if (!this.state.loaded && typeof dataFetch !== 'undefined') {
249 doFetchData(store, getRenderPropsFromComponentProps(this.props))
250 .then(() => {
251 if (!this.mounted) return
252 this.setState({
253 loaded: true,
254 })
255 })
256 }
257
258 if (everMounted) {
259 this.clientUpdatePageInfo()
260 } else {
261 everMounted = true
262 }
263 }
264
265 componentWillUnmount() {
266 this.mounted = false
267 if (hasStyles) {
268 removeStyle(this.context, styles)
269 }
270 }
271
272 //
273
274 render = () => {
275 // console.log('styles', styles)
276 // console.log('this', this)
277 // console.log('this.kootClassNames', this.kootClassNames)
278 // console.log('this.props.className', this.props.className)
279
280 if (__CLIENT__ && this.kootClassNames instanceof HTMLElement) {
281 // console.log(this.kootClassNames)
282 this.kootClassNames = [this.kootClassNames.getAttribute('id')]
283 }
284
285 const props = Object.assign({}, this.props, {
286 className: this.kootClassNames.concat(this.props.className).join(' ').trim(),
287 "data-class-name": this.kootClassNames.join(' ').trim(),
288 })
289
290 // if (__SERVER__) console.log('extender this.state.loaded', this.state.loaded)
291 if (typeof dataFetch !== 'undefined' && typeof dataCheck === 'function')
292 props.loaded = this.state.loaded
293
294 return <WrappedComponent {...props} />
295 }
296 }
297
298 // if (_hot && __DEV__ && __CLIENT__) {
299 // const { hot, setConfig } = require('react-hot-loader')
300 // setConfig({ logLevel: 'debug' })
301 // KootComponent = hot(module)(KootComponent)
302 // }
303
304 let KootComponent = hoistStatics(KootReactComponent, WrappedComponent)
305
306 // if (typeof styles === 'object' &&
307 // typeof styles.wrapper === 'string'
308 // ) {
309 // KootComponent = ImportStyle(styles)(KootComponent)
310 // }
311
312 if (_connect === true) {
313 KootComponent = connect(() => ({}))(KootComponent)
314 } else if (typeof _connect === 'function') {
315 KootComponent = connect(_connect)(KootComponent)
316 } else if (Array.isArray(_connect)) {
317 KootComponent = connect(..._connect)(KootComponent)
318 }
319
320 return KootComponent
321}
322
\No newline at end of file