项目开始前的配置,除了新建项目的步骤之外,主要以环境和 Redux 相关为主。
这里主要使用 yarn 去下载与更新依赖,主要是因为使用 npm 的命令数次都无法启动,但是使用 yarn 就没有问题。
项目的分析参见:项目开始前的准备工作
配置完的的代码在:React 实战系列-电商项目的搭建与配置。
新建项目
新建项目的步骤还是比较简单的,主要分为三步:
-
使用官方提供的 CRA(create-react-app) 脚手架去新建一个 React 项目 C:> npx create-react-app front-proj --template typescript
-
安装项目依赖包 这一步主要也是根据上一篇 [项目开始前的准备工作] 中的技术栈进行的安装。 C:front-proj> yarn add antd axios moment redux react-redux react-router-dom redux-saga connected-react-router redux-devtools-extension @types/react-redux @types/react-router-dom
-
在 public/index.html 中引入 antd CSS 的 CDN <head>
<link
rel="stylesheet"
href="https://cdn.bootcdn.net/ajax/libs/antd/4.8.3/antd.min.css"
/>
</head>
官方文档的使用方法是在 src/App.css 文件中进行引入 antd 的样式,不过部署环境下最好是使用 CDN。 官方的引入方式如下: @import '~antd/dist/antd.css';
环境配置与读取
主要分为在 .env (dotenv)文件 中将配置写入到项目中,与在项目中读取 .env 文件 中的配置文件,两个步骤。
环境配置
这个环境主要指的是 .env 文件 的配置,该文件处于项目的根目录处,即和 public 与 src 同级。.env 文件内容为:
FAST_REFRESH=false
REACT_APP_PRODUCTION_API_URL=http://someproduction.com/api
REACT_APP_DEVLOPMENT_API_URL=http://localhost/api
其中,FAST_REFRESH=false 是为了让 React 能够成功实现热刷新,只需要在 v17 以上版本进行配置,其原因在 React v17 热刷新 不起作用 中有所简述。
REACT_APP_PRODUCTION_API_URL 与 REACT_APP_DEVLOPMENT_API_URL 是为了导出环境变量。CRA(create-react-app) 脚手架内置了 dotenv,因此开发者可以在项目中自行配制环境变量,但是 CRA 同时规定环境变量的命名规范必须遵从 REACT_APP_APINAME ,即以 REACT_APP_ 开头。
读取环境配置
在 src 下新建一个 config.ts 文件去进行配置的导出,对 .env 文件 中导出的配置获取方法如下:
export let API: string;
if (process.env.NODE_ENV === 'development') {
API = process.env.REACT_APP_DEVLOPMENT_API_URL!;
} else if (process.env.NODE_ENV === 'production') {
API = process.env.REACT_APP_PRODUCTION_API_URL!;
}
确实,还有一种做法是直接在项目中获取 process.env.REACT_APP_DEVLOPMENT_API_URL 的方式去获得 API,不过那会花费很多代码去在不同的地方写判断:
if (process.env.NODE_ENV === 'development') {
} else {
}
与其这样,不如直接在配置文件中导出全局可用的 API 变量,之后再新增其他的环境:如 QA,也不需要改变项目中的代码。
测试环境配置
主要是通过在 index.tsx 中导入一下 API,使用 console.log() 在终端中输出 API 的值进行确认。
代码实现如下:
import { API } from './config';
console.log(API);
随后运行 yarn start 开启开发环境:
C:front-proj> yarn start
结果如下:
使用 yarn build 去进行生产环境的打包,随后使用在 项目开始前的准备工作 中介绍过的 http-server 去运行生产环境:
C:front-proj> http-server build
结果如下:
可以看到,通过 yarn start 运行的本地开发环境中,API 的值是 http://localhost/api ;而通过 http-server 运行打包好的生产环境代码中,API 的值就是 http://someproduction.com/api ,正如 .env 文件中配置的那样。
组件实现
目前的核心组件总共分为 3 个:Layout(布局),Home(主页) 与 ShopCart(购物车) 三个页面,并且目前还不涉及核心业务,只是简单渲染一下页面。
Layout
Layout 是一个布局的页面,其主要意义还是对 Header、Footer 之类的可公用组件进行封装,不至重复写同样的代码。
实现如下:
import { FC } from 'react';
interface Props {
children: React.ReactNode;
}
const Layout: FC<Props> = ({ children }) => {
return <div>Layout {children}</div>;
};
export default Layout;
注*:这个实现方法使用的是 函数式组件的方法。类的实现方法之前在其他的案例中写过,实现类似于:
import React from 'react';
import Header from '../header/index';
export default function HeaderFooterHOC(WrappedComp) {
class HOC extends React.Component {
render() {
return (
<>
<Header />
<WrappedComp />
<Footer />
</>
);
}
}
return HOC;
}
可以看出来,使用 Hook(钩子函数) 比 类函数(class based components) 要简洁很多。
Home
Home 与 ShopCart 的实现方法相对而言都更简单一些,现在只需要渲染一个标识即可。
import Layout from './Layout';
const Home = () => {
return <Layout>Home</Layout>;
};
export default Home;
渲染效果如下:
可以看到,Layout 中的内容和 Home 的内容拼接起来了。
ShopCart
ShopCart 的实现方法也一样:
import Layout from './Layout';
const ShopCart = () => {
return <Layout>ShopCart</Layout>;
};
export default ShopCart;
渲染结果如下:
路由配置
稍微详细一点的笔记在这里:React Router 的基本应用,这里主要就是讲实现了。
因为白屏无法显示页面内容这一问题,这里会使用 HashRouter 去实现。
新增 Route.tsx
用来匹配 Router 和 Component,实现:
import { HashRouter, Route, Switch } from 'react-router-dom';
import Home from './components/core/Home';
import ShopCart from './components/core/ShopCart';
const Routes = () => {
return (
<HashRouter>
<Switch>
<Route path="/" component={Home} exact />
<Route path="/shopcart" component={ShopCart} />
</Switch>
</HashRouter>
);
};
export default Routes;
App.tsx 修改路由
依旧只是个人偏好问题,我喜欢把 Application 相关的配置放到 App.tsx,其他的配置放到 index.tsx 中。
import Routes from './Routes';
function App() {
return <Routes />;
}
export default App;
配置完成后效果如下:
图中使用的是 React 提供的开发工具,用于 Debug。注意被红框圈出来的地方,可以随着页面的变动,组件也从 Home 变更为乐 Shopcart。
使用 React Developer Tools 可以较为直观的展现出当前页面中的嵌套关系,也能够更为直观地展示页面中的组建的 state 和 props。
ShopCart 与 Home 包含的 state 和 props 对比为:
ShopCart | Home |
---|
| |
ShopCart 下的 Layout 与 Home 下的 Layout 的对比为:
Shopcart | Home |
---|
| |
仔细判别能够看到,内容结构基本上是相似的,不同之处也在图中显示了出来,如 Home 和 ShopCart 中的 location > pathname ,Layout 中的 Rendered by 的内容中,也分别显示了 Home 和 ShopCart 。这也是因为 Layout 分别由 Home 和 ShopCart 两个组件渲染了,的确是 被渲染(rendered by) 两个不同的组件。
Redux 配置
暂时不包含 Redux Saga 的配置,现在还没有需要 dispatch 的 action,所以暂时只配置 Reducer。
Redux 的基础学习之前做过笔记:一文快速上手 Redux。
这里总结一下 Redux 的特性:Redux 有三个最重要的特性:状态树,Action,Reducer 和 Dispatch。Redux 通过更新 一个 不可变状态树 来进行状态的管理;在 Redux 中,action 是用来描述状态变化;dispatch 用来发送一个 action,触发状态的变化;Reducer 最终实现状态的变化。
基础 Redux 的配置
基本的 Reducer 的结构是这样的:
|- src
| |- store
| | |- reducers
| | | |- reducers.ts
| | | |- index.ts
| | |- index.ts
所以,基础 Redux 也会按照这个配置实现。
reducers.ts
测试用 reducer,暂时只是导出一个状态作为测试。这里的 reducer 的文件名为 test.reducers.ts 。实现为:
export default function testReducer(state: number = 0) {
return state;
}
store/reducers/index.ts
这个文件负责组合所有的 reducers 中导出的状态,并且会根据 发送(dispatch) 的 actions 去更新 Redux 树。在还没有实现任何的 action,现在只是单纯的结合所有的状态。
实现如下:
import { combineReducers } from 'redux';
import testReducer from './test.reducer';
const rootReducer = combineReducers({
test: testReducer,
});
export default rootReducer;
store/index.ts
这里是用来创建 store,也就是管理的地方。如果要应用 中间件(middleware),就可以在这里实现。
实现如下:
import { createStore } from 'redux';
import rootReducer from './reducers';
const store = createStore(rootReducer);
export default store;
修改 App.tsx 应用 Redux 变化
主要还是在外面包一层由 react-redux 提供的 Provider,并且将 store 作为参数传给 Provier。
实现如下:
import { Provider } from 'react-redux';
import Routes from './Routes';
import store from './store';
function App() {
return (
<Provider store={store}>
<Routes />
</Provider>
);
}
export default App;
修改 Home.tsx 查看 Redux 变化
这里会用到 react-redux 提供的一个钩子函数:useSelector,去获取状态,并且在页面上输出状态。写入的状态是 state: number = 0 ,并且在 combineReducers 中声明的是 test: testReducer ,输出的结果应该就是 {test: 0} 。
实现如下:
import { useSelector } from 'react-redux';
import Layout from './Layout';
const Home = () => {
const state = useSelector((state) => state);
return <Layout>Home {JSON.stringify(state)}</Layout>;
};
export default Home;
页面效果如下:
可以看到状态这部分,除了 props 之外,还多了一个 hooks,hooks 下面也多了一个 Selector 的对象,其中获取的就是导出的状态;同时,页面上也输出了 Selector 中获取的值。
至此可以证明基础的 Redux 已经配置好了,接下来就需要配置 Redux 的其他中间件/工具,为后面的开发提供便利。
connected-react-router 的配置
使用 connected-react-router 主要是为了能够更方便的使用 history 相关属性。react-router 有一个奇怪的特性,那就是只有被配置到 Routes.tsx 中的组件可以直接访问 history 的属性:
ShoprCart | ShoprCart > Layout |
---|
| |
而子组件想要访问 props > history 只能通过应用 withRouther 包裹住组件。当层叠的组件多了之后,一层层套用 withRouter 就会变得非常的麻烦。
使用 connected-react-router 就可以直接在子组件访问 history 属性,应用起来也就更加的方便。
这里的配置都是跟着 npmjs 上的步骤实现的,官方已经停手把手的了。
修改 root reducer 文件
即 store/reducers/index.ts 中的内容,原本是一个对象对象,现在将其修改为一个可以接受 history 作为参数的函数。
实现如下:
import { connectRouter } from 'connected-react-router';
import { History } from 'history';
import { combineReducers } from 'redux';
import testReducer from './test.reducer';
const createRootReducer = (history: History) =>
combineReducers({
test: testReducer,
router: connectRouter(history),
});
export default createRootReducer;
修改 store 文件
即 store/index.ts ,本来函数中接受的是一个对象,现在需要接受一个参数,并传一个 history 作为参数。
实现如下:
import { routerMiddleware } from 'connected-react-router';
import { createHashHistory } from 'history';
import { createStore } from 'redux';
import createRootReducer from './reducers';
export const history = createHashHistory();
const store = createStore(createRootReducer(history));
export default store;
修改 App.tsx 应用 connected router
import { ConnectedRouter } from 'connected-react-router';
import { Provider } from 'react-redux';
import Routes from './Routes';
import store, { history } from './store';
function App() {
return (
<Provider store={store}>
<ConnectedRouter history={history}>
<Routes />
</ConnectedRouter>
</Provider>
);
}
export default App;
新旧 Layout 的对比:
原本的 Layout | 新的 Layout |
---|
| |
可以看到对比起原本的 props > children ,引用了 connected-react-router 的对象中多出了一个包含 history 属性的对象。
页面的输出结果也变了:
能够看到 JSON.stringify(state) 中的值,新增了 history 属性。
Redux DevTools 的配置
现在的问题就在于,一旦想要开始 Debug,很难直接获得 Redux 中的值。直接在页面中输出也不是不行,只是测试起来还是相对比较麻烦,也很难追踪由状态变化引起 Redux 的变化。
而 Redux DevTools 也是需要配制的,这里修改的东西不是很多,只有 store/index.ts 下的内容。本质上来说,添加 Redux DevTools 就是增添一个 中间件(middleware)。
实现方式如下:
import { routerMiddleware } from 'connected-react-router';
import { createHashHistory } from 'history';
import { applyMiddleware, createStore } from 'redux';
import { composeWithDevTools } from 'redux-devtools-extension';
import createRootReducer from './reducers';
export const history = createHashHistory();
const store = createStore(
createRootReducer(history),
composeWithDevTools(applyMiddleware(routerMiddleware(history)))
);
export default store;
之后,就能在浏览器提供的 Redux DevTools 中看到结果:
Redux DevTools 的优势在适配完 Action 后,会有更显著的效果。通过下面的方向键可以查看状态在不同时间的变化。
Redux Logger
Redux Logger 是我私人挺喜欢的一个中间件,主要可以把 Redux 的状态输出到命令行中,属于一个在开发状态中使用还挺友好的中间件。
个人觉得,Redux DevTools 跟 Redux Logger 可以二选一,而 Redux DevTools 相对而言应用范围更加的广泛。
安装方式如下:
C:front-proj> yarn add --dev @types/redux-logger
JavaScript 上次更新还是很久之前的事情了,TypeScript 的版本更新还是挺勤快的,使用方式也很简单,依旧只需要配置 store/index.ts :
import logger from 'redux-logger';
const store = createStore(
composeWithDevTools(applyMiddleware(routerMiddleware(history), logger))
);
渲染结果:
|