今時の画像遅延読み込み(lazyload-image)と無限スクロール(infinite-scroll)

- 今時なので、jQueryを使わず[IntersectionObserver](https://caniuse.com/#feat=intersectionobserver)を使います! :point_up:
- 今時なので、[TypeScript](https://www.typescriptlang.org/)でWebコンポーネント([LitElement](https://lit-element.polymer-project.org/))にします!! :sweat_smile:

→ [動作URL](https://howking.github.io/lit-lazy-image-infinite-scroll/build/default/)と[リポジトリ](https://github.com/howking/lit-lazy-image-infinite-scroll)

![ezgif-1-d445c4ef9afa.gif](https://qiita-image-store.s3.amazonaws.com/0/90678/d9aa756e-0fd2-8a3e-ffe7-ea582edb276c.gif)

下記のPolyfillを使ってES5変換・gzip後で`index.js`のサイズは**11K程度**。IE11、Safariでも動いてます。

```html:index.html
<script src="https://unpkg.com/@webcomponents/webcomponentsjs/webcomponents-loader.js"></script>
<script src="https://unpkg.com/intersection-observer/intersection-observer.js"></script>
```
※ 画像は[ここ](https://unsplash.com/search/photos/tokyo)から借りました。

## 画像遅延読み込み(lazy-image)

将来的には[lazyload属性](https://caniuse.com/#feat=lazyload)や[Blink LazyLoad](https://github.com/whatwg/html)(参考: [img要素とiframe要素のlazyload属性](https://1000ch.net/posts/2018/lazyload-attributes.html))の普及に期待しつつ、[Let's Build Web Components! Part 5: LitElement](https://dev.to/bennypowers/lets-build-web-components-part-5-litelement-906)の記事を参考に書くと、

```ts:index.ts#LazyImage
const isInters = ({isIntersecting}:IntersectionObserverEntry) => isIntersecting

@customElement('lazy-image' as any)
export class LazyImage extends LitElement {
  
  @property() load = false
  
  @property() src = ''

  constructor(){
    super()
    new IntersectionObserver((entries, observer)=>{
      if(entries.some(isInters)){
        this.load = true
        observer.unobserve(this)
      }
    }).observe(this)
  }

  render() {
    return html`<img src=${this.load ? this.src : ''}>`
  }
  
}
```

`IntersectionObserver`が自身の要素を監視し、画面に表示されたら`this.load`が`true`となって、画像が表示されます。

```ts
const isInters = ({isIntersecting}:IntersectionObserverEntry) => isIntersecting
...
new IntersectionObserver((entries, observer)=>{
  if(entries.some(isInters)){
     ...
```

は

```ts
new IntersectionObserver((entries, observer)=>{
  entries.forEach( entry => {
    if(entry.isIntersecting){
      ...
```

と基本同じような処理なんですが、使い方かっこいい。。。

## 無限スクロール(infinite-scroll)

[&lt;virtual-scroller&gt;](https://github.com/valdrinkoshi/virtual-scroller)を使ってみたかったのですが、Chromeでしか動かなかったのでlazy-imageを参考に書くと、

```ts:index.ts#infiniteScroll
@customElement('infinite-scroll' as any)
export class InfiniteScroll extends LitElement {
  
  @property({ type: { fromAttribute: (attr:string) => JSON.parse(attr) } })
  items: string[] = []
  
  @query('button') next?: HTMLButtonElement
  
  firstUpdated() {
    new IntersectionObserver( entries =>{
      if(entries.some(isInters)){
        this.items = [...this.items, ...this.items]
      }
    }).observe(this.next!)
  }
  
  render() {
    return html`
      <style>lazy-image { display: block; width: 100%; height: 480px; }</style>
      ${this.items.map(i=>html`<lazy-image src=${i}></lazy-image>`)}
      <button>next</button>
    `
  }

}
```

ここではページの最下部にある`<button>`を監視し、画面に表示されたら`items`属性に指定されたデータを読み込み続けます。

```html:index.html
<infinite-scroll items='["URL1","URL2","URL3"...]'></infinite-scroll>
```

LitElementでは属性に渡された値を配列として使いたい場合に`fromAttribute`を使って変換処理を定義できます(JSONを渡す場合は`'シングルクォート`を使わないといけない)。

```ts
this.items = [...this.items, ...this.items]
```

は

```ts
this.items.push( ...this.items)
```

でもよいのですが、配列に値が追加されただけではLitElement側で感知してくれないので、

```ts
this.items.push( ...this.items)
this.requestUpdate // or this.requestUpdate('item')
```

で再描画をリクエストする必要があります。

あと、実際に無限ループするページは[SEO的によくない](https://webmasters.googleblog.com/2014/02/infinite-scroll-search-friendly.html)らしいので、

```ts
new IntersectionObserver((entries, observer)=>{
  if(entries.some(isInters)){
    ...
    if(/* isItemsLast? */){
       this.next!.hidden = true
       observer.unobserve(this.next!)
    }
```

などで、Observerを止めます。

## ビルドについて

[rollup](https://rollupjs.org/)を試してみたのですが、結局[polymer-cli](https://github.com/Polymer/tools/tree/master/packages/cli)が一番楽でした、、、

```json:package.json
  "dependencies": {
    "@polymer/lit-element": "^0.6.4"
  },
  "devDependencies": {
    "polymer-cli": "^1.9.1",
    "typescript": "^3.1.3"
  }
```

```json:polymer.json
{
  "entrypoint": "index.html",
  "shell": "index.js",
  "builds": [{
    "name": "default",
    "preset": "es5-bundled",
    "addServiceWorker": false
  }],
  "moduleResolution": "node",
  "npm": true
}
```

あまり単体で使うものじゃなさそう(デフォルトでServiceWorkerを作ってくれたりとか)ですが、index.htmlも含めたPolyfillやminifyが便利です。

以上です。