Gatsby + realworld 案例实践 - 01 案例说明、创建项目、构建案例组件、配置 Redux

案例说明

使用 Gatsby 开发基于 realworld 模板的混合应用。

realworld 模板和API:https://github.com/gothinkster/realworld-starter-kit/blob/master/FRONTEND_INSTRUCTIONS.md

Gatsby 不仅是静态站点生成器,还支持混合应用,即静态内容和动态内容的混合。

  • 静态内容就是在构建页面时生成的静态 HTML。
    • 在 realworld 项目中,不需要登录就可以展示给用户的页面需要做成静态页面,因为这种类型的页面既需要实现 SEO 又需要兼顾访问速度,如文章列表页。
  • 动态内容就是通过 Ajax 动态的从服务器获取内容。
    • 在 realworld 项目中,需要登录后才能访问的页面可以做成动态的,因为这种类型的页面不需要 SEO 优化,如发布文章页面、个人信息修改页面。
    • 本例动态数据使用 Redux 管理
  • 有的页面还可以做成动静混合的,如文章详情页面,文章内容可以做成静态的,但是文章评论需要做成动态的
  • 在一个页面中哪些内容需要做成静态的或者动态的,取决于这些内容是否会发生及时变化,如果内容不会随意发生变化就可以做成静态的,否则就要做成动态的

创建项目

# 创建项目
gatsby new realworld https://github.com/gatsbyjs/gatsby-starter-hello-world

#运行项目
cd realworld
npm start

构建案例所需组件

html to JSX 插件

vscode 可以安装 html to JSX 扩展插件,用于将 HTML 代码转化成 JSX 语法,如将 class 替换为 className,将 <!---->注释替换为 {/**/},将 style="" 转化为 style={{}}等。

使用:安装完成后,选中要转化的 HTML 代码,右键点击 Convert HTML to JSX

注意:插件在转化一些空值属性时有些特殊,效果如:<a href="" alt=""> 会被转化为 <a href alt>,需要手动将无效的 hrefalt 删掉。

头部组件 Header

// src\components\Header.js
import React from "react"

export default function Header() {
  return (
    <nav className="navbar navbar-light">
      <div className="container">
        <a className="navbar-brand" href="index.html">
          conduit
        </a>
        <ul className="nav navbar-nav pull-xs-right">
          <li className="nav-item">
            {/* Add "active" class when you're on that page" */}
            <a className="nav-link active">Home</a>
          </li>
          <li className="nav-item">
            <a className="nav-link">
              <i className="ion-compose" />
              &nbsp;New Post
            </a>
          </li>
          <li className="nav-item">
            <a className="nav-link">
              <i className="ion-gear-a" />
              &nbsp;Settings
            </a>
          </li>
          <li className="nav-item">
            <a className="nav-link">Sign up</a>
          </li>
        </ul>
      </div>
    </nav>
  )
}

底部组件 Footer

// src\components\Footer.js
import React from "react"

export default function Header() {
  return (
    <footer>
      <div className="container">
        <a href="/" className="logo-font">
          conduit
        </a>
        <span className="attribution">
          An interactive learning project from{" "}
          <a href="https://thinkster.io">Thinkster</a>. Code &amp; design
          licensed under MIT.
        </span>
      </div>
    </footer>
  )
}

布局组件 Layout

// src\components\Layout.js
import React from "react"
import Header from "./Header"
import Footer from "./Footer"

export default function Layout({ children }) {
  return (
    <>
      <Header />
      {children}
      <Footer />
    </>
  )
}

首页组件 - Banner

// src\components\Banner.js
import React from "react"

export default function Banner() {
  return (
    <div className="banner">
      <div className="container">
        <h1 className="logo-font">conduit</h1>
        <p>A place to share your knowledge.</p>
      </div>
    </div>
  )
}

首页组件 - Toggle

// src\components\Toggle.js
import React from "react"

export default function Toggle() {
  return (
    <div className="feed-toggle">
      <ul className="nav nav-pills outline-active">
        <li className="nav-item">
          <a className="nav-link disabled">Your Feed</a>
        </li>
        <li className="nav-item">
          <a className="nav-link active">Global Feed</a>
        </li>
      </ul>
    </div>
  )
}

首页组件 - Sidebar

// src\components\Sidebar.js
import React from "react"

export default function Sidebar() {
  return (
    <div className="sidebar">
      <p>Popular Tags</p>
      <div className="tag-list">
        <a className="tag-pill tag-default">programming</a>
        <a className="tag-pill tag-default">javascript</a>
        <a className="tag-pill tag-default">emberjs</a>
        <a className="tag-pill tag-default">angularjs</a>
        <a className="tag-pill tag-default">react</a>
        <a className="tag-pill tag-default">mean</a>
        <a className="tag-pill tag-default">node</a>
        <a className="tag-pill tag-default">rails</a>
      </div>
    </div>
  )
}

首页

// src\pages\index.js
import React from "react"
import Banner from "../components/Banner"
import Toggle from "../components/Toggle"
import Sidebar from "../components/Sidebar"

export default function Home() {
  return (
    <div className="home-page">
      <Banner />
      <div className="container page">
        <div className="row">
          <div className="col-md-9">
            <Toggle />
            <div className="article-preview">
              <div className="article-meta">
                <a href="profile.html">
                  <img src="http://i.imgur.com/Qr71crq.jpg" />
                </a>
                <div className="info">
                  <a className="author">Eric Simons</a>
                  <span className="date">January 20th</span>
                </div>
                <button className="btn btn-outline-primary btn-sm pull-xs-right">
                  <i className="ion-heart" /> 29
                </button>
              </div>
              <a className="preview-link">
                <h1>How to build webapps that scale</h1>
                <p>This is the description for the post.</p>
                <span>Read more...</span>
              </a>
            </div>
            <div className="article-preview">
              <div className="article-meta">
                <a href="profile.html">
                  <img src="http://i.imgur.com/N4VcUeJ.jpg" />
                </a>
                <div className="info">
                  <a className="author">Albert Pai</a>
                  <span className="date">January 20th</span>
                </div>
                <button className="btn btn-outline-primary btn-sm pull-xs-right">
                  <i className="ion-heart" /> 32
                </button>
              </div>
              <a className="preview-link">
                <h1>
                  The song you won't ever stop singing. No matter how hard you
                  try.
                </h1>
                <p>This is the description for the post.</p>
                <span>Read more...</span>
              </a>
            </div>
          </div>
          <div className="col-md-3">
            <Sidebar />
          </div>
        </div>
      </div>
    </div>
  )
}

登录页面

// src\pages\login.js
import React from "react"

export default function Login() {
  return (
    <div className="auth-page">
      <div className="container page">
        <div className="row">
          <div className="col-md-6 offset-md-3 col-xs-12">
            <h1 className="text-xs-center">Sign up</h1>
            <ul className="error-messages">
              <li>That email is already taken</li>
            </ul>
            <form>
              <fieldset className="form-group">
                <input
                  className="form-control form-control-lg"
                  type="text"
                  placeholder="Email"
                />
              </fieldset>
              <fieldset className="form-group">
                <input
                  className="form-control form-control-lg"
                  type="password"
                  placeholder="Password"
                />
              </fieldset>
              <button className="btn btn-lg btn-primary pull-xs-right">
                Sign up
              </button>
            </form>
          </div>
        </div>
      </div>
    </div>
  )
}

个人信息修改页面

// src\pages\settings.js
import React from "react"

export default function Settings() {
  return (
    <div className="settings-page">
      <div className="container page">
        <div className="row">
          <div className="col-md-6 offset-md-3 col-xs-12">
            <h1 className="text-xs-center">Your Settings</h1>
            <form>
              <fieldset>
                <fieldset className="form-group">
                  <input
                    className="form-control"
                    type="text"
                    placeholder="URL of profile picture"
                  />
                </fieldset>
                <fieldset className="form-group">
                  <input
                    className="form-control form-control-lg"
                    type="text"
                    placeholder="Your Name"
                  />
                </fieldset>
                <fieldset className="form-group">
                  <textarea
                    className="form-control form-control-lg"
                    rows={8}
                    placeholder="Short bio about you"
                    defaultValue={""}
                  />
                </fieldset>
                <fieldset className="form-group">
                  <input
                    className="form-control form-control-lg"
                    type="text"
                    placeholder="Email"
                  />
                </fieldset>
                <fieldset className="form-group">
                  <input
                    className="form-control form-control-lg"
                    type="password"
                    placeholder="Password"
                  />
                </fieldset>
                <button className="btn btn-lg btn-primary pull-xs-right">
                  Update Settings
                </button>
              </fieldset>
            </form>
          </div>
        </div>
      </div>
    </div>
  )
}

文章创建/修改页面

// src\pages\create.js
import React from "react"

export default function Create() {
  return (
    <div className="editor-page">
      <div className="container page">
        <div className="row">
          <div className="col-md-10 offset-md-1 col-xs-12">
            <form>
              <fieldset>
                <fieldset className="form-group">
                  <input
                    type="text"
                    className="form-control form-control-lg"
                    placeholder="Article Title"
                  />
                </fieldset>
                <fieldset className="form-group">
                  <input
                    type="text"
                    className="form-control"
                    placeholder="What's this article about?"
                  />
                </fieldset>
                <fieldset className="form-group">
                  <textarea
                    className="form-control"
                    rows={8}
                    placeholder="Write your article (in markdown)"
                    defaultValue={""}
                  />
                </fieldset>
                <fieldset className="form-group">
                  <input
                    type="text"
                    className="form-control"
                    placeholder="Enter tags"
                  />
                  <div className="tag-list" />
                </fieldset>
                <button
                  className="btn btn-lg pull-xs-right btn-primary"
                  type="button"
                >
                  Publish Article
                </button>
              </fieldset>
            </form>
          </div>
        </div>
      </div>
    </div>
  )
}

文章详情

// src\pages\article.js
import React from "react"

export default function Article() {
  return (
    <div className="article-page">
      <div className="banner">
        <div className="container">
          <h1>How to build webapps that scale</h1>
          <div className="article-meta">
            <a>
              <img src="http://i.imgur.com/Qr71crq.jpg" />
            </a>
            <div className="info">
              <a className="author">Eric Simons</a>
              <span className="date">January 20th</span>
            </div>
            <button className="btn btn-sm btn-outline-secondary">
              <i className="ion-plus-round" />
              &nbsp; Follow Eric Simons <span className="counter">(10)</span>
            </button>
            &nbsp;&nbsp;
            <button className="btn btn-sm btn-outline-primary">
              <i className="ion-heart" />
              &nbsp; Favorite Post <span className="counter">(29)</span>
            </button>
          </div>
        </div>
      </div>
      <div className="container page">
        <div className="row article-content">
          <div className="col-md-12">
            <p>
              Web development technologies have evolved at an incredible clip
              over the past few years.
            </p>
            <h2 id="introducing-ionic">Introducing RealWorld.</h2>
            <p>It's a great solution for learning how other frameworks work.</p>
          </div>
        </div>
        <hr />
        <div className="article-actions">
          <div className="article-meta">
            <a href="profile.html">
              <img src="http://i.imgur.com/Qr71crq.jpg" />
            </a>
            <div className="info">
              <a className="author">Eric Simons</a>
              <span className="date">January 20th</span>
            </div>
            <button className="btn btn-sm btn-outline-secondary">
              <i className="ion-plus-round" />
              &nbsp; Follow Eric Simons <span className="counter">(10)</span>
            </button>
            &nbsp;
            <button className="btn btn-sm btn-outline-primary">
              <i className="ion-heart" />
              &nbsp; Favorite Post <span className="counter">(29)</span>
            </button>
          </div>
        </div>
        <div className="row">
          <div className="col-xs-12 col-md-8 offset-md-2">
            <form className="card comment-form">
              <div className="card-block">
                <textarea
                  className="form-control"
                  placeholder="Write a comment..."
                  rows={3}
                  defaultValue={""}
                />
              </div>
              <div className="card-footer">
                <img
                  src="http://i.imgur.com/Qr71crq.jpg"
                  className="comment-author-img"
                />
                <button className="btn btn-sm btn-primary">Post Comment</button>
              </div>
            </form>
            <div className="card">
              <div className="card-block">
                <p className="card-text">
                  With supporting text below as a natural lead-in to additional
                  content.
                </p>
              </div>
              <div className="card-footer">
                <a className="comment-author">
                  <img
                    src="http://i.imgur.com/Qr71crq.jpg"
                    className="comment-author-img"
                  />
                </a>
                &nbsp;
                <a className="comment-author">Jacob Schmidt</a>
                <span className="date-posted">Dec 29th</span>
              </div>
            </div>
            <div className="card">
              <div className="card-block">
                <p className="card-text">
                  With supporting text below as a natural lead-in to additional
                  content.
                </p>
              </div>
              <div className="card-footer">
                <a className="comment-author">
                  <img
                    src="http://i.imgur.com/Qr71crq.jpg"
                    className="comment-author-img"
                  />
                </a>
                &nbsp;
                <a className="comment-author">Jacob Schmidt</a>
                <span className="date-posted">Dec 29th</span>
                <span className="mod-options">
                  <i className="ion-edit" />
                  <i className="ion-trash-a" />
                </span>
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>
  )
}

配置布局页面

为了避免在每一个页面导入布局组件(Layout),可以使用 Gatsby Browser APIs。

在项目根目录添加 gatsby-browser.js 文件,该文件中的内容会在客户端应用运行的时候被调用。

在文件中导出一个 wrapPageElement 方法,该方法接收的第一个参数是包含 element 属性的对象,element 就是每个页面的页面内容。

接着只需要在这个文件中导入 Layout 组件,用 Layout 组件包裹 element 就可以了。

// gatsby-browser.js
// 用到 JSX 语法,所以要引入 React
const React = require("react")
// 组件是 ESM 语法,所以用 default 引入默认导出
const Layout = require("./src/components/Layout").default

// element 是每个页面组件的内容
exports.wrapPageElement = ({ element }) => {
  return <Layout>{element}</Layout>
}

添加样式表

复制 .cache/default-html.js 文件到 src 目录下,并重命名为 html.js,可以覆盖 Gatsby 的默认模板页。

将样式表添加到模板页面。

// src\html.js
import React from "react"
import PropTypes from "prop-types"

export default function HTML(props) {
  return (
    <html {...props.htmlAttributes}>
      <head>
        <meta charSet="utf-8" />
        <meta httpEquiv="x-ua-compatible" content="ie=edge" />
        <meta
          name="viewport"
          content="width=device-width, initial-scale=1, shrink-to-fit=no"
        />
        {props.headComponents}

        {/* realworld 的样式表 */}
        <link
          href="//code.ionicframework.com/ionicons/2.0.1/css/ionicons.min.css"
          rel="stylesheet"
          type="text/css"
        />
        <link
          href="//fonts.googleapis.com/css?family=Titillium+Web:700|Source+Serif+Pro:400,700|Merriweather+Sans:400,700|Source+Sans+Pro:400,300,600,700,300italic,400italic,600italic,700italic"
          rel="stylesheet"
          type="text/css"
        />
        <link rel="stylesheet" href="//demo.productionready.io/main.css" />
      </head>
      <body {...props.bodyAttributes}>
        {props.preBodyComponents}
        <div
          key={`body`}
          id="___gatsby"
          dangerouslySetInnerHTML={{ __html: props.body }}
        />
        {props.postBodyComponents}
      </body>
    </html>
  )
}

HTML.propTypes = {
  htmlAttributes: PropTypes.object,
  headComponents: PropTypes.array,
  bodyAttributes: PropTypes.object,
  preBodyComponents: PropTypes.array,
  body: PropTypes.string,
  postBodyComponents: PropTypes.array,
}

配置 Redux

安装 Redux

npm i redux react-redux

创建 Store 和 示例 Reducer

点击按钮 +1 的示例

// src\store\reducers\counter.reducer.js
const initialState = {
  count: 0,
}

export default function (state = initialState, action) {
  switch (action.type) {
    case "increment":
      return {
        count: state.count + 1,
      }
      break
    default:
      return state
  }
}

// src\store\reducers\root.reducer.js
import { combineReducers } from "redux"
import counterReducer from "./counter.reducer"

export default combineReducers({
  counterReducer,
})

// src\store\createStore.js
import { createStore } from "redux"
import rootReducer from "./reducers/root.reducer"

export default function () {
  const store = createStore(rootReducer)
  return store
}

配置客户端 Provider

react-redux 的 Provider 组件需要配置在所有组件的最外层。

Gatsby Browser APIs 提供了一个 wrapRootElement 方法,用于获取最外层组件。

// gatsby-browser.js
// 用到 JSX 语法,所以要引入 React
const React = require("react")
// 组件是 ESM 语法,所以用 default 引入默认导出
const Layout = require("./src/components/Layout").default
// Redux
const { Provider } = require("react-redux")
const createStore = require("./src/store/createStore").default

// element 是每个页面组件的内容
exports.wrapPageElement = ({ element }) => {
  return <Layout>{element}</Layout>
}

// element 是应用的最外层组件
exports.wrapRootElement = ({ element }) => {
  return <Provider store={createStore()}>{element}</Provider>
}

配置服务器端 Provider

服务器端在构建页面的时候也需要用到 Store 中的数据。

Gatsby 提供 Gatsby SSR APIs 用于服务端配置,它也提供了 wrapRootElement 方法,用法和 Gatsby Browser APIs 一样。

在项目根目录创建 gatsby-ssr.js

// gatsby-browser.js
const React = require("react")
// Redux
const { Provider } = require("react-redux")
const createStore = require("./src/store/createStore").default

// element 是应用的最外层组件
exports.wrapRootElement = ({ element }) => {
  return <Provider store={createStore()}>{element}</Provider>
}

开发环境测试

// src\components\Banner.js
import React from "react"
import { useDispatch, useSelector } from "react-redux"

export default function Banner() {
  const dispatch = useDispatch()
  const counterReducer = useSelector(state => state.counterReducer)
  return (
    <div className="banner">
      <div className="container">
        <h1 className="logo-font">conduit {counterReducer.count}</h1>
        <p>A place to share your knowledge.</p>
        <button onClick={() => dispatch({ type: "increment" })}>+1</button>
      </div>
    </div>
  )
}

访问首页点击按钮查看是否生效。

构建环境测试

# ctrl + c 打断运行
# 构建应用
npm run build
# 运行构建的应用
npm run serve

访问 http://localhost:9000

配置 redux-saga

配置 redux-saga 中间件支持异步请求。

安装模块 npm i redux-saga

创建示例,点击按钮延迟 +1:

// src\store\sagas\counter.saga.js
import { takeEvery, put, delay } from "redux-saga/effects"

function* increment_async() {
  yield delay(1000)
  yield put({ type: "increment" })
}

export default function* counterSaga() {
  yield takeEvery("increment_async", increment_async)
}

// src\store\sagas\root.saga.js
import { all } from "redux-saga/effects"
import counterSaga from "./counter.saga"

export default function* rootSaga() {
  yield all([counterSaga()])
}

配置中间件:

// src\store\createStore.js
import { createStore, applyMiddleware } from "redux"
import rootReducer from "./reducers/root.reducer"
import createSagaMiddleware from "redux-saga"
import rootSaga from "./sagas/root.saga"

export default function () {
  const sagaMiddleware = createSagaMiddleware()
  const store = createStore(rootReducer, applyMiddleware(sagaMiddleware))
  sagaMiddleware.run(rootSaga)
  return store
}

测试示例:

// src\components\Banner.js
import React from "react"
import { useDispatch, useSelector } from "react-redux"

export default function Banner() {
  const dispatch = useDispatch()
  const counterReducer = useSelector(state => state.counterReducer)
  return (
    <div className="banner">
      <div className="container">
        <h1 className="logo-font">conduit {counterReducer.count}</h1>
        <p>A place to share your knowledge.</p>
        <button onClick={() => dispatch({ type: "increment" })}>同步 +1</button>
        <button onClick={() => dispatch({ type: "increment_async" })}>异步 +1</button>
      </div>
    </div>
  )
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值