项目初始化
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
各文件内容
// 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文件
各文件内容
// 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',
]
},
]
}
};
重启服务,正常运行