開發現代化的靜態 PWA

黃胤翔 (Ben)

黃胤翔 (Ben)

  • SHOPLINE Sr. Frontend Engineer
  • 飛肯設計學苑兼課講師
  • COSCUP 2017 分享 ReactXP
  • 新手貓奴
  • 樂於推坑助人

大綱

  • 聊聊為什麼需要新工具 (GatsbyJS)
  • 想辦法推你坑
  • 分享一下我怎麼用
  • 比較看看其他的工具

3. 連聽都沒聽過

可能被推坑、打算下次開發時用

1. 正使用 Gatsby 開發

你並不孤單,有機會還能參考一下我的經驗

2. 正在 Gatsby 與其他工具間做選擇

更清楚自己的需求,並找到最適合你的工具

動機

  • 心血來潮寫個開源 Side Project
  • 架部落格,準備拿來寫技術文章放履歷
  • 接案賺錢

需求

  • Landing Page
  • SEO
  • 用最短時間結案領錢
  • 想寫 React、用一些潮潮工具

GatsbyJS

  • Static Site Generator 的一種
  • Based on React and GraphQL
  • 完美支援各種 Headless CMS
  • 整合 / 使用各種現代化開發工具
  • 遵循 PRPL Pattern
  • Hexo
  • Jekyll, Octopress
  • Hugo
  • VuePress
  • Middleman
  • Pelican

現代化?

Modern web development bundles advances in performance (bundle splitting, asset prefetching, offline support, image optimization, or server side rendering), developer experience (componentization via React, transpilation via Babel, webpack, hot reloading), accessibility, and security together.

https://www.gatsbyjs.org/docs/gatsby-core-philosophy/

Better Performance, Better DX tools or tech

https://www.datocms.com/blog/static-ecommerce-website-snipcart-gatsbyjs-datocms/

On demand

Traditional SSR

每個 HTTP Request 都會回傳一個唯一的頁面,適合內容有高度動態需求的網站

Static export

預先把內容渲染出來並輸出到 html 檔,可以放到任何 Static Hosting 而不需要 Application Server

兩種 Pre-rendering

起手式

# Create new project
gatsby new [SITE_DIRECTORY] [URL_OF_STARTER_GIT_REPO]

# Run locally
gatsby develop

Official Starters

功能 適用情境
gatsby-starter-default (Default) 安裝基本的設定和樣板 適用大部分的情境
gatsby-starter-blog 安裝 Blog 所需的文章列表與文章內頁 架 Blog
gatsby-starter-hello-world  僅安裝 Gatsby 核心部分 研究或開發用途

https://www.gatsbyjs.org/starters

Starters from community

Gatsby 專案架構

/
|-- /.cache
|-- /plugins
|-- /public  # Build 完後的檔案都會放在這(自動產生)
|-- /src
    |-- /pages      # 根據檔名建立路由規則
    |-- /templates  # 建立動態頁面
    |-- html.js
|-- /static
|-- gatsby-config.js
|-- gatsby-node.js
|-- gatsby-ssr.js
|-- gatsby-browser.js

Data Layer

  • 使用 GraphQL 定義每個 Component 所需要的資料
  • Page query
  • StaticQuery

Page query

import { graphql } from 'gatsby'

export default ({ data }) => (
  <>
    {
      data.allPost.edges.map(({ node }) => (
        <div key={node.id}>
          <Link to={node.slug}>
            <h3>{node.title}</h3>
          </Link>
        </div>
      ))
    }
  </>
)

export const query = graphql`
  query {
    allPost(
      sort: { fields: [createdAt] order: [DESC] }
    ) {
      edges {
        node {
          id
          slug
          title
        }
      }
    }
  }
`

src/pages/post.js

StaticQuery

export default props => {
  return (
    <StaticQuery
      query={graphql`
        query {
          allProduct(
            limit: 10,
            filter: { qty: { gt: 0 } },
            sort: { fields: [order, createdAt] order: [DESC, DESC] }
          ) {
            edges {
              node {
                id
                slug
                name
                product_tags {
                  id
                  name
                  group_number
                }
              }
            }
          }
        }
      `}
      render={({ allProduct }) => (
        <>{/* render UI by renderProps */}</>
      )}
    />
  )
}

src/components/ProductBanner.js

Packages/Plugins

  • Source
  • Transformer
  • Others

(gatsby-plugin-sharp, gatsby-image...)

常用的

  • gatsby-image
  • gatsby-plugin-sharp
  • gatsby-source-filesystem
  • gatsby-plugin-offline
  • gatsby-plugin-manifest
  • gatsby-plugin-feed
  • gatsby-plugin-react-helmet

客製化你的 Gatsby

  • gatsby-config.js
  • gatsby-node.js
  • gatsby-browser.js
  • gatsby-ssr.js
module.exports = {
  siteMetadata: {
    title: 'ModernWeb 形象官網',
    description: 'ModernWeb 2019',
    author: 'Ben',
  },
  plugins: [
    `gatsby-plugin-react-helmet`,
    {
      resolve: `gatsby-source-filesystem`,
      options: {
        name: `images`,
        path: `${__dirname}/src/images`,
      },
    },
    `gatsby-transformer-sharp`,
    `gatsby-plugin-sharp`,
    `gatsby-plugin-sass`
  ],
}

gatsby-config.js

使用場景

Code Snippet

https://www.gatsbyjs.org/packages/gatsby-remark-prismjs

Gatsby + Prism.js

https://www.gatsbyjs.org/packages/gatsby-transformer-remark

tomorrow theme

// import at top level
import 'prismjs/themes/prism-tomorrow.css'

// Use line numbers
import 'prismjs/plugins/line-numbers/prism-line-numbers.css'
// require at gatsby-browser.js
require('prismjs/themes/prism-tomorrow.css')

Styling

  • Global CSS
  • Modular Stylesheets
  • CSS-in-JS

CSS Modules

.container {
  display: flex;
  margin: 3rem auto;
}

src/components/container.module.css

src/components/container.js

import React from 'react'
import containerStyles from './container.module.css'

export default ({ children }) => (
  <section className={containerStyles.container}>{children}</section>
)

Emotion

module.exports = {
  plugins: ['gatsby-plugin-emotion'],
}

gatsby-config.js

src/pages/index.js

import styled from '@emotion/styled'
import { css } from '@emotion/core'

const Container = styled.div`
  display: flex;
  margin: 3rem auto;
`

const underline = css`
  text-decoration: underline;
`

export default () => (
  <Container>
    <h1 css={underline}>Emotion 棒棒</h1>
  </Container>
)

styled-components

module.exports = {
  plugins: ['gatsby-plugin-styled-components'],
}

gatsby-config.js

src/pages/index.js

import React from 'react'
import styled from 'styled-components'
import User from '../components/User'

const Container = styled.div`
  display: flex;
  margin: 3rem auto;
`

export default () => (
  <Container>
    <User name="Ben" />
    <User name="Elvis" />
  </Container>
)

動態生成頁面

slug === '暫停出貨通知'

Template

import React from "react"
import NewsCover from "../components/banners/newsCover"
import NewsDetail from "../components/banners/newsDetail"
import SEO from "../components/seo"

export default ({ data }) => (
  <>
    <SEO title={data.contentfulPost.title} keywords={['最新消息', 'ModernWeb', 'Gatsby', 'Contentful']} />
    <NewsCover />
    <NewsDetail data={data.contentfulPost} />
  </>
)

export const query = graphql`
  query($id: String!) {
    contentfulPost(id: { eq: $id }) {
      id
      title
      body {
        json
      }
      createdAt
      covers {
        sizes(maxWidth: 1000) {
          ...GatsbyContentfulSizes_withWebp
        }
      }
    }
  }
`

src/templates/news.js

exports.createPages = ({ graphql, actions }) => {
  const { createPage } = actions

  const promises = [
    new Promise((resolve, reject) => {
      const template = path.resolve('src/templates/news.js')

      resolve(
        graphql(
          `
            {
              allContentfulPost {
                edges {
                  node {
                    id
                    slug
                  }
                }
              }
            }
          `
        ).then(result => {
          if (result.errors) reject(result.errors)
  
          result.data.allContentfulPost.edges.forEach(({ node }) => {
            createPage({
              path: `/news/${node.slug}`,
              component: template,
              context: {
                id: node.id,
              },
            })
          })
        })
      )
    })
  ]

  return Promise.all(promises)
}

gatsby-node.js

Troubleshooting

小心 Browser globals

const module = typeof window !== `undefined` ? require("module") : null

Build Failed

rm -rf .cache
  • 無法正常從 Data Source 獲取資料
  • Plugin 沒有被正確調用

Github + Contentful + Netlify

plugins: [
  {
    resolve: 'gatsby-source-contentful',
    options: {
      spaceId: 'your_space_id_grab_it_from_contentful',
      accessToken: 'your_token_id_grab_it_from_contentful',
    },
  },
]

gatsby-config.js

export const query = graphql`
  query($id: String!) {
    allContentfulProductTag(sort: { fields: [group_number, order] order: [ASC, DESC] }) {
      edges {
        node {
          id
          name
          group_number
        }
      }
    }
  }
`

Search

  • 數量小做在前端
  • 數量大或全文搜尋再考慮用 Elasticsearch

?keyword=外套&tags=CHAMPION

const getFromSearch = data => {
  let search = queryString.parse(window.location.search, { arrayFormat: 'comma' })

  if (!Array.isArray(search.tags)) {
    search = { ...search, tags: [search.tags] }
  }

  return {
    keyword: search.keyword,
    tags: data.allContentfulProductTag.edges.reduce((tags, cur) => {
        if (search.tags.find(nameOfTag => nameOfTag === cur.node.name)) return tags.concat(cur)
        return tags
      }, [])
  }
}
useEffect(() => {
  const { tags: initTags, keyword: initKeyword } = getFromSearch(data)
  setSelectedTags(initTags)
  setKeyword(initKeyword)

  window.onpopstate = function(event) {
    const { tags: newTags, keyword: newKeyword } = getFromSearch(data)
    setSelectedTags(newTags)
    setKeyword(newKeyword)
  }

  setDidMount(true)

  return () => {
    setSelectedTags(false)
    setKeyword(false)
    window.onpopstate = null
  }
}, [(typeof window === 'undefined') ? null : window.location.href])

Gatsby 適合你嗎

有什麼選擇?

  • 自幹 SSR
  • React Static
  • Next.js
  • 其他能寫 React 的 Static Site Generator?

如果兩者相比,Gatsby 更適合拿來快速開發靜態頁面,Next 則適合拿來開發大型架構

擇你所需

歡迎面聊

Made with Slides.com