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

mkdir stage && cd stage // 创建项目文件夹进入项目
npm init // 初始化依赖
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

 

// babel.config.js
module.exports = {
    presets: [
        "@babel/preset-env",
        "@babel/preset-react",
    ],
}

 

// 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.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;
}

 

<!-- 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>

 

修改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中的内容(如果有报错请检查依赖是否安装成功)

 

 ###### ###### ###### ###### ###### ###### ###### ###### ###### ###### ######

接入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相关依赖
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文件

 

// 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 }} />
            &nbsp;&nbsp;
            <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}`}
                    &nbsp;&nbsp;
                    <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'))

 

 接入react-router和react-redux完成,可以看到todolist,此处贴上完整的package.json

{
  "name": "stage",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "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"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "react": "^16.13.1",
    "react-dom": "^16.13.1",
    "react-redux": "^7.2.0",
    "redux": "^4.0.5"
  },
  "devDependencies": {
    "@babel/core": "^7.10.4",
    "@babel/preset-env": "^7.10.4",
    "@babel/preset-react": "^7.10.4",
    "babel-loader": "^8.1.0",
    "clean-webpack-plugin": "^3.0.0",
    "css-loader": "^3.6.0",
    "html-webpack-plugin": "^4.3.0",
    "less": "^3.11.3",
    "less-loader": "^6.2.0",
    "react-router-dom": "^5.2.0",
    "style-loader": "^1.2.1",
    "webpack": "^4.43.0",
    "webpack-cli": "^3.3.12",
    "webpack-dev-server": "^3.11.0"
  }
}

 

 ###### ###### ###### ###### ###### ###### ###### ###### ###### ###### ######

接入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/models/*/index.js 都改为index.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.js 改为src/index.tsx,并添加相应接口,指定变量类型

// 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 }} />
            &nbsp;&nbsp;
            <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}`}
                    &nbsp;&nbsp;
                    <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',
                ]
            },
        ]
    }
};

 

cd ../../ && 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'

 重启服务

posted @ 2020-07-10 20:14  叮叮猫写BUG  阅读(330)  评论(0编辑  收藏  举报