使用React+dva+TypeScript+Scss开发你的应用

1,337 阅读7分钟

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

前言

想练习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

随着一系列信息的打印,我们的项目初步已经搭建好了,我们来看看项目的目录都有什么:

image.png

通过create-react-app,我们现在生成了一个基于webpack的前端构建流水线应用,它现在集成了ReactTypeScript,前端性能检测、单元测试等功能。

我们先补充一下类型声明文件:

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启动项目:

image.png

可以看到已经启动成功了。但是现在就结束了吗,那远远还没有,我们还需要什么呢?那就是路由管理和全局状态管理,我们这里使用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它们。

image.png

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页面)

06.gif

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> // 实例:使用写法

这样此项目基本就已经搭建完成了,此时的文件目录:

image.png

修改配置

因为此项目是基于create-react-app的,如果想要修改项目的webpack的配置,可以使用如下命令:

npm run eject

它是一个单向操作,意味着你如果使用了它,就不能再转换回去了。

image.png

当使用此脚本后,他会在项目中暴露出webpack配置信息文件,我们可以使用其进行修改。具体可以看官方文档

image.png

当然,我们除了使用官方的解决方案之外,还可以使用其他社区的中的解决方案,这里可以使用craco这个库,具体可以看antd文档

结语

按照以上教程,我们可以从零开始搭建一个基于React + dva + TypeScript + scss的应用,可以方便的使用此项目运用到日常的开发过程中。如果过程中有什么不清楚的地方,可以参考下此项目的完整示例,详情请戳下方链接:

项目完整示例:Github

如果看完这篇文章,对您有所帮助的话,还烦请您点个赞,点点关注,祝您生活愉快。