十一、实现服务器端渲染
本章将介绍以下配方:
- 实现服务器端渲染
- 使用服务器端呈现实现承诺
- 实现 Next.js
介绍
React 通常使用客户端渲染(CSR)。这意味着它会在目标div
中动态注入 HTML 代码(它通常使用#app
或#root
ID),这就是为什么如果您尝试直接查看页面代码(右键单击查看页面代码),您会看到如下内容:
查看实际代码的唯一方法是使用 Chrome Dev 工具或其他工具检查站点,以下是 React 使用 CSR 生成的代码:
通过查看页面,您可以看到注入我们的#root
div 的代码。服务器端呈现(SSR)对于提高我们网站的SEO非常有用,并被主要搜索引擎索引,如谷歌、雅虎、Bing。如果你不太关心 SEO,你可能不需要担心SSR。目前,谷歌机器人支持CSR,它可以在谷歌上为我们的网站建立索引,但是如果您关心 SEO,并且担心在其他搜索引擎上提高 SEO,例如雅虎、必应或DuckDuckGo,那么,使用 SSR 就是一条出路。
实现服务器端渲染
在这个配方中,我们将在我们的项目中实现 SSR。
准备
我们将使用第 10 章中最后一个配方的代码(使用 React/Redux 实现 Node.js 和 Webpack 4】,掌握 Webpack 4.x,并安装一些其他依赖项:
npm install --save-dev webpack-node-externals webpack-dev-middleware webpack-hot-middleware webpack-hot-server-middleware webpack-merge babel-cli babel-preset-es2015
怎么做。。。
现在让我们看一下渲染的步骤:
- 首先,我们需要将我们的 npm 脚本添加到我们的
package.json
文件中:
"scripts": {
"clean": "rm -rf dist/ && rm -rf public/app",
"start": "npm run clean & NODE_ENV=development
BABEL_ENV=development
nodemon src/server --watch src/server --watch src/shared --
exec babel-node --presets es2015",
"start-analyzer": "npm run clean && NODE_ENV=development
BABEL_ENV=development ANALYZER=true babel-node src/server"
}
File: package.json
- 现在我们必须更改我们的
webpack.config.js
文件。因为我们将要实现 SSR,所以我们需要将 Web 包配置分为客户端配置和服务器配置,并将它们作为数组返回。该文件应如下所示:
// Webpack Configuration (Client & Server)
import clientConfig from './webpack/webpack.config.client';
import serverConfig from './webpack/webpack.config.server';
export default [
clientConfig,
serverConfig
];
File: webpack.config.js
- 现在我们需要在
webpack
文件夹中为我们的客户机配置创建一个文件。我们需要称之为webpack.config.client.js
:
// Dependencies
import webpackMerge from 'webpack-merge';
// Webpack Configuration
import commonConfig from './webpack.config.common';
import {
context,
devtool,
entry,
name,
output,
optimization,
plugins,
target
} from './configuration';
// Type of Configuration
const type = 'client';
export default webpackMerge(commonConfig(type), {
context: context(type),
devtool,
entry: entry(type),
name: name(type),
output: output(type),
optimization,
plugins: plugins(type),
target: target(type)
});
File: webpack/webpack.config.client.js
- 现在,服务器配置应如下所示:
// Dependencies
import webpackMerge from 'webpack-merge';
// Webpack Configuration
import commonConfig from './webpack.config.common';
// Configuration
import {
context,
entry,
externals,
name,
output,
plugins,
target
} from './configuration';
// Type of Configuration
const type = 'server';
export default webpackMerge(commonConfig(type), {
context: context(type),
entry: entry(type),
externals: externals(type),
name: name(type),
output: output(type),
plugins: plugins(type),
target: target(type)
});
File: webpack/webpack.config.server.js
- 如您所见,在这两个文件中,我们都导入了一个公共配置文件,其中包含需要添加到客户端和服务器的配置:
// Configuration
import { module, resolve, mode } from './configuration';
export default type => ({
module: module(type),
resolve,
mode
});
File: webpack/webpack.config.common.js
- 我们需要为 Webpack 节点添加新的配置文件,还需要修改一些我们已经拥有的文件。我们需要创建的第一个是
context.js
。在这个文件(以及其他一些文件)中,我们将导出一个带有类型参数的函数,它可以是客户端或服务器,根据该值,我们将返回不同的配置:
// Dependencies
import path from 'path';
export default type => type === 'server'
? path.resolve(__dirname, '../../src/server')
: path.resolve(__dirname, '../../src/client');
File: webpack/configuration/context.js
- 条目文件是我们将要添加到捆绑包中的所有文件的添加位置。我们的条目文件现在应该如下所示:
// Environment
const isDevelopment = process.env.NODE_ENV !== 'production';
export default type => {
if (type === 'server') {
return './render/serverRender.js';
}
const entry = [];
if (isDevelopment) {
entry.push(
'webpack-hot-middleware/client',
'react-hot-loader/patch'
);
}
entry.push('./index.jsx');
return entry;
};
File: webpack/configuration/entry.js
- 我们需要创建一个名为 externals.js 的文件,其中包含我们不会捆绑的模块(除非它们在白名单上):
// Dependencies
import nodeExternals from 'webpack-node-externals';
export default () => [
nodeExternals({
whitelist: [/^redux\/(store|modules)/]
})
];
File: webpack/configuration/externals.js
- 另外,我们需要修改
module.js
文件,根据环境或配置类型返回规则:
// Dependencies
import ExtractTextPlugin from 'extract-text-webpack-plugin';
// Environment
const isDevelopment = process.env.NODE_ENV !== 'production';
export default type => {
const rules = [
{
test: /\.(js|jsx)$/,
use: 'babel-loader',
exclude: /node_modules/
}
];
if (!isDevelopment || type === 'server') {
rules.push({
test: /\.scss$/,
use: ExtractTextPlugin.extract({
fallback: 'style-loader',
use: [
'css-loader?minimize=true&modules=true&localIdentName=
[name]__[local]_[hash:base64]',
'sass-loader'
]
})
});
} else {
rules.push({
test: /\.scss$/,
use: [
{
loader: 'style-loader'
},
{
loader: 'css-loader',
options: {
modules: true,
importLoaders: 1,
localIdentName: '[name]__[local]_[hash:base64]',
sourceMap: true,
minimize: true
}
},
{
loader: 'sass-loader'
}
]
});
}
return {
rules
};
};
File: webpack/configuration/module.js
- 现在我们需要为名称创建一个节点:
export default type => type;
File: webpack/configuration/name.js
- 对于输出配置,我们需要根据配置类型(客户端或服务器)返回一个对象:
// Dependencies
import path from 'path';
export default type => {
if (type === 'server') {
return {
filename: 'server.js',
path: path.resolve(__dirname, '../../dist'),
libraryTarget: 'commonjs2'
};
}
return {
filename: '[name].bundle.js',
path: path.resolve(__dirname, '../../public/app'),
publicPath: '/'
};
};
File: webpack/configuration/output.js
- 在我们的
plugins.js
文件中,我们正在验证用户是否发送了ANALYZER
变量来显示BundleAnalyzerPlugin
,就在这种情况下,而不是每次我们在开发模式下运行我们的应用时:
// Dependencies
import CompressionPlugin from 'compression-webpack-plugin';
import ExtractTextPlugin from 'extract-text-webpack-plugin';
import webpack from 'webpack';
import WebpackNotifierPlugin from 'webpack-notifier';
import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer';
// Environment
const isDevelopment = process.env.NODE_ENV !== 'production';
// Analyzer
const isAnalyzer = process.env.ANALYZER === 'true';
export default type => {
const plugins = [
new ExtractTextPlugin({
filename: '../../public/css/style.css'
})
];
if (isAnalyzer) {
plugins.push(
new BundleAnalyzerPlugin()
);
}
if (isDevelopment) {
plugins.push(
new webpack.HotModuleReplacementPlugin(),
new webpack.NoEmitOnErrorsPlugin(),
new WebpackNotifierPlugin({
title: 'CodeJobs'
})
);
} else {
plugins.push(
new CompressionPlugin({
asset: '[path].gz[query]',
algorithm: 'gzip',
test: /\.js$|\.css$|\.html$/,
threshold: 10240,
minRatio: 0.8
})
);
}
return plugins;
};
File: webpack/configuration/plugins.js
- 我们需要在解析文件中指定我们的模块;文件应如下所示:
// Dependencies
import path from 'path';
export default {
extensions: ['.js', '.jsx'],
modules: [
'node_modules',
path.resolve(__dirname, '../../src/client'),
path.resolve(__dirname, '../../src/server')
]
};
File: webpack/configuration/resolve.js
- 我们需要创建的最后一个配置是
target.js
文件:
export default type => type === 'server' ? 'node' : 'web';
File: webpack/configuration/target.js
- 配置完我们的 Webpack 后,我们需要修改我们的
App.jsx
文件,其中我们需要使用<BrowserRouter>
组件为客户端创建路由,使用<StaticRouter>
组件为服务器创建路由:
// Dependencies
import React from 'react';
import {
BrowserRouter,
StaticRouter,
Switch,
Route
} from 'react-router-dom';
// Components
import About from '@components/About';
import Home from '@components/Home';
export default ({ server, location, context = {} }) => {
const routes = (
<Switch>
<Route exact path="/" component={Home} />
<Route exact path="/about" component={About} />
</Switch>
);
// Client Router
let router = (
<BrowserRouter>
{routes}
</BrowserRouter>
);
// Server Router
if (server) {
router = (
<StaticRouter location={location} context={context}>
{routes}
</StaticRouter>
);
}
return router;
};
File: src/client/App.jsx
- 现在我们需要修改我们的服务器文件(
index.js
,以使用我们的clientRender和serverRender中间件:
// Dependencies
import express from 'express';
import path from 'path';
import webpackDevMiddleware from 'webpack-dev-middleware';
import webpackHotMiddleware from 'webpack-hot-middleware';
import webpackHotServerMiddleware from 'webpack-hot-server-middleware';
import webpack from 'webpack';
// Utils
import { isMobile, isBot } from '@utils/device';
// Client Render
import clientRender from './render/clientRender';
// Webpack Configuration
import webpackConfig from '@webpack';
// Environment
const isProduction = process.env.NODE_ENV === 'production';
// Express Application
const app = express();
// Webpack Compiler
const compiler = webpack(webpackConfig);
// Public directory
app.use(express.static(path.join(__dirname, '../../public')));
// Device Detection
app.use((req, res, next) => {
req.isMobile = isMobile(req.headers['user-agent']);
// We detect if a search bot is accessing...
req.isBot = isBot(req.headers['user-agent']);
next();
});
// Webpack Middleware
if (!isProduction) {
// Hot Module Replacement
app.use(webpackDevMiddleware(compiler));
app.use(webpackHotMiddleware(
compiler.compilers.find(compiler => compiler.name === 'client'))
);
} else {
// GZip Compression just for Production
app.get('*.js', (req, res, next) => {
req.url = `${req.url}.gz`;
res.set('Content-Encoding', 'gzip');
next();
});
}
// Client Side Rendering
app.use(clientRender());
if (isProduction) {
try {
// eslint-disable-next-line
const serverRender = require('../../dist/server.js').default;
app.use(serverRender());
} catch (e) {
throw e;
}
}
// For Server Side Rendering on Development Mode
app.use(webpackHotServerMiddleware(compiler));
// Disabling x-powered-by
app.disable('x-powered-by');
// Listen Port...
app.listen(3000);
File: src/server/index.js
- 我们需要修改我们的
clientRender.js
文件。如果我们检测到具有isBot
功能的搜索机器人,我们将返回next()
中间件。否则,我们呈现 HTML 并使用 CSR 执行应用:
// HTML
import html from './html';
// Initial State
import initialState from './initialState';
export default function clientRender() {
return (req, res, next) => {
if (req.isBot) {
return next();
}
res.send(html({
title: 'Codejobs',
initialState: initialState(req)
}));
};
}
File: src/server/render/clientRender.js
- 现在让我们创建
serverRender.js
文件。这里,我们需要使用react-dom/server
库中的renderToString
方法呈现App
组件:
// Dependencies
import React from 'react';
import { renderToString } from 'react-dom/server';
import { Provider } from 'react-redux';
// Redux Store
import configureStore from '@configureStore';
// Components
import App from '../../client/App';
import html from './html';
// Initial State
import initialState from './initialState';
export default function serverRender() {
return (req, res, next) => {
// Configuring Redux Store
const store = configureStore(initialState(req));
const markup = renderToString(
<Provider store={store}>
<App
server
location={req.url}
/>
</Provider>
);
res.send(html({
title: 'Codejobs',
markup,
initialState: initialState(req)
}));
};
}
File: src/server/render/serverRender.js
它是如何工作的。。。
您可以通过运行npm start
命令来启动应用。
如果您在浏览器中的http://localhost:3000
处打开应用(例如 Chrome),然后右键单击并查看页面源代码,您可能会注意到我们没有使用 SSR:
这是因为我们将只对搜索机器人使用 SSR。isBot功能将检测所有搜索机器人,为了测试,我添加了curl作为机器人来测试我们的 SSR;这是该功能的代码:
export function isBot(ua) {
const b = /curl|bot|googlebot|google|baidu|bing|msn|duckduckgo|teoma|slurp|yandex|crawler|spider|robot|crawling/i;
return b.test(ua);
}
File: src/shared/utils/device.js
当应用在另一个终端中运行时,打开一个新终端,然后执行以下命令:
curl http://localhost:3000
如您所见,#root div 中的 HTML 代码使用 SSR 进行渲染。
此外,如果您想尝试在 curl 中运行/about
,您将看到它也将使用 SSR 进行渲染:
Chrome 有一个扩展名为 CChrome的用户代理切换器,您可以在其中指定要在浏览器中使用的用户代理。通过这种方式,您可以为 Googlebot 添加一个特殊的用户代理,例如:
然后,如果在 User Agent Switcher 中选择 Chrome | Bot,则在查看页面源代码时,可以看到 HTML 代码将其呈现为 SSR:
还有更多。。。
当我们使用 SSR 时,在尝试为客户机使用对象窗口时,我们必须非常小心。如果直接使用 SSR 使用,则会出现如下引用错误:
ReferenceError: window is not defined
要解决此问题,可以验证窗口对象是否存在,但这可能非常重复。我更喜欢创建一个函数来验证我们使用的是浏览器(客户端)还是服务器。您可以这样做:
export function isBrowser() {
return typeof window !== 'undefined';
}
然后,每次需要使用窗口对象时,都可以执行以下操作:
const store = isBrowser() ? configureStore(window.initialState) : {};
使用服务器端呈现实现承诺
在上一个配方中,我们看到了 SSR 是如何工作的,但该配方仅限于用简单的组件显示 SSR。在本教程中,我们将学习如何实现将组件连接到 Redux 的承诺,使用 API 获取数据并使用 SSR 呈现组件。
准备
我们将使用上一个配方中的相同代码,但我们将进行一些更改。在此配方中,我们需要安装以下软件包:
npm install axios babel-preset-stage-0 react-router-dom redux-devtools-extension redux-thunk
怎么做。。。
对于这个方法,我们将实现一个从 API 中提取的基本 todo 列表,以展示如何使用 SSR 将 Redux 连接到我们的应用:
- 我们需要做的第一件事是添加一个简单的 API 来显示待办事项列表:
import express from 'express';
const router = express.Router();
// Mock data, this should come from a database....
const todo = [
{
id: 1,
title: 'Go to the Gym'
},
{
id: 2,
title: 'Dentist Appointment'
},
{
id: 3,
title: 'Finish homework'
}
];
router.get('/todo/list', (req, res, next) => {
res.json({
response: todo
});
});
export default router;
File: src/server/controllers/api.js
- 第二步是将此 API 控制器导入我们的
src/server/index.js
文件,并将其作为中间件添加到/api
路由上:
...
// Controllers
import apiController from './controllers/api';
...
// Express Application
const app = express();
// Webpack Compiler
const compiler = webpack(webpackConfig);
// Routes
app.use('/api', apiController);
...
File: src/server/index.js
- 之前,在我们的
serverRender.js
文件中,我们直接呈现了App
组件。现在我们需要从具有静态方法initialAction
的组件获取承诺,将其保存到承诺数组中,解析它们,然后呈现我们的App
方法:
// Dependencies
import React from 'react';
import { renderToString } from 'react-dom/server';
import { Provider } from 'react-redux';
import { matchPath } from 'react-router-dom';
// Redux Store
import configureStore from '@configureStore';
// Components
import App from '../../client/App';
// HTML
import html from './html';
// Initial State
import initialState from './initialState';
// Routes
import routes from '@shared/routes';
export default function serverRender() {
return (req, res, next) => {
// Configuring Redux Store
const store = configureStore(initialState(req));
// Getting the promises from the components which has
// initialAction.
const promises = routes.paths.reduce((promises, route) => {
if (matchPath(req.url, route) && route.component && route.component.initialAction) {
promises.push(Promise.resolve(store.dispatch(route.component.initialAction())));
}
return promises;
}, []);
// Resolving our promises
Promise.all(promises)
.then(() => {
// Getting Redux Initial State
const initialState = store.getState();
// Rendering with SSR
const markup = renderToString(
<Provider store={store}>
<App
server
location={req.url}
/>
</Provider>
);
// Sending our HTML code.
res.send(html({
title: 'Codejobs',
markup,
initialState
}));
})
.catch(e => {
// eslint-disable-line no-console
console.log('Promise Error: ', e);
});
};
}
File: src/server/render/serverRender.js
- 在这个方法中,我们需要在客户端目录中稍微更改一下文件夹结构。以前,我们有一个
components
目录,我们的组件在里面。现在我们将把我们的组件封装成小应用,在里面我们可以创建我们的动作、API、组件、容器和还原器。我们的新结构应如下所示:
- 我们将创建一个 todo 应用。要做到这一点,首先我们需要添加 actions 文件夹,并在其中创建第一个
actionTypes.js
文件。在这个文件中,我们需要添加我们的FETCH_TODO
操作。我更喜欢创建一个具有两个功能的对象,一个用于请求,另一个用于成功;当我们在减速器上使用此功能时,以及当我们采取行动时,您将看到此功能的优势:
// Actions
export const FETCH_TODO = {
request: () => 'FETCH_TODO_REQUEST',
success: () => 'FETCH_TODO_SUCCESS'
};
File: src/client/todo/actions/actionTypes.js
- 在我们的
index.js
文件中,我们将创建一个 fetchTodo 操作,以从 API 检索我们的 todo 列表项:
// Base Actions
import { request, received } from '@baseActions';
// Api
import api from '../api';
// Action Types
import { FETCH_TODO } from './actionTypes';
export const fetchTodo = () => dispatch => {
const action = FETCH_TODO;
const { fetchTodo } = api;
dispatch(request(action));
return fetchTodo()
.then(response => dispatch(received(action, response.data)));
};
File: src/client/todo/actions/index.js
- 如您所见,我们正在使用来自基本操作的两个特定方法(请求和接收)。这些函数将帮助我们轻松地分派操作(您还记得我们在操作中使用了请求和成功方法吗?):
// Base Actions
export const request = ACTION => ({
type: ACTION.request()
});
export const received = (ACTION, data) => ({
type: ACTION.success(),
payload: data
});
File: src/shared/redux/baseActions.js
- 现在让我们创建我们的
api
文件夹,我们需要在其中添加constants.js
文件和index.js
文件:
export const API = Object.freeze({
TODO: 'api/todo/list'
});
File: src/client/todo/api/constants.js
- 在我们的
index.js
文件中,我们必须创建 Api 类并添加一个名为fetchTodo
的静态方法:
// Dependencies
import axios from 'axios';
// Configuration
import config from '@configuration';
// Utils
import { isBrowser } from '@utils/frontend';
// Constants
import { API } from './constants';
class Api {
static fetchTodo() {
// For Node (SSR) we have to specify our base domain
// (http://localhost:3000/api/todo/list)
// For Client Side Render just /api/todo/list.
const url = isBrowser()
? API.TODO
: `${config.baseUrl}/${API.TODO}`;
return axios(url);
}
}
export default Api;
File: src/client/todo/api/index.js
- 在我们的 Todo 容器中,我们需要映射 Todo 列表,并将fetchTodo操作添加到 Redux。我们将导出一个布局组件,并将其他组件添加到该组件中,并操纵显示布局的方式:
// Dependencies
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
// Components
import Layout from '../components/Layout';
// Actions
import { fetchTodo } from '../actions';
export default connect(({ todo }) => ({
todo: todo.list
}), dispatch => bindActionCreators(
{
fetchTodo
},
dispatch
))(Layout);
File: src/client/todo/container/index.js
- 我们的布局组件应如下所示:
// Dependencies
import React from 'react';
// Shared Components
import Header from '@layout/Header';
import Content from '@layout/Content';
import Footer from '@layout/Footer';
// Componenets
import Todo from '../components/Todo';
const Layout = props => (
<main>
<Header {...props} />
<Content>
<Todo {...props} />
</Content>
<Footer {...props} />
</main>
);
export default Layout;
File: src/client/todo/components/Layout.jsx
- 在这个配方中,我们不会看到布局组件(页眉、内容和页脚),因为它们非常通用,我们在过去的配方中使用过它们。现在,让我们创建 reducer 文件:
// Utils
import { getNewState } from '@utils/frontend';
// Action Types
import { FETCH_TODO } from '../actions/actionTypes';
// Initial State
const initialState = {
list: []
};
export default function todoReducer(state = initialState, action) {
switch (action.type) {
case FETCH_TODO.success(): {
const { payload: { response = [] } } = action;
return getNewState(state, {
list: response
});
}
default:
return state;
}
}
File: src/client/todo/reducer/index.js
- 我们的 Todo 组件将在 componentDidMount 方法中执行 fetchTodo 操作,然后将 Todo 列表呈现为 HTML 列表;非常简单:
// Dependencies
import React, { Component } from 'react';
// Utils
import { isFirstRender } from '@utils/frontend';
// Styles
import styles from './Todo.scss';
class Todo extends Component {
componentDidMount() {
const { fetchTodo } = this.props;
fetchTodo();
}
render() {
const {
todo
} = this.props;
if (isFirstRender(todo)) {
return null;
}
return (
<div>
<div className={styles.Todo}>
<ol>
{todo.map((item, key) =>
<li key={key}>{item.title}</li>)}
</ol>
</div>
</div>
);
}
}
export default Todo;
File: src/client/todo/components/Todo.jsx
- 最后,我们需要为 todo 应用创建一个
index.jsx
文件,在该文件中,我们将添加 initialAction(这将返回一个承诺),以执行 fetchTodo 操作并使用 SSR 呈现该 todo 列表:
// Dependencies
import React from 'react';
// Actions
import { fetchTodo } from './actions';
// Main Container
import Container from './container';
// Main Component
const Main = props => <Container {...props} />;
// Initial Action
Main.initialAction = () => fetchTodo();
export default Main;
File: src/client/todo/index.jsx
它是如何工作的。。。
正如您在我们的serverRender.js
文件中所看到的,我们得到承诺并解决它们,然后使用 SSR 呈现我们的应用。
如果要测试应用,需要转到 http://localhost:3000/todo 在浏览器中。
请记住,在我们的应用中,搜索机器人和 curl 只使用 SSR,否则将使用 CSR。这是因为我们必须使用 SSR 的唯一原因是为了改进我们在谷歌、雅虎和必应的 SEO。
如果我们使用 CSR,我们执行操作的方式是 Todo 组件中的componentDidMount()
方法;如果我们使用 SSR,我们将使用initialAction
方法,它返回一个承诺,将在serverRender.js
中解决。
如果打开该页面,您将看到以下内容:
如果您想查看 SSR 是否工作,可以使用curl
命令在您的终端中执行相同的 URL:
如您所见,todo 列表缩减器已添加到initialState
中,从那里,我们可以使用 SSR 呈现列表。
实现 Next.js
js 是一个用于服务器应用的最低限度的框架
在本教程中,我们将学习如何使用 Sass 实现 Next.js,我们还将使用 axios 从服务获取数据。
准备
首先,我们创建一个名为nextjs
的新目录,初始化package.json
,最后在其中创建一个新目录:
mkdir nextjs
cd nextjs
npm init -y
mkdir src
然后我们需要安装一些依赖项:
npm install next react react-dom axios node-sass @zeit/next-sass
怎么做。。。
现在我们已经安装了依赖项,让我们创建第一个 Next.js 应用:
- 我们需要做的第一件事是在 package.json 中创建一些脚本。在每个脚本中,我们需要指定
src
目录。否则,它将尝试从根目录而不是从src
路径开始下一步:
"scripts": {
"start": "next start src",
"dev": "next src",
"build": "next build src"
}
File: package.json
- Next 中的主目录名为
pages
。在这里,我们将包括我们希望使用 Next 渲染的所有pages
:
cd src && mkdir pages
- 我们需要创建的第一个页面是
index.jsx
:
const Index = () => <h1>Home</h1>;
export default Index;
File: src/pages/index.jsx
- 现在,让我们使用 dev 脚本运行应用:
npm run dev
- 如果一切正常,您应该在终端中看到:
- 打开
http://localhost:3000
:
Next.js has its own Webpack configuration and hot reloading enabled. That means if you edit the index.js file you will see the changes reflected without refreshing the page.
- 现在,让我们创建一个关于页面,看看路由是如何工作的:
const About = () => <h1>About</h1>;
export default About;
File: src/pages/about.jsx
- 现在,如果转到,您将看到关于页面 http://localhost:3000/about. 如您所见,Next.js 会为我们创建的每个页面自动创建一个新路由。这意味着我们不需要安装 React 路由来处理路由。
In Next pages, it is not necessary to import React because it is automatically handled by Next as well.
- 现在,我们需要创建一个
next.config.js
文件,并导入 WITHASS 方法,以便在我们的项目中使用 Sass。不幸的是,该文件需要用 ES5 语法编写,因为目前不支持使用 ES6 的 babel 扩展名(https://github.com/zeit/next.js/issues/2916 :
const withSass = require('@zeit/next-sass');
module.exports = withSass();
File: src/next.config.js In this file, we can also add custom Webpack configuration if we need it.
- 然后我们需要在
pages
目录中创建一个名为_document.js
的特殊文件。此文件由 Next.js 自动处理,在这里我们可以定义文档的头部和正文:
import Document, { Head, Main, NextScript } from 'next/document';
export default class MyDocument extends Document {
render() {
return (
<html>
<Head>
<title>Codejobs with Next</title>
<link
rel="stylesheet"
href="/_next/static/style.css" />
</Head>
<body>
<Main />
<NextScript />
</body>
</html>
);
}
}
File: src/pages/_document.js The path to the CSS file (/_next/static/style.css
) is by default; we should use that one to use styles in our project.
- 现在我们可以创建一些组件来包装页面。我们需要创建的第一个选项是菜单选项的导航栏:
import Link from 'next/link';
import './Navbar.scss';
const Navbar = () => (
<div className="navbar">
<ul>
<li>Codejobs</li>
<li><Link href="/">Home</Link></li>
<li><Link href="/about">About</Link></li>
</ul>
</div>
)
export default Navbar;
File: src/components/Navbar.jsx The Link component is not the same as the React Router Link. There are a few differences; for example, the React Router Link uses the "to" prop and the Next Link uses "href" to specify the URL.
- 现在我们可以为我们的
navbar
添加 Sass 样式:
.navbar {
background: black;
color: white;
height: 60px;
ul {
padding: 0;
margin: 0;
list-style: none;
li {
display: inline-block;
margin-left: 30px;
text-align: center;
a {
display: block;
color: white;
line-height: 60px;
width: 150px;
&:hover {
background: white;
color: black;
}
}
}
}
}
File: src/components/Navbar.scss
- 然后我们需要创建布局组件:
import Navbar from './Navbar';
import './Layout.scss';
const Layout = ({ children }) => (
<div className="layout">
<Navbar />
<div className="wrapper">
{children}
</div>
</div>
)
export default Layout;
File: src/components/Layout.jsx
- 我们的布局样式如下所示:
body {
font-family: verdana;
padding: 0;
margin: 0;
}
.layout {
a {
text-decoration: none;
}
.wrapper {
margin: 0 auto;
width: 96%;
}
}
File: src/components/Layout.scss
- 您还记得第 5 章精通 Redux中关于从 CoinMarketCap(
Repository: Chapter05/Recipe2/coinmarketcap
中列出前 100 种加密货币的配方吗?在这个配方中,我们将使用 Next.js 执行相同的操作。我们需要做的第一件事是修改页面的index.js
文件,并在getInitialProps
方法中执行异步axios
请求:
import axios from 'axios';
import Layout from '../components/Layout';
import Coins from '../components/Coins';
const Index = ({ coins }) => (
<Layout>
<div className="index">
<Coins coins={coins} />
</div>
</Layout>
);
Index.getInitialProps = async () => {
const url = 'https://api.coinmarketcap.com/v1/ticker/';
const res = await axios.get(url);
return {
coins: res.data
};
};
export default Index;
File: src/pages/index.js
- 现在让我们创建
Coins
组件:
// Dependencies
import React, { Component } from 'react';
import { array } from 'prop-types';
// Styles
import './Coins.scss';
const Coins = ({ coins }) => (
<div className="Coins">
<h1>Top 100 Coins</h1>
<ul>
{coins.map((coin, key) => (
<li key={key}>
<span className="left">{coin.rank} {coin.name} <strong>
{coin.symbol}</strong></span>
<span className="right">${coin.price_usd}</span>
</li>
))}
</ul>
</div>
);
Coins.propTypes = {
coins: array
};
export default Coins;
File: src/components/Coins.jsx
Coins
组件的样式如下:
.Coins {
h1 {
text-align: center;
}
ul {
margin: 0 auto;
margin-bottom: 20px;
padding: 0;
list-style: none;
width: 400px;
li {
border-bottom: 1px solid black;
text-align: left;
padding: 10px;
display: flex;
justify-content: space-between;
a {
display: block;
color: #333;
text-decoration: none;
background: #5ed4ff;
&:hover {
color: #333;
text-decoration: none;
background: #baecff;
}
}
}
}
}
File: src/components/Coins.scss
它是如何工作的。。。
现在我们已经创建了所有页面和组件,让我们通过运行npm run dev
来测试下一个应用:
现在让我们看看它是如何在 HTML 视图中呈现的:
万岁!HTML 是用 SSR 呈现的,非常适合改进 SEO。正如您所看到的,使用 Next 创建应用非常快,并且我们在启用 SSR 时避免了大量配置。*
版权属于:月萌API www.moonapi.com,转载请注明出处