1. 程式人生 > 實用技巧 >菜鳥webpack/react/redux/react-router/ts一步步搭建架子

菜鳥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'

重啟服務