teratail header banner
teratail header banner
質問するログイン新規登録

質問編集履歴

1

ミス修正

2020/01/11 14:04

投稿

kumakuma112
kumakuma112

スコア21

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
- そのため、ページ間の移動はSPAではなくリンクで遷移してます。**
4
+ 工数削減という理由もあり、ページ間の移動はSPAではなくリンクで遷移してます。**
5
5
 
6
6
  # お聞きしたい課題
7
+ 待機時間改善のためにFacebookのようなデータがないときのスケルトンスクリーンの実装をしてます。
7
- 一つはAPIを複数回呼び出さないようにるための記述の改善
8
+ その結果色々改善しないといけない点が出てしまい、苦戦してます。
8
- もう一つは、リロード時のページ位置の制御の仕方。
9
9
 
10
+ 1. **api読み込みの書き方もっと良い方法はないか?**
11
+ 2. **複数のAPIのデータを使い何かactionを実行したい場合、データありの場合でif文書くと無限ループに陥る**
12
+ 3. **スケルトンスクリーンのもっとスマートな書き方がないか**
13
+ 4. **リロード時に少しでもスクロール中だと高確率でページの最上部に位置が変わってしまう**
14
+
15
+ ![イメージ説明](ee5913b95ea1b3015ef7b518110185bb.png)
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
- │   │  └─ Component/
36
+ │   │  └─ component/
27
- │   │  └─ Layout/
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
- ## APIを複数回呼び出したくない
64
+ ```javascript
65
+ import axios from 'axios';
52
- 待機時間の向上のため、データがなくてもダミーでFacebookアプリのような「スケルトンスクリーン」を実装しました。
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
+ これは、原因と対策の検討もついてないです。