webpack/react/redux/react-router/ts搭建架子

300 阅读5分钟

项目初始化

mkdir stage && cd stage #创建项目文件夹进入项目
npm init -y #初始化依赖
npm install -S react react-dom  #安装react相关依赖
npm install -D webpack webpack-cli webpack-dev-server #安装webpack相关依赖
npm install -D html-webpack-plugin clean-webpack-plugin #安装生成html和清理html文件的插件
npm install -D babel-loader @babel/core @babel/preset-env @babel/preset-react #安装babel-loader解析react
npm install -D less style-loader css-loader less-loader #安装less依赖及相关的开发loader
mkdir src config build #根目录下创建src、config、build文件夹
touch babel.config.js #根目录下创建babel.config.js
cd build && touch webpack.config.js #build目录下创建webpack.config.js
cd ../src && touch index.js && touch index.less && touch index.html #src目录下创建index.js、index.less和index.html

image.png

各文件内容

// build/webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');

module.exports = {
    entry: './src/index.js',
    output: {
        filename: 'bundle.js',
        path: path.resolve(__dirname, 'dist'),
    },
    devServer: {
        port: 3001,
    },
    devtool: 'inline-source-map',
    plugins: [
        new CleanWebpackPlugin(),
        new HtmlWebpackPlugin({
            template: './src/index.html',
        })
    ],
    module: {
        rules: [
            {
                test: /\.js$/,
                exclude: /node_modules/,
                loader: "babel-loader",
            },
            {
                test: /\.less$/,
                exclude: /node_modules/,
                use: [
                    'style-loader',
                    { loader: 'css-loader', options: { modules: true } },
                    'less-loader',
                ]
            },
        ]
    }
};
<!-- src/index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>STAGE</title>
</head>
<body>
    <div id="root"></div>
</body>
</html>
// src/index.js
import React from 'react'
import { render } from 'react-dom'
import styles from './index.less'
const App = () => (
    <div className={styles.hohoho}>STAGE HOHOHO</div>
)
render(<App />, document.getElementById('root'))
// src/index.less
.hohoho {
    color: #008000;
}
// babel.config.js
module.exports = {
    presets: [
        "@babel/preset-env",
        "@babel/preset-react",
    ],
}

demo跑起来

修改package.json,添加执行脚本

"scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "webpack-dev-server --config ./build/webpack.config.js --open",
    "build": "webpack --config ./build/webpack.config.js"
  },

此时执行npm run build可以看到build目录下生成了dist文件夹 ,npm start可以启动3001端口访问到写的index.js中的内容(如果有报错请检查依赖是否安装成功,注意webpack与webpack-cli的版本问题,例子中的5.27的webpack和4.5的webpack-cli就不能正常运行,降级webpack-cli为3.3.12解决

接入react-router

npm install -D react-router-dom #安装react-router-dom依赖

修改src/index.js文件,此处使用的是HashRouter,如果使用BrowserRouter需要服务端做相应的响应,原理可以对比hash路由和history的区别(可以分别使用两种Router,切换路由时看具体网络请求就明白了)

// src/index.js
import React from 'react'
import { render } from 'react-dom'
import {
  HashRouter,
  Route,
  Switch,
  Redirect,
} from 'react-router-dom'
import styles from './index.less'

const Home = () => (<div>HOME HOHOHO</div>)
const Page1 = () => (
  <div>PAGE1 HOHOHO</div>)
const Page2 = () => (
  <div>PAGE2 HOHOHO</div>)

const App = () => (
  <>
    <div className={styles.hohoho}>STAGE HOHOHO</div>
    <li><a href='#/home'>去home</a></li>
    <li><a href='#/page1'>去page1</a></li>
    <li><a href='#/page2'>去page2</a></li>
    <hr />
    <HashRouter>
      <Switch>
        <Route exact path='/home' component={Home} />
        <Route exact path='/page1' component={Page1} />
        <Route exact path='/page2' component={Page2} />
        <Redirect from='/' to='/home' />
      </Switch>
    </HashRouter>
  </>
)

render(<App />, document.getElementById('root'))

此时可以来回切换home、page1、page2三个页面

接入redux

npm install -S redux react-redux #安装redux相关依赖
cd src && mkdir models && cd models && mkdir stores actions reducers #在src目录下创建redux相关的文件夹,并分别在目录下创建index.js
cd stores && touch index.js && cd ../actions && touch index.js && cd ../reducers && touch index.js #分别创建index.js文件

image.png

各文件内容

// src/models/actions/index.js
export const CREATE_TODO = 'CREATE'; // 增加一个todo
export const DELETE_TODO = 'DELETE'; // 删除一个todo
export const CREATE_TYPE = 'CREATE_TYPE'; // 添加操作
export const DELETE_TYPE = 'DELETE_TYPE'; // 删除操作
// src/models/reducers/index.js
import {
  CREATE_TODO,
  DELETE_TODO,
  CREATE_TYPE,
  DELETE_TYPE,
} from '../actions'

export function todos(state = [], action) {
  switch (action.type) {
    case CREATE_TODO: {
      return [...state, { id: action.id, text: action.text, completed: false }]
    }
    case DELETE_TODO: {
      return [...state].filter(({ id }) => id !== action.id)
    }
    default: {
      return state;
    }
  }
}

export function operateCounter(state = { createCounter: 0, deleteCounter: 0 }, action) {
  const { createCounter, deleteCounter } = state;
  switch (action.type) {
    case CREATE_TYPE: {
      return { ...state, createCounter: createCounter + 1 }
    }
    case DELETE_TYPE: {
      return { ...state, deleteCounter: deleteCounter + 1 }
    }
    default: {
      return state;
    }
  }
}
// src/models/stores/index.js
import { combineReducers, createStore } from 'redux'
import * as reducers from '../reducers'

const todoApp = combineReducers(reducers)
export default createStore(todoApp)

修改src/index.js,里面的HOME,PAGE1,PAGE2组件应该分别抽离在不同的页面中

// src/index.js
import React from 'react'
import { render } from 'react-dom'
import {
  HashRouter,
  Route,
  Switch,
  Redirect,
} from 'react-router-dom'
import { Provider, connect } from 'react-redux'
import store from './models/stores'
import {
  CREATE_TODO,
  DELETE_TODO,
  CREATE_TYPE,
  DELETE_TYPE,
} from './models/actions'
import styles from './index.less'

const HomeOld = (props) => {
  const {
    todos = [],
    operateCounter: {
      createCounter = 0,
      deleteCounter = 0,
    },
  } = props;
  return (
    <>
      <div>HOME HOHOHO</div>
      <div>当前todos如下,可以在page1与page2中操作todos列表:</div>
      <div className={styles.hohoho}>添加操作: {createCounter} 次,删除操作: {deleteCounter} 次</div>
      {todos.map(({ text, id }) => (<li key={id}>{`id:${id}-text:${text}`}</li>))}
    </>
  )
}
const mapStateToPropsHome = state => {
  return {
    todos: state.todos,
    operateCounter: state.operateCounter,
  };
};
const Home = connect(mapStateToPropsHome)(HomeOld);

const Page1Old = (props) => {
  const { todos = [], dispatch } = props;
  let input;
  function onClick() {
    const { id = 0 } = [...todos].pop() || {};
    dispatch({
      type: CREATE_TODO,
      id: id + 1,
      text: input.value,
    });
    dispatch({ type: CREATE_TYPE });
  }
  return (
    <>
      <div>PAGE1 HOHOHO</div>
      <input ref={node => { input = node }} />

      <button onClick={onClick}>添加</button>
      {todos.map(({ text, id }) => (<li key={id}>{`id:${id}-text:${text}`}</li>))}
    </>
  )
}
const mapStateToPropsPage1 = state => {
  return {
    todos: state.todos,
  };
};
const Page1 = connect(mapStateToPropsPage1)(Page1Old);

const Page2Old = (props) => {
  const { todos = [], dispatch } = props;
  function onClick(id) {
    dispatch({
      type: DELETE_TODO,
      id,
    });
    dispatch({ type: DELETE_TYPE });
  }
  return (
    <>
      <div>PAGE2 HOHOHO</div>
      {todos.map(({ text, id }) => (
        <li key={id}>
          {`id:${id}-text:${text}`}

          <a href="javascript:;" onClick={onClick.bind(null, id)}>删除该项</a>
        </li>
      ))}
    </>
  )
}
const mapStateToPropsPage2 = state => {
  return {
    todos: state.todos,
  };
};
const Page2 = connect(mapStateToPropsPage2)(Page2Old);

const App = () => (
  <Provider store={store}>
    <div className={styles.hohoho}>STAGE HOHOHO</div>
    <li><a href='#/home'>去home</a></li>
    <li><a href='#/page1'>去page1</a></li>
    <li><a href='#/page2'>去page2</a></li>
    <hr />
    <HashRouter>
      <Switch>
        <Route exact path='/home' component={Home} />
        <Route exact path='/page1' component={Page1} />
        <Route exact path='/page2' component={Page2} />
        <Redirect from='/' to='/home' />
      </Switch>
    </HashRouter>
  </Provider>
)

render(<App />, document.getElementById('root'))

redux todolist完成

接入typescript

npm install -D @types/react @types/react-dom @types/react-router-dom @types/react-redux typescript ts-loader
npm install -g typescript
tsc -init

修改生成的tsconfig.json

{
  "compilerOptions": {
    "jsx": "react",
    "target": "es5", 
    "module": "commonjs", 
    "sourceMap": true,                     
    "removeComments": true,                
    "strict": true, 
    "noImplicitAny": true,                 
    "esModuleInterop": true, 
    "skipLibCheck": true, 
    "forceConsistentCasingInFileNames": true 
  },
  "include": [
    "src/**/*"
  ],
  "exclude": [
    "node_modules",
    "build",
  ]
}

在src目录下加入全局ts文件

mkdir types && cd types && touch global.d.ts #在src目录下创建types文件夹添加global.d.ts文件
// src/types/global.d.ts
declare module '*.svg'
declare module '*.png'
declare module '*.jpg'
declare module '*.jpeg'
declare module '*.gif'
declare module '*.bmp'
declare module '*.tiff'
declare module '*.less'

将原来的js/jsx都改为ts/tsx,并加入相应的ts变量、接口

// src/models/actions/index.ts
export const CREATE_TODO: string = 'CREATE'; // 增加一个todo
export const DELETE_TODO: string = 'DELETE'; // 删除一个todo
export const CREATE_TYPE: string = 'CREATE_TYPE'; // 添加操作
export const DELETE_TYPE: string = 'DELETE_TYPE'; // 删除操作
// src/models/reducers/index.ts
import {
  CREATE_TODO,
  DELETE_TODO,
  CREATE_TYPE,
  DELETE_TYPE,
} from '../actions'

interface TodoAction {
  type: string;
  id: number;
  text: string;
}
interface OperateAction {
  type: string;
}
export interface TodoState {
  id: number;
  text: string;
  completed: boolean;
}
export interface OperateState {
  createCounter: number;
  deleteCounter: number;
}

export function todos(state: TodoState[] = [], action: TodoAction) {
  switch (action.type) {
    case CREATE_TODO: {
      return [...state, { id: action.id, text: action.text, completed: false }]
    }
    case DELETE_TODO: {
      return [...state].filter(({ id }) => id !== action.id)
    }
    default: {
      return state;
    }
  }
}

export function operateCounter(state: OperateState = { createCounter: 0, deleteCounter: 0 }, action: OperateAction) {
  const { createCounter, deleteCounter } = state;
  switch (action.type) {
    case CREATE_TYPE: {
      return { ...state, createCounter: createCounter + 1 }
    }
    case DELETE_TYPE: {
      return { ...state, deleteCounter: deleteCounter + 1 }
    }
    default: {
      return state;
    }
  }
}
// src/models/stores/index.ts
import { combineReducers, createStore } from 'redux'
import * as reducers from '../reducers'

const todoApp = combineReducers(reducers)
export default createStore(todoApp)
// src/index.tsx
import React from 'react'
import { render } from 'react-dom'
import {
  HashRouter,
  Route,
  Switch,
  Redirect,
} from 'react-router-dom'
import { Provider, connect } from 'react-redux'
import { Dispatch } from 'redux'
import store from './models/stores'
import {
  CREATE_TODO,
  DELETE_TODO,
  CREATE_TYPE,
  DELETE_TYPE,
} from './models/actions'
import { TodoState, OperateState } from './models/reducers'
import styles from './index.less'

interface HomeProps {
  todos: TodoState[];
  operateCounter: OperateState;
  dispatch: Dispatch;
}

const HomeOld: React.FC<HomeProps> = (props) => {
  const {
    todos = [],
    operateCounter: {
      createCounter = 0,
      deleteCounter = 0,
    },
  } = props;
  return (
    <>
      <div>HOME HOHOHO</div>
      <div>当前todos如下,可以在page1与page2中操作todos列表:</div>
      <div className={styles.hohoho}>添加操作: {createCounter} 次,删除操作: {deleteCounter} 次</div>
      {todos.map(({ text, id }) => (<li key={id}>{`id:${id}-text:${text}`}</li>))}
    </>
  )
}
const mapStateToPropsHome = (state: HomeProps) => {
  return {
    todos: state.todos,
    operateCounter: state.operateCounter,
  };
};
const Home = connect(mapStateToPropsHome)(HomeOld);

const Page1Old: React.FC<HomeProps> = (props) => {
  const { todos = [], dispatch } = props;
  let input: HTMLInputElement | null;
  function onClick() {
    const { id = 0 } = [...todos].pop() || {};
    dispatch({
      type: CREATE_TODO,
      id: id + 1,
      text: (input as HTMLInputElement).value,
    });
    dispatch({ type: CREATE_TYPE });
  }
  return (
    <>
      <div>PAGE1 HOHOHO</div>
      <input ref={node => { input = node }} />

      <button onClick={onClick}>添加</button>
      {todos.map(({ text, id }) => (<li key={id}>{`id:${id}-text:${text}`}</li>))}
    </>
  )
}
const mapStateToPropsPage1 = (state: HomeProps) => {
  return {
    todos: state.todos,
  };
};
const Page1 = connect(mapStateToPropsPage1)(Page1Old);

const Page2Old: React.FC<HomeProps> = (props) => {
  const { todos = [], dispatch } = props;
  function onClick(id: number) {
    dispatch({
      type: DELETE_TODO,
      id,
    });
    dispatch({ type: DELETE_TYPE });
  }
  return (
    <>
      <div>PAGE2 HOHOHO</div>
      {todos.map(({ text, id }) => (
        <li key={id}>
          {`id:${id}-text:${text}`}

          <a href="javascript:;" onClick={onClick.bind(null, id)}>删除该项</a>
        </li>
      ))}
    </>
  )
}
const mapStateToPropsPage2 = (state: HomeProps) => {
  return {
    todos: state.todos,
  };
};
const Page2 = connect(mapStateToPropsPage2)(Page2Old);

const App = () => (
  <Provider store={store}>
    <div className={styles.hohoho}>STAGE HOHOHO</div>
    <li><a href='#/home'>去home</a></li>
    <li><a href='#/page1'>去page1</a></li>
    <li><a href='#/page2'>去page2</a></li>
    <hr />
    <HashRouter>
      <Switch>
        <Route exact path='/home' component={Home} />
        <Route exact path='/page1' component={Page1} />
        <Route exact path='/page2' component={Page2} />
        <Redirect from='/' to='/home' />
      </Switch>
    </HashRouter>
  </Provider>
)

render(<App />, document.getElementById('root'))

同时需要修改build/webpack.config.js,修改入口文件将原来的index.js改为index.tsx,添加resolve配置

// build/webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');

module.exports = {
  entry: './src/index.tsx',
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist'),
  },
  resolve: {
    extensions: ['.ts', '.tsx', '.js', '.jsx', '.json'],
  },
  devServer: {
    port: 3001,
  },
  devtool: 'inline-source-map',
  plugins: [
    new CleanWebpackPlugin(),
    new HtmlWebpackPlugin({
      template: './src/index.html',
    })
  ],
  module: {
    rules: [
      {
        test: /\.tsx?$/,
        exclude: /node_modules/,
        loader: 'ts-loader',
      },
      {
        test: /\.js$/,
        exclude: /node_modules/,
        loader: 'babel-loader',
      },
      {
        test: /\.less$/,
        exclude: /node_modules/,
        use: [
          'style-loader',
          { loader: 'css-loader', options: { modules: true } },
          'less-loader',
        ]
      },
    ]
  }
};

重启服务,正常运行