小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。
前言
想练习React或者TypeScript无从下手怎么办?该如何创建一个项目呢?网上教程这么多,如何自己从零开始搭建一个React项目呢?本文教你从零开始搭建一个基于React + dva + TypeScript + scss的应用,项目完整示例也发布在了我的github上面,详情请戳下方链接。
项目完整示例:Github
话不多说,让我们开始把~
创建一个React项目
我们先使用React官方文档推荐的create-react-app来创建我们的应用。
Create React App是一个用于学习React的舒适环境,也是用React创建新的单页应用的最佳方式。它会配置你的开发环境,以便使你能够使用最新的 JavaScript 特性,提供良好的开发体验,并为生产环境优化你的应用程序。你需要在你的机器上安装Node >= 8.10 和 npm >= 5.6。
我们首先要全局安装create-react-app这个包:
# 全局安装
npm install -g create-react-app
因为此项目使用TypeScript开发,所以我们使用如下命令创建我们的项目:
npx create-react-app react-demo --typescript
cd react-demo
随着一系列信息的打印,我们的项目初步已经搭建好了,我们来看看项目的目录都有什么:
通过create-react-app,我们现在生成了一个基于webpack的前端构建流水线应用,它现在集成了React、TypeScript,前端性能检测、单元测试等功能。
我们先补充一下类型声明文件:
react-app-env.d.ts:
/// <reference types="node" />
/// <reference types="react" />
/// <reference types="react-dom" />
declare namespace NodeJS {
interface ProcessEnv {
readonly NODE_ENV: "development" | "production" | "test";
readonly PUBLIC_URL: string;
}
}
declare module "*.avif" {
const src: string;
export default src;
}
declare module "*.bmp" {
const src: string;
export default src;
}
declare module "*.gif" {
const src: string;
export default src;
}
declare module "*.jpg" {
const src: string;
export default src;
}
declare module "*.jpeg" {
const src: string;
export default src;
}
declare module "*.png" {
const src: string;
export default src;
}
declare module "*.webp" {
const src: string;
export default src;
}
declare module "*.svg" {
import * as React from "react";
export const ReactComponent: React.FunctionComponent<
React.SVGProps<SVGSVGElement> & { title?: string }
>;
const src: string;
export default src;
}
declare module "*.module.css" {
const classes: { readonly [key: string]: string };
export default classes;
}
declare module "*.module.scss" {
const classes: { readonly [key: string]: string };
export default classes;
}
declare module "*.module.sass" {
const classes: { readonly [key: string]: string };
export default classes;
}
我们可以使用脚本npm run start启动项目:
可以看到已经启动成功了。但是现在就结束了吗,那远远还没有,我们还需要什么呢?那就是路由管理和全局状态管理,我们这里使用dva。
引入dva
我们使用命令:
npm i dva -save
npm i dva-loading -save
npm i dva-core -save
npm i redux -save
npm i react-redux -save
安装好dva后,我们在src目录下新建一个文件夹dva,其中新建一个index.tsx文件。
创建dva实例
src/dva/index.tsx:
import React from "react";
import { Provider, connect } from "react-redux";
import createLoading from "dva-loading";
import { create } from "dva-core";
import { Model } from "dva";
export { connect };
export interface Options {
models: Model[];
extraReducers?: any;
initialState: any;
onError: (e: any) => void;
onAction?: any[];
}
export function dva(options: Options) {
/**
* 创建APP实例
*/
const app = create(options);
/**
* 注册全局model
*/
options.models.forEach((model: Model) => app.model(model));
/**
* 添加loading中间件插件必须在start()之前注册
*/
app.use(createLoading());
/**
* 启动APP,必须放在app创建之后的第一步,只有app启动了,app实例才存在
*/
app.start();
/**
* 重载app的start方法
*/
const store = app._store;
app.start = (container: any) => () =>
<Provider store={store}>{container}</Provider>;
/**
* 暴露获取store的接口
*/
app.getStore = () => store;
return app;
}
在这个文件中我们封装了dva实例,而因为ts检测不到dva-core以及dva-loading,我们需要自定义一个declaration.d.ts文件。
src/declaration.d.ts:
declare module "dva-loading";
declare module "dva-core";
我们先删除一些不需要的文件,我们把App组件相关的都给删除掉,以及删除性能测试文件。
我们再新建一个page目录用来存放我们的页面文件,再在其中新建一个home.tsx来验证。
src/page/home.tsx:
import React, { FC } from "react";
const home: FC<any> = (props: any) => {
return <div>123</div>;
};
export default home;
然后我们再把我们的应用改造成dva应用,在src/index.tsx中:
import React from "react";
import ReactDOM from "react-dom";
import "./index.css";
import Home from "./page/home";
import { dva } from "./dva";
/**
* 创建dva app
*/
const app = dva({
initialState: {},
models: [], // 公共的model 在这里引入
extraReducers: {},
onError(e: any) {
console.error("onError", e);
},
});
//加载渲染根路由方法
const render = (Component: any) => {
ReactDOM.render(<Component />, document.getElementById("root"));
};
const App = app.start(<Home />);
render(App);
路由管理
在src目录下新建一个router文件夹,用于存放我们的路由文件。
src/router/index.tsx
import React from "react";
import { Router, Route, Switch, Redirect } from "react-router-dom";
import { createBrowserHistory } from "history";
import dynamic from "../dva/dynamic";
import { routes, RouteConf } from "./config";
const RouterConfig = ({ app }: any) => {
return (
<Router history={createBrowserHistory()}>
<Switch>
{routes.map((value: RouteConf, key: number) => (
<Route
exact
key={key}
path={value.path}
component={dynamic({
app,
...value,
})}
/>
))}
<Redirect from="/*" to="/404"></Redirect>
</Switch>
</Router>
);
};
export default RouterConfig;
那么../dva/dynamic和./config里又是什么呢?
src/router/config.ts文件用来存放我们的路由表:
export interface RouteConf {
path: string;
component: Function;
name?: string;
models?: Function;
loadingComponent?: Function;
}
export const routes: RouteConf[] = [
{
path: "/home",
component: () => import("../page/home"),
models: () => [import("../models/home")],
},
];
src/dva/dynamic.tsx用来注册我们的组件,并注册 model。
import React, { Component } from "react";
import { Model } from "dva";
//缓存已注册的module
interface Cached {
[key: string]: number;
}
const cached: Cached = {};
function registerModel(app: any, model: Model) {
if (!cached[model.namespace]) {
app.model(model);
cached[model.namespace] = 1;
}
}
let defaultLoadingComponent = () => null;
function asyncComponent(config: any) {
const { resolve } = config;
return class DynamicComponent extends Component {
state: any = {
AsyncComponent: null,
};
loadingComponent: Function;
mounted: boolean;
constructor(props: any) {
super(props);
this.load();
this.loadingComponent =
config.loadingComponent || defaultLoadingComponent;
this.mounted = false;
}
componentDidMount() {
this.mounted = true;
}
componentWillUnmount() {
this.mounted = false;
}
load() {
resolve().then((m: any) => {
const AsyncComponent = m.default || m;
if (this.mounted) {
this.setState({ AsyncComponent });
} else {
this.state.AsyncComponent = AsyncComponent; // eslint-disable-line
}
});
}
render() {
const { AsyncComponent }: any = this.state;
const LoadingComponent: Function = this.loadingComponent;
if (AsyncComponent) return <AsyncComponent {...this.props} />;
return <LoadingComponent {...this.props} />;
}
};
}
export default function dynamic(config: any) {
const { app, models: resolveModels, component: resolveComponent } = config;
return asyncComponent({
resolve:
config.resolve ||
function () {
const models =
typeof resolveModels === "function" ? resolveModels() : [];
const component = resolveComponent();
return new Promise((resolve) => {
Promise.all([...models, component]).then((ret) => {
if (!models || !models.length) {
return resolve(ret[0]);
} else {
const len = models.length;
ret.slice(0, len).forEach((m) => {
m = m.default || m;
if (!Array.isArray(m)) {
m = [m];
}
m.map((model: Model) => registerModel(app, model));
});
resolve(ret[len]);
}
});
});
},
...config,
});
}
状态管理
我们在src目录下新建一个models文件夹用来存放我们的状态管理文件,并在其中新建一个home.ts来测试一下。
src/models/home.ts:
import { Effect } from "dva";
import { Reducer } from "redux";
interface stateType {
name: String;
}
interface options {
namespace: String;
state: stateType;
reducers: {
setState: Reducer<any, any>;
clearState: Reducer<any, any>;
};
effects: {
initState: Effect;
};
}
const home: options = {
namespace: "home",
state: {
name: "",
},
reducers: {
setState(state, action) {
const { payload } = action;
return Object.assign({}, state, payload);
},
clearState(state) {
return {
...state,
};
},
},
effects: {
*initState({ payload }: any, { put }: any) {
yield put({
type: "setState",
payload: { name: payload.name },
});
},
},
};
export default home;
在home.ts中有个name变量,它就是我们后面要用的全局变量。我们在路由配置文件中已经绑定过这个文件了,接下来就是要在页面文件中connect它们。
src/page/home.tsx:
import React, { FC } from "react";
import { connect } from "../dva";
const Home: FC<any> = (props: any) => {
const { dispatch, home } = props;
return (
<div>
<div>{home.name}</div>
<div
onClick={() => {
dispatch({ type: "home/initState", payload: { name: "kryst4l" } });
}}
>
点我
</div>
</div>
);
};
const mapStateToProps = (state: any) => ({ home: state.home });
export default connect(mapStateToProps)(Home);
接下来我们要把src/index.tsx里的app的根组件给替换掉,之前用的是home组件。
src/index.tsx
import React from "react";
import ReactDOM from "react-dom";
import { dva } from "./dva";
import Router from "./router/index";
/**
* 创建dva app
*/
const app = dva({
initialState: {},
models: [], // 公共的model 在这里引入
extraReducers: {},
onError(e: any) {
console.error("onError", e);
},
});
//加载渲染根路由方法
const render = (Component: any) => {
ReactDOM.render(<Component />, document.getElementById("root"));
};
const App = app.start(<Router app={app} />);
render(App);
验证一下
此时我们引入dva就已经大功告成了,分别引入了它的路由管理以及状态管理。我们有一个home的model以及一个页面home。我们现在来启动一下这个应用,看看它到底生效了没有。输入命令npm run start,并且把路由切换到home路径下。(因为一开始根目录重定向到了404页面)
css预处理器
我这里使用了sass,所以我们先安装sass。使用命令npm i sass来安装依赖。等它安装好了之后,在src/page目录下新增一个styles文件夹用来存放我们的样式文件。
src/page/styles/home.module.scss
.title {
color: red;
}
然后再在src/page/home.tsx中引入并且使用:
import styles from "./styles/home.module.scss"; // 引入写法
// ...
<div className={styles.title}>{home.name}</div> // 实例:使用写法
这样此项目基本就已经搭建完成了,此时的文件目录:
修改配置
因为此项目是基于create-react-app的,如果想要修改项目的webpack的配置,可以使用如下命令:
npm run eject
它是一个单向操作,意味着你如果使用了它,就不能再转换回去了。
当使用此脚本后,他会在项目中暴露出webpack配置信息文件,我们可以使用其进行修改。具体可以看官方文档。
当然,我们除了使用官方的解决方案之外,还可以使用其他社区的中的解决方案,这里可以使用craco这个库,具体可以看antd文档。
结语
按照以上教程,我们可以从零开始搭建一个基于React + dva + TypeScript + scss的应用,可以方便的使用此项目运用到日常的开发过程中。如果过程中有什么不清楚的地方,可以参考下此项目的完整示例,详情请戳下方链接:
项目完整示例:Github
如果看完这篇文章,对您有所帮助的话,还烦请您点个赞,点点关注,祝您生活愉快。