当单页应用程序(SPA)的主题出现时,我们倾向于考虑浏览器,JavaScript,速度和对搜索引擎的隐蔽性。 这是因为SPA使用JavaScript渲染页面的内容,并且由于网络搜寻器不使用浏览器来查看网页,因此它们无法查看和索引内容-或至少大多数不能。
这是一些开发人员试图以各种方式解决的问题:
- 添加一个转义的网站片段版本,该版本要求所有页面均以静态形式可用,并增加了许多额外的工作( 现已弃用 )。
- 使用付费服务将SPA取消浏览化为静态标记,以供搜索引擎爬网程序抓取。
- 相信搜索引擎现在已经足够先进,可以读取我们仅JavaScript的内容。 (我还不会。)
使用服务器上的Node.js和客户端上的React,我们可以将我们的JavaScript应用构建为通用 (或同构 )的。 这可以从服务器端和浏览器端渲染中提供一些好处,从而使搜索引擎和使用浏览器的人员都可以查看我们的SPA内容。
在本循序渐进的教程中,我将向您展示如何构建React Universal Blog App,该应用程序将首先在服务器端呈现标记,以使我们的内容可用于搜索引擎。 然后,它将使浏览器接管快速且响应迅速的单个页面应用程序。
入门
我们的通用博客应用将使用以下技术和工具:
- 用于包管理和服务器端渲染的Node.js
- 对UI视图做出反应
- Express提供简单的后端JS服务器框架
- React Router进行路由
- React Hot Loader用于开发中的热加载
- 通量数据流
- 用于内容管理的Cosmic JS
首先,运行以下命令:
mkdir react-universal-blog
cd react-universal-blog
现在创建一个package.json
文件并添加以下内容:
{
"name": "react-universal-blog",
"version": "1.0.0",
"engines": {
"node": "4.1.2",
"npm": "3.5.2"
},
"description": "",
"main": "app-server.js",
"dependencies": {
"babel-cli": "^6.26.0",
"babel-loader": "^7.1.2",
"babel-preset-es2015": "^6.24.1",
"babel-preset-es2017": "^6.24.1",
"babel-preset-react": "^6.24.1",
"babel-register": "^6.26.0",
"cosmicjs": "^2.4.0",
"flux": "^3.1.3",
"history": "1.13.0",
"hogan-express": "^0.5.2",
"html-webpack-plugin": "^2.30.1",
"path": "^0.12.7",
"react": "^15.6.1",
"react-dom": "^15.6.1",
"react-router": "1.0.1",
"webpack": "^3.5.6",
"webpack-dev-server": "^2.7.1"
},
"scripts": {
"webpack-dev-server": "NODE_ENV=development PORT=8080 webpack-dev-server --content-base public/ --hot --inline --devtool inline-source-map --history-api-fallback",
"development": "cp views/index.html public/index.html && NODE_ENV=development webpack && npm run webpack-dev-server"
},
"author": "",
"license": "ISC",
"devDependencies": {
"react-hot-loader": "^1.3.0"
}
}
在此文件中,您会注意到我们添加了以下内容:
- Babel打包我们的CommonJS模块并将ES6和React JSX转换为与浏览器兼容的JavaScript
- Cosmic JS官方Node.js客户端,可轻松通过Cosmic JS云托管内容API提供我们的博客内容
- 用于应用程序数据管理的Flux(这是我们React应用程序中非常重要的元素)。
- 对服务器和浏览器上的UI管理做出反应
- React Router在服务器和浏览器上的路由
- 的WebPack为将一切汇集成一个
bundle.js
文件。
我们还在package.json
文件中添加了一个脚本。 当我们运行npm run development
,脚本npm run development
index.html
文件从我们的views
文件夹复制到我们的public
文件夹中。 然后,它将webpack-dev-server
的内容库设置为public/
并启用热重载(保存.js
文件)。 最后,它可以帮助我们从源头调试组件,并为我们找不到的页面提供后备(后退到index.html
)。
现在,通过编辑文件webpack.config.js
设置我们的webpack配置文件:
// webpack.config.js
var webpack = require('webpack')
module.exports = {
devtool: 'eval',
entry: './app-client.js',
output: {
path: __dirname + '/public/dist',
filename: 'bundle.js',
publicPath: '/dist/'
},
module: {
loaders: [
{ test: /\.js$/, loaders: 'babel-loader', exclude: /node_modules/ },
{ test: /\.jsx$/, loaders: 'babel-loader', exclude: /node_modules/ }
]
},
plugins: [
new webpack.DefinePlugin({
'process.env.COSMIC_BUCKET': JSON.stringify(process.env.COSMIC_BUCKET),
'process.env.COSMIC_READ_KEY': JSON.stringify(process.env.COSMIC_READ_KEY),
'process.env.COSMIC_WRITE_KEY': JSON.stringify(process.env.COSMIC_WRITE_KEY)
})
]
};
您会注意到,我们添加了一个值为app-client.js
的entry
属性。 该文件用作我们的应用程序客户端入口点,这意味着webpack将捆绑我们的应用程序并将其输出到/public/dist/bundle.js
(在output
属性中指定)。 我们还使用加载程序让Babel在我们的ES6和JSX代码上发挥其魔力。 React Hot Loader在开发过程中用于热加载(不刷新页面!)。
在进入与React相关的内容之前,让我们准备好我们博客的外观。 由于我希望您在本教程中更多地关注功能而不是样式,因此在此我们将使用预先构建的前端主题。 我从Start Bootstrap中选择了一个称为Clean Blog的 。 在您的终端中运行以下命令:
创建一个名为views
的文件夹,并在其中创建一个index.html
文件。 打开HTML文件并添加以下代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="">
<meta name="author" content="">
<title>{{ site.title }}{{# page }} | {{ page.title }}{{/ page }}</title>
<!-- Bootstrap Core CSS -->
<link href="/css/bootstrap.min.css" rel="stylesheet">
<!-- Custom CSS -->
<link href="/css/clean-blog.min.css" rel="stylesheet">
<link href="/css/cosmic-custom.css" rel="stylesheet">
<!-- Custom Fonts -->
<link href="//maxcdn.bootstrapcdn.com/font-awesome/4.1.0/css/font-awesome.min.css" rel="stylesheet" type="text/css">
<link href="//fonts.googleapis.com/css?family=Lora:400,700,400italic,700italic" rel="stylesheet" type="text/css">
<link href="//fonts.googleapis.com/css?family=Open+Sans:300italic,400italic,600italic,700italic,800italic,400,300,600,700,800" rel="stylesheet" type="text/css">
<!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries -->
<!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
<!--[if lt IE 9]>
<script src="https://oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script>
<script src="https://oss.maxcdn.com/libs/respond.js/1.4.2/respond.min.js"></script>
<![endif]-->
</head>
<body class="hidden">
<div id="app">{{{ reactMarkup }}}</div>
<script src="/js/jquery.min.js"></script>
<script src="/js/bootstrap.min.js"></script>
<script src="/js/clean-blog.min.js"></script>
<script src="/dist/bundle.js"></script>
</body>
</html>
要获取所有public
包含的JS和CSS文件,可以从GitHub存储库中获取它们。 单击此处下载文件 。
通常,我会使用出色的React Bootstrap软件包,并避免使用jQuery。 但是,为了简洁起见,我们将保留主题的预构建jQuery功能。
在我们的index.html
文件中,我们将在div
id="app"
处设置我们的React挂载点。 模板变量{{{ reactMarkup }}}
将被转换为服务器渲染的标记,然后一旦浏览器启动,我们的React应用程序将接管并使用id="app"
挂载至div
。 为了改善我们的JavaScript加载所有内容时的用户体验,我们在正文中添加了class="hidden"
。 然后,一旦安装了React,我们就删除此类。 听起来可能有点复杂,但是我将在稍后展示给我们看如何进行。
此时,您的应用应具有以下结构:
package.json
public
|-css
|-bootstrap.min.css
|-cosmic-custom.css
|-js
|-jquery.min.js
|-bootstrap.min.js
|-clean-blog.min.js
views
|-index.html
webpack.config.js
现在我们已经完成了静态工作,让我们开始构建一些React组件。
我们的博客应用程序组件(基本示例)
让我们通过设置博客页面开始为我们的应用程序构建UI。 因为这将成为创意专业人士的投资组合博客,所以我们的博客将包含以下页面:
- 家
- 关于
- 工作
- 联系
让我们开始创建一个名为app-client.js
的文件,并向其中添加以下内容:
// app-client.js
import React from 'react'
import { render } from 'react-dom'
import { Router } from 'react-router'
import createBrowserHistory from 'history/lib/createBrowserHistory'
const history = createBrowserHistory()
// Routes
import routes from './routes'
const Routes = (
<Router history={history}>
{ routes }
</Router>
)
const app = document.getElementById('app')
render(Routes, app)
为了更好地了解React Router的工作原理,您可以访问其GitHub repo 。 要点在于,我们在app-client.js
中拥有Router
组件,该组件具有用于客户端路由的浏览器历史记录。 我们的服务器呈现的标记不需要浏览器历史记录,因此我们将创建一个单独的routes.js
文件,以在我们的服务器和客户端入口点之间共享。
将以下内容添加到routes.js
文件:
// routes.js
import React, { Component } from 'react'
import { Route, IndexRoute, Link } from 'react-router'
// Main component
class App extends Component {
componentDidMount(){
document.body.className=''
}
render(){
return (
<div>
<h1>React Universal Blog</h1>
<nav>
<ul>
<li><Link to="/">Home</Link></li>
<li><Link to="/about">About</Link></li>
<li><Link to="/work">Work</Link></li>
<li><Link to="/contact">Contact</Link></li>
</ul>
</nav>
{ this.props.children }
</div>
)
}
}
// Pages
class Home extends Component {
render(){
return (
<div>
<h2>Home</h2>
<div>Some home page content</div>
</div>
)
}
}
class About extends Component {
render(){
return (
<div>
<h2>About</h2>
<div>Some about page content</div>
</div>
)
}
}
class Work extends Component {
render(){
return (
<div>
<h2>Work</h2>
<div>Some work page content</div>
</div>
)
}
}
class Contact extends Component {
render(){
return (
<div>
<h2>Contact</h2>
<div>Some contact page content</div>
</div>
)
}
}
class NoMatch extends Component {
render(){
return (
<div>
<h2>NoMatch</h2>
<div>404 error</div>
</div>
)
}
}
export default (
<Route path="/" component={App}>
<IndexRoute component={Home}/>
<Route path="about" component={About}/>
<Route path="work" component={Work}/>
<Route path="contact" component={Contact}/>
<Route path="*" component={NoMatch}/>
</Route>
)
从这里开始,我们有一个非常基本的工作示例,其中包含几个页面。 现在,让我们运行我们的应用程序并将其签出! 在您的终端中,运行以下命令:
mkdir public
npm install
npm run development
然后在浏览器中导航到http:// localhost:8080 ,以查看基本的博客正在运行。
这些事情已经完成,现在是时候让它在服务器上运行了。 创建一个名为app-server.js
的文件并添加以下内容:
// app-server.js
import React from 'react'
import { match, RoutingContext } from 'react-router'
import ReactDOMServer from 'react-dom/server'
import express from 'express'
import hogan from 'hogan-express'
// Routes
import routes from './routes'
// Express
const app = express()
app.engine('html', hogan)
app.set('views', __dirname + '/views')
app.use('/', express.static(__dirname + '/public/'))
app.set('port', (process.env.PORT || 3000))
app.get('*',(req, res) => {
match({ routes, location: req.url }, (error, redirectLocation, renderProps) => {
const reactMarkup = ReactDOMServer.renderToStaticMarkup(<RoutingContext {...renderProps} />)
res.locals.reactMarkup = reactMarkup
if (error) {
res.status(500).send(error.message)
} else if (redirectLocation) {
res.redirect(302, redirectLocation.pathname + redirectLocation.search)
} else if (renderProps) {
// Success!
res.status(200).render('index.html')
} else {
res.status(404).render('index.html')
}
})
})
app.listen(app.get('port'))
console.info('==> Server is listening in ' + process.env.NODE_ENV + ' mode')
console.info('==> Go to http://localhost:%s', app.get('port'))
在app-server.js
,我们正在加载已设置的基本路由。 这些将渲染的标记转换为字符串,然后将其作为变量传递给我们的模板。
我们已经准备好启动服务器并查看其上的代码,但首先,让我们创建一个脚本来执行此操作。
打开package.json
文件,然后编辑script
部分,如下所示:
// …
"scripts": {
"start": "npm run production",
"production": "rm -rf public/index.html && NODE_ENV=production webpack -p && NODE_ENV=production babel-node app-server.js --presets es2015",
"webpack-dev-server": "NODE_ENV=development PORT=8080 webpack-dev-server --content-base public/ --hot --inline --devtool inline-source-map --history-api-fallback",
"development": "cp views/index.html public/index.html && NODE_ENV=development webpack && npm run webpack-dev-server"
},
// …
现在我们已经建立了production
脚本,我们可以在服务器端和客户端上运行代码。 在您的终端中执行:
npm start
在浏览器中导航到http:// localhost:3000 。 您应该看到简单的博客内容,并且能够在SPA模式下快速轻松地浏览页面。
继续并点击view source
。 请注意,我们的SPA代码也可供所有机器人找到。 我们两全其美!
结论
在第一部分中,我们开始研究React的世界,并了解如何与Node.js一起使用它来构建React Universal Blog App。
如果您希望将博客带入一个新的水平,并且知道如何添加和编辑内容,请不要忘记阅读第二部分“ 构建React Universal Blog App:实现Flux ”。 我们将深入探讨如何使用React组织概念和Flux模式轻松扩展React Universal Blog App。
我们已经与Open SourceCraft合作,为您带来了来自React Developers的6个专业提示 。 有关更多开源内容,请查看Open SourceCraft 。
From: https://www.sitepoint.com/building-a-react-universal-blog-app-a-step-by-step-guide/