设计思想
(1)Web 应用是一个状态机,视图与状态是一一对应的。
(2)所有的状态,保存在一个对象里面。
基本原则
- 整个应用只有唯一一个可信数据源,也就是只有一个 Store
- State 只能通过触发 Action 来更改
- State 的更改必须写成纯函数,也就是每次更改总是返回一个新的 State,在 Redux 里这种函数称为 Reducer
Redux工作流
store 数据仓库,通过createStore创建状态树store,通过订阅store.subscribe将state传给UI组件,用于页面渲染, 如果想改变state, 可以通过dispatch分发action, 传给store, store将旧数据的state和action传给reducer进行数据处理,返回新的state, 只要state一改变,就又会触发订阅subscribe。
下面以切换语言为例来:
首先npm 安装redux, 我这里用到了i18n语言包,所以安装了i18next和react-i18next.
npm install redux i18next react-i18next --save
// react-i18next官网:https://react.i18next.com/guides/quick-start
然后创建redux/store.ts文件
//store.ts
import { createStore } from "redux";
import languageReducer from "./language/languageReducer";
/**
* 创建store数据中心
* store只读,不做其他处理
*/
const store = createStore(languageReducer);
export default store;
createStore需要传入一个reducer,创建redux/lanuage/languageReducer.ts文件
//languageReducer.ts
import i18n from "i18next";
import { CHANGE_LANGUAGE, ADD_LANGUAGE, LanguageActionTypes } from "./languageActions";
export interface LanguageState{
language: "zh" | "en";
languageList : {name: string, code: string}[];
}
const defaultState: LanguageState ={
language:"zh",
languageList: [
{name: "中文", code: "zh"},
{name: "English", code: "en"}
]
}
export default (state = defaultState, action: LanguageActionTypes) => {
switch (action.type) {
case CHANGE_LANGUAGE:
//i18n语言切换
i18n.changeLanguage(action.payload);
return {...state, language: action.payload}
case ADD_LANGUAGE:
return {...state, languageList: [...state.languageList, action.payload]}
default:
return state
}
}
创建redux/lanuage/languageActions.ts文件,将所有的action都放在这个文件中
//lanuageActions.ts
export const CHANGE_LANGUAGE = 'change_language' ;
export const ADD_LANGUAGE = 'add_language' ;
interface ChangeLanguageAction {
type: typeof CHANGE_LANGUAGE;
payload: "zh" | "en";
}
interface AddLanguageAction{
type: typeof ADD_LANGUAGE;
payload: { name: string, code : string};
}
export type LanguageActionTypes = ChangeLanguageAction | AddLanguageAction;
export const changeLanguageActionCreator = (languageCode : "zh"|"en") : ChangeLanguageAction => {
return {
type: CHANGE_LANGUAGE,
payload : languageCode
}
}
export const addLanguageActionCreator = (name: string, code: string) : AddLanguageAction => {
return {
type: ADD_LANGUAGE,
payload: { name, code}
}
}
接下来是在类组件中应用订阅store和分发dispatch, 创建pages/Header.class.tsx文件
//Header.class.tsx
import React from "react";
import style from "./Header.module.css";
import logo from "../../assets/images/logo.svg";
import { Layout, Typography, Input,Button, Dropdown,Menu } from "antd";
import { GlobalOutlined} from '@ant-design/icons';
import {withRouter, RouteComponentProps} from 'react-router-dom';
import store from '../../redux/store';
import {LanguageState} from '../../redux/language/languageReducer';
import { withTranslation, WithTranslation } from "react-i18next";
import { addLanguageActionCreator, changeLanguageActionCreator } from "../../redux/language/languageActions";
interface State extends LanguageState{}
class HeaderComponent extends React.Component<RouteComponentProps & WithTranslation, State>{
constructor(props) {
super(props);
const storeState = store.getState();
this.state = {
language: storeState.language,
languageList: storeState.languageList
}
}
handleStoreChange = ()=>{
const storestate = store.getState();
this.setState({
language : storestate.language,
languageList: storestate.languageList,
})
}
menuClickHander = (e)=>{
if(e.key === 'new'){
//分发
const action = addLanguageActionCreator('新语言', 'new_lang');
store.dispatch(action);
}else{
//分发
const action = changeLanguageActionCreator(e.key);
store.dispatch(action);
}
}
componentDidMount(){
//订阅
store.subscribe(this.handleStoreChange);
}
render(){
// const { history} = this.props;
const { t } = this.props;
return <div className={style.appHeader}>
<div className={style.inner}>
<Typography.Text >{ t('header.slogan') }</Typography.Text>
<Dropdown.Button
overlay={
<Menu onClick={this.menuClickHander}>
{this.state.languageList.map(l => <Menu.Item key={l.code}>{l.name}</Menu.Item>)}
<Menu.Item key={'new'}>{t('header.add_new_language')}</Menu.Item>
</Menu>
}
icon={<GlobalOutlined/>}
style={{marginLeft:15}}
>
{this.state.language === 'en' ? 'English' : '中文'}
</Dropdown.Button>
<Button.Group className={style.buttonGroup}>
<Button>{ t('header.register') }</Button>
<Button>{ t('header.signin') }</Button>
</Button.Group>
</div>
<Layout.Header className={style.inner}>
<img src={logo} alt="" className={style.appLogo}/>
<Typography.Title level={3} className={style.title} >{ t('header.title') }</Typography.Title>
<Input.Search placeholder="请输入旅游的目的地" className={style.inputSearch}/>
</Layout.Header>
<Menu className={style.navMenu} mode={'horizontal'}>
<Menu.Item key={1}>{t('header.home_page')}</Menu.Item>
<Menu.Item key={2}>{t('header.weekend')}</Menu.Item>
<Menu.Item key={3}>{t('header.group')}</Menu.Item>
<Menu.Item key={4}>{t('header.backpack')}</Menu.Item>
<Menu.Item key={5}>{t('header.private')}</Menu.Item>
<Menu.Item key={6}>{t('header.cruise')}</Menu.Item>
<Menu.Item key={7}>{t('header.hotel')}</Menu.Item>
</Menu>
</div>
}
}
export const Header = withTranslation()(withRouter(HeaderComponent));
以下是i18n的配置--------------------------------------------------------------------------------------------------------------------------------------------------
i18n的配置文件config.ts
// i18n/config.ts
import i18n from "i18next";
import { initReactI18next } from "react-i18next";
import translation_zh from './zh.json';
import translation_en from './en.json';
const resources = {
en: {
translation: translation_en
},
zh: {
translation: translation_zh
}
};
i18n
.use(initReactI18next) // passes i18n down to react-i18next
.init({
resources,
lng: "zh",
// keySeparator: false, // we do not use keys in form messages.welcome
interpolation: {
escapeValue: false // react already safes from xss
}
});
export default i18n;
zh.json文件
// i18n/zh.json
{
"header": {
"slogan": "让旅行更幸福",
"add_new_language": "添加新语言",
"title": "React 旅游网",
"search_placeholder": "请输入旅游的目的地",
"register":"注册",
"signin":"登陆",
"home_page": "旅游首页",
"weekend": "周末游",
"group": "跟团游",
"backpack": "自由行",
"private": "私家团",
"cruise": "邮轮",
"hotel": "酒店+景点",
"local": "当地玩乐",
"theme": "主题游",
"custom": "定制游",
"study": "游学",
"visa":"签证",
"enterprise":"企业游",
"high_end":"高端游",
"outdoor":"爱玩户外",
"insurance":"保险"
},
"footer": {
"detail" : "版权所有 @ React 旅游网"
},
"home_page": {
"hot_recommended": "爆款推荐",
"new_arrival": "新品上市",
"domestic_travel": "国内游推荐",
"joint_venture": "合作企业",
"start_from": "(起)"
}
}
在App.tsx文件中引入import './i18n/config';就可以了