react + ts 项目构建

啊哈??!又来了又来了,没错!!!之前那篇《从零开始的react》我弃了(理不直气也壮)

也没什么特别的原因,就、因为之前那个已经有点老了,不适合新的项目需求,而且还很重,balabala… 所以弃了,新开坑,耶~

新建工程

create-react-app新建一个react + ts 的模板工程

$ npx create-react-app my-app --template typescript

支持sass

https://create-react-app.bootcss.com/docs/adding-a-sass-stylesheet/

$ npm install node-sass --save
$ # or
$ yarn add node-sass

css reset

normalize.css 实现跨浏览器的默认样式标准

$ yarn add normalize.css 

使用

@import "~normalize.css";

提交自动格式化代码

https://create-react-app.bootcss.com/docs/setting-up-your-editor/#displaying-lint-output-in-the-editor

$ yarn add husky lint-staged prettier

package.json

+  "husky": {
+    "hooks": {
+      "pre-commit": "lint-staged"
+    }
+  }

提交的时候,Prettier自动格式化更改的文件

"dependencies": {
    // ...
  },
+ "lint-staged": {
+   "src/**/*.{js,jsx,ts,tsx,json,css,scss,md}": [
+     "prettier --write"
+   ]
+ },
  "scripts": {

添加eslint

https://juejin.im/entry/6844903509628813326

eslint-plugin-react –dev

"eslint-config-airbnb": "^18.2.0",
"eslint-config-prettier": "^6.11.0",
"eslint-plugin-react-hooks": "^4.1.2",
eslint-plugin-prettier

规范 commit message

https://juejin.im/post/6844903793033756680

https://commitlint.js.org/#/guides-local-setup?id=install-husky

$ yarn add commitlint @commitlint/config-conventional

commitlint.config.js

module.exports = {
    extends: ['@commitlint/config-conventional'],
    rules: {
        'type-enum': [2, 'always', [
            "feat", "fix", "docs", "style", "refactor", "test", "chore"
        ]],
    }
};

package.json

  "husky": {
    "hooks": {
      "pre-commit": "lint-staged",
+      "commit-msg": "commitlint -E  HUSKY_GIT_PARAMS"
    }
  },

增加提交检验

package.json

  "lint-staged": {
    "src/**/*.{js,jsx,ts,tsx,json,css,scss,md}": [
      "prettier --write"
    ],
+    "**/*.{js,ts,tsx}": "yarn eslint"
  },

增加路由

相关的库react-router-dom @types/react-router-dom

$ yarn add react-router-dom @types/react-router-dom

引入history

$ yarn add history

封装路由

routerInterface.ts

export interface PropsIRoute {
  // 图标
  icon?: string;
  // 菜单名
  name?: string;
  // 路由
  path?: string;
  // 组件
  component?: any;
  // 子路由
  routes?: PropsIRoute[];
  // 精确匹配
  exact?: boolean;
  // 重定向地址
  redirect?: string;
  // 不作为节点显示在菜单
  hideInMenu?: boolean;
  // 子路由不作为节点显示在菜单上
  hideChildrenInMenu?: boolean;
}

export interface PropsRouteWith {
  path: string;
  exact?: boolean;
  strict?: boolean;
  render: (props: any) => any;
  location?: any;
  rest?: [];
}

renderRouter.tsx

import React from 'react';
import { Switch, Route, Redirect } from 'react-router-dom';
import { PropsIRoute, PropsRouteWith } from './routerInterface';

const RouteWithProps = ({ path, exact, strict, render, location }: PropsRouteWith) => {
  return (
    <Route
      path={path}
      exact={exact}
      strict={strict}
      location={location}
      render={(props) => render({ ...props })}
    />
  );
};

const renderRouter = (routes: PropsIRoute[], extraProps = {}, switchProps = {}) => {
  return routes ? (
    <Switch {...switchProps}>
      {routes.map((route: any, i: number) => {
        if (route.redirect) {
          return (
            <Redirect
              key={route.key || i}
              from={route.path}
              to={route.redirect}
              exact={route.exact}
              strict={route.strict}
            />
          );
        }
        return (
          <RouteWithProps
            key={route.key || i}
            path={route.path}
            exact={route.exact}
            strict={route.strict}
            render={(props) => {
              const childRoutes = renderRouter(
                route.routes,
                {},
                {
                  location: props.location,
                }
              );
              if (route.component) {
                const newProps = {
                  ...props,
                  ...extraProps,
                };
                return (
                  <route.component {...newProps} route={route}>
                    {childRoutes}
                  </route.component>
                );
              }
              return childRoutes;
            }}
          />
        );
      })}
    </Switch>
  ) : null;
};

export default renderRouter;

routerConfig.ts

import {PropsIRoute} from "./routerInterface";

const routes: PropsIRoute[] = [
    {
        component: require('../components/test').default,
        path: '/test',
      	hideInMenu: true,
    }
];


export default routes

https://www.cnblogs.com/soyxiaobi/p/9573897.html

增加404 notfound 页面

routerConfig.ts

import { PropsIRoute } from "./routerInterface";
import NotFound from "../pages/NotFound";
import infrastructure from "../pages/infrastructure";
import test from "../components/test";

const routes: PropsIRoute[] = [
  {
    component: infrastructure,
    path: "/test",
    hideInMenu: true,
    routes: [
      {
        path: "/user",
        redirect: "/user/test",
        component: test
      }
    ]
  }
];

const notFoundRoute: PropsIRoute = {
  component: NotFound
};

function addExactToRoute(routeConfig: PropsIRoute[]) {
  routeConfig.forEach(route => {
    if (route.routes) {
      addExactToRoute(route.routes);
    } else {
      route.exact = true;
    }
  });
}

function addNotFoundAsFallbackRoute(routeConfig: PropsIRoute[]) {
  routeConfig.forEach(route => {
    if (route.routes) {
      addNotFoundAsFallbackRoute(route.routes);
      route.routes.push(notFoundRoute);
    }
  });
}


addExactToRoute(routes);
addNotFoundAsFallbackRoute(routes);
routes.push(notFoundRoute);

export default routes;

利用history进行路由记录

https://stackoverflow.com/questions/42701129/how-to-push-to-history-in-react-router-v4

history.ts

import { createBrowserHistory } from 'history';

export default createBrowserHistory();

App.tsx

!! 注意,这里应该使用 import {Router} 而不是 import {BrowserRouter as Router}

import { Router, Route, Link } from 'react-router-dom';
import history from './history';

ReactDOM.render(
  <Provider store={store}>
    <Router history={history}>
      <div>
        <ul>
          <li><Link to="/">Home</Link></li>
          <li><Link to="/login">Login</Link></li>
        </ul>
        <Route exact path="/" component={HomePage} />
        <Route path="/login" component={LoginPage} />
      </div>
    </Router>
  </Provider>,
  document.getElementById('root'),
);

使用

import history from '../history';

export function login(credentials) {
  return function (dispatch) {
    return loginRemotely(credentials)
      .then((response) => {
        // ...
        history.push('/');
      });
  };
}

我在上面这样使用history的时候,路由变化了,但是页面一直跳转到404,我找了很久,按照下面各种方法去解决

react-router v4 使用 history 控制路由跳转

react-router(v4)路由跳转页面不更新的问题

关于react-router点击Link后url刷新,但是页面无法刷新的问题

React路由的跳转的方法

How to push to History in React Router v4?]

!!!!全都没有用!!!!哭泣 o(╥﹏╥)o

最后的最后,我用了hook!!!react hook 万岁!!!

import { useHistory } from "react-router-dom";

function HomeButton() {
  let history = useHistory();

  function handleClick() {
    history.push("/home");
  }

  return (
    <button type="button" onClick={handleClick}>
      Go home
    </button>
  );
}

注意!!!这里有个坑,要先把history删掉,不然history.goBack()会报类型错误!!!

增加axios,封装axios请求

$ yarn add axios

这部分目前不是很满意,应该涉及token权限部分,然后消息提示也要整合,但是暂时先这么玩吧

/src/api/index.tx

import axios, { AxiosInstance, AxiosResponse, AxiosRequestConfig } from "axios";
import { paramsSerializer } from "../utils/index";
import codeMessages from "./codeMessages";
import config from "../config";

export interface HttpSameResponse<T = any> {
  // http状态码
  code: any;
  // 对这个状态的解析说明
  msg: string;
  // 附加内容
  data: T;
}

export interface HttpAxiosParams extends AxiosRequestConfig {
  // 成功信息
  successMsg?: string;
  // 成功回调
  successCallback?: (data: any) => void;

  // 错误信息是否提示
  errorAlert?: boolean;
  // 失败信息
  errorMsg?: string;
  // 失败回调
  errorCallback?: (data: HttpSameResponse) => void;
}

class HttpRequest {

  baseURL = "";

  constructor(url = config.baseURL) {
    this.baseURL = url;
  }

  paramsSerializer = (params, arrayFormat = "repeat") => {
    return paramsSerializer(params, arrayFormat);
  };

  getInsideConfig({ headers, ...options }: HttpAxiosParams): HttpAxiosParams {
    const $config: HttpAxiosParams = {
      baseURL: this.baseURL,
      timeout: 10000,
      headers: {
        "Content-Type": "application/json; charset=utf-8",
        ...headers
      },
      errorAlert: true,
      paramsSerializer: this.paramsSerializer
    };
    return Object.assign($config, options);
  }

  public interceptors = (instance: AxiosInstance, options: HttpAxiosParams) => {
    const { successMsg, successCallback, errorAlert, errorMsg, errorCallback } = options;
    let response = {
      data: {
        code: 500,
        msg: codeMessages[500],
        data: null
      } as HttpSameResponse
    };
    instance.interceptors.request.use(
      (requestConfig: AxiosRequestConfig) => {
        return requestConfig;
      },
      (e) => {
        return Promise.reject(e);
      }
    );

    instance.interceptors.response.use(
      (res: AxiosResponse) => {
        const { data } = res;
        const resDef = {
          ...response.data,
          code: 200
        };
        let $data = data || resDef;
        if (!("code" in $data)) {
          $data = {
            ...resDef,
            data
          };
        }

        if ($data.code >= 200 && $data.code <= 299) {
          if (successMsg) {
            console.log(successMsg);
          }
          if (typeof successCallback === "function") {
            successCallback($data.data);
          }
        }
        return $data;
      },
      async (error: any) => {
        const errMsg = error.message;
        if (errMsg.includes("timeout")) {
          response.data = {
            code: 408,
            msg: codeMessages[408],
            data: null
          };
        } else if (error.response) {
          response = error.response;
        }
        if (errorMsg) {
          response.data.msg = errorMsg;
        }
        if (typeof window !== "undefined" && errorAlert && !errMsg.includes("401")) {
          console.log(response.data.msg);
        }
        if (typeof errorCallback === "function") {
          errorCallback(response.data);
        }
        return Promise.resolve(response.data);
      }
    );
  };

  public http(options: HttpAxiosParams): Promise<HttpSameResponse> {
    const instance: AxiosInstance = axios.create();
    const $options = this.getInsideConfig(options);
    this.interceptors(instance, $options);
    return instance($options) as Promise<any>;
  }
}

export const $request = new HttpRequest();

export default $request;

codeMessages.ts

export default {
  401: '用户无权限',
  403: '用户得到授权,但是访问是被禁止的。',
  404: '发出的请求针对的是不存在的记录,服务器没有进行操作。',
  406: '请求的格式不可得。',
  408: '请求超时',
  410: '请求的资源被永久删除,且不会再得到的。',
  422: '当创建一个对象时,发生一个验证错误。',
  500: '服务器发生错误,请检查服务器。',
  502: '网关错误。',
  503: '服务不可用,服务器暂时过载或维护。',
  504: '网关超时。',
};

通用封装可参考 axios请求的封装,Vue、React都可使用

第三库

这部分的话,就主要是根据项目需求来看是否需要引入了。

$ yarn add immer
$ yarn add use-immer
$ yarn add qs
$ yarn add lodash
$ yarn add moment

一些常用的工具

/src/utils/index.ts

import qs from "qs";

const utils = {};

/**
 * 对象转url字符串
 * @param params
 * @param arrayFormat
 */
export function paramsSerializer(params, arrayFormat = "repeat") {
  return qs.stringify(params, { arrayFormat });
}

export default utils;

项目目录结构

项目主要目录是src

|-- src               
    |-- api
    |-- assets
    |-- common
    |-- components
    |-- config
    |-- container
    |-- pages
    |-- router
    |-- styles
    |-- utils
    |--...

api:api请求接口文件,可通过swagger文档动态生成

assets:静态资源文件,存放字体和图片

common: 一些请求通用hook

components:通用组件,目前有36个

config:存放配置文件

container:页面模块构成部分

pages:主页面

router:路由相关文件

styles:全局通用样式

utils:一些通用工具

搭建过程中遇到的问题

‘expect’ is not defined no-undef

解决:让eslint支持jest环境

.eslintrc

{
  "overrides": [
    {
      "files": [
        "**/*.spec.js",
        "**/*.spec.jsx"
      ],
      "env": {
        "jest": true
      }
    }
  ]
}

@commitlint/cli@11.0.0: The engine “node” is incompatible with this module. Expected version “>=v10.22.0”. Got “10.16.0”

解决:切换 node 环境

$ nvm use 12.4.0

Node Sass could not find a binding for your current environment

Error: Missing binding /Users/dany/M_PROJECT/react-base-frame/node_modules/node-sass/vendor/darwin-x64-64/binding.node
Node Sass could not find a binding for your current environment: OS X 64-bit with Node.js 10.x

Found bindings for the following environments:
  - OS X 64-bit with Node.js 12.x

This usually happens because your environment has changed since running `npm install`.
Run `npm rebuild node-sass` to download the binding for your current environment.

解决:

$ npm rebuild node-sass
// or 
$ yarn install --force

如果上面执行了之后仍然不起效,查看一下自己的IDE是不是环境node路径配置有问题,像我的话情况,用了nvm,就算切换了node版本之后,webstorm默认自带的环境还是10.x的那个版本,所以把webstorm的环境设置成12.x的就可以啦~

react打包build后,各种静态资源路径报错

解决:老生常谈了,homepage 加上就好~

package.json

{
  "name": "react-base-frame",
  "version": "0.1.0",
  "private": true,
+  "homepage": ".",
  ...
}

[Violation] Added non-passive event listener to a scroll-blocking ‘mousewheel’ event. Consider marking event handler as ‘passive’ to make the page more responsive.

[Violation] Added non-passive event listener to a scroll-blocking 'mousewheel' event. Consider marking event handler as 'passive' to make the page more responsive. See https://www.chromestatus.com/feature/5745543795965952

使用 onMouseEnteronMouseLeave 的时候控制台报的错误

解决:

yarn add default-passive-events

Index.tsx

import 'default-passive-events'

参考:

https://blog.csdn.net/weixin_34403976/article/details/102900928


基本上到这里就差不多了,我觉得一个基础框架也就这样吧,已经能满足大部分的需求了,但是还有想优化的,不过优化的部分另外做记录啦~比如我想加mock数据,想加根据swagger文档自动生成的api文件,想加路由的滑动切换……后面慢慢来吧~

ps:附上一个按这个步骤最后搭建成的项目工程 ┏ (゜ω゜)=☞

参考资料

你可能不知道的 JavaScript 代码规范

超详细的Git提交规范引入指南

React Router 4.x 开发,这些雷区我们都帮你踩过了

来,让我们谈一谈 Normalize.css

使用 Webpack 加载 CSS + 使用 Normalize.css 实现跨浏览器的默认样式标准化

【第十三课】更合理的CSS结构

CSS架构的理想与现实