啊哈??!又来了又来了,没错!!!之前那篇《从零开始的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点击Link后url刷新,但是页面无法刷新的问题
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
使用 onMouseEnter
和 onMouseLeave
的时候控制台报的错误
解决:
yarn add default-passive-events
Index.tsx
import 'default-passive-events'
参考:
https://blog.csdn.net/weixin_34403976/article/details/102900928
基本上到这里就差不多了,我觉得一个基础框架也就这样吧,已经能满足大部分的需求了,但是还有想优化的,不过优化的部分另外做记录啦~比如我想加mock数据,想加根据swagger文档自动生成的api文件,想加路由的滑动切换……后面慢慢来吧~
ps:附上一个按这个步骤最后搭建成的项目工程 ┏ (゜ω゜)=☞
参考资料
React Router 4.x 开发,这些雷区我们都帮你踩过了