mkdir zhufeng_typescript_react
cd zhufeng_typescript_react
npm init -y
cd zhufeng_typescript_react
tsc --init
基本参数
参数 | 解释 |
---|---|
target | 用于指定编译之后的版本目标 |
module | 生成的模块形式:none、commonjs、amd、system、umd、es6、es2015 或 esnext 只有 amd 和 system 能和 outFile 一起使用 target 为 es5 或更低时可用 es6 和 es2015 |
lib | 编译时引入的 ES 功能库,包括:es5 、es6、es7、dom 等。如果未设置,则默认为: target 为 es5 时: ["dom", "es5", "scripthost"] target 为 es6 时: ["dom", "es6", "dom.iterable", "scripthost"] |
allowJs | 是否允许编译JS文件,默认是false,即不编译JS文件 |
checkJs | 是否检查和报告JS文件中的错误,默认是false |
jsx | 指定jsx代码用于的开发环境 preserve 指保留JSX语法,扩展名为.jsx ,react-native是指保留jsx语法,扩展名js,react指会编译成ES5语法 详解 |
declaration | 是否在编译的时候生成相应的.d.ts 声明文件 |
declarationDir | 生成的 .d.ts 文件存放路径,默认与 .ts 文件相同 |
declarationMap | 是否为声明文件.d.ts生成map文件 |
sourceMap | 编译时是否生成.map 文件 |
outFile | 是否将输出文件合并为一个文件,值是一个文件路径名,只有设置module 的值为amd 和system 模块时才支持这个配置 |
outDir | 指定输出文件夹 |
rootDir | 编译文件的根目录,编译器会在根目录查找入口文件 |
composite | 是否编译构建引用项目 |
removeComments | 是否将编译后的文件中的注释删掉 |
noEmit | 不生成编译文件 |
importHelpers | 是否引入tslib 里的辅助工具函数 |
downlevelIteration | 当target为ES5 或ES3 时,为for-of 、spread 和destructuring 中的迭代器提供完全支持 |
isolatedModules | 指定是否将每个文件作为单独的模块,默认为true |
严格检查
参数 | 解释 |
---|---|
strict | 是否启动所有类型检查 |
noImplicitAny | 不允许默认any类型 |
strictNullChecks | 当设为true时,null和undefined值不能赋值给非这两种类型的值 |
strictFunctionTypes | 是否使用函数参数双向协变检查 |
strictBindCallApply | 是否对bind、call和apply绑定的方法的参数的检测是严格检测的 |
strictPropertyInitialization | 检查类的非undefined属性是否已经在构造函数里初始化 |
noImplicitThis | 不允许this 表达式的值为any 类型的时候 |
alwaysStrict | 指定始终以严格模式检查每个模块 |
额外检查
参数 | 解释 |
---|---|
noUnusedLocals | 检查是否有定义了但是没有使用的变量 |
noUnusedParameters | 检查是否有在函数体中没有使用的参数 |
noImplicitReturns | 检查函数是否有返回值 |
noFallthroughCasesInSwitch | 检查switch中是否有case没有使用break跳出 |
模块解析检查
参数 | 解释 |
---|---|
moduleResolution | 选择模块解析策略,有node 和classic 两种类型,详细说明 |
baseUrl | 解析非相对模块名称的基本目录 |
paths | 设置模块名到基于baseUrl 的路径映射 |
rootDirs | 可以指定一个路径列表,在构建时编译器会将这个路径列表中的路径中的内容都放到一个文件夹中 |
typeRoots | 指定声明文件或文件夹的路径列表 |
types | 用来指定需要包含的模块 |
allowSyntheticDefaultImports | 允许从没有默认导出的模块中默认导入 |
esModuleInterop | 为导入内容创建命名空间,实现CommonJS和ES模块之间的互相访问 |
preserveSymlinks | 不把符号链接解析为其真实路径 |
sourcemap检查
参数 | 解释 |
---|---|
sourceRoot | 调试器应该找到TypeScript文件而不是源文件位置 |
mapRoot | 调试器找到映射文件而非生成文件的位置,指定map文件的根路径 |
inlineSourceMap | 指定是否将map文件的内容和js文件编译在一个同一个js文件中 |
inlineSources | 是否进一步将.ts文件的内容也包含到输出文件中 |
试验选项
参数 | 解释 |
---|---|
experimentalDecorators | 是否启用实验性的装饰器特性 |
emitDecoratorMetadata | 是否为装饰器提供元数据支持 |
试验选项
参数 | 解释 |
---|---|
files | 配置一个数组列表,里面包含指定文件的相对或绝对路径,编译器在编译的时候只会编译包含在files中列出的文件 |
include | include也可以指定要编译的路径列表,但是和files的区别在于,这里的路径可以是文件夹,也可以是文件 |
exclude | exclude表示要排除的、不编译的文件,他也可以指定一个列表 |
extends | extends可以通过指定一个其他的tsconfig.json文件路径,来继承这个配置文件里的配置 |
compileOnSave | 在我们编辑了项目中文件保存的时候,编辑器会根据tsconfig.json 的配置重新生成文件 |
references | 一个对象数组,指定要引用的项目 |
cnpm i typescript webpack webpack-cli webpack-dev-server ts-loader cross-env webpack-merge clean-webpack-plugin html-webpack-plugin -D
cnpm i babel-loader @babel/core @babel/cli @babel/plugin-proposal-class-properties @babel/plugin-proposal-object-rest-spread @babel/preset-env @babel/preset-typescript -D
"scripts": {
"dev": "cross-env NODE_ENV=development webpack-dev-server --config ./config/webpack.dev.js",
"build": "cross-env NODE_ENV=production webpack --config ./config/webpack.prod.js"
},
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry: "./src/index.tsx",
output: {
filename: "main.js"
},
resolve: {
extensions: ['.ts', '.tsx', '.js']
},
module: {
rules: [{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/
}]
},
devServer: {
contentBase: './dist'
},
plugins: [
new CleanWebpackPlugin({
cleanOnceBeforeBuildPatterns: ['./dist']
}),
new HtmlWebpackPlugin({
template: './src/index.html'
})
]
}
config\webpack.dev.js
const { smart } = require('webpack-merge');
const base = require('./webpack.base');
module.exports = smart(base, {
mode: 'development',
devtool: 'inline-source-map'
});
config\webpack.prod.js
const { smart } = require('webpack-merge');
const base = require('./webpack.base');
module.exports = smart(base, {
mode: 'production',
devtool: false
});
cnpm i eslint @typescript-eslint/eslint-plugin @typescript-eslint/parser -D
.eslintrc.json
{
"parser": "@typescript-eslint/parser",
"plugins": [
"@typescript-eslint/eslint-plugin"
],
"extends": [
"plugin:@typescript-eslint/recommended"
],
"rules": {
"@typescript-eslint/no-unused-vars": "off"
}
}
"scripts": {
"dev": "cross-env NODE_ENV=development webpack-dev-server --config ./config/webpack.dev.js",
"build": "cross-env NODE_ENV=production webpack --config ./config/webpack.prod.js",
+ "eslint": "eslint src --ext .js,.ts,.tsx"
},
cnpm i jest @types/jest ts-jest -D
npx ts-jest config:init
src\calculator.tsx
function sum(a: number, b: number) {
return a + b;
}
function minus(a: number, b: number) {
return a - b;
}
module.exports = {
sum,
minus
}
tests\calculator.spec.tsx
let math = require('../src/calculator');
test('1+1=2', () => {
expect(math.sum(1, 1)).toBe(2);
});
test('1-1=0', () => {
expect(math.minus(1, 1)).toBe(0);
});
package.json
"scripts": {
+ "test": "jest"
},
cnpm i react @types/react react-dom @types/react-dom -S
src\index.tsx
import React, { ReactElement, DOMElement, DetailedReactHTMLElement } from 'react';
import ReactDOM from 'react-dom';
let root: HTMLElement | null = document.getElementById('root');
interface Props {
className: string
}
let props: Props = { className: 'title' };
let element: DetailedReactHTMLElement<Props, HTMLHeadingElement> = React.createElement<Props, HTMLHeadingElement>('h1', props, 'hello');
ReactDOM.render(element, root);
src\typings\react.tsx
export interface ReactHTML { h1: any }
export type ReactText = string | number;
export type ReactChild = ReactElement | ReactText;
export type ReactNode = ReactChild | boolean | null | undefined;
export interface RefObject<T> {
readonly current: T | null;
}
export interface ReactElement<P = any, T extends string = string> {
type: T;
props: P;
key: Key | null;
}
export interface DOMElement<P extends HTMLAttributes<T>, T extends Element> extends ReactElement<P, string> {
ref: RefObject<T>;
}
export interface DetailedReactHTMLElement<P extends HTMLAttributes<T>, T extends HTMLElement> extends DOMElement<P, T> {
type: keyof ReactHTML;
}
export type Key = string | number;
export interface Attributes {
key?: Key;
}
export interface ClassAttributes<T> extends Attributes {
ref?: RefObject<T>;
}
export interface DOMAttributes<T> {
children?: ReactNode;
}
export interface HTMLAttributes<T> extends DOMAttributes<T> {
className?: string;
}
export interface Element { }
export interface HTMLElement extends Element { }
export declare function createElement<P extends HTMLAttributes<T>, T extends HTMLElement>(
type: keyof ReactHTML,
props?: ClassAttributes<T> & P | null,
...children: ReactNode[]): DetailedReactHTMLElement<P, T>;
src\index.tsx
import React, { ReactElement, DOMElement, DetailedReactHTMLElement, FunctionComponentElement } from 'react';
import ReactDOM from 'react-dom';
let root: HTMLElement | null = document.getElementById('root');
+interface Props {
+ className: string;
+}
+function Welcome(props: Props) {
+ return React.createElement('h1', { className: props.className }, 'hello');
+}
+let props: Props = { className: 'title' };
+let element: FunctionComponentElement<Props> = React.createElement<Props>(Welcome, props);
ReactDOM.render(element, root);
src\typings\react.tsx
export interface ReactHTML { h1: any }
export type ReactText = string | number;
export type ReactChild = ReactElement | ReactText;
export type ReactNode = ReactChild | boolean | null | undefined;
export interface RefObject<T> {
readonly current: T | null;
}
export type JSXElementConstructor<P> =
| ((props: P) => ReactElement | null)
export interface ReactElement<P = any, T extends string | JSXElementConstructor<any> = string | JSXElementConstructor<any>> {
type: T;
props: P;
key: Key | null;
}
export interface DOMElement<P extends HTMLAttributes<T>, T extends Element> extends ReactElement<P, string> {
ref: RefObject<T>;
}
export interface DetailedReactHTMLElement<P extends HTMLAttributes<T>, T extends HTMLElement> extends DOMElement<P, T> {
type: keyof ReactHTML;
}
export type Key = string | number;
export interface Attributes {
key?: Key;
}
export interface ClassAttributes<T> extends Attributes {
ref?: RefObject<T>;
}
export interface DOMAttributes<T> {
children?: ReactNode;
}
export interface HTMLAttributes<T> extends DOMAttributes<T> {
className?: string;
}
export interface Element { }
export interface HTMLElement extends Element { }
+type PropsWithChildren<P> = P & { children?: ReactNode };
+interface FunctionComponent<P = {}> {
+ (props: PropsWithChildren<P>): ReactElement | null;
+}
+interface FunctionComponentElement<P> extends ReactElement<P, FunctionComponent<P>> {
+ ref?: 'ref' extends keyof P ? P extends { ref?: infer R } ? R : never : never;
+}
+export declare function createElement<P extends {}>(
+ type: FunctionComponent<P>,
+ props?: Attributes & P | null,
+ ...children: ReactNode[]): FunctionComponentElement<P>;
export declare function createElement<P extends HTMLAttributes<T>, T extends HTMLElement>(
type: keyof ReactHTML,
props?: ClassAttributes<T> & P | null,
...children: ReactNode[]): DetailedReactHTMLElement<P, T>;
src\index.tsx
import React, { ReactElement, DOMElement, DetailedReactHTMLElement, FunctionComponentElement, ComponentClass } from 'react';
import ReactDOM from 'react-dom';
let root: HTMLElement | null = document.getElementById('root');
interface Props {
className: string;
}
+interface State {
+ id: string
+}
+class Welcome extends React.Component<Props, State> {
+ state = { id: 'id' }
+ render() {
+ return React.createElement('h1', { className: props.className }, 'hello');
+ }
+}
+let props: Props = { className: 'title' };
+let element: ReactElement<Props> = React.createElement<Props>(Welcome, props);
ReactDOM.render(element, root);
src\typings\react.tsx
export interface ReactHTML { h1: any }
export type ReactText = string | number;
export type ReactChild = ReactElement | ReactText;
export type ReactNode = ReactChild | boolean | null | undefined;
export interface RefObject<T> {
readonly current: T | null;
}
export type JSXElementConstructor<P> =
| ((props: P) => ReactElement | null)
export interface ReactElement<P = any, T extends string | JSXElementConstructor<any> = string | JSXElementConstructor<any>> {
type: T;
props: P;
key: Key | null;
}
export interface DOMElement<P extends HTMLAttributes<T>, T extends Element> extends ReactElement<P, string> {
ref: RefObject<T>;
}
export interface DetailedReactHTMLElement<P extends HTMLAttributes<T>, T extends HTMLElement> extends DOMElement<P, T> {
type: keyof ReactHTML;
}
export type Key = string | number;
export interface Attributes {
key?: Key;
}
export interface ClassAttributes<T> extends Attributes {
ref?: RefObject<T>;
}
export interface DOMAttributes<T> {
children?: ReactNode;
}
export interface HTMLAttributes<T> extends DOMAttributes<T> {
className?: string;
}
export interface Element { }
export interface HTMLElement extends Element { }
type PropsWithChildren<P> = P & { children?: ReactNode };
interface FunctionComponent<P = {}> {
(props: PropsWithChildren<P>): ReactElement | null;
}
interface FunctionComponentElement<P> extends ReactElement<P, FunctionComponent<P>> {
ref?: 'ref' extends keyof P ? P extends { ref?: infer R } ? R : never : never;
}
+type ComponentState = any;
+declare class Component<P, S> {
+ setState<K extends keyof S>(
+ state: ((prevState: Readonly<S>, props: Readonly<P>) => (Pick<S, K> | S | null)) | (Pick<S, K> | S | null),
+ callback?: () => void
+ ): void;
+ render(): ReactNode;
+}
+interface ComponentClass<P = {}, S = ComponentState> {
+ new(props: P, context?: any): Component<P, S>;
+}
+export declare function createElement<P extends {}>(
+ type: FunctionComponent<P> | ComponentClass<P> | string,
+ props?: Attributes & P | null,
+ ...children: ReactNode[]): ReactElement<P>;
cnpm i redux react-redux @types/react-redux redux-logger redux-promise redux-thunk @types/redux-logger @types/redux-promise -D
src\index.tsx
import React from 'react';
import ReactDOM from 'react-dom';
+import Counter1 from './components/Counter1';
+import Counter2 from './components/Counter2';
+import store from './store';
+import { Provider } from 'react-redux';
+ReactDOM.render(
+ <Provider store={store}>
+ <Counter1 />
+ <Counter2 />
+ </Provider>, document.getElementById('root'));
import { createStore, Store, AnyAction, applyMiddleware, StoreEnhancer, StoreEnhancerStoreCreator } from 'redux';
import logger from 'redux-logger';
import thunk from 'redux-thunk';
import promise from 'redux-promise';
import reducer from './reducers';
import { CombinedState } from './reducers';
/* let thunkMiddleware = thunk.withExtraArgument<{ amount: number }>({ amount: 5 });
type Ext = { name: string };
type StateExt = { age: number };
const enhancer: StoreEnhancer<Ext, StateExt> = (createStore: any): StoreEnhancerStoreCreator<Ext, StateExt> => (
<S, A extends Action = AnyAction>(...args: any[]): Store<S & StateExt, A> & Ext => {
const store = createStore(...args);
let getState = store.getState;
store.getState = function () {
let currentState = getState();
return {
...currentState,
age: 10
}
}
return {
...store,
name: 'zhufeng'
}
}
)
const storeEnhancer: StoreEnhancer<Ext, StateExt> = compose(enhancer, applyMiddleware(routerMiddleware(history), thunkMiddleware));
const storeEnhancerStoreCreator: StoreEnhancerStoreCreator<Ext, StateExt> = storeEnhancer(createStore);
const store: Store<CombinedState & StateExt, AnyAction> & Ext = storeEnhancerStoreCreator(reducer);
console.log(store.name);
console.log(store.getState().age);
*/
const storeEnhancer: StoreEnhancer = applyMiddleware(promise, thunk, logger);
const storeEnhancerStoreCreator: StoreEnhancerStoreCreator = storeEnhancer(createStore);
const store: Store<CombinedState, AnyAction> = storeEnhancerStoreCreator(reducer);
export default store;
src\store\action-types.tsx
export const INCREMENT1 = 'INCREMENT1';
export const DECREMENT1 = 'DECREMENT1';
export const INCREMENT2 = 'INCREMENT2';
export const DECREMENT2 = 'DECREMENT2';
src\store\reducers\counter1.tsx
import * as types from '../action-types';
import { AnyAction } from 'redux';
export interface Counter1State {
number: number
}
let initialState: Counter1State = { number: 0 }
export default function (state: Counter1State = initialState, action: AnyAction): Counter1State {
switch (action.type) {
case types.INCREMENT1:
return { number: state.number + 1 };
case types.DECREMENT1:
return { number: state.number - 1 };
default:
return state;
}
}
src\store\reducers\counter2.tsx
import * as types from '../action-types';
import { AnyAction } from 'redux';
export interface Counter2State {
number: number
}
let initialState: Counter2State = { number: 0 };
export default function (state: Counter2State = initialState, action: AnyAction): Counter2State {
switch (action.type) {
case types.INCREMENT2:
return { number: state.number + 1 };
case types.DECREMENT2:
return { number: state.number - 1 };
default:
return state;
}
}
src\store\reducers\index.tsx
import { combineReducers, ReducersMapObject, Reducer, AnyAction } from 'redux';
import counter1, { Counter1State } from './counter1';
import counter2, { Counter2State } from './counter2';
interface Reducers {
counter1: Counter1State;
counter2: Counter2State;
}
let reducers: ReducersMapObject<Reducers, AnyAction> = {
counter1,
counter2
};
export type CombinedState = {
[key in keyof typeof reducers]: ReturnType<typeof reducers[key]>
}
let rootReducer: Reducer<CombinedState, AnyAction> = combineReducers<CombinedState, AnyAction>(reducers);
export default rootReducer;
src\store\actions\counter1.tsx
import * as types from '../action-types';
import { AnyAction, Dispatch, Store } from 'redux';
const actions = {
increment1(): AnyAction {
return { type: types.INCREMENT1 };
},
promisePlus(): { type: string, payload: Promise<undefined> } {
return (
{
type: types.INCREMENT1,
payload: new Promise((resolve) => {
setTimeout(() => {
resolve();
}, 1000);
})
}
)
},
thunkPlus(): Function {
return (dispatch: Dispatch, getState: Store['getState']) => {
setTimeout(() => {
dispatch({ type: types.INCREMENT1 });
}, 1000);
}
},
decrement1(): AnyAction {
return { type: types.DECREMENT1 };
}
}
export default actions;
src\store\actions\counter2.tsx
import * as types from '../action-types';
import { AnyAction } from 'redux';
export default {
increment2(): AnyAction {
return { type: types.INCREMENT2 };
},
decrement2(): AnyAction {
return { type: types.DECREMENT2 };
}
}
src\components\Counter1.tsx
import React, { Component } from 'react';
import actions from '../store/actions/counter1';
import { CombinedState } from '../store/reducers';
import { Counter1State } from '../store/reducers/counter1';
import { connect } from 'react-redux';
type Props = Counter1State & typeof actions;
class Counter1 extends Component<Props> {
render() {
let { number, increment1, promisePlus, thunkPlus } = this.props;
return (
<div>
<p>{number}</p>
<button onClick={increment1}>+</button>
<button onClick={promisePlus}>promise+</button>
<button onClick={thunkPlus}>thunk+</button>
</div>
)
}
}
let mapStateToProps = (state: CombinedState): Counter1State => state.counter1;
export default connect(
mapStateToProps,
actions
)(Counter1)
src\components\Counter2.tsx
import React, { Component } from 'react';
import actions from '../store/actions/counter2';
import { CombinedState } from '../store/reducers';
import { Counter2State } from '../store/reducers/counter2';
import { connect } from 'react-redux';
type Props = Counter2State & typeof actions;
class Counter2 extends Component<Props> {
render() {
let { number, increment2, decrement2 } = this.props;
return (
<div>
<p>{number}</p>
<button onClick={increment2}>+</button>
<button onClick={decrement2}>-</button>
</div>
)
}
}
let mapStateToProps = (state: CombinedState): Counter2State => state.counter2;
type ThunkDispatchs = ThunkDispatch<CombinedState, { amount: number }, AnyAction>;
function mapDispatchToProps(dispatch: ThunkDispatchs) {
return ({
thunkPlus: () => dispatch<void>((dispatch: ThunkDispatchs, getState: Store['getState'], extraArgument: { amount: number }): void => {
setTimeout(() => {
dispatch<AnyAction>({ type: types.INCREMENT1, payload: extraArgument.amount });
}, 1000);
})
})
}
export default connect(
mapStateToProps,
actions
)(Counter2)
src\redux-thunk\index.tsx
import { Middleware, Action, AnyAction } from 'redux';
type MiddlewareExt = Middleware & {
withExtraArgument: typeof createThunkMiddleware
}
export type ThunkAction<R, S, E, A extends Action> = (
dispatch: ThunkDispatch<S, E, A>,
getState: () => S,
extraArgument: E
) => R;
export interface ThunkDispatch<S, E, A extends Action> {
<T extends A>(action: T): T;
<R>(asyncAction: ThunkAction<R, S, E, A>): R;
}
function createThunkMiddleware<S = {}, A extends Action = AnyAction, E = undefined>(extraArgument?: any): Middleware {
let middleware: Middleware<ThunkDispatch<S, E, A>, S, ThunkDispatch<S, E, A>> = ({ dispatch, getState }) => next => action => {
if (typeof action === 'function') {
return action(dispatch, getState, extraArgument);
}
return next(action);
};
return middleware;
}
const thunk: MiddlewareExt = createThunkMiddleware() as MiddlewareExt;
thunk.withExtraArgument = createThunkMiddleware;
export default thunk;
cnpm i react-router-dom @types/react-router-dom connected-react-router -S
src\index.tsx
import React from 'react';
import ReactDOM from 'react-dom';
import Counter1 from './components/Counter1';
import Counter2 from './components/Counter2';
import { Provider } from 'react-redux';
+import { Route, Link, Redirect, Switch } from 'react-router-dom';
+import { ConnectedRouter } from 'connected-react-router';
+import history from './history';
+import store from './store';
ReactDOM.render(
<Provider store={store}>
+ <ConnectedRouter history={history}>
+ <Link to="/counter1">Counter1</Link>
+ <Link to="/counter2">Counter2</Link>
+ <Link to="/users">用户管理</Link>
+ <Switch>
+ <Route exact={true} path="/counter1" component={Counter1} />
+ <Route exact={true} path="/counter2" component={Counter2} />
+ <Redirect to="/counter1" />
+ </Switch>
+ </ConnectedRouter>
+ </Provider>, document.getElementById('root'));
src\history.tsx
import { createHashHistory } from 'history'
let history = createHashHistory();
export default history;
src\store\index.tsx
import { createStore, Store, AnyAction, applyMiddleware, StoreEnhancer, StoreEnhancerStoreCreator } from 'redux';
import logger from 'redux-logger';
import thunk from 'redux-thunk';
import promise from 'redux-promise';
import reducer from './reducers';
import { CombinedState } from './reducers';
+import { routerMiddleware } from 'connected-react-router';
+import history from '../history';
+const storeEnhancer: StoreEnhancer = applyMiddleware(routerMiddleware(history), promise, thunk, logger);
const storeEnhancerStoreCreator: StoreEnhancerStoreCreator = storeEnhancer(createStore);
const store: Store<CombinedState, AnyAction> = storeEnhancerStoreCreator(reducer);
export default store;
src\store\reducers\index.tsx
import { combineReducers, ReducersMapObject, Reducer, AnyAction } from 'redux';
import counter1, { Counter1State } from './counter1';
import counter2, { Counter2State } from './counter2';
+import { LocationState } from 'history';
+import { connectRouter, RouterState } from 'connected-react-router';
import history from '../../history';
interface Reducers {
+ router: RouterState<LocationState>,
counter1: Counter1State;
counter2: Counter2State;
}
+let reducers: ReducersMapObject<Reducers, any> = {
+ router: connectRouter(history),
counter1,
counter2
};
export type CombinedState = {
[key in keyof typeof reducers]: ReturnType<typeof reducers[key]>
}
let rootReducer: Reducer<CombinedState, AnyAction> = combineReducers<CombinedState, AnyAction>(reducers);
export default rootReducer;
src\store\actions\counter1.tsx
import * as types from '../action-types';
import { AnyAction, Dispatch, Store } from 'redux';
+import { LocationDescriptorObject } from 'history';
+import { push } from 'connected-react-router';
const actions = {
increment1(): AnyAction {
return { type: types.INCREMENT1 };
},
promisePlus(): { type: string, payload: Promise<undefined> } {
return (
{
type: types.INCREMENT1,
payload: new Promise((resolve) => {
setTimeout(() => {
resolve();
}, 1000);
})
}
)
},
thunkPlus(): Function {
return (dispatch: Dispatch, getState: Store['getState']) => {
setTimeout(() => {
dispatch({ type: types.INCREMENT1 });
}, 1000);
}
},
+ goto(locationDescriptorObject: LocationDescriptorObject): AnyAction {
+ return push(locationDescriptorObject);
+ }
}
export default actions;
src\components\Counter1.tsx
+import React, { Component, PropsWithChildren } from 'react';
import actions from '../store/actions/counter1';
import { CombinedState } from '../store/reducers';
import { Counter1State } from '../store/reducers/counter1';
import { connect } from 'react-redux';
+import { RouteComponentProps } from 'react-router';
+interface IParams { }
+type RouteProps = RouteComponentProps<IParams>;
+type Props = PropsWithChildren<RouteProps & Counter1State & typeof actions>;
class Counter1 extends Component<Props> {
render() {
+ let { number, increment1, promisePlus, thunkPlus, goto } = this.props;
return (
<div>
<p>{number}</p>
<button onClick={increment1}>+</button>
<button onClick={promisePlus}>promise+</button>
<button onClick={thunkPlus}>thunk+</button>
+ <button onClick={() => goto({ pathname: '/counter2' })}>/counter2</button>
</div>
)
}
}
let mapStateToProps = (state: CombinedState): Counter1State => state.counter1;
export default connect(
mapStateToProps,
actions
)(Counter1)
src\components\Counter2.tsx
+import React, { Component, PropsWithChildren } from 'react';
import actions from '../store/actions/counter2';
import { CombinedState } from '../store/reducers';
import { Counter2State } from '../store/reducers/counter2';
import { connect } from 'react-redux';
+import { RouteComponentProps } from 'react-router';
+interface IParams { }
+type RouteProps = RouteComponentProps<IParams>;
+type Props = PropsWithChildren<RouteProps & Counter2State & typeof actions>;
class Counter2 extends Component<Props> {
render() {
let { number, increment2, decrement2 } = this.props;
return (
<div>
<p>{number}</p>
<button onClick={increment2}>+</button>
<button onClick={decrement2}>-</button>
</div>
)
}
}
let mapStateToProps = (state: CombinedState): Counter2State => state.counter2;
export default connect(
mapStateToProps,
actions
)(Counter2)
cnpm i redux-thunk antd axios react-router-dom connected-react-router -S
cnpm i style-loader css-loader @types/react-router-dom -D
config\webpack.base.js
module: {
rules: [
+ {
+ test: /\.css?$/,
+ use: [
+ 'style-loader',
+ 'css-loader'
+ ]
+ }
+ ]
},
src\index.tsx
import React from 'react';
import ReactDOM from 'react-dom';
import Counter1 from './components/Counter1';
import Counter2 from './components/Counter2';
+import User from './components/User';
import { Provider } from 'react-redux';
import { Route, Link, Redirect, Switch } from 'react-router-dom';
import { ConnectedRouter } from 'connected-react-router';
import history from './history';
import store from './store';
+import { Layout } from 'antd';
+import NavBar from './components/NavBar';
+import 'antd/dist/antd.css';
+const { Content } = Layout;
ReactDOM.render(
<Provider store={store}>
<ConnectedRouter history={history}>
+ <Layout>
+ <NavBar />
+ <Content>
+ <Switch>
+ <Route exact={true} path="/counter1" component={Counter1} />
+ <Route exact={true} path="/counter2" component={Counter2} />
+ <Route path="/user" component={User} />
+ <Redirect to="/counter1" />
+ </Switch>
+ </Content>
+ </Layout>
</ConnectedRouter>
</Provider>, document.getElementById('root'));
src\store\action-types.tsx
+ export const SET_USERS = 'SET_USERS';
src\store\reducers\index.tsx
import { combineReducers, ReducersMapObject, Reducer, AnyAction } from 'redux';
import counter1, { Counter1State } from './counter1';
import counter2, { Counter2State } from './counter2';
+import user, { UserState } from './user';
import { LocationState } from 'history';
import { connectRouter, RouterState } from 'connected-react-router';
import history from '../../history';
interface Reducers {
router: RouterState<LocationState>,
counter1: Counter1State;
counter2: Counter2State;
+ user: UserState
}
let reducers: ReducersMapObject<Reducers, any> = {
router: connectRouter(history),
counter1,
counter2,
+ user
};
export type CombinedState = {
[key in keyof typeof reducers]: ReturnType<typeof reducers[key]>
}
let rootReducer: Reducer<CombinedState, AnyAction> = combineReducers<CombinedState, AnyAction>(reducers);
export default rootReducer;
src\typings\response.tsx
export interface User {
_id: string;
username: string
}
export interface UserlistResponse {
code: number;
data: Array<User>
}
export interface AddUserResponse {
code: number;
data: User
}
src\store\reducers\user.tsx
import * as types from '../action-types';
import { AnyAction } from 'redux';
import { User } from '../../typings/response';
export interface UserState {
list: Array<User>
}
let initialState: UserState = {
list: []
};
export default function (state: UserState = initialState, action: AnyAction): UserState {
switch (action.type) {
case types.SET_USERS:
return { ...state, list: action.payload };
default:
return state;
}
}
src\store\actions\user.tsx
import * as types from '../action-types';
import { User } from '../../typings/response';
export default {
setUsers(list: Array<User>) {
return { type: types.SET_USERS, payload: list };
}
}
src\api\request.tsx
import axios from 'axios';
const instance = axios.create({
timeout: 20000,
baseURL: 'http://localhost:4000'
});
export * from 'axios';
export default instance;
src\components\NavBar.tsx
import React, { Component, PropsWithChildren } from 'react';
import { Link, withRouter } from 'react-router-dom';
import { Layout, Menu } from 'antd';
import { RouteComponentProps } from 'react-router';
interface IParams { }
type RouteProps = RouteComponentProps<IParams>;
type Props = PropsWithChildren<RouteProps>;
class NavBar extends Component<Props> {
render() {
return (
<Layout.Header>
<Menu
theme="dark"
style={{ lineHeight: '64px' }}
mode="horizontal"
selectedKeys={[this.props.location.pathname]} >
<Menu.Item key="/counter1"><Link to="/counter1">Counter1</Link></Menu.Item>
<Menu.Item key="/counter2"><Link to="/counter2">Counter2</Link></Menu.Item>
<Menu.Item key="/user"><Link to="/user">用户管理</Link></Menu.Item>
</Menu>
</Layout.Header>
)
}
}
export default withRouter(NavBar);
src\components\User.tsx
import React, { Component, PropsWithChildren } from 'react';
import actions from '../store/actions/user';
import { CombinedState } from '../store/reducers';
import { UserState } from '../store/reducers/user';
import { connect } from 'react-redux';
import { RouteComponentProps, Link, Route } from 'react-router-dom';
import { Layout, Menu, Icon } from 'antd';
import UserList from './UserList';
import UserAdd from './UserAdd';
const { Sider, Content } = Layout;
interface IParams { }
type RouteProps = RouteComponentProps<IParams>;
type Props = PropsWithChildren<RouteProps & UserState & typeof actions>;
class User extends Component<Props> {
render() {
return (
<Layout>
<Sider>
<Menu
theme="dark"
defaultSelectedKeys={['/user/list']}
mode="inline"
>
<Menu.Item key={'/user/add'}>
<Link to={'/user/add'}><Icon type={'plus'} />添加用户</Link>
</Menu.Item>;
<Menu.Item key={'/user/list'}>
<Link to={'/user/list'}><Icon type={'user'} />用户列表</Link>
</Menu.Item>;
</Menu>
</Sider>
<Content style={{ padding: '20px' }}>
<Route path="/user/list" component={UserList} />
<Route path="/user/add" component={UserAdd} />
</Content>
</Layout>
)
}
}
let mapStateToProps = (state: CombinedState): UserState => state.user;
export default connect(
mapStateToProps,
actions
)(User)
src\components\UserAdd.tsx
import React, { Component, PropsWithChildren, useState, useCallback } from 'react';
import { Link, withRouter } from 'react-router-dom';
import { Layout, Menu, Form, Input, Button, message } from 'antd';
import { RouteComponentProps } from 'react-router';
import request, { AxiosResponse } from '../api/request';
import { User, AddUserResponse } from '../typings/response';
interface IParams { }
type RouteProps = RouteComponentProps<IParams>;
type Props = PropsWithChildren<RouteProps>;
function UserAdd(props: Props) {
let [user, setUser] = useState<User>({} as User);
const handleSubmit = useCallback((event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
request.post('/api/users', user).then((response: AxiosResponse<AddUserResponse>) => {
let data = response.data;
if (data.code == 0) {
props.history.push('/user/list');
} else {
message.error('添加用户失败!');
}
});
}, [user]);
const handleNameChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
setUser({
...user,
username: event.target.value
});
}, [user])
return (
<Form onSubmit={handleSubmit}>
<Form.Item>
<Input
placeholder="用户名"
style={{ width: 120 }}
value={user.username}
onChange={handleNameChange}
/>
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit">添加</Button>
</Form.Item>
</Form>
)
}
export default UserAdd;
src\components\UserList.tsx
import React, { PropsWithChildren, useState, useEffect } from 'react';
import { RouteComponentProps } from 'react-router-dom';
import { Table } from 'antd';
import request, { AxiosResponse } from '../api/request';
import actions from '../store/actions/user';
import { User, UserlistResponse } from '../typings/response';
import { CombinedState } from '../store/reducers';
import { UserState } from '../store/reducers/user';
import { connect } from 'react-redux';
interface IParams { }
type RouteProps = RouteComponentProps<IParams>;
type Props = PropsWithChildren<RouteProps & UserState & typeof actions>;
const columns = [
{
title: '用户名',
dataIndex: 'username',
key: 'username'
}
];
function UserList(props: Props) {
let [users, setUsers] = useState<Array<User>>(props.list);
useEffect(() => {
(async function () {
if (users.length == 0) {
let response: AxiosResponse<UserlistResponse> = await request.get<any, AxiosResponse<UserlistResponse>>('/api/users');
let list = response.data.data;
setUsers(list);
props.setUsers(list);
}
})()
}, []);
return (
<Table columns={columns} dataSource={users} rowKey={(record: User) => record._id} />
)
}
let mapStateToProps = (state: CombinedState): UserState => state.user;
export default connect(
mapStateToProps,
actions
)(UserList);
mkdir C:\vipdata\prepare8\zhufeng_ts_react_api
cd zhufeng_ts_react_api
cnpm init -y
cnpm i @types/node express @types/express body-parser cors @types/cors mongoose @types/mongoose shelljs -S
{
"compilerOptions": {
"target": "es5",
"module": "commonjs",
"lib": [
"dom",
"es2015"
],
"outDir": "./dist",
"strict": true,
"baseUrl": "./",
"paths": {
"*": [
"*",
"node_modules/*",
"typings/*"
]
},
"esModuleInterop": true,
}
}
server.ts
import express, { Express, Request, Response } from 'express';
import bodyParser from 'body-parser';
import cors from 'cors';
import Models from './db';
import config from './config';
import path from 'path';
let app: Express = express();
app.use(
cors({
origin: config.origin,
credentials: true,
allowedHeaders: "Content-Type,Authorization",
methods: "GET,HEAD,PUT,PATCH,POST,DELETE,OPTIONS"
})
);
app.use(express.static(path.resolve(__dirname, 'public')));
app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.json());
app.get('/api/users', async (req: Request, res: Response) => {
let users = await Models.UserModel.find();
res.json({
code: 0,
data: users
});
});
app.post('/api/users', async (req: Request, res: Response) => {
let user = req.body;
user = await Models.UserModel.create(user);
res.json({
code: 0,
data: user
});
});
app.listen(4000, () => {
console.log('服务器在4000端口启动!');
});
interface IConfig {
secret: string;
dbUrl: string;
origin: Array<string>
}
let config: IConfig = {
secret: 'zhufengcms',
dbUrl: "mongodb://localhost:27017/zhufengcms",
origin: ["http://localhost:8080"]
}
export = config;
db.ts
import mongoose, { Schema, Connection, Model } from 'mongoose';
import config from './config';
const conn: Connection = mongoose.createConnection(config.dbUrl, { useNewUrlParser: true, useUnifiedTopology: true });
const UserModel = conn.model('User', new Schema({
username: { type: String },
}));
export = {
UserModel
}
typings\shelljs\index.d.ts
declare module 'shelljs';
copy.ts
import shell from 'shelljs';
shell.cp("-R", "./public/", "./dist/");
package.json
{
"scripts": {
"dev": "cross-env NODE_ENV=development nodemon -e ts,tsx --exec 'ts-node' ./server.ts",
"serve": "cross-env NODE_ENV=production tsc && ts-node copy.ts && nodemon ./dist/server.js"
}
}
import React from 'react';
import { Dispatch } from 'redux';
import dva, { connect } from 'dva';
import keymaster from 'keymaster';
import { RouterAPI } from 'dva';
import { Router, Route } from 'dva/router';
interface Counter1State {
number: 0
}
interface Counter2State {
number: 0
}
interface CombinedState {
counter1: Counter1State;
counter2: Counter2State;
}
const app = dva();
const delay = (millseconds: number) => {
return new Promise(function (resolve, reject) {
setTimeout(function () {
resolve();
}, millseconds);
});
}
app.model({
namespace: 'counter1',
state: { number: 0 },
reducers: {//接收老状态,返回新状态
add(state) { //dispatch({type:'add'});
return { number: state.number + 1 };
},
minus(state) {//dispatch({type:'minus'})
return { number: state.number - 1 };
}
},
// 延时操作 调用接口 等待
effects: {
*asyncAdd(action, { put, call }) { //redux-saga/effects {put,call}
yield call(delay, 1000);//把100传给delay并调用,yield会等待promise完成
yield put({ type: 'add' });
}
},
subscriptions: {
keyboard({ dispatch }) {
keymaster('space', () => {
dispatch({ type: 'add' });
});
},
changeTitle({ history }) {
setTimeout(function () {
history.listen(({ pathname }) => {
document.title = pathname;
});
}, 1000);
}
}
});
app.model({
namespace: 'counter2',
state: { number: 0 },
reducers: {//接收老状态,返回新状态
add(state) { //dispatch({type:'add'});
return { number: state.number + 1 };
},
minus(state) {//dispatch({type:'minus'})
return { number: state.number - 1 };
}
}
});
type Counter1Props = Counter1State & { dispatch: Dispatch };
const Counter1 = (props: Counter1Props) => {
return (
<div>
<p>{props.number}</p>
<button onClick={() => props.dispatch({ type: 'counter1/add' })}>add</button>
<button onClick={() => props.dispatch({ type: 'counter1/asyncAdd' })}>asyncAdd</button>
<button onClick={() => props.dispatch({ type: 'counter1/minus' })}>-</button>
</div>
)
}
type Counter2Props = Counter2State & { dispatch: Dispatch };
const Counter2 = (props: Counter2Props) => {
return (
<div>
<p>{props.number}</p>
<button onClick={() => props.dispatch({ type: 'counter2/add' })}>+</button>
<button onClick={() => props.dispatch({ type: 'counter2/minus' })}>-</button>
</div>
)
}
const mapStateToProps1 = (state: CombinedState): Counter1State => state.counter1;
const ConnectedCounter = connect(
mapStateToProps1
)(Counter1);
const mapStateToProps2 = (state: CombinedState): Counter2State => state.counter2;
const ConnectedCounter2 = connect(
mapStateToProps2
)(Counter2);
app.router(
(api?: RouterAPI) => {
let { history } = api!;
return (
(
<Router history={history}>
<>
<Route path="/counter1" component={ConnectedCounter} />
<Route path="/counter2" component={ConnectedCounter2} />
</>
</Router>
)
)
}
);
app.start('#root');