前言
Hello 大家好! 我是前端 无名
背景
每天都在不间断的写业务,React 项目做了一个又一个,好多年了,要新建一个React项目,直接copy 旧项目,然后删除旧业务逻辑,修修减减一个新的项目框架就起来了。
这样导致的结果:
- 创建项目耗时太长
- 修修减减项目框架不稳定
- 不利于迭代优化框架
- ...
所以就有了搭建自己的一套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);
后语
欢迎大家多提意见。项目模板在不断优化,一赞一回!欢迎评论。