28、Redux与React集成及服务器端渲染全解析

Redux与React集成及服务器端渲染全解析

1. Redux与React集成:绑定动作到组件事件处理程序

在将Redux与React集成时,为了让应用对用户操作做出响应,需要使用 mapDispatchToProps 函数。该函数会接收一个 dispatch 参数,此参数为存储的 dispatch 方法,并将其注入到组件中。即便不提供 mapDispatchToProps 函数,容器的 props 中也会自动注入 dispatch 方法,但使用 mapDispatchToProps 能将特定于组件的动作逻辑与组件本身分离,便于测试。

mapDispatchToProps 函数会被 react-redux 调用,其返回的对象会合并到组件的 props 中。借助这个函数可以设置动作创建器,并使其在组件中可用。同时,可以利用Redux的 bindActionCreators 辅助工具,它能将值为动作创建器的对象转换为具有相同键的对象,不同之处在于每个动作创建器都被包裹在 dispatch 调用中,可直接调用。

以下是一个使用 mapDispatchToProps 的示例代码:

import { createError } from '../actions/error';
import { createNewPost, getPostsForPage } from '../actions/posts';
import { showComments } from '../actions/comments';
import Ad from '../components/ad/Ad';
import CreatePost from '../components/post/Create';
import Post from '../components/post/Post';
import Welcome from '../components/welcome/Welcome';

export class Home extends Component {
    componentDidMount() {
        this.props.actions.getPostsForPage();
    }
    componentDidCatch(err, info) {
        this.props.actions.createError(err, info);
    }
    render() {
        return (
            <div className="home">
                <Welcome />
                <div>
                    <CreatePost onSubmit={this.props.actions.createNewPost} />
                    {this.props.posts && (
                        <div className="posts">
                            {this.props.posts.map(post => (
                                <Post
                                    key={post.id}
                                    post={post}
                                    openCommentsDrawer={this.props.actions.showComments} 
                                />
                            ))}
                        </div>
                    )}
                    <button className="block" 
                        onClick={this.props.actions.getNextPageOfPosts}>  
                        Load more posts
                    </button>
                </div>
                <div>
                    <Ad url="https://ifelse.io/book" imageUrl="/static/assets/ads/ria.png" />
                    <Ad url="https://ifelse.io/book" imageUrl="/static/assets/ads/orly.jpg" />
                </div>
            </div>
        );
    }
}

export const mapDispatchToProps = dispatch => {
    return {
        actions: bindActionCreators(
            {
                createNewPost,
                getPostsForPage,
                showComments,
                createError,
                getNextPageOfPosts: getPostsForPage.bind(this, 'next') 
            },
            dispatch
        )
    };
};

export default connect(mapStateToProps, mapDispatchToProps)(Home);

已连接到Redux的组件还有以下这些:
| 组件名称 | 文件路径 |
| ---- | ---- |
| App | src/app.js |
| Comments | src/components/comment/Comments.js |
| Error | src/components/error/Error.js |
| Navigation | src/components/nav/navbar.js |
| PostActionSection | src/components/post/PostActionSection.js |
| Posts | src/components/post/Posts.js |
| Login | src/pages/login.js |
| SinglePost | src/pages/post.js |

2. 更新测试

Home 组件转换为使用Redux后,之前编写的测试会失效,需要进行更新。以下是更新后的测试文件示例:

jest.mock('mapbox');
import React from 'react';
import renderer from 'react-test-renderer';
import { Provider } from 'react-redux';
import { Home, mapStateToProps, mapDispatchToProps } from '../../src/pages/home';
import configureStore from '../../src/store/configureStore';
import initialState from '../../src/constants/initialState';

const now = new Date().getTime();

describe('Single post page', () => {
    const state = Object.assign({}, initialState, {
        posts: {
            2: { content: 'stuff', likes: [], date: now },
            1: { content: 'stuff', likes: [], date: now }
        },
        postIds: [1, 2]
    });
    const store = configureStore(state);

    test('mapStateToProps', () => {
        expect(mapStateToProps(state)).toEqual({
            posts: [
                { content: 'stuff', likes: [], date: now },
                { content: 'stuff', likes: [], date: now }
            ]
        });
    });

    test('mapDispatchToProps', () => {
        const dispatchStub = jest.fn();
        const mappedDispatch = mapDispatchToProps(dispatchStub);
        expect(mappedDispatch.actions.createNewPost).toBeDefined();
        expect(mappedDispatch.actions.getPostsForPage).toBeDefined();
        expect(mappedDispatch.actions.showComments).toBeDefined();
        expect(mappedDispatch.actions.createError).toBeDefined();
        expect(mappedDispatch.actions.getNextPageOfPosts).toBeDefined();
    });

    test('should render posts', function() {
        const props = {
            posts: [
                { id: 1, content: 'stuff', likes: [], date: now },
                { id: 2, content: 'stuff', likes: [], date: now }
            ],
            actions: {
                getPostsForPage: jest.fn(),
                createNewPost: jest.fn(),
                createError: jest.fn(),
                showComments: jest.fn()
            }
        };
        const component = renderer.create(
            <Provider store={store}>
                <Home {...props} />
            </Provider>
        );
        let tree = component.toJSON();
        expect(tree).toMatchSnapshot();
    });
});
3. 服务器端渲染概述

React支持通过React DOM的服务器API进行服务器端渲染(SSR)。SSR通常是指生成静态HTML标记,可通过HTTP或其他协议发送到浏览器,这是在服务器环境下的“渲染”操作。在某些情况下,将SSR集成到应用中是有用的,而在其他情况下则可能不必要。

在过去,许多应用仅使用服务器渲染视图,服务器会创建包含用户相关或其他数据的HTML字符串,并通过HTTP发送到浏览器。起初,服务器端脚本比较原始,手动拼接HTML字符串,这不仅耗时,而且难以更改。随着时间的推移,出现了许多框架和语言,帮助开发者构建主要在服务器上渲染的用户界面。

以下是服务器端渲染的简单流程:

graph LR
    A[浏览器请求] --> B[服务器数据查找/计算]
    B --> C[最终格式化、模板处理]
    C --> D[服务器响应:动态生成的HTML]
    D --> E[浏览器]
4. 深入了解服务器端渲染

在实现SSR之前,先了解非React上下文中的SSR方面知识是有帮助的。以使用ERB(嵌入式Ruby)的SSR为例,ERB是Ruby编程语言的一个特性,可用于创建HTML模板。许多Ruby on Rails应用会使用ERB模板生成视图,框架会读取开发者创建的 .erb 模板文件,并使用服务器或其他地方的数据填充它们,最终将填充好数据的文本发送到用户浏览器。

以下是一个简单的ERB文件示例:

<h1>Listing Books</h1> 
<table>
  <tr>
    <th>Title</th>
    <th>Summary</th>
    <th></th>
    <th></th>
    <th></th>
  </tr>
<% @books.each do |book| %>
  <tr>
    <td><%= book.title %></td>
    <td><%= book.content %></td>
    <td><%= link_to "Show", book %></td>
    <td><%= link_to "Edit", edit_book_path(book) %></td>
    <td><%= link_to "Remove", book, method: :delete, data: { confirm: "Are you sure?" } %></td>
  </tr>
<% end %>
</table>
<br>
<%= link_to "New book", new_book_path %>

服务器处理完这样的模板后,会向浏览器发送文本响应。以下是一个HTTP(版本1/1.1)响应的文本表示示例:

> GET / HTTP/1.1
> Host: example.com
> User-Agent: curl/7.51.0
> Accept: */*
< HTTP/1.1 200 OK
< Cache-Control: max-age=604800
< Content-Type: text/html
< Date: Mon, 01 May 2017 16:34:13 GMT
< Etag: "359670651+gzip+ident"
< Expires: Mon, 08 May 2017 16:34:13 GMT

综上所述,通过将Redux与React集成,能够更好地管理应用状态,而服务器端渲染则为应用带来了更好的性能和SEO等优势。在实际开发中,可以根据具体需求选择合适的技术和方法。

5. 服务器端渲染的优势与应用场景分析

服务器端渲染具有多方面的优势,以下为详细分析:
- 性能优化 :SSR可以在服务器端生成完整的HTML页面,浏览器接收到后可以直接渲染,减少了客户端的加载时间,尤其对于性能较差的设备和网络环境,能显著提升用户体验。
- SEO友好 :搜索引擎爬虫可以直接抓取服务器端生成的HTML内容,有利于提高网站在搜索引擎中的排名。
- 首屏加载速度快 :用户在请求页面时,能更快地看到完整的页面内容,而不是等待客户端JavaScript加载和执行后再渲染。

然而,服务器端渲染并非适用于所有场景,以下是一些适用和不适用的场景分析:
| 适用场景 | 不适用场景 |
| ---- | ---- |
| 对SEO要求较高的网站,如新闻网站、电商网站等 | 对实时性要求极高的应用,如在线游戏、实时聊天等 |
| 内容更新不频繁但需要快速加载的页面 | 简单的静态页面,使用SSR会增加服务器负担 |
| 客户端性能有限的设备访问的网站 | 开发资源有限,无法承担SSR开发和维护成本的项目 |

6. 过渡到React Router

为了更好地支持服务器端渲染和未来的改进,可以将现有的路由设置过渡到React Router。React Router是一个用于React应用的强大路由库,提供了丰富的功能和灵活的配置选项。

以下是过渡到React Router的基本步骤:
1. 安装React Router :使用npm或yarn安装React Router库。

npm install react-router-dom
  1. 创建路由配置 :在应用中创建路由配置文件,定义不同路径对应的组件。
import React from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import Home from './pages/Home';
import About from './pages/About';

const AppRouter = () => {
    return (
        <Router>
            <Routes>
                <Route path="/" element={<Home />} />
                <Route path="/about" element={<About />} />
            </Routes>
        </Router>
    );
};

export default AppRouter;
  1. 在应用中使用路由 :在根组件中引入路由配置。
import React from 'react';
import ReactDOM from 'react-dom/client';
import AppRouter from './AppRouter';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<AppRouter />);
7. 使用React Router处理认证路由

在实际应用中,有些页面需要用户登录后才能访问,这就需要处理认证路由。以下是使用React Router处理认证路由的示例:

import React from 'react';
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
import Home from './pages/Home';
import Dashboard from './pages/Dashboard';
import Login from './pages/Login';

// 模拟用户认证状态
const isAuthenticated = false;

const PrivateRoute = ({ children }) => {
    return isAuthenticated ? children : <Navigate to="/login" />;
};

const AppRouter = () => {
    return (
        <Router>
            <Routes>
                <Route path="/" element={<Home />} />
                <Route path="/login" element={<Login />} />
                <Route path="/dashboard" element={
                    <PrivateRoute>
                        <Dashboard />
                    </PrivateRoute>
                } />
            </Routes>
        </Router>
    );
};

export default AppRouter;

在上述代码中, PrivateRoute 组件用于检查用户是否已认证,如果未认证则重定向到登录页面。

8. 服务器端渲染过程中获取数据

在服务器端渲染过程中,可能需要获取一些数据来填充页面。可以使用异步操作来获取数据,并在服务器端等待数据加载完成后再生成HTML页面。

以下是一个简单的示例,展示如何在服务器端渲染过程中获取数据:

import React from 'react';
import ReactDOMServer from 'react-dom/server';
import axios from 'axios';
import Home from './pages/Home';

const serverRender = async () => {
    try {
        // 模拟异步数据获取
        const response = await axios.get('https://api.example.com/data');
        const data = response.data;

        // 将数据传递给组件
        const html = ReactDOMServer.renderToString(<Home data={data} />);

        return html;
    } catch (error) {
        console.error('Error fetching data:', error);
        return '<div>Error fetching data</div>';
    }
};

export default serverRender;

在上述代码中,使用 axios 库发送异步请求获取数据,然后将数据传递给 Home 组件,并使用 ReactDOMServer.renderToString 方法将组件渲染为HTML字符串。

9. 在服务器端渲染过程中使用Redux

在服务器端渲染过程中使用Redux可以更好地管理应用状态。以下是在服务器端渲染过程中使用Redux的基本步骤:
1. 创建Redux store :在服务器端创建Redux store,并初始化状态。

import { createStore } from 'redux';
import rootReducer from './reducers';

const initialState = {};
const store = createStore(rootReducer, initialState);

export default store;
  1. 在服务器端获取数据并更新store :在服务器端获取数据后,使用action creators更新store的状态。
import axios from 'axios';
import { fetchDataSuccess } from './actions';
import store from './store';

const serverRender = async () => {
    try {
        const response = await axios.get('https://api.example.com/data');
        const data = response.data;

        // 更新store的状态
        store.dispatch(fetchDataSuccess(data));

        const html = ReactDOMServer.renderToString(
            <Provider store={store}>
                <Home />
            </Provider>
        );

        return html;
    } catch (error) {
        console.error('Error fetching data:', error);
        return '<div>Error fetching data</div>';
    }
};

export default serverRender;
  1. 将store状态传递给客户端 :在服务器端将store的状态传递给客户端,以便客户端可以继续使用相同的状态。
const html = ReactDOMServer.renderToString(
    <Provider store={store}>
        <Home />
    </Provider>
);

const preloadedState = store.getState();

const responseHtml = `
    <!DOCTYPE html>
    <html>
        <head>
            <title>My App</title>
        </head>
        <body>
            <div id="root">${html}</div>
            <script>
                window.__PRELOADED_STATE__ = ${JSON.stringify(preloadedState).replace(/</g, '\\u003c')}
            </script>
            <script src="client.js"></script>
        </body>
    </html>
`;

res.send(responseHtml);

通过以上步骤,可以在服务器端渲染过程中有效地使用Redux管理应用状态。

综上所述,通过将Redux与React集成、使用服务器端渲染以及过渡到React Router等技术,可以构建出性能更优、功能更强大的Web应用。在实际开发中,需要根据具体需求和场景选择合适的技术和方法,并不断优化和改进应用。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值