手动使用typescript+webpack搭建react开发环境(一)

539 阅读4分钟

一、项目基础构建

  • 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.htmlindex.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.tsreducer文件

    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(不能上应用市场的要自己想办法)

  • 2、redux-devtools-extension插件地址

  • 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-thunkaxios请求的使用

  • 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.tsactionsdispath的数据存到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')
    )