react--随笔 1

REACT

介绍

一款javascript前端框架,把用户界面抽象成一个个的组件,按需组合成页面,官网,与其他框架的共同点是,都采用虚拟dom,和数据驱动

angularJs reactJs vueJs angularTs
控制器 - - 弱化
过滤器 -
指令 -
模板语法 -
服务 - -
组件 -
jsx - 加入 -

angularJs 双向绑定,依赖注入 reactJs一切皆组件 VueJs 小而美 angularTs 大而全

环境搭建

官方脚手架

安装 yarn,重启(不重启,别的盘符有可能用不了)

//查询当前镜像
yarn config get registry 
//设置为淘宝镜像
yarn config set registry https://registry.npm.taobao.org/
//设置为官方镜像
//yarn config set registry https://registry.yarnpkg.com

官网 [create-react-app](https://create-react-app.dev/docs/getting-started

创建 react项目

yarn create react-app 目录 | npx create-react-app 目录 | npm init react-app 目录

yarn eject   解构出所有的配置文件 可选
yarn start |  npm start 			开发
yarn build |  npm run build	 打包

//调试 需要安装给chrome浏览器一个插件 react-dev-tools  谷歌助手->chrome商店-搜索

环境解析

  • react: 核心包,解析组件 演示
  • react-dom: 编译 -> 浏览器 演示
  • react-scrpts: react的项目环境配置
  • manifest.json 生成一个网页的桌面快捷方式时,会以这个文件中的内容作为图标和文字的显示内容
  • registerServiceWorker.js支持离线访问,所以用起来和原生app的体验很接近,只有打包生成线上版本的react项目时,registerServiceWorker.js才会有效。服务器必须采用https协议
  • 对Internet Explorer 9,10和11的支持需要polyfill。

环境配置和错误处理

运行yarn 提示“不是内部或外部命令,”
	装完重启
  
create-react-app 提示“不是内部或外部命令,” //yarn无法全局安装包
	npm i create-react-app  -g      用npm重装
  
create-react-app 能用安装到一半报错(error)
	node 全量安装  ,一路下一步安装过程中有个复选框选中,时长30分钟


报缺少babel 包: 安装一下(yarn add xxx -S)

配置
修改端口
//修改script/start.js
const DEFAULT_PORT = parseInt(process.env.PORT, 10) || 3001;

去除eslint 警告
//config/webpack.config.js
//注释关于eslint的导入和rules规则 (react16.x)
//注释关于new ESLintPlugin (react17.x)

第三方脚手架

yomen/umi

webpack手动搭建

资源限制

  • 本地资源导入(import) 不可以导入src之外的包
  • 相对 路径以文件所在位置为基准顶层到src,绝对路径 的根是 public目录
  • 前景图片, 相对 和 绝对路径 都指向了 public目录(因为是数据)

JSX

jsx是一个 JavaScript 的语法扩展,可以理解为js的一个新的数据类型,出现在js当中,文件为xx.js|xx.jsx

var b= 强壮

语法要求

  • 标签要闭合
  • 元素必须要有一个顶层元素
  • 变量首字母大写代表组件,小写对应是js数据类型
  • 属性,小驼峰命名

JSX 是一个 JavaScript 语法扩展。它类似于模板语言,但它具有 JavaScript 的全部能力。JSX 最终会被编译为 React.createElement() 函数调用,返回称为 “React 元素” 的普通 JavaScript 对象

es6

class Person2223{
  constructor(name){
    this.name=name||'alex'  //实例属性创建,赋值
  }
  show(){//实例方法
    console.log('show',this.name);
  }
}
Person2223.VERSION='1.2.3';//静态属性|类属性

//子类
class Worker123 extends Person2223{
  constructor(name,job){
    super(name);//类如果有继承 super就要出现
    this.job=job||'卖烧饼';
  }
  show2(){
    console.log(this.job,this.name);
  }
}

es6+

//es7 类
class Person123{
  name='alex'; //实例属性  放在类内部,设置默认值
  age; //没有默认值的实例属性
  static VER='1.11.1';  //类属性 静态属性
  constructor(name,age){
    this.name=name;
    this.age=age||20; //构造器里面可以初始化实例属性
  }

  show(){//方法
    console.log(this.name,this.age,this.show);//访问实例属性
  }

  static show2(){//静态|类 方法定义
    console.log(this.name)
  }
}

class Workerr321 extends Person123{

  job; //实例属性

  static SUM=100;

  constructor(name,age,job){
    super(name,age);//调用父类 影响父类传入到当前的实例属性
    this.job=job||'卖闲鱼'; //构造器初始化
    // this.address='外滩18号';//实例属性,要实现声明
  }

  showJob(){
    console.log(this.job);
  }

}

组件

react组件:类式组件和函数式组件和api式组件

创建组件

//es6
import React from 'react';

class 组件名 extends React.Component{
  
  //每一个组件都会重Component基类上继承props,state,refs,context

  state={} 实例属性 组件状态

  static msg;  类属性

  constrctor(props){ //需要在构造时,修改组件的状态时,constrctor才会出现
    super(props) //类如果有继承 super就要出现
      需要在组件构造器内处理传递过来的props时,props参数就出现

    this.state={ // 本地状态

    }
  }
  render(){
    return jsx|null   //jsx~~要渲染   null不渲染
  }
  方法1(){} 自定义的方法
  static 方法2(){}
}

//es5
var  createReactClass = require('create-react-class');
var Greeting = createReactClass({
  getDefaultProps: function() {
    return {
      name: 'Mary'
    };
  },
  getInitialState: function() {
    return {count: this.props.name||0};
  },
  render: function() {
    // return 

Hello, {this.props.name}/{this.state.count}

; return React.createElement('h1',{},`Hello, ${this.props.name}/${this.state.count}`) } }); //函数式 function 组件名(){ return jsx|null //jsx~~要渲染 null不渲染 }

使用组件

//声明式

//编程式 new 组件名({props}) 组件名({props})

渲染(描画)页面

import ReactDom from 'react-dom';
var RactDom = require('react-dom');
ReactDom.render(jsx,插入点,回调)

props

传递属性

<组件名 属性名=值 属性名2=值2 .. />

propName=“字符” propName={js数据类型}

使用属性

{this.props.属性名}

this 代表的是组件本身

对象无法直接通过{对象}展示

类型检查

import propsTypes from 'prop-types'

//默认值:		
组件.defaultProps={propName:值,xx:oo}

//类型约定:
组件.propTypes={propsName:propsTypes库.类型名,xx:类型}
//propsTypes库.array/bool/func/number/object/string

//必传参数
propName: propsTypes库.类型名.isRequired

组件无论是使用函数声明还是通过 class 声明,都决不要修改自身的 props

事件

  • React 事件的命名采用小驼峰式(camelCase),而不是纯小写。
  • 使用 JSX 语法时你需要传入一个函数作为事件处理函数,而不是一个字符串
  • 类组件,事件函数内部this会丢失

事件绑定

修正this

onClick={this.方法.bind(this,值)}
onClick={()=>this.方法()}
构造器: this.方法=this.方法.bind(this)  √
实例方法=()=>{箭头函数定义方法}  √√

事件对象

实例方法(ev)	ev 代理事件对象 ev.target 返回虚拟Vdom √

冒泡

阻止: ev.stopPropagation()

默认行为

阻止: ev.preventDefault()

传参

onClick={this.clickHandler2.bind(this, 12)}
onClick={()=>this.clickHandler2(12)}

组件状态

state|数据|私有状态|本地状态

定义

//es6+ 
//实例属性: state    
class App{state:{}}

//es6:构造器 this.state  
class App extends React.Component{
  constructor(){
    this.state={}
  }
}

//ES5:
createReactClass({
  getInitialState:function(){
    return {
      状态名:值,xx:oo
    }
  }   
})

获取

//渲染
{this.state.proname}
//获取
this.state.proname

修改状态

//修改
this.setState(对象)  //浅合并state

this.setState((asyncState,prevProps)=>{
  //一般是用于在setState之前做一些操作
  //this.state==同步结果
  //asyncState==异步结果
  //this.props==后
  //prevProps==前
  return {
    sname:value
  }
}) 

this.setState({
  sname:value
}, () => {
  //一般是用于在setState之后做一些操作
  //this.state == 修改之后的state
})

setState的结果是异步的

列表渲染

//对象 数组 string 数字
this.props|state.属性名.map(function(val,index){
  return jsx
})

条件渲染

//表达式渲染
this.state|props.proname ? jsx1 : jsx2
this.state|props.proname && jsx

//render里面写语句
render(){
  let el=null;
  if(this.state|props.proname){
    el=jsx1
  }else{
    el=jsx2
  }
  
  return el
}

//渲染写成实例方法
renderFn(参数){
  ...参数 做判断
  return el
}
render(){
  return {this.renderFn(条件)}
}

refs

需要抓取dom元素与第三方 DOM 库集成,触发命令式动画,管理焦点,文本选择或媒体播放

用法

refs用法 有4种

//1、 string refs

  
this.firstRef 访问 -》 {current:dom}
  
// 3. callback refs  回调 √
 this.定义一个实例属性 = el}
this.定义一个实例属性 //后期用作访问元素本身

// 4. 转发 refs
  //引用编译后的组件内部的元素,要被引用的组件是一个函数式组件
  
<包装后的子组件 ref={el=>this.inputRef=el} />
  
//子组件是个函数式
const 包装后的子组件 = React.forwardRef((props, ref) => (
  ...
  )
  ...                       
));
  
//当组件挂载时,将 DOM el元素传递给 ref 的回调
//当组件卸载时,则会传递 null。
//ref 回调会在 componentDidMount 和 componentDidUpdate 生命周期之前调用

受控元素

表单的value受控,受数据控制

value={this.state.数据名}  //model->view
onChange={this.方法}   //view->model

处理多个输入元素

可以为每个元素添加一个 name 属性(通常和数据名一致),处理函数根据 event.target.name 的值来选择要做什么

双向绑定

非受控元素

要编写一个非受控组件,而不是为每个状态更新都编写数据处理函数,你可以 使用 ref 来从 DOM 节点中获取表单数据


默认值

表单元素上的 value 将会覆盖 DOM 节点中的值,在非受控组件中,你经常希望 React 能赋予组件一个初始值,但是不去控制后续的更新,指定一个 defaultValue 属性,而不是 value

留言板

样式

css

引用

定义

  • index.html : 引入 link/style 公共样式 不优化 第三方样式
  • index.jsx: import ‘./css/xx.css’ 是全局 公共样式 会优化
  • 组件.jsx import ‘./css/xx.css’ 全局 希望私有 会优化

选择器冲突解决方案

  • 命名空间 BEM
  • BEM:块(block)、元素(element)、修饰符(modifier)
  • 模块化
import 变量  from './css/xx.module.css' 
 xx.module.css 
//需要模块化的才修改,不影响其他非模块化css写法

scss

需要下载安装包:网页: node-sass 软件: dart-sass

yarn add node-sass -S
/*定义scss*/
$bg-color: #399;
.box{
  background: $bg-color;
}
//引入
import 'xx/xx.scss'

//使用

引入scss全局变量

  • 局部scss文件内部: @import ‘./全局.scss’
  • webpack配置一次,局部scss内部直接使用
//1. 安装插件 : sass-resources-loader
//2. 配置修改webpack.config.js

{
  test:sassRegex,
  ...
  use: [
    {loader:'style-loader'},
    {loader:'css-loader'}, 
    {loader:'sass-loader'},
    {
      loader: 'sass-resources-loader',
      options:{
        resources:'./src/xx/全局主题.scss'
      }
    }
  ]
}

注意:
loader:‘css-loader?modules’ ?modules 模块化时需要添加
resources 指向作用域在项目环境下

组件拆分规则

组件拆分目标:为了复用

组件如何拆:单一原则

状态应该给谁(状态提升)

  • 尽量给顶层组件(状态提升),->props->子组件
  • 可以从 props(属性) 得到,那么它可能不应该在 state(状态) 中
  • 方法-》操作数据(数据|状态在哪,方法就应该在哪)
  • props取名从组件本身的角度来命名, 而不是它被使用的上下文环境

动画

tansition

transition: .5s ease all;
进度条

AntMotion

官网,是一款蚂蚁金服的动画组件库,支持单元素,css、进出场动画、及文字动画

组件内部的 一级元素&& 做动画
一级元素要有key,根据编号依次做动画,无key不动画,路由离场动画无效
包裹路由组件无效(一级元素&& 进退场)

生命周期

实例化 -> 更新期 -> 销毁时

es5版

实例化

  1. 取得默认属性(getDefaultProps) 外部传入的props
  2. 初始状态(getInitailState) state状态
  3. 即将挂载 componentWillMount
  4. 描画VDOM render
  5. 挂载完毕 componentDidMount

次新版

实例化

  1. 取得默认属性,初始状态在constructor中完成

    运行一次,可读数据,可同步修改state,异步修改state需要setState,setState在实例产生后才可以使用,可以访问到props

  2. 即将挂载 componentWillMount

  3. 描画VDOM render

  4. 挂载完毕 componentDidMount

    使用ref,使用setState,读取数据

更新期

  1. props改变 componentWillReceiveProps(nextProps)
    初始化render时不执行 这里调用更新状态是安全的,并不会触发额外的render调用,nextProps 更新后 this.props更新前,路由监听

  2. 是否更新 shouldComponentUpdate

    指视图 return true/false

  3. 即将更新 componentWillUpdate

  4. 描画dom render

    不要在这里修改数据

  5. 描画结束 componentDidUpdate

销毁时

componentWillUnmount即将卸载,可以做一些组件相关的清理工作,例如取消计时器、网络请求等

所有子挂载完,才标志着父挂载完,父更新子更新,子更新父不更新

新版

脑图,挂载前、更新前、props更新前统一用getDerivedStateFromProps代替,并添加了返回快照钩子getSnapshotBeforeUpdate

返回快照:发生在render完了,但还没有去编译真实dom之前,返回dom的快照

实例化

  1. 渲染前 static getDerivedStateFromProps(nextProps,nextState) {}

    无法访问this
    nextProps,nextState是更新后的
    必须返回 一个对象,用来更新state 或者 返回 null不更新
    必须要初始化state
    场景:state 的值在任何时候都取决于 props时

  2. 渲染中 render

    必须return jsx|string|number|null
    不会直接与浏览器交互:不要操作DOM|和数据

  3. 挂载后 componentDidMount

    使用ref,使用setState,读取数据

更新期

  1. 渲染前 static getDerivedStateFromProps(nextProps, nextState)

  2. 是否渲染 shouldComponentUpdate(nextProps, nextState)

    是否更新,必须返回true/false
    首次渲染或使用 forceUpdate() 时不会调用该方法
    nextProps,nextState更新后的,this.props,this.state 更新前的
    return false 只阻止当前组件渲染

  3. 渲染中 render

  4. dom快照 getSnapshotBeforeUpdate(prevProps, prevState)

    组件能在发生更改之前从 DOM 中捕获一些信息(dom渲染前的状态)
    返回的 值|null 会给 componentDidUpdate
    prevProps, prevState 更新前 this.props,this.state更新后

    事例

  5. 更新后 componentDidUpdate(prevProps, prevState,snopshot)

    this.props.更新后的
    snopshot 是 getSnapshotBeforeUpdate构造的返回值

    抓取到的是渲染后的dom状态,通过snopshot拿到dom渲染前的状态

销毁时

即将卸载 componentWillUnmount

数据交互

fetch

js原生api,是promise的语法糖,用法如下

fetch(url+get数据,{配置}).then((res)=>{}).catch((err)=>{})

//配置
//method:'POST'  默认get
//headers:{"Content-type":"application/x-www-form-urlencoded"},
//body:'a=1&b=2'|URLSearchParams
//注意: body数据为字符时,需要携带请求头
//async + await 用法

res.ok : true/false 成功/失败
res.status: 状态码
res.body : 数据 数据流(stream)
res.text() : 转换 文本(string),过程异步,return res.text()
res.json() : 转 对象

文档

jsonp

fetch不带jsonp请求 需要依赖第三库yarn add fetch-jsonp --save

import fetchJsonp from 'fetch-jsonp'

fetchJsonp(url+数据,{配置}).then((res)=>{}).catch(err=>{})

//是个promise 返回promise 数据是个流
//res.json()  -> 流转换数据 是异步

timeout: 延时 5000 配置
jsonpCallback: 回调函数key callback
jsonpCallbackFunction: null

百度下拉(函数节流、事件、setState异步)

axios

同vue

umi-request

文档

客户端代理

正向代理隐藏真实客户端,反向代理隐藏真实服务端,正向代理实现,反向代理实现跨域,客户端代理指的就是代码写在客户端,不过实现的是跨域

方案1

//配置: package.json
"proxy":"https://uncle9.top"

//组件
/api/xx ...

问题: 只能代理一个服务器

方案2

利用客户端代理中间件(http-proxy-middleware)完成, 官网给了新的使用方式,在src下新建文件setupProxy.js加下面代码,无需单独应用,webpack会自动引入文件。

// src/ 创建 setupProxy.js

//verion < 1.0
const proxy = require('http-proxy-middleware'); //需要安装中间件  
module.exports = function(app) {
  app.use(
    proxy("/api", {
      target: 'https://uncle9.top',
      changeOrigin: true
    })
  );
  app.use(
    proxy("/v2", {
      target: "https://api.douban.com",
      changeOrigin: true
    })
  );
};

//组件: /api/xx ... | /v2/...

//verion > 1.0
const { createProxyMiddleware } = require('http-proxy-middleware');

module.exports = function(app) {

  app.use('/api', createProxyMiddleware({
    target: 'http://localhost:3001',
    changeOrigin: true,
  }));

  app.use('/api2', createProxyMiddleware({
    target: 'http://vareyoung.top',
    changeOrigin: true,
    pathRewrite: { //路径替换
      '^/api2': '/api', // axios 访问/api2 == target + /api
    }
  }));

};

方案3

配置create-react-app环境下的webpack

// config/webpackDevServer.js

 proxy: { //代理是从指定的target后面开始匹配的,不是任意位置;配置pathRewrite可以做替换
      '/api': { //axios访问 /api ==  target + /api
        target: 'http://localhost:3001',
        changeOrigin: true, //创建虚拟服务器 
        ws: true, //websocket代理
      },
      '/douban': { // axios 访问 /douban == target + '/douban' +  '/v2/movie/in_theaters?start=0&count=10'
        target: 'https://douban.uieee.com',
        changeOrigin: true,
        pathRewrite: { //路径替换
          '^/douban': '', // axios 访问/douban/v2 == target + /v2
        }
      }
    }
  }

mock

JSON-Server 是一个 Node 模块,运行 Express 服务器,你可以指定一个 json 文件作为 api 的数据源。

安装json-server

yarn add json-server -S

启动 json-server

json-server可以直接把一个json文件托管成一个具备全RESTful风格的API,并支持跨域、jsonp、路由订制、数据快照保存等功能的 web 服务器。

db.json文件的内容:

{
  "course": [
    {
      "id": 1000,
      "course_name": "马连白米且",
      "autor": "袁明",
      "college": "金并即总变史",
      "category_Id": 2
    },
    {
      "id": 1001,
      "course_name": "公拉农题队始果动",
      "autor": "高丽",
      "college": "先了队叫及便",
      "category_Id": 2
    }
  ]
}

例如以下命令,把db.json文件托管成一个 web 服务。

$ json-server --watch --port 53000 db.json

输出类似以下内容,说明启动成功。

\{^_^}/ hi!

Loading db.json
Done

Resources
http://localhost:53000/course

Home
http://localhost:53000

Type s + enter at any time to create a snapshot of the database
Watching...

此时,你可以打开你的浏览器,然后输入:http://localhost:53000/course

json-server 的相关启动参数

  • 语法:json-server [options]
  • 选项列表:
参数 简写 默认值 说明
–config -c 指定配置文件 [默认值: “json-server.json”]
–port -p 设置端口 [默认值: 3000] Number
–host -H 设置域 [默认值: “0.0.0.0”] String
–watch -w Watch file(s) 是否监听
–routes -r 指定自定义路由
–middlewares -m 指定中间件 files [数组]
–static -s Set static files directory 静态目录,类比:express的静态目录
–readonly –ro Allow only GET requests [布尔]
–nocors –nc Disable Cross-Origin Resource Sharing [布尔]
–no gzip , --ng Disable GZIP Content-Encoding [布尔]
–snapshots -S Set snapshots directory [默认值: “.”]
–delay -d Add delay to responses (ms)
–id -i Set database id property (e.g. _id) [默认值: “id”]
–foreignKeySuffix fks Set foreign key suffix (e.g. _id as in post_id) [默认值: “Id”]
–help -h 显示帮助信息 [布尔]
–version -v 显示版本号 [布尔]
  • source可以是json文件或者js文件。实例:
json-server --watch -c ./jsonserver.json
json-server --watch db.js  命令行里面要的db是个函数
json-server db.json
json-server --watch -port 8888 db.json

动态生成模拟数据

启动json-server的命令:json-server --watch db.js 是把一个js文件返回的数据托管成web服务。

db.js配合mockjs库可以很方便的进行生成模拟数据。

// 用mockjs模拟生成数据
var Mock = require('mockjs');

module.exports = () => {
  // 使用 Mock
  var data = Mock.mock({
    'course|227': [
      {
        // 属性 id 是一个自增数,起始值为 1,每次增 1
        'id|+1': 1000,
        course_name: '@ctitle(5,10)',
        autor: '@cname',
        college: '@ctitle(6)',
        'category_Id|1-6': 1
      }
    ],
    'course_category|6': [
      {
        "id|+1": 1,
        "pid": -1,
        cName: '@ctitle(4)'
      }
    ]
  });
  // 返回的data会作为json-server的数据
  return data;
};

路由

默认的路由

json-server为提供了GET,POST, PUT, PATCH ,DELETE等请求的API,分别对应数据中的所有类型的实体。

# 获取所有的课程信息
GET    /course

# 获取id=1001的课程信息
GET    /course/1001

# 添加课程信息,请求body中必须包含course的属性数据,json-server自动保存。
POST   /course

# 修改课程,请求body中必须包含course的属性数据
PUT    /course/1
PATCH  /course/1

# 删除课程信息
DELETE /course/1

# 获取具体课程信息id=1001
GET    /course/1001

自定义路由

当然你可以自定义路由:

$ json-server --watch --routes route.json db.json

route.json文件

{
  "/api/*": "/$1",    //   /api/course   <==>  /course
  "/:resource/:id/show": "/:resource/:id",
  "/posts/:category": "/posts?category=:category",
  "/articles\\?id=:id": "/posts/:id"
}

自定义配置文件

通过命令行配置路由、数据文件、监控等会让命令变的很长,而且容易敲错,可以把命令写到npm的scripts中,但是依然配置不方便。

json-server允许我们把所有的配置放到一个配置文件中,这个配置文件默认json-server.json;

例如:

{
  "port": 53000,
  "watch": true,
  "static": "./public",
  "read-only": false,
  "no-cors": false,
  "no-gzip": false,
  "routes": "route.json"
}

使用配置文件启动json-server:

# 默认使用:json-server.json配置文件
$ json-server db.js  
$ json-server db.json 

# 指定配置文件
$ json-server --watch -c jserver.json db.json

过滤查询

查询数据,可以额外提供

GET /posts?title=json-server&author=typicode
GET /posts?id=1&id=2

# 可以用 . 访问更深层的属性。
GET /comments?author.name=typicode

还可以使用一些判断条件作为过滤查询的辅助。

GET /posts?views_gte=10&views_lte=20

可以用的拼接条件为:

  • _gte : 大于等于
  • _lte : 小于等于
  • _ne : 不等于
  • _like : 包含
GET /posts?id_ne=1
GET /posts?id_lte=100
GET /posts?title_like=server

分页查询

默认后台处理分页参数为: _page 第几页, _limit一页多少条。

GET /posts?_page=7
GET /posts?_page=7&_limit=20

默认一页10条。

后台会返回总条数,总条数的数据在响应头:X-Total-Count中。

排序

  • 参数: _sort设定排序的字段
  • 参数: _order设定排序的方式(默认升序)
GET /posts?_sort=views&_order=asc
GET /posts/1/comments?_sort=votes&_order=asc

支持多个字段排序:

GET /posts?_sort=user,views&_order=desc,asc

任意切片数据

GET /posts?_start=20&_end=30
GET /posts/1/comments?_start=20&_end=30
GET /posts/1/comments?_start=20&_limit=10

全文检索

可以通过q参数进行全文检索,例如:GET /posts?q=internet

实体关联

关联子实体

包含children的对象, 添加_embed

GET /posts?_embed=comments
GET /posts/1?_embed=comments

关联父实体

包含 parent 的对象, 添加_expand

GET /comments?_expand=post
GET /comments/1?_expand=post

其他高级用法

json-server本身就是依赖express开发而来,可以进行深度定制。细节就不展开,具体详情请参考官网。

const jsonServer = require('json-server');//在node里面使用json-server包
const db = require('./db.js');//引入mockjs配置模块
const path = require('path');
const Mock = require('mockjs');
let mock='/mock';//定义路由根别名

//创建服务器
const server = jsonServer.create();//创建jsonserver 服务对象


//配置jsonserver服务器 中间件
server.use(jsonServer.defaults({
  static:path.join(__dirname, '/public'),//静态资源托管
}));
server.use(jsonServer.bodyParser);//抓取body数据使用json-server中间件


//响应
server.use((request, res, next) => {//可选 统一修改请求方式
  // console.log(1)
  // request.method = 'GET';
  next();
});

//登录注册校验
let mr = Mock.Random;//提取mock的随机对象
server.get(mock+'/login', (req, res) => {
  // console.log(req.query, req.body);//抓取提交过来的query和body
  let username=req.query.username;
  let password=req.query.password;
  (username === 'aa' && password === 'aa123')?
    res.jsonp({
      "err": 0,
      "msg": "登录成功",
      "data": {
        "follow": mr.integer(1,5),
        "fans": mr.integer(1,5),
        "nikename": mr.cname(),
        "icon": mr.image('20x20',mr.color(),mr.cword(1)),
        "time": mr.integer(13,13)
      }
    }) :
    res.jsonp({
      "err": 1,
      "msg": "登录失败",
    })

});
server.post(mock+'/reg', (req, res) => {
  let username=req.body.username;
  (username !== 'aa') ?
    res.jsonp({
      "err": 0,
      "msg": "注册成功",
      "data": {
        "follow": mr.integer(0,0),
        "fans": mr.integer(0,0),
        "nikename": mr.cname(),
        "icon": mr.image('20x20',mr.color(),mr.cword(1)),
        "time": mr.integer(13,13)
      }
    }) :
    res.jsonp({
      "err": 1,
      "msg": "注册失败",
    })

});

//响应mock接口 自定义返回结构 定义mock接口别名
const router = jsonServer.router(db);//创建路由对象 db为mock接口路由配置  db==object

router.render = (req, res) => {//自定义返回结构
  let len = Object.keys(res.locals.data).length; //判断数据是不是空数组和空对象
  // console.log(len);

  setTimeout(()=>{//模拟服务器延时
    res.jsonp({
      err: len !== 0 ? 0 : 1,
      msg: len !== 0 ? '成功' : '失败',
      data: res.locals.data
    })
  },1000)

  // res.jsonp(res.locals.data)

};

server.use(jsonServer.rewriter({//路由自定义别名
  [mock+"/*"]: "/$1",

  // "/product\\?dataName=:dataName": "/:dataName",
  // "/banner\\?dataName=:dataName": "/:dataName",
  // "/detail\\?dataName=:dataName&id=:id": "/:dataName/:id",

  // "/product/del\\?dataName=:dataName&id=:id": "/:dataName/:id",
  // "/product/add\\?dataName=:dataName": "/:dataName",
  // "/product/check\\?dataName=:dataName&id=:id": "/:dataName/:id"
}));

server.use(router);//路由响应



//开启jsonserver服务
server.listen(3333, () => {
  console.log('mock server is running')
});

单页VS多页

页面模式 多页面模式(MPA Multi-page Application) 单页面模式(SPA Single-page Application)
页面组成 多个完整页面, 例如page1.html、page2.html等 由一个初始页面和多个页面模块组成, 例如:index.html
公共文件加载 跳转页面前后,js/css/img等公用文件重新加载 js/css/img等公用文件只在加载初始页面时加载,更换页面内容前后无需重新加载
页面跳转/内容更新 页面通过window.location.href = "./page2.html"跳转 通过使用js方法,append/remove或者show/hide等方式来进行页面内容的更换
数据的传递 可以使用路径携带数据传递的方式,例如:http://index.html?account=“123”&password=123456"",或者localstorage、cookie等存储方式 直接通过参数传递,或者全局变量的方式进行,因为都是在一个页面的脚本环境下
用户体验 如果页面加载的文件相对较大(多),页面切换加载会很慢 页面片段间切换较快,用户体验好,因为初次已经加载好相关文件。但是初次加载页面时需要调整优化,因为加载文件较多
场景 适用于高度追求高度支持搜索引擎的应用 高要求的体验度,追求界面流畅的应用
转场动画 不容易实现 容易实现

单页面模式:相对比较有优势,无论在用户体验还是页面切换的数据传递、页面切换动画,都可以有比较大的操作空间 多页面模式:比较适用于页面跳转较少,数据传递较少的项目中开发,否则使用cookie,localstorage进行数据传递,是一件很可怕而又不稳定的无奈选择

路由

官网 中文

vue-router react-router
配置 分离式(统一位置配置) 嵌套式(路由配置在组件内部)
匹配 排他性(只有一个路由被渲染) 包容性(多路由渲染)
形态 静态路由 动态路由

理念

遵循Just Component的 API 设计理念 万物皆组件,路由规则位于布局和 UI 本身之间

安装

React Router被拆分成三个包:react-router,react-router-dom和react-router-native。react-router提供核心的路由组件与函数。其余两个则提供运行环境(即浏览器与react-native)所需的特定组件

yarn add react-router-dom --save

提供组件

组件 作用
BrowserRouter 约定模式 为 history,使用 HTML5 提供的 history API 来保持 UI 和 URL 的同步
HashRouter 约定模式 为 hash,使用 URL 的 hash (例如:window.location.hash) 来保持 UI 和URL 的同步
NavLink 声明式跳转 还可以约定 路由激活状态
Link 声明式跳转 ~~ push 无激活状态
Redirect 重定向 ~~ replace
Route 匹配、展示
Switch 排他性匹配
Prompt 后置守卫
withRouter 把不是通过路由切换过来的组件中,将 history、location、match 三个对象传入props对象上

结构

  • BrowserRouter|HashRouter
    • 根组件(App)
      • NavLink|Link
      • Route
      • Redirect
        • 子组件
          • NavLink|Link
          • Route

BrowserRouter

属性 类型 作用
basename string 所有位置的基本URL。如果您的应用是从服务器上的子目录提供的,则需要将其设置为子目录。格式正确的基本名称应以斜杠开头,但不能以斜杠结尾
getUserConfirmation Function 用于确认导航的功能。默认使用window.confirm
forceRefresh boolean 是否调整时强制刷新,模拟旧式服务器渲染

Route

属性 类型 作用
path string | string[] 路由匹配路径。没有path属性的Route 总是会 匹配
exact boolean 为true时,要求全路径匹配(/home)。路由默认为“包含”的(/和/home都匹配),这意味着多个 Route 可以同时进行匹配和渲染
component Function ReactElement 在地址匹配的时候React的组件才会被渲染,route props也会随着一起被渲染
render Function 内联渲染和包装组件,要求要返回目标组件的调用

Link

属性 类型 作用
to string | {pathname,search,hash} 要跳转的路径或地址
replace boolean 是否替换历史记录

NavLink

属性 类型 作用
to string object 要跳转的路径或地址
replace boolean 是否替换历史记录
activeClassName string 当元素被选中时,设置选中样式,默认值为 active
activeStyle object 当元素被选中时,设置选中样式
exact boolean 严格匹配

Switch

该组件用来渲染匹配地址的第一个Route或者Redirect,仅渲染一个路由,排他性路由,默认全匹配(场景:侧边栏和面包屑,引导选项卡等

属性 类型 作用
location string object
children node

Redirect

将导航到新位置。新位置将覆盖历史记录的当前位置

属性 类型 作用
from string 来自
to string object 去向
push boolean 添加历史记录
exact boolean 严格匹配
sensitive boolean 区分大小写

404

 总是会匹配

参数数据传递

let {history,location,match}=props


url - (浏览器 URL 中的实际路径) URL 匹配的部分。 用于构建嵌套的
path - (路由编写的路径) 用于匹配路径模式。用于构建嵌套的

接收

//接参数:
{match.params.aid}
//接数据
{location.search}
//接地址:
{location.pathname}

无法从v4+ 中获取 URL 的查询字符串了。因为没有关于如何处理复杂查询字符串的标准。所以,作者让开发者去选择如何处理查询字符串。推荐qs库|query-string

编程式跳转

history.push('/user?a=1&b=2')
history.push({pathname:'/user',search:'?a=11&b=22'})
history.replace({pathname:'/user',search:'?a=111&b=222'})
history.go(-1)

非路由跳转组件

不是所有组件会通过路由跳转,也需要抓取路由上下文时,解决方案

  1. 通过路由跳转
  2. 通过属性传递 <丢失上下文的组件 {…this.props} this==父组件
  3. 通过withRouter包装
import {withRouter} from 'react-router-dom'
class 组件 extends Component{}
export default withRouter(组件)

前置授权路由

需要自定义路由,具体为,自定义一个组件,代替Route,其内部根据条件返回一个Route 组件指向目标组件,或者Route的render函数内部判断加载目标,最后组件树关系为:switch>自定义组件>Route>目标组件




export default class Auth extends React.Component{

  state={
    hasSendAuth:false,//是否发送过介权请求
    auth:false,//介权是否通过
    data:{}//预载数据
  };

  async componentDidMount(){
    let res = await axios({url:'/data/user.json'})
    console.log('数据回来了')
    this.setState({
      auth:res.data.auth,
      hasSendAuth:true,
      data:res.data.data
    })
  }

  render(){
    // console.log('渲染了',this.props)  //包含了path,component的一个传入
    let {component:Component} = this.props;//目标组件
    if (!this.state.hasSendAuth) return null;

    return (//...props 目标组件需要用到的路由信息
      this.state.auth ?
         :// 数据预载
        
    )}/>

  }
}

后置守卫

// reg.jsx
import { Prompt } from 'react-router-dom'
...}
/>

message: 后面可以跟简单的提示语,也可以跟函数,函数是有默认参数的。
when: when的属性值为true时防止跳转;

单页滚动条

路由切换,每次切换到页面顶部

static getDerivedStateFromProps(nextProps){//props改变时
  if(this.props.location !== nextProps.location){//当前地址不等于目标地址
    window.scrollTo(0,0);//滚动到顶部
  }
}

页面切换出去再切换回来后怎样保持之前的滚动位置

//sTop =  模块内部变量 | 类属性
componentDidMount(){
  window.scrollTo(0,sTop)
}
componentWillUnmount(){
  sTop = document.documentElement.scrollTop
}			

项目

技术栈选型

前端

create-react-app

react-router-dom

axios

redux/react-redux/react-think

mockjs/json-server

prop-types

qs|query-string

后端

nodejs

express

mongodb

bcrypt

jsonwebtoken

multer

环境规划

|-config	 CRA配置
|-scirpts  CRA配置
|-pubilc
  |- data
    |- 静态数据
  |-index.html 浏览器入口
|-node_modules
|-mock 数据模拟
	|- public
  |-db.js
  |-app.js
|-src
  |-library 公司内部库
    |-jquery.js
    |-swiper.js
  |-utils 工具包
    |-date.js / fillzero.js/...
  |-layouts 布局
    |- Default/Header/Footer
  |-components 应用内部基础通用组件、木偶组件
    |- swiper、input、loading
    |- cell、uc-nav
    |- button
  |-pages  智能组件 页面
    |- Home / Follow / Column / User
    |- Detail / Login / Reg / Error
	|-guard
			守卫组件
  |- assets
    |- img
    |- css、sass
    |- font
  |- store
    |- state/reducer/asyncAction
	|- plugins
		|- axios /  ....
  Index.js

组件开发

准备工作

移动端(设置视口,设置字体比例,基础样式normal,base)

资源引入

  • index.html引入 不优化
  • index.js 引入 优化
  • 组件 引入 优化

资源指向

相对路径 以src为根静态资源,绝对路径 以public为根动态资源, jsx前景图片默认都指向public, jsx里面行间样式链接图片资源指向了pubic,

布局方案

  • 切图,需要设计稿,用户端开发时用到
  • UI库,管理端开发时用到,常用的UI库(elementUI/ant.design)
  • 模板移植,老项目重构时用到

数据交互

客户端代理

module.exports = function(app) {

  app.use('/api', createProxyMiddleware({
    target: 'http://localhost:3001',
    changeOrigin: true,
  }));

  app.use('/api2', createProxyMiddleware({
    target: 'http://vareyoung.top',
    changeOrigin: true,
    pathRewrite: { //路径替换
      '^/api2': '/api', // axios 访问/api2 == target + /api
    }
  }));

};

拦截器axios

import React from 'react';
import axios from 'axios';
import {BrowserRouter as Router} from 'react-router-dom'
import {baseLocalUrl} from '../server'
import qs from 'qs'

// 添加一个请求的拦截
axios.interceptors.request.use((config) => {
  //1抓取本地token,携带在请求头里
  let user = window.localStorage.getItem('user');
  user = user ? qs.parse(user) : '';
  config.headers={'token': user.token}

  //显示loading...

  return config;//2返回请求

}, function(error) {
  // 请求错误时做点事
  return Promise.reject(error);
});

//添加一个响应拦截
axios.interceptors.response.use(function(response) {
  console.log('响应拦截',response);
  
  let router=new Router();
  //token过期: 返回值2,当前路由不是login时跳转 
  if (response.data.err === 2 && !router.history.location.pathname.includes('/login')) {
    console.log('token 失败 跳转到login',router);
    window.location.href=baseLocalUrl+'/login?path='+router.history.location.pathname

    /*router.history.push({  //hash 模式可以,history模式有问题
      pathname: '/login',
      search: "path="+router.history.location.pathname
    })*/

  }
  return response;

}, function(error) {

  return Promise.reject(error);
});

React.axios = axios;//axios绑到对象包上
React.Component.prototype.axios = axios; // axios绑定到Component类的原型   组件|this.axios
window.axios = axios;  //×   希望全局使用axios , 使用webpack 来配置
export default axios;

拦截器umi-request

import React from 'react';
import {BrowserRouter as Router} from 'react-router-dom'
import request,{ extend } from 'umi-request';
import qs from 'qs'

// request拦截器, 改变url 或 options.
request.interceptors.request.use((url, options) => {
  
  //1抓取本地token,携带在请求头里
  let user = window.localStorage.getItem('user');
  user = user ? qs.parse(user) : '';
  options.headers={'token': user.token}
  
  return (
    {
      url,
      options
    }
  );
});

// 提前对响应做异常处理
request.interceptors.response.use(async (response) => {
  
  const codeMaps = {
    200: '服务器成功返回请求的数据。',
    201: '新建或修改数据成功。',
    202: '一个请求已经进入后台排队(异步任务)。',
    204: '删除数据成功。',
    400: '发出的请求有错误,服务器没有进行新建或修改数据的操作。',
    401: '用户没有权限(令牌、用户名、密码错误)。',
    403: '用户得到授权,但是访问是被禁止的。',
    404: '发出的请求针对的是不存在的记录,服务器没有进行操作。',
    406: '请求的格式不可得。',
    410: '请求的资源被永久删除,且不会再得到的。',
    422: '当创建一个对象时,发生一个验证错误。',
    500: '服务器发生错误,请检查服务器。',
    502: '网关错误。',
    503: '服务不可用,服务器暂时过载或维护。',
    504: '网关超时。',
  };
  
  console.log(codeMaps[response.status]);
  
	const data = await response.clone().json();//克隆响应对象做解析处理
  
  let router=new Router();
  //token过期: 返回值2,当前路由不是login时跳转 
  if (data.err === 2 && !router.history.location.pathname.includes('/login')) {
    console.log('token 失败 跳转到login',router);
    window.location.href=baseLocalUrl+'/login?path='+router.history.location.pathname

    /*router.history.push({  //hash 模式可以,history模式有问题
      pathname: '/login',
      search: "path="+router.history.location.pathname
    })*/
  }
  
  return response;
});


React.request = request;//request绑到对象包上
React.Component.prototype.request = request; // request绑定到Component类的原型   组件|this.request
window.request = request;  //×   希望全局使用request , 使用webpack 来配置
export default request;

服务封装api

export const queryDetail = async ({id, apiname}) => {
  return axios({
    url: `/mock/${apiname}/${id}`
  })
};

export const queryAll = async (arr) => {
  return axios.all(arr).then(axios.spread((banner, home)=>{
    return {home, banner}
  }))
};

export const queryReg = async ({username,password,icon}) => {

  let params = new URLSearchParams();
  params.append("username",username);
  params.append("password",password);

  // await checkUserInput(username,password,icon)
  //icon input:file
  if (icon.files.length>0){
    params.append("icon",icon.files[0]);
  }

  return axios({
    url:'/mock/reg',
    method:'post',
    data: params//携带数据 对象  字符  URLSearchParams
  });
};

登录

//更新同步localStrage
window.localStorage.setItem('user',qs.stingify(...))
//跳转到之前
history.push({
  pathname:qs.parse(
    this.props.location.search,{ 
    ignoreQueryPrefix:true
  }).path
})

列表、详情

home-> cell/swiper -> detail 拿到id dataName

危险数据的信任和转换

全局方法过滤

|-common|utils
  date.js
  fillzero.js
  ...
  index.js
    import date/fillzero ..
    export {
      date,fillzero
    }

公共数据

//路由检测: pathname的变化

static getDerivedStateFromProps(nextProps,nextState){

  let path = nextProps.location.pathname;

  if (/home|follow|column/.test(path)){
    return {bNav:true,bFoot:true}
  }
  if (/detail|login|reg/.test(path)){
    return {bNav:false,bFoot:false}
  }
  if (/user/.test(path)){
    return {bNav:false,bFoot:true}
  }

  return null;

}

//loading数据 
//订阅发布库
//订阅发布库: App订阅,  组件求数据时发布 | 拦截器发布

pubsub-js

安装

 yarn add pubsub-js -S

订阅

token = PubSub.subscribe('事件名称', 函数(msg,data));
//msg == 事件名称
//data == 传入的数据

发布

PubSub.publish('事件名称', '数据')

取消订阅

PubSub.unsubscribe(token);  //取消指定订阅
PubSub.clearAllSubscriptions(); //取消所有订阅 不推荐使用

先订阅,再发布,组件卸载时,移除订阅

部署

react的项目打包(dist),拷贝到空node项目环境(public)下,利用node做后端代理,访问json-server服务器的数据(mock),再一同拷贝到购买的云服务器上,阿里云的服务器类型选择centos

前端 代理端 服务端
react node json-server + mock | 第三方接口服务|自行开发node
js/html/css 提供静态资源|代理 提供api和库的动态请求

node做代理

方案1

// node项目环境 下安装 http-proxy-middleware 中间件
npm i http-proxy-middleware --save

// app.js
const { createProxyMiddleware } = require('http-proxy-middleware');

//因为 bodyParser 导致的代理转发带有 body 数据的 post 请求会失败,代理中加上把解析后的 body 数据再转回来即可
var restream = function(proxyReq, req) {
  if (req.body) {
      let bodyData = JSON.stringify(req.body);
      // incase if content-type is application/x-www-form-urlencoded -> we need to change to application/json
      proxyReq.setHeader('Content-Type','application/json');
      proxyReq.setHeader('Content-Length', Buffer.byteLength(bodyData));
      // stream the content
      proxyReq.write(bodyData);
  }
}

//响应mock请求,交由中间件转发
app.use('/mock', createProxyMiddleware({
  target: 'http://localhost:3333',
  changeOrigin: true,
  secure: false,
  onProxyReq: restream
}));

方案2

// node项目环境 下安装 express-http-proxy 中间件
npm i express-http-proxy --save

// app.js
const proxy = require('express-http-proxy');

//配置
let opts = {
  preserveHostHdr: true,
  reqAsBuffer: true,
  //转发之前触发该方法
  proxyReqPathResolver: function(req, res) {
      //这个代理会把匹配到的url(下面的 ‘/api’等)去掉,转发过去直接404,这里手动加回来,
      req.url = req.baseUrl+req.url;
      return require('url').parse(req.url).path;
  },
}

//响应mock请求,交由中间件转发
app.use('/mock',proxy('http://localhost:3333',opts));

json-server服务器:三目有问题,压缩分号 ***

无法携带multer文件体,***

nginx代理

扩展

阿里云部署

简洁型部署

买服务器(机器)

  • 选择云服务器ECS、centos系统,学生特惠地址
  • 支付宝-》注册-》实名认证填写身份证的信息-》ecs
  • 重设密码初始化磁盘:ecs服务器->控制台

使用finalShell连接服务器

  • 安装 finalShell
  • 启动 finalShell-》新建会话-》SSH链接->主机:公网IP-》端口 : 22-》用户名:root-》密码: 登录密码

給服务器安装环境

//安装node 在 finalShell里面

curl --silent --location https://rpm.nodesource.com/setup_12.x | sudo bash -
yum install -y nodejs
检测: node -v

上传代码

  • react 打包: yarn build -> build目录

    创建空的node环境: express -e .

  • build里面的文件 copy -> node 的 public下面

  • 把node项目 -》 拖拽到 finalSheel/usr/local/创建目录/

  • //让阿里云支持node里面的3000端口
    找到控制台->安全组-》配置规则-》添加规则-》端口范围(3000/3000,授权对象(0.0.0.0/0)
    
  • finalShell 里面-> cd /usr/local/你的目录 -> npm start
    

    测试: 浏览器输入: http://公网IP:3000

高要求部署

买服务器(机器)

  • 成人特惠地址,认准云服务器ECS/centos系统
  • 支付宝-》注册-》实名认证填写身份证的信息-》ecs
  • 手动停止服务器 ----> 初始化磁盘 —> 重设密码(登录密码)

使用finalShell连接服务器

  • 安装 finalShell
  • 启动 finalShell-》新建会话-》主机:公网IP-》端口 : 22-》用户名:root-》密码: 登录密码

給服务器安装环境

curl --silent --location https://rpm.nodesource.com/setup_12.x | sudo bash -
sudo yum install -y nodejs
检测: node -v

上传代码

  • react 打包: npm run build -> build
  • 本地测试生产环境(css有时打包后有出错)
npm i serve -g
serve -s build -l 8080
问题 : 生产环境下 不能访问 3001
原因  : 生产环境下客户端代理是无效的,部署后的代码需要在服务端做代理
解决: 服务器端 安装ngnix  来完成代理
  • 拷贝 build -> node的public下面 + 本地测试(启动node服务)
  • 整合好的node 拖到 finalShell 下面(不拽node_modules)
npm i 
npm start

給服务器安装json-server服务

//1 copy react下面的mock 到服务器其他目录
|-app.js
|-db.js
|-public
//2 安装依赖
npm init
npm i mockjs json-server
//3 开一个3333安全组(防火墙)

问题汇总

关闭finalShell ,服务断了

//安装pm2, nodejs服务器管理器
npm i pm2 -g

//启动服务器:
pm2 start 启动文件.js 

//浏览器访问项目即可
http://公网IP:node端口

//如果想停掉服务器: 
pm2 stop all

pm2使用

可以有多个app?使用一个实例?

分析:app指向不同端口就好了
解决:app指向不同端口,安全组里添加多个端口,pm2 进入到对应服务器位置,逐个启动,如果端口重复,先启用的应用会占用端口

不想要端口可以?

分析:使用http协议默认的80端口,使用https协议默认端口443
解决: 修改本地的端口号指向80,安全组添加80

不使用ip,使用网址?

分析: 是一个IP和域名关联的过程

解决: 必须得用于一个已经备过案的域名(未备案不可使用一级域名和省略端口),域名购买地址

备案: 特惠专区-》域名与网站->域名新手多重礼(实名,备案15工作日)

域名解析:域名-》解析-》添加记录->记录值(ip)

www:解析后的域名为www.aliyun.com。
@:直接解析主域名 aliyun.com。
二级域名:如:abc.aliyun.com,填写abc。

不备案有什么影响

小程序上线时不能部署,但不影响学习
没有域名不便于宣传,解决:做成二维码
无法使用https安全协议访问

启用https访问

流程:SSL证书->获取https免费证书->配置(node服务器使用https模块响应)

获取https免费证书

下载: 证书通过后->下载 other类型的 xx.key/xx.pem 下载到-> bin/www

配置node:

var https = require('https');
const fs = require('fs');
const port=443;		
app.set('port', port);

const options = {
  key: fs.readFileSync('./bin/1826016_uncle9.top.key'),//指向key
  cert: fs.readFileSync('./bin/1826016_uncle9.top.pem'),
}; 
var server = https.createServer(options,app);//查看nodejs.cn>https模块|或已完成的node项目

安全组规则:添加443 ,443是https的默认端口

在阿里云配置apache+mysql+php

参考资料

历史记录模式路由,强刷找不到

现象:客户端路由服务找/todos/42时,服务器会找/todos/42的接口(没有这个子服务接口)
解决:服务器路由优先,找不到时,返回vue的前端index.html,交还给客户端路由

// node项目 app.js

app.use(function(err, req, res, next) {
	...
  
  if(req.url.includes('/api')){//webApi接口错误
    res.send({
      err:1,
      msg:'不存在的接口名'
    })
  }else if(req.url.includes('/admin')){//服务端Api接口错误
    res.render('error');
  }else{//交还给客户端判断
    res.sendFile(path.join(__dirname, 'public','template', 'index.html'));
  }

});

也可以通过中间件 connect-history-api-fallback 实现

你可能感兴趣的:(reactjs)