开箱即用的React项目模板

8,283 阅读5分钟

前言

Hello 大家好! 我是前端 无名

背景

每天都在不间断的写业务,React 项目做了一个又一个,好多年了,要新建一个React项目,直接copy 旧项目,然后删除旧业务逻辑,修修减减一个新的项目框架就起来了。

这样导致的结果:

  1. 创建项目耗时太长
  2. 修修减减项目框架不稳定
  3. 不利于迭代优化框架
  4. ...

所以就有了搭建自己的一套React项目框架的想法,开箱即用。

本篇文章先介绍项目模板的功能,后续会介绍该项目如何搭建以及如何设计。

项目结构

react-project-template
├── .babelrc # babel配置
├──  Webpack # webpack公用配置目录
│ │  ├──plugins # 公用插件集合
│ │  ├──resolve # webpack resolve配置
│ │  ├──utils # webpack 工具类
│ │  ├──variable # webpack 变量配置
│ ├── webpack.base.js # Webpack 基础配置文件
│ ├── webpack.dev.js # Webpack 开发环境配置文件
│ └── webpack.prod.js # Webpack 生产环境配置文件
├── yarn.lock # 锁定 npm 包依赖版本文件
├── package.json
├── postcss.config.js # 自动兼容 CSS3 样式配置文件
├── .editorconfig # IDE格式化规范
├── .eslintignore  # eslint忽略文件配置
├── .eslintrc.js # eslint配置文件
├── .prettierignore # prettierc忽略文件配置
├── .prettierrc # prettierc配置文件
├── .husky # 配置git提交钩子
├── .commitlint.config.js # 配置git提交规范
├── tsconfig.eslint.js # eslint检查typescript配置项配置文件
├── eslintError.html # eslint报告文件
├── public # 存放html模板
├── README.md
├── src
│ ├── assets # 存放会被 Webpack 处理的静态资源文件:一般是自己写的 js、css 或者图片等静态资源
│ │ ├── fonts # iconfont 目录
│ │ ├── images # 图片资源目录
│ │ ├── css # 全局样式目录
│ │ │  ├── common.scss # 全局通用样式目录
│ │ │  ├── core.scss # 全局sass 变量目录,直接使用,不需要引用,全局已统一引入。
│ │ │  └── init.scss # 全局初始化css
│ │ └── js # 全局js
│ ├── common # 存放项目通用文件
│ │ ├── Resolution.ts # 布局适配配置中心
│ │ └── AppContext.ts # 全局App上下文
│ ├── components # 项目中通用的业务组件目录
│ ├── config # 项目配置文件
│ ├── pages # 项目页面目录
│ ├── typings # 项目中d.ts 声明文件目录
│ ├── types # 项目中声明文件
│ │ ├── service # 项目中服务相关声明文件
│ │ ├── enum.ts # 项目中枚举类型
│ │ ├── IContext.ts  # 全局App上下文声明
│ │ ├── IRedux.ts # redux相关声明
│ │ └── IRouterPage.ts # 路由相关声明
│ ├── uiLibrary # 组件库
│ ├── routes # 路由目录
│ │ ├── index.tsx # 路由配置入口文件
│ │ └── RouterUI.tsx # 路由转换
│ ├── services # 和后端相关的文件目录
│ │ ├── api # 调用后端接口定义目录
│ │ │ ├── index.ts
│ │ ├── axios.ts # 基于 axios 二次封装
│ │ ├── BaseService.ts # 基础请求服务类型
│ │ ├── ServerResponseManager.ts # 服务返回统一管理
│ │ ├── serviceConfig.ts # 服务地址配置文件
│ ├── store # redux 仓库
│ │ ├── actionCreaters # action创建与分发绑定
│ │ ├── action  # 项目中action
│ │ ├── reducers  # 项目中reducers
│ │ │  ├──history # 项目中路由相关history
│ │ ├── index.ts # 全局 store 获取
│ │ ├── connect.ts # react 页面与store 连接
│ ├── utils # 全局通用工具函数目录
│ ├── App.tsx # App全局
│ ├── index.tsx # 项目入口文件
│ ├── index.scss # 项目入口引入的scss
└── tsconfig.json # TS 配置文件

项目地址

如果您觉得上面的React模板项目结构能够吸引你,可以去看看源码 react-project-template

并且也做了相关的脚手架quanyj-react-cli,你的 star 是我不断前行的动力!

项目介绍

  • 项目采用Webpack5 + Typescript + React

  • 项目中引入了 core.scss , 全局公用,直接使用不需要每个scss文件@import

  • 构建项目时会自动兼容 CSS3 样式,所以不需要自己去写浏览器兼容样式

  • 项目支持配置路由

  • 项目中集成了 connected-react-router ,路由存储在store中,界面直接从store获取

  • 项目中默认使用immer来代替immutable

  • 项目中默认配置了一些常用工具函数

  • 项目中针对 axios 做了二次封装

  • 项目直接使用px即可

  • 项目大量使用装饰器,例如@connect,@context等来简化代码

  • 项目采用ESLint和Prettier 来规范代码

  • 项目引入husky + lint-staged 来校验git提交

部分代码片段

页面使用案例

使用装饰器connect从store中获取数据

使用装饰器withAppContextDecorators 从AppContext内容提供者中获取数据

import React from "react";
import Page from "@/components/Page";
import { UPDATE_USER_ID } from "@/store/actions/user";
import connect from "@/store/connect";
import CPng from "@/assets//images/02.png";
import HomeChild from "./HomeChild";
import { withAppContextDecorators } from '@/common/AppContext';
import "./index.scss";
import { IAppContext } from '@/types/IContext';

interface IHomeProps {
    userId: number;
    updateId: (id: number) => void;
}

interface IHomeState {
    count: number
}

const mapStateToProps = {
    userId: "user.userId"
};

const mapDispatchToProps = {
    updateId: UPDATE_USER_ID
};

@withAppContextDecorators
@connect(mapStateToProps, mapDispatchToProps)
class Home extends Page<IHomeProps & IAppContext, IHomeState>{
    constructor(props) {
        super(props);
        this.state = {
            count: 0
        };
    }

    componentDidMount = () => {
        setTimeout(() => {
            this.props.updateId(5);
        }, 3000)
        console.log(this.props.test());
        this.setState({
            count: 1
        })
    }

    render() {
        const { userId } = this.props;
        const { count } = this.state;
        console.log("this.props==", this.props);
        return (
            <div className='home'>
                <div className="text1">Hello, World!{userId} </div>
                <div className="bg1">{count}</div>
                <img className="img1" src={CPng} ></img>
                <HomeChild></HomeChild>
            </div>
        );
    }
}

export default Home;

路由配置

路由可配置,实现懒加载以及分包

import React from 'react';

export default {
    routes: [
        {
            exact: true,
            path: '/',
            isDynamic: true,
            component: React.lazy(() =>
                import(/* webpackChunkName: "home",webpackPrefetch: true */ '@/pages/Home'),
            ),
        },
        {
            exact: true,
            path: '/page1',
            isDynamic: true,
            component: React.lazy(() =>
                import(/* webpackChunkName: "page1",webpackPrefetch: true */ '@/pages/Page1'),
            ),
        },
    ],
}

自适配组件封装

import React from "react";
import Resolutions from "@/common/Resolution";

interface IResolutionComProps {
    WIDTH?: number;
}

export default class ResolutionCom extends React.Component<IResolutionComProps> {
    componentDidMount() {
        this.resize();
        window.onresize = this.resize;
    }
    resize = () => {
        //获取设计稿的尺寸
        const design = Resolutions.getDesign();
        const getHtmlFs = () => {
            return parseFloat(window.getComputedStyle(html, null)["font-size"]);
        };
        const getScreenWidth = () => {
            let htmlWidth = 0;
            try {
                let htmlElement = document.documentElement;
                htmlWidth = Math.max(htmlElement.offsetWidth || 0, htmlElement.clientWidth || 0, htmlElement.getBoundingClientRect().width || 0);
                // 读取失败,其他方式读取
                if (!htmlWidth || htmlWidth <= 0) {
                    if (window.orientation == 180 || window.orientation == 0) {
                        //竖屏
                        htmlWidth = window.innerWidth || (window.screen && window.screen.width) || (window.screen && window.screen.availWidth) || 0;
                    } else if (window.orientation == 90 || window.orientation == -90) {
                        //横屏
                        htmlWidth = window.innerHeight || (window.screen && window.screen.height) || (window.screen && window.screen.availHeight) || 0;
                    }
                }
            } catch (e) {
                console.log("获取屏幕宽度出错");
            }
            return htmlWidth | 0;
        }
        let html = document.documentElement,
            WIDTH = this.props.WIDTH || design.WIDTH,
            //第一次进来没有设置过html标签font-size的时候
            screenWidth = getScreenWidth(),
            htmlFs = getHtmlFs(),
            mediaFs = (design.RATIO / WIDTH) * screenWidth; //获取页面宽度  设备宽度/fontSize=设计稿(750)/100=7.5;

        html.style.fontSize = mediaFs + "px"; //根据页面大小算出font-size

        //以下是特殊处理 试过一台htc下的某个浏览器设置字体大小后再获取font-size会比所设的值会相对变小 所以设置大一点让它font-size的结果是想设的结果

        if (htmlFs !== mediaFs && Math.abs(htmlFs - mediaFs) > 2) {
            html.style.fontSize = "100px";
            html.style.fontSize = (100 / getHtmlFs()) * mediaFs + "px";
        }
    };
    render() {
        return this.props.children;
    }
}

connected-react-router 修改源码,支持使用immer

import { createHashHistory } from 'history';
import produce from "immer";
import { LOCATION_CHANGE } from 'connected-react-router';
import { IActionParam } from "@/types/IRedux";

let history = createHashHistory();
export default history;

//import { push } from 'connected-react-router';
//提供了push,go,goBack,replace,block,goForward方法。
//push("/home") || push({pathname:"/home",search:"name=1",hash:"1"})
//history 可以分为两部分,切换和修改,切换历史状态:back,forward,go对应浏览器的后退,跳转,前进。history.go(2);//前进两次

//push 把页面状态保存在state对象中,当页面回来的时候,可以通过event.state获取到state对象。




//查看connected-react-router 的connectRouter 方法,使immer与history 结合使用
export interface HistoryState {
    location: any,
    action: any,
}

const injectQuery = (location) => {
    if (location && location.query) {
        // Don't inject query if it already exists in history
        return location
    }

    const searchQuery = location && location.search

    if (typeof searchQuery !== 'string' || searchQuery.length === 0) {
        return {
            ...location,
            query: {}
        }
    }

    // Ignore the `?` part of the search string e.g. ?username=codejockie
    const search = searchQuery.substring(1)
    // Split the query string on `&` e.g. ?username=codejockie&name=Kennedy
    const queries = search.split('&')
    // Contruct query
    const query = queries.reduce((acc, currentQuery) => {
        // Split on `=`, to get key and value
        const [queryKey, queryValue] = currentQuery.split('=')
        return {
            ...acc,
            [queryKey]: queryValue
        }
    }, {})

    return {
        ...location,
        query
    }
}


const initHistoryState: HistoryState = {
    location: injectQuery(history.location),
    action: history.action,
};

/* eslint-disable no-param-reassign  */
export const reducer = produce((draft: HistoryState, actionParam: IActionParam) => {
    if (actionParam.type === LOCATION_CHANGE) {
        const { location, action, isFirstRendering } = actionParam.payload;
        draft.action = action;
        draft.location = injectQuery(location);
        return draft;
    }
    return draft;
}, initHistoryState);



后语

欢迎大家多提意见。项目模板在不断优化,一赞一回!欢迎评论。