UNPKG

8.09 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, localeId } 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} from './styles'
25import clientUpdatePageInfo from './client-update-page-info'
26
27//
28
29// 是否已挂载了组件
30let everMounted = false
31const defaultPageInfo = {
32 title: '',
33 metas: []
34}
35
36/**
37 * 获取数据
38 * @callback callbackFetchData
39 * @param {Object} state 当前 state
40 * @param {Object} renderProps 封装的同构 props
41 * @param {Function} dispatch Redux dispatch
42 * @returns {Promise}
43 */
44
45/**
46 * 判断数据是否准备好
47 * @callback callbackCheckLoaded
48 * @param {Object} state 当前 state
49 * @param {Object} renderProps 封装的同构 props
50 * @returns {Boolean}
51 */
52
53/**
54 * 获取页面信息
55 * @callback callbackGetPageInfo
56 * @param {Object} state 当前 state
57 * @param {Object} renderProps 封装的同构 props
58 * @returns {Object}
59 */
60
61/**
62 * 高阶组件/组件装饰器:组件扩展
63 * @param {Object} options 选项
64 * @param {Boolean|Function} [options.connect] react-redux 的 connect() 的参数。如果为 true,表示使用 connect(),但不连接任何数据
65 * @param {callbackGetPageInfo} [options.pageinfo]
66 * @param {Object} [options.data] 同构数据相关
67 * @param {callbackFetchData} [options.data.fetch]
68 * @param {callbackCheckLoaded} [options.data.check]
69 * @param {Object} [options.styles] 组件 CSS 结果
70 * @returns {Function} 封装好的 React 组件
71 */
72export default (options = {}) => (WrappedComponent) => {
73
74 const {
75 connect: _connect = false,
76 pageinfo,
77 data: {
78 fetch: _dataFetch,
79 check: dataCheck,
80 } = {},
81 styles: _styles,
82 // hot: _hot = true,
83 } = options
84
85 const styles = (!Array.isArray(_styles) ? [_styles] : styles).filter(obj => (
86 typeof obj === 'object' && typeof obj.wrapper === 'string'
87 ))
88 const hasStyles = (
89 Array.isArray(styles) &&
90 styles.length > 0
91 )
92 const dataFetch = typeof options.data === 'function' || Array.isArray(options.data)
93 ? options.data
94 : (typeof _dataFetch === 'function' || Array.isArray(_dataFetch) ? _dataFetch : undefined)
95
96 const doPageinfo = (store, props) => {
97 if (typeof pageinfo !== 'function')
98 return { ...defaultPageInfo }
99
100 let infos = pageinfo(store.getState(), props)
101 if (typeof infos !== 'object')
102 infos = { ...defaultPageInfo }
103
104 const {
105 title = defaultPageInfo.title,
106 metas = defaultPageInfo.metas
107 } = infos
108
109 if (localeId)
110 metas.push({
111 name: 'koot-locale-id',
112 content: localeId
113 })
114
115 return {
116 title,
117 metas
118 }
119 }
120
121 const doFetchData = (renderProps) => {
122 const r = dataFetch(store.getState(), renderProps, store.dispatch)
123 if (Array.isArray(r))
124 return Promise.all(r)
125 return r
126 }
127
128 class KootReactComponent extends React.Component {
129 static onServerRenderHtmlExtend = ({ htmlTool, store, renderProps = {} }) => {
130 const {
131 title,
132 metas
133 } = doPageinfo(store, getRenderPropsFromServerProps(renderProps))
134 htmlTool.title = title
135 htmlTool.metas = metas
136 }
137
138 static onServerRenderStoreExtend({ /*store,*/ renderProps }) {
139 if (typeof dataFetch === 'undefined')
140 return new Promise(resolve => resolve())
141 // console.log('onServerRenderStoreExtend')
142 return doFetchData(getRenderPropsFromServerProps(renderProps))
143 }
144
145 //
146
147 clientUpdatePageInfo() {
148 if (typeof pageinfo !== 'function')
149 return
150
151 const {
152 title,
153 metas
154 } = doPageinfo(store, getRenderPropsFromComponentProps(this.props))
155 clientUpdatePageInfo(title, metas)
156 }
157
158 //
159
160 state = {
161 loaded: typeof dataCheck === 'function'
162 ? dataCheck(store.getState(), getRenderPropsFromComponentProps(this.props))
163 : undefined,
164 }
165 mounted = false
166 kootClassNames = []
167
168 //
169
170 constructor(props) {
171 super(props)
172
173 if (hasStyles) {
174 this.kootClassNames = styles.map(obj => obj.wrapper)
175 appendStyle(styles)
176 // console.log('----------')
177 // console.log('styles', styles)
178 // console.log('theStyles', theStyles)
179 // console.log('this.classNameWrapper', this.classNameWrapper)
180 // console.log('----------')
181 }
182 }
183
184 componentDidUpdate(prevProps) {
185 if (typeof prevProps.location === 'object' &&
186 typeof this.props.location === 'object' &&
187 prevProps.location.pathname !== this.props.location.pathname
188 )
189 this.clientUpdatePageInfo()
190 }
191
192 componentDidMount() {
193 this.mounted = true
194
195 if (!this.state.loaded && typeof dataFetch !== 'undefined') {
196 doFetchData(getRenderPropsFromComponentProps(this.props))
197 .then(() => {
198 if (!this.mounted) return
199 this.setState({
200 loaded: true,
201 })
202 })
203 }
204
205 if (everMounted) {
206 this.clientUpdatePageInfo()
207 } else {
208 everMounted = true
209 }
210 }
211
212 componentWillUnmount() {
213 this.mounted = false
214 if (hasStyles) {
215 removeStyle(styles)
216 }
217 }
218
219 //
220
221 render = () => {
222 // console.log('styles', styles)
223 // console.log('this', this)
224 // console.log('this.kootClassNames', this.kootClassNames)
225 // console.log('this.props.className', this.props.className)
226 if (__CLIENT__ && this.kootClassNames instanceof HTMLElement) {
227 // console.log(this.kootClassNames)
228 this.kootClassNames = [this.kootClassNames.getAttribute('id')]
229 }
230 const props = Object.assign({}, this.props, {
231 loaded: this.state.loaded,
232 className: this.kootClassNames.concat(this.props.className).join(' ').trim(),
233 "data-class-name": this.kootClassNames.join(' ').trim(),
234 })
235 return <WrappedComponent {...props} />
236 }
237 }
238
239 // if (_hot && __DEV__ && __CLIENT__) {
240 // const { hot, setConfig } = require('react-hot-loader')
241 // setConfig({ logLevel: 'debug' })
242 // KootComponent = hot(module)(KootComponent)
243 // }
244
245 let KootComponent = hoistStatics(KootReactComponent, WrappedComponent)
246
247 // if (typeof styles === 'object' &&
248 // typeof styles.wrapper === 'string'
249 // ) {
250 // KootComponent = ImportStyle(styles)(KootComponent)
251 // }
252
253 if (_connect === true) {
254 KootComponent = connect(() => ({}))(KootComponent)
255 } else if (typeof _connect === 'function') {
256 KootComponent = connect(_connect)(KootComponent)
257 }
258
259 return KootComponent
260}
261
\No newline at end of file