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 }} /> <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'))
接入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 }} />
<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',
]
},
]
}
};
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'
重启服务