一、项目基础构建
-
1、创建一个文件,并且初始化
npm init -y tsc --init touch .gitignore
-
2、安装依赖包
# 基础的包 npm install react react-dom @types/react @types/react-dom react-router-dom @types/react-router-dom # 安装关于webpack的包 npm install webpack webpack-cli webpack-dev-server html-webpack-plugin -D # 安装处理ts的 npm i typescript ts-loader source-map-loader -D # 安装redux相关的 npm install redux react-redux @types/react-redux redux-thunk redux-logger @types/redux-logger redux-promise @types/redux-promise # 安装路由与redux连接的库 npm install connected-react-router # 处理样式的 npm install style-loader css-loader less-loader less -D # 处理前缀的 npm install autoprefixer postcss-loader -D # 处理图片地址类的 npm install url-loader file-loader lib-flexible -D # px转换rem(看需要安装,仅用于手机网站) npm install px2rem-loader # ajax请求库 npm install axios # react轮播图组件库(看需要安装) npm install react-swipe @types/react-swipe # react动画库(看需要安装) npm install react-transition-group @types/react-transition-group
-
3、修改
tsconfig.json
文件{ "compilerOptions": { "outDir": "./dist", /*指定输出的目录*/ "sourceMap": true, /*把ts文件编译成js文件的时候,同时生成对应的sourceMap文件*/ "noImplicitAny": true, /*如果为true的话,ts编译器无法推断出类型的时候,依然会编译成js文件,但是会*/ "module": "commonjs", /*规范*/ "target": "es5", /*转换为es5*/ "jsx": "react", "esModuleInterop": true, "baseUrl": ".", // 查找非相对路径的模块的起始位置 "paths": { // 定义别名 "@/*": [ "src/*" ] } }, "include": [ /*需要编译的文件*/ "./src/**/*" ] }
-
4、配置
webpack
文件项目下创建一个
webpack.config.js
的文件const path = require('path'); const webpack = require('webpack'); const HtmlWebpackPlugin = require('html-webpack-plugin'); module.exports = { mode: process.env.NODE_ENV === 'production' ? 'production' : 'development', entry: './src/index.tsx', output: { path: path.resolve(__dirname, 'dist'), filename: 'bundle.js', }, devtool: 'source-map', resolve: { // 定义别名 alias: { '@': path.resolve(__dirname, 'src'), '~': path.resolve(__dirname, 'node_modules') }, // 当你加载一个文件的时候,没有指定扩展名的时候,会自动寻找哪些扩展名 extensions: ['.ts', '.tsx', '.js', '.json'] }, module: { rules: [ { test: /\.(j|t)sx?/, loader: 'ts-loader', options: { transpileOnly: true, //只编译不检查 compilerOptions: { module: 'es2015' } } }, { test: /\.css$/, use: [ 'style-loader', { loader: 'css-loader', options: { importLoaders: 0 } }, //{ // loader: 'postcss-loader', // options: { // plugins: [require('autoprefixer')] // } //} ] }, { test: /\.less$/, use: [ 'style-loader', { loader: 'css-loader', options: { importLoaders: 0 } }, //{ // loader: 'postcss-loader', // options: { // plugins: [require('autoprefixer')] // } //}, // 如果是手机端就要配置 // { // loader: 'px2remote-loader', // options: { // remUnit: 75, // 基础尺寸 // remPrecesion: 8 // 精确到多少位 // } // }, 'less-loader', ] }, // 处理图片类 { test: /\.(jpg|png|gif|svg|jpeg)$/, use: ['url-loader'] } ] }, plugins: [ new HtmlWebpackPlugin({ template: './src/index.html', }), // 热更新 new webpack.HotModuleReplacementPlugin(), ], devServer: { hot: true, contentBase: path.join(__dirname, 'dist'), open: false, port: 3000, historyApiFallback: { // browserHistory的时候,刷新会报404. 自动重定向到index.html index: './index.html' } } }
-
5、创建一个
src
目录,并且创建两个文件index.html
和index.tsx
文件 -
6、在
package.json
中配置启动命令{ ... "scripts": { "dev": "webpack-dev-server", "build": "webpack" }, }
-
7、在
src/index.tsx
的文件中写上react
代码import React from 'react'; import ReactDOM from 'react-dom'; ReactDOM.render( <div>我是使用react创建的</div>, document.getElementById('root') )
-
8、在
src/index.html
中要写一个基本的容器<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>手动搭建react项目</title> </head> <body> <div id="root"></div> </body> </html>
-
9、如果你是手机网站就要加上适配的(看项目需求来写的)
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>手动搭建react项目</title> </head> <body> <script> const docEl = document.documentElement; function setRemUtils() { docEl.style.fontSize = docEl.clientWidth / 10 + 'px'; } setRemUtils(); window.addEventListener('resize', setRemUtils, false); </script> <div id="root"></div> </body> </html>
-
10、启动项目
二、react
结合redux
一起使用
-
1、在
src
下创建一个store
的文件夹 -
2、大体的目录结构
➜ store tree . ├── action-types.ts # 定义常量 ├── actions # 一个组件对应一个action ├── index.ts └── reducers # 一个组件对应一个reducer └── index.ts 2 directories, 3 files
-
3、在
store/index.ts
文件中import { createStore, applyMiddleware } from 'redux'; import logger from 'redux-logger'; import promise from 'redux-promise'; import thunk from 'redux-thunk'; import rootReducer from './reducers'; const store = applyMiddleware( promise, thunk, logger)(createStore)(rootReducer); // 挂载到window上,方便查看,可以不写 (<any>window).store = store; export default store;
-
4、
store/reducers/index.ts
文件用来整合各个组件中的reducer
import { ReducersMapObject, Reducer, combineReducers, AnyAction } from 'redux' // 各个组件的reducer的状态类型,可以单独到一个文件中 export interface CombinedState { } const reducers: ReducersMapObject<CombinedState, AnyAction> = { // 各个组件的reducer }; const rootReducers: Reducer<CombinedState, any> = combineReducers(reducers); export default rootReducers;
-
5、在
src/index.tsx
中使用store
import React from 'react'; import ReactDOM from 'react-dom'; import { Provider } from 'react-redux'; import store from './store'; ReactDOM.render( <Provider store={ store}> <div>我是使用react创建的</div> </Provider> , document.getElementById('root') )
三、测试store
是否配置成功
-
1、在
action-types.ts
中定义两个常量export const ADD_COUNT = 'ADD_COUNT'; export const MINUS_COUNT = 'MINUS_COUNT';
-
2、创建一个文件
src/store.counter.ts
的reducer
文件import { AnyAction } from 'redux'; import * as types from './../action-types'; export interface Counter { count: number } const initState: Counter = { count: 0, }; export default function (state: Counter = initState, action: AnyAction) { switch (action.type) { case types.ADD_COUNT: return { ...state, count: state.count + 1 }; case types.MINUS_COUNT: return { ...state, count: state.count - 1 }; default: return state; } }
-
3、在
src/store/reducers/index.ts
中引入组件的reducer
... import counter, { Counter } from './counter'; // 各个组件的reducer的状态类型 export interface CombinedState { counter: Counter, } const reducers: ReducersMapObject<CombinedState, AnyAction> = { // 各个组件的reducer counter, }; ...
-
4、定义
actions
文件import * as types from './../action-types'; export default { // 增加的方法 // payload表示触发该函数传递的参数 addCount(payload: number) { console.log('我是传递进来的 payload', payload); return { type: types.ADD_COUNT, payload, } }, // 减少的方法 minusCount(payload: number) { return { type: types.MINUS_COUNT, payload, } } }
-
5、创建一个组件
components/Counter.tsx
import React, { PropsWithChildren } from 'react'; import { connect } from 'react-redux'; import action from '@/store/actions/counter'; type Props = PropsWithChildren<ReturnType<typeof mapStateToProps> & typeof action>; // 随便定义一个,可以不写 type State = { [propsName: string]: any } class Counter extends React.Component<Props, State> { render() { console.log(this.props); return ( <> <button onClick={ this.props.minusCount}>-</button> { this.props.count} { /* 需要传递payload参数就要这样写 */} <button onClick={ () => this.props.addCount(10)}>+</button> </> ) } } const mapStateToProps = (state: any) => (state.counter) export default connect( mapStateToProps, action, )(Counter);
-
6、在
src/index.tsx
中使用该组件并且测试是否成功
四、浏览器配置Redux DevTools
观察状态的变化
-
1、谷歌浏览器安装插件
Redux DevTools
(不能上应用市场的要自己想办法) -
3、在
src/store/index.ts
中使用- 直接使用
... import { composeWithDevTools } from 'redux-devtools-extension'; // 采用另外一种写法 const store = createStore(rootReducer, composeWithDevTools(applyMiddleware(promise, thunk, logger))); ...
- 区分开发环境和生产环境的写法
// 开发环境使用工具及日志中间件 const enhancers = process.env.NODE_ENV === "development" ? composeWithDevTools( applyMiddleware(promise, thunk, logger) ) : applyMiddleware(promise, thunk); const store = createStore(rootReducer, enhancers);
-
4、运行效果图
五、优化代码,定义store
的类型约束
-
1、在项目创建一个文件夹
src/typings
-
2、创建一个文件
typings/counter.ts
约束刚刚的counter.tsx
组件的数据export interface CounterState { count: number }
-
3、创建一个文件
typings/state.ts
的文件import { CounterState } from '.'; // 各个组件的reducer的状态类型 export interface CombinedState { counter: CounterState, } export interface CounterPayload { count: number, }
-
4、精简
store/reducers/counter.ts
代码import { AnyAction } from 'redux'; import * as types from './../action-types'; import { CounterState } from '@/typings'; const initState: CounterState = { count: 0, }; export default function (state: CounterState = initState, action: AnyAction) { ... }
-
5、精简
store/reducers/index.ts
代码import { ReducersMapObject, Reducer, combineReducers, AnyAction } from 'redux' import counter from './counter'; import { CombinedState } from '@/typings'; const reducers: ReducersMapObject<CombinedState, AnyAction> = { // 各个组件的reducer counter, }; const rootReducers: Reducer<CombinedState, any> = combineReducers(reducers); export default rootReducers;
-
6、优化
store/actions/counter.ts
文件,让类型约束payload
import * as types from './../action-types'; import { CounterPayload } from '@/typings'; export default { // 增加的方法 // payload表示触发该函数传递的参数 addCount(payload: CounterPayload) { console.log('我是传递进来的 payload', payload); return { type: types.ADD_COUNT, payload, } }, // 减少的方法 minusCount() { // 如果需要传递参数的时候就加上payload,不传递的时候就不要加 return { type: types.MINUS_COUNT, } } }
-
7、在组件中也可以使用抽取出的类型约束
import React, { PropsWithChildren } from 'react'; import { connect } from 'react-redux'; import action from '@/store/actions/counter'; import { CombinedState } from '@/typings'; type Props = PropsWithChildren<ReturnType<typeof mapStateToProps> & typeof action>; // 随便定义一个,可以不写 type State = { [propsName: string]: any } class Counter extends React.Component<Props, State> { render() { ... } } // 使用类型约束 const mapStateToProps = (state: CombinedState) => (state.counter) export default connect( mapStateToProps, action, )(Counter)
六、使用redux-thunk
和axios
请求的使用
-
1、大体的流程
- 页面点击按钮,发送一个
action
- 在
action
中接收到事件后,发起ajax
请求 - 在
ajax
请求数据中,根据条件判断是否派发dispatch
(注意这个地方返回一个函数,函数中使用立即执行函数) - 在
reducer
中处理刚刚action
派发出来的types
将数据存放到state
中 - 在组件中使用刚刚
state
中的数据
- 页面点击按钮,发送一个
-
2、先在项目的
src
目录下创建一个utils
的文件夹,我们先对axios
简单的封装下// 实际业务中可能更加具体,现在只是简单的封装 import axios, { AxiosRequestConfig } from 'axios'; axios.defaults.baseURL = 'http://test.dancebox.cn/api/v1'; axios.defaults.headers.post['Content-Type'] = 'application/json;charset=UTF-8'; axios.interceptors.request.use((config: AxiosRequestConfig) => { let access_token = sessionStorage.getItem('access_token'); if (access_token) config.headers['Authorization'] = `Bearer ${ access_token}`; return config; }, (error: any) => Promise.reject(error)); //response拦截器里把AxiosResponse=>AxiosResponse.data axios.interceptors.response.use(response => response.data, error => Promise.reject(error)); export { axios };
-
3、在项目的
src
目录下创建一个api
专门来存放数据请求的方法import https from '@/utils/https'; // 定义请求接口 export const activityList = () => { return https.get('/front/activity'); }
-
4、在
reducers/index.ts
定义两个数据类型... export type StoreDispatch = Dispatch; export type StoreGetState = () => CombinedState; export default rootReducers;
-
5、在
actions/counter.ts
文件中请求活动数据import * as types from './../action-types'; import { CounterPayload } from '@/typings'; import { activityList } from '@/api'; import { StoreDispatch, StoreGetState } from '../reducers'; export default { ... // 使用ajax请求返回数据,因为我们使用redux-thunk,那么reducer可以返回一个函数 activityList() { return function (dispatch: StoreDispatch, getState: StoreGetState) { // 这里要使用async就要使用立即执行函数 (async function () { const response: { [propsName: string]: any } = await activityList(); const { code, message, result } = response; if (Object.is(code, 0)) { dispatch({ type: types.GET_ACTIVITY_DATA, payload: result, }) } else { console.log('获取数据失败', message); } })(); } } }
-
6、在
src/typings/counter.ts
文件中新增活动的export interface CounterState { count: number, activityListData: any[], // 新增存放活动列表的数据 }
-
7、处理
reducers/counter.ts
将actions
中dispath
的数据存到state
中import { AnyAction } from 'redux'; import * as types from './../action-types'; import { CounterState } from '@/typings'; const initState: CounterState = { count: 0, activityListData: [], }; export default function (state: CounterState = initState, action: AnyAction) { switch (action.type) { ... case types.GET_ACTIVITY_DATA: // 获取到请求的数据,放到store中 console.log(action.payload); return { ...state, activityListData: action.payload.data } default: return state; } }
-
8、在组件中使用
import React, { PropsWithChildren } from 'react'; import { connect } from 'react-redux'; import action from '@/store/actions/counter'; import { CombinedState } from '@/typings'; type Props = PropsWithChildren<ReturnType<typeof mapStateToProps> & typeof action>; // 随便定义一个,可以不写 type State = { [propsName: string]: any } class Counter extends React.Component<Props, State> { render() { console.log(this.props); return ( <> <button onClick={ this.props.activityList}>获取活动数据</button> <ul> { this.props.activityListData.map(item => { return ( <li key={ item.id}>{ item.title}</li> ) }) } </ul> </> ) } } const mapStateToProps = (state: CombinedState) => (state.counter) export default connect( mapStateToProps, action, )(Counter)
七、配置antd
前端UI
库
-
1、安装依赖包
npm install antd npm install ts-import-plugin
-
2、修改
webpack.config.js
的配置const tsImportPluginFactory = require('ts-import-plugin'); module.exports = { ... module: { rules: [ { test: /\.(j|t)sx?/, loader: 'ts-loader', options: { transpileOnly: true, //只编译不检查 getCustomTransformers: () => ({ // 获取或者说定义自定义的转换器 before: [tsImportPluginFactory({ 'libraryName': 'antd', // 对哪个模块进行按需加载 'libraryDirectory': 'es', // 按需加载的模块,如果实现按需加载,必须是ES Modules 'style': 'css' // 自动引入它对应的CSS })] }), compilerOptions: { module: 'es2015' } } }, ] }, ... }
-
3、在组件中使用
import React, { PropsWithChildren } from 'react'; import { connect } from 'react-redux'; import { Button } from 'antd'; import action from '@/store/actions/counter'; import { CombinedState } from '@/typings'; type Props = PropsWithChildren<ReturnType<typeof mapStateToProps> & typeof action>; // 随便定义一个,可以不写 type State = { [propsName: string]: any } class Counter extends React.Component<Props, State> { render() { console.log(this.props); return ( <> ... <Button type="primary" onClick={ this.props.activityList}>获取活动数据</Button> <ul> { this.props.activityListData.map(item => { return ( <li key={ item.id}>{ item.title}</li> ) }) } </ul> </> ) } } const mapStateToProps = (state: CombinedState) => (state.counter) export default connect( mapStateToProps, action, )(Counter)
-
4、配置
antd
的中文包(在src/index.tsx
文件中)import React from 'react'; import ReactDOM from 'react-dom'; import { Provider } from 'react-redux'; import { ConfigProvider } from 'antd' import zh_CN from 'antd/lib/locale-provider/zh_CN'; import store from './store'; import Counter from '@/components/Counter'; // 全局样式 import './assets/style/common.less'; ReactDOM.render( <Provider store={ store}> <ConfigProvider locale={ zh_CN}> <Counter /> </ConfigProvider> </Provider>, document.getElementById('root') )