質問編集履歴
1
ミス修正
title
CHANGED
@@ -1,1 +1,1 @@
|
|
1
|
-
SPAで
|
1
|
+
SPAでAPIの読み込みやデータのありなし処理をもっとスマートに書きたい
|
body
CHANGED
@@ -1,12 +1,20 @@
|
|
1
1
|
**タイトルで、SPAと書きましたが、
|
2
|
-
今作ってるものはポータルサイトで開発の都合で
|
2
|
+
今作ってるものはポータルサイトで開発の都合で、
|
3
3
|
動的ページはフレームワークのHyperappを使い、静的ページはhtmlとcssで通常のコーディングをしてます。
|
4
|
-
|
4
|
+
工数削減という理由もあり、ページ間の移動はSPAではなくリンクで遷移してます。**
|
5
5
|
|
6
6
|
# お聞きしたい課題
|
7
|
+
待機時間改善のためにFacebookのようなデータがないときのスケルトンスクリーンの実装をしてます。
|
7
|
-
|
8
|
+
その結果、色々改善しないといけない点が出てしまい、苦戦してます。
|
8
|
-
もう一つは、リロード時のページ位置の制御の仕方。
|
9
9
|
|
10
|
+
1. **api読み込みの書き方もっと良い方法はないか?**
|
11
|
+
2. **複数のAPIのデータを使い何かactionを実行したい場合、データありの場合でif文書くと無限ループに陥る**
|
12
|
+
3. **スケルトンスクリーンのもっとスマートな書き方がないか**
|
13
|
+
4. **リロード時に少しでもスクロール中だと高確率でページの最上部に位置が変わってしまう**
|
14
|
+
|
15
|
+

|
16
|
+
|
17
|
+
|
10
18
|
## データ説明
|
11
19
|
- Hyperapp
|
12
20
|
- axios
|
@@ -14,6 +22,7 @@
|
|
14
22
|
#### API
|
15
23
|
- GET / master(最初に読み込む設定データ)
|
16
24
|
- GET / top(トップページのデータ)
|
25
|
+
- GET / news(ニュースのデータ)
|
17
26
|
|
18
27
|
|
19
28
|
#### 構成
|
@@ -21,12 +30,13 @@
|
|
21
30
|
sample/
|
22
31
|
├─ src/
|
23
32
|
│ ├─ stores/
|
33
|
+
│ │ └─ modules/
|
24
34
|
│ │ ├─ Store.js
|
25
35
|
│ ├─ views/
|
26
|
-
│ │ └─
|
36
|
+
│ │ └─ component/
|
27
|
-
│ │ └─
|
37
|
+
│ │ └─ layout/
|
38
|
+
│ │ └─ project/
|
28
39
|
│ │ └─ Page/
|
29
|
-
│ │ └─ Page/
|
30
40
|
│ │ └─ Top.js
|
31
41
|
│ │ └─ App.js
|
32
42
|
│ ├─ index.js
|
@@ -39,21 +49,312 @@
|
|
39
49
|
├─ webpack.config.js
|
40
50
|
```
|
41
51
|
|
42
|
-
src/index.js
|
52
|
+
#### src/index.js
|
43
53
|
```jacascript
|
44
54
|
import { app } from 'hyperapp'
|
45
55
|
import { State, Actions } from 'src/stores/Store.js'
|
46
56
|
import App from 'src/views/App.js'
|
57
|
+
import route from 'src/route.js';
|
47
58
|
|
59
|
+
let routeInfo = route(); // urlから {type,params..} を取得
|
48
|
-
app(State, Actions, App, document.body).init();
|
60
|
+
app(State, Actions, App, document.body).init(routeInfo);
|
49
61
|
```
|
50
62
|
|
63
|
+
#### src/Store/store.js
|
51
|
-
|
64
|
+
```javascript
|
65
|
+
import axios from 'axios';
|
52
|
-
|
66
|
+
import StoreTop from 'src/stores/modules/top/StoreTop.js';
|
67
|
+
import StoreNews from 'src/stores/modules/news/StoreNews.js';
|
53
68
|
|
69
|
+
export default {
|
70
|
+
state: {
|
71
|
+
route: null,
|
72
|
+
master: null,
|
73
|
+
top: StoreTop.state,
|
74
|
+
news: StoreNews.state,
|
75
|
+
},
|
76
|
+
actions: {
|
77
|
+
top: StoreTop.actions,
|
78
|
+
news: StoreNews.actions,
|
79
|
+
// 初期設定
|
80
|
+
init: routeInfo => (state, actions) => {
|
81
|
+
actions.getMaterData(); // 共通データを先に取得
|
82
|
+
if (routeInfo) {
|
83
|
+
// url情報をrouteに反映
|
84
|
+
return {
|
85
|
+
route: routeInfo
|
86
|
+
};
|
87
|
+
}
|
88
|
+
},
|
89
|
+
//--------------------------------------------------
|
90
|
+
// 【API】共通
|
91
|
+
//--------------------------------------------------
|
92
|
+
// ローカルストレージに保存する処理も必要だがまだ実装しきれてない
|
93
|
+
getData: ({ key, apiPath, params, fn }) => (state, actions) => {
|
94
|
+
let url = null;
|
95
|
+
// ローカルのJSONデータ読み込みの名残でゲットパラメータをテキストでつなげてます
|
96
|
+
let paramsText = null;
|
97
|
+
if (params) {
|
98
|
+
for (let key in params) {
|
99
|
+
if (params[key]) {
|
100
|
+
paramsArray.push(key + '=' + encodeURIComponent(params[key]));
|
101
|
+
}
|
102
|
+
}
|
103
|
+
if (paramsArray.length > 0) {
|
104
|
+
paramsText = '?' + paramsArray.join('&');
|
105
|
+
}
|
106
|
+
}
|
107
|
+
if (paramsText) {
|
108
|
+
url = path + apiPath + paramsText;
|
109
|
+
} else {
|
110
|
+
url = path + apiPath;
|
111
|
+
}
|
54
112
|
|
113
|
+
//actions.loading.open();
|
114
|
+
actions.loadApi({
|
115
|
+
key: key,
|
116
|
+
url: url,
|
117
|
+
fn: result => {
|
118
|
+
actions.getResult({ key: key, result: result, fn: fn });
|
119
|
+
}
|
120
|
+
});
|
121
|
+
},
|
122
|
+
loadApi: ({ key, url, fn }) => (state, actions) => {
|
123
|
+
axios.get(url).then(res => {
|
124
|
+
if (!res.error) {
|
125
|
+
const data = res.data;
|
126
|
+
fn &&
|
127
|
+
fn({
|
128
|
+
success: true,
|
129
|
+
message: `API-${key} Success`,
|
130
|
+
data: data
|
131
|
+
});
|
132
|
+
} else {
|
133
|
+
fn &&
|
134
|
+
fn({
|
135
|
+
success: false,
|
136
|
+
error: res.error,
|
137
|
+
message: res.errorMessage,
|
138
|
+
data: null
|
139
|
+
});
|
140
|
+
}
|
141
|
+
});
|
142
|
+
},
|
143
|
+
getResult: ({ key, result, fn }) => (state, actions) => {
|
144
|
+
if (result.success) {
|
145
|
+
switch (key) {
|
146
|
+
case 'master':
|
147
|
+
actions.setMasterData(result.data);
|
148
|
+
break;
|
149
|
+
case 'top':
|
150
|
+
actions.top.setTopData(result.data);
|
151
|
+
break;
|
152
|
+
case 'news':
|
153
|
+
actions.news.setNewsData(result.data);
|
154
|
+
break;
|
155
|
+
}
|
156
|
+
//actions.loading.close();
|
157
|
+
fn && fn();
|
158
|
+
} else {
|
159
|
+
//actions.loading.close();
|
160
|
+
console.log(result.message);
|
161
|
+
}
|
162
|
+
},
|
163
|
+
//--------------------------------------------------
|
164
|
+
// 【API】master
|
165
|
+
//--------------------------------------------------
|
166
|
+
getMaterData: ({ fn }) => (state, actions) => {
|
167
|
+
actions.getData({
|
168
|
+
key: 'master',
|
169
|
+
apiPath: 'https://xxxx.com/api/master',
|
170
|
+
params: null,
|
171
|
+
fn: () => {
|
172
|
+
fn && fn();
|
173
|
+
}
|
174
|
+
});
|
175
|
+
},
|
176
|
+
setMasterData: data => (state, actions) => {
|
177
|
+
return {
|
178
|
+
master: data
|
179
|
+
};
|
180
|
+
},
|
181
|
+
//--------------------------------------------------
|
182
|
+
// 【API】トップ
|
183
|
+
//--------------------------------------------------
|
184
|
+
getTopData: ({ fn }) => (state, actions) => {
|
185
|
+
actions.getData({
|
186
|
+
key: 'top',
|
187
|
+
apiPath: 'https://xxxx.com/api/top',
|
188
|
+
params: null,
|
189
|
+
fn: () => {
|
190
|
+
fn && fn();
|
191
|
+
}
|
192
|
+
});
|
193
|
+
},
|
194
|
+
//--------------------------------------------------
|
195
|
+
// 【API】ニュース
|
196
|
+
//--------------------------------------------------
|
197
|
+
getNewsData: ({ year, category, limit, fn }) => (state, actions) => {
|
198
|
+
actions.getData({
|
199
|
+
key: 'news',
|
200
|
+
apiPath: 'https://xxxx.com/api/news',
|
201
|
+
params: {
|
202
|
+
year: year,
|
203
|
+
category: category,
|
204
|
+
limit: limit
|
205
|
+
},
|
206
|
+
fn: () => {
|
207
|
+
fn && fn();
|
208
|
+
}
|
209
|
+
});
|
210
|
+
},
|
211
|
+
}
|
212
|
+
};
|
213
|
+
```
|
55
214
|
|
215
|
+
#### src/App.js
|
216
|
+
```javascript
|
217
|
+
import { h } from 'hyperapp';
|
218
|
+
import Top from 'src/views/page/Top.js';
|
219
|
+
export default (state, actions) => {
|
220
|
+
console.log('State : ', state);
|
221
|
+
return (
|
222
|
+
<div>
|
223
|
+
<Top />
|
224
|
+
</div>
|
225
|
+
);
|
226
|
+
};
|
227
|
+
```
|
56
228
|
|
229
|
+
#### src/Page/Top.js
|
230
|
+
```javascript
|
231
|
+
import { h } from 'hyperapp';
|
232
|
+
import styles from 'src/views/page/top/top.scss';
|
233
|
+
export default (props, children) => (state, actions) => {
|
234
|
+
if (state.route.type === 'top') {
|
235
|
+
// この書き方で同じAPIを複数回実行してしまわないか不安
|
236
|
+
if (state.master && state.top.data == null) {
|
237
|
+
actions.getTopData({
|
238
|
+
fn: () => {
|
239
|
+
actions.getNewsData({
|
240
|
+
year: null,
|
241
|
+
category: null,
|
242
|
+
limit: 10,
|
243
|
+
fn: null
|
244
|
+
});
|
245
|
+
}
|
246
|
+
});
|
247
|
+
}
|
248
|
+
if (state.master && state.top.data) {
|
249
|
+
// masterとtopデータを使いたい場合、この書き方だとstate更新の無限ループ陥る
|
250
|
+
actions.top.setYear(state.master.year|| state.top.data.year);
|
251
|
+
}
|
252
|
+
return (
|
253
|
+
<div key="top" class={styles['top']}>
|
254
|
+
<PTopMainvisual
|
255
|
+
data={state.top.data && state.top.data.mainvisualItems[0]}
|
256
|
+
dummyData={state.top.dummyData.mainvisualItems}
|
257
|
+
isLoading={state.top.data ? 0 : 1}
|
258
|
+
/>
|
259
|
+
// nullにすればAPIから来たときの空データ[]と別物扱いしますか?
|
260
|
+
{state.news.data == null ? (
|
261
|
+
<div>
|
262
|
+
{state.news.data.length > 0 && (
|
263
|
+
<PTopSection title="ニュース">
|
264
|
+
<PList type="news" data={state.news.data} isLoading={0} />
|
265
|
+
</PTopSection>
|
266
|
+
)}
|
267
|
+
</div>
|
268
|
+
) : (
|
269
|
+
<PTopSection title="ニュース">
|
270
|
+
<PList type="news" dummyData={state.news.dummyData} isLoading={1} />
|
271
|
+
</PTopSection>
|
272
|
+
)}
|
273
|
+
</div>
|
274
|
+
);
|
275
|
+
}
|
276
|
+
};
|
277
|
+
```
|
57
278
|
|
279
|
+
#### src/project/list.js
|
280
|
+
```javascript
|
281
|
+
import { h } from 'hyperapp';
|
282
|
+
export default (props, children) => (state, actions) => {
|
283
|
+
return (
|
284
|
+
<div class={[styles['p-list'], props.class].join(' ')}>
|
285
|
+
{props.isLoading == 1 ? (
|
286
|
+
// APIからデータ受け取ってなかったら
|
287
|
+
<div>
|
288
|
+
{[...Array(props.dummyData.limit)].map(item => {
|
289
|
+
return (
|
290
|
+
<image src="/images/dummy/text.png">
|
291
|
+
);
|
292
|
+
})}
|
293
|
+
</div>
|
294
|
+
) : (
|
58
|
-
APIからデータ
|
295
|
+
// APIからデータ受け取っていたら
|
296
|
+
<div>
|
297
|
+
{props.data && (
|
298
|
+
// データが有れば
|
299
|
+
<div>
|
300
|
+
{props.data.map(item => {
|
301
|
+
return (
|
302
|
+
<div>{item.text}</div>
|
303
|
+
);
|
304
|
+
})}
|
305
|
+
</div>
|
306
|
+
)}
|
307
|
+
</div>
|
308
|
+
)}
|
309
|
+
</div>
|
310
|
+
);
|
311
|
+
};
|
312
|
+
```
|
313
|
+
|
314
|
+
|
315
|
+
## 1. api読み込みの書き方もっと良い方法はないか?
|
316
|
+
待機時間の向上のため、データがなくてもダミーで表示できるようにしてます。
|
317
|
+
そのため、APIが揃う前にpageをreturnで表示させてます。
|
318
|
+
`if (state.master && state.top.data == null) {...}`で書いてる箇所は、問題ないか?
|
319
|
+
|
320
|
+
## 2. 複数のAPIのデータを使い何かactionを実行したい場合、データありの場合でif文書くと無限ループに陥る
|
59
|
-
|
321
|
+
1回だけ実行してほしいのに無限に実行してしまい、処理に負担がかかる。
|
322
|
+
またAPIから返ってきたデータがエラーではなく、1件もなしのようなケースも想定できていないので、改善したいが、
|
323
|
+
どうしたら良いのか?
|
324
|
+
```javascript
|
325
|
+
if (state.master && state.top.data) {
|
326
|
+
actions.top.setYear(state.master.year|| state.top.data.year);
|
327
|
+
}
|
328
|
+
```
|
329
|
+
|
330
|
+
## 3. スケルトンスクリーンのもっとスマートな書き方がないか
|
331
|
+
同じコードを何回も書いてる気がして、もっと良い方法がないのか。
|
332
|
+
src/page/Top.js
|
333
|
+
src/project/List.js
|
334
|
+
|
335
|
+
```
|
336
|
+
{state.news.data ? (
|
337
|
+
// APIデータ来た場合
|
338
|
+
<div>
|
339
|
+
// APIデータが1件でもあったら表示
|
340
|
+
{state.news.data.length > 0 && (
|
341
|
+
<PTopSection title="ニュース">
|
342
|
+
<PList type="news" data={state.news.data} isLoading={0} />
|
343
|
+
</PTopSection>
|
344
|
+
)}
|
345
|
+
</div>
|
346
|
+
) : (
|
347
|
+
// APIデータ来てないときの状態
|
348
|
+
<PTopSection title="ニュース">
|
349
|
+
<PList type="news" dummyData={state.news.dummyData} isLoading={1} />
|
350
|
+
</PTopSection>
|
351
|
+
)}
|
352
|
+
```
|
353
|
+
|
354
|
+
また`state.news.data ?`この書き方で、空配列はどちらの判定なるのか?
|
355
|
+
nullは、true
|
356
|
+
[]はfalseだとダミーデータが表示されるので困る
|
357
|
+
|
358
|
+
|
359
|
+
## 4. リロード時に少しでもスクロール中だと高確率でページの最上部に位置が変わってしまう
|
360
|
+
これは、原因と対策の検討もついてないです。
|