案例说明
使用 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>
,需要手动将无效的href
和alt
删掉。
头部组件 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" />
New Post
</a>
</li>
<li className="nav-item">
<a className="nav-link">
<i className="ion-gear-a" />
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 & 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" />
Follow Eric Simons <span className="counter">(10)</span>
</button>
<button className="btn btn-sm btn-outline-primary">
<i className="ion-heart" />
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" />
Follow Eric Simons <span className="counter">(10)</span>
</button>
<button className="btn btn-sm btn-outline-primary">
<i className="ion-heart" />
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>
<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>
<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>
)
}