一款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商店-搜索
环境解析
环境配置和错误处理
运行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
jsx是一个 JavaScript 的语法扩展,可以理解为js的一个新的数据类型,出现在js当中,文件为xx.js|xx.jsx
var b= <strong>强壮</strong>
语法要求
<xx tabIndex="2">
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 <h1>Hello, {this.props.name}/{this.state.count}</h1>;
return React.createElement('h1',{},`Hello, ${this.props.name}/${this.state.count}`)
}
});
//函数式
function 组件名(){
return jsx|null //jsx~~要渲染 null不渲染
}
使用组件
//声明式
<App/>
<Header></Header>
//编程式
new 组件名({props})
组件名({props})
渲染(描画)页面
import ReactDom from 'react-dom';
var RactDom = require('react-dom');
ReactDom.render(jsx,插入点,回调)
传递属性
<组件名 属性名=值 属性名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
事件绑定
<JSX元素 onClick={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(条件)}
}
需要抓取dom元素与第三方 DOM 库集成,触发命令式动画,管理焦点,文本选择或媒体播放
用法
refs用法 有4种
//1、 string refs
<jsx元素 ref="名字"...
this.refs.名字
//2. 实例化
this.firstRef = React.createRef() //发生在类内 | 构造器
<jsx ref={this.firstRef} />
this.firstRef 访问 -》 {current:dom}
// 3. callback refs 回调 √
<jsx ref={el => this.定义一个实例属性 = el}
this.定义一个实例属性 //后期用作访问元素本身
// 4. 转发 refs
//引用编译后的组件内部的元素,要被引用的组件是一个函数式组件
<包装后的子组件 ref={el=>this.inputRef=el} />
//子组件是个函数式
const 包装后的子组件 = React.forwardRef((props, ref) => (
...
<input type="text" ref={ref}/>)
...
));
//当组件挂载时,将 DOM el元素传递给 ref 的回调
//当组件卸载时,则会传递 null。
//ref 回调会在 componentDidMount 和 componentDidUpdate 生命周期之前调用
表单的value受控,受数据控制
value={this.state.数据名} //model->view
onChange={this.方法} //view->model
处理多个输入元素
可以为每个元素添加一个 name 属性(通常和数据名一致),处理函数根据 event.target.name 的值来选择要做什么
<input name="inputUserName"
<input name="inputContent"
this.setState({[ev.target.name]:ev.target.value})
双向绑定
要编写一个非受控组件,而不是为每个状态更新都编写数据处理函数,你可以 使用 ref 来从 DOM 节点中获取表单数据
<input type="text" ref="xx" />
默认值
表单元素上的 value
将会覆盖 DOM 节点中的值,在非受控组件中,你经常希望 React 能赋予组件一个初始值,但是不去控制后续的更新,指定一个 defaultValue
属性,而不是 value
留言板
引用
<jsx className="类名 类名2" className={返回字符}
<jsx style={{key:value,key:value}}
//style的属性值,可以不给单位,默认px 子属性小驼峰
定义
选择器冲突解决方案
import 变量 from './css/xx.module.css'
<jsx className={变量.类名|id}
//配置1
//webpack配置 "style-loader!css-loader?modules" | module:true
//问题:所有css都需要模块化使用
//配置2
//改名xx.css -> xx.module.css
//需要模块化的才修改,不影响其他非模块化css写法
需要下载安装包:网页: node-sass 软件: dart-sass
yarn add node-sass -S
/*定义scss*/
$bg-color: #399;
.box{
background: $bg-color;
}
//引入
import 'xx/xx.scss'
//使用
<jsx className="box"
//模块化
import style form xx.module.scss
<xx className={style.box}
引入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 指向作用域在项目环境下
组件拆分目标:为了复用
组件如何拆:单一原则
状态应该给谁(状态提升)
tansition
transition: .5s ease all;
进度条
AntMotion
官网,是一款蚂蚁金服的动画组件库,支持单元素,css、进出场动画、及文字动画
组件内部的 一级元素&& 做动画
一级元素要有key,根据编号依次做动画,无key不动画,路由离场动画无效
包裹路由组件无效(一级元素&& 进退场)
实例化 -> 更新期 -> 销毁时
实例化
实例化
取得默认属性,初始状态在constructor中完成
运行一次,可读数据,可同步修改state,异步修改state需要setState,setState在实例产生后才可以使用,可以访问到props
即将挂载 componentWillMount
描画VDOM render
挂载完毕 componentDidMount
使用ref,使用setState,读取数据
更新期
props改变 componentWillReceiveProps(nextProps)
初始化render时不执行 这里调用更新状态是安全的,并不会触发额外的render调用,nextProps 更新后 this.props更新前,路由监听
是否更新 shouldComponentUpdate
指视图 return true/false
即将更新 componentWillUpdate
描画dom render
不要在这里修改数据
描画结束 componentDidUpdate
销毁时
componentWillUnmount即将卸载,可以做一些组件相关的清理工作,例如取消计时器、网络请求等
所有子挂载完,才标志着父挂载完,父更新子更新,子更新父不更新
脑图,挂载前、更新前、props更新前统一用getDerivedStateFromProps代替,并添加了返回快照钩子getSnapshotBeforeUpdate
返回快照:发生在render完了,但还没有去编译真实dom之前,返回dom的快照
实例化
渲染前 static getDerivedStateFromProps(nextProps,nextState) {}
无法访问this
nextProps,nextState是更新后的
必须返回 一个对象,用来更新state 或者 返回 null不更新
必须要初始化state
场景:state 的值在任何时候都取决于 props时
渲染中 render
必须return jsx|string|number|null
不会直接与浏览器交互:不要操作DOM|和数据
挂载后 componentDidMount
使用ref,使用setState,读取数据
更新期
渲染前 static getDerivedStateFromProps(nextProps, nextState)
是否渲染 shouldComponentUpdate(nextProps, nextState)
是否更新,必须返回true/false
首次渲染或使用 forceUpdate() 时不会调用该方法
nextProps,nextState更新后的,this.props,this.state 更新前的
return false 只阻止当前组件渲染
渲染中 render
dom快照 getSnapshotBeforeUpdate(prevProps, prevState)
组件能在发生更改之前从 DOM 中捕获一些信息(dom渲染前的状态)
返回的 值|null 会给 componentDidUpdate
prevProps, prevState 更新前 this.props,this.state更新后
更新后 componentDidUpdate(prevProps, prevState,snopshot)
this.props.更新后的
snopshot 是 getSnapshotBeforeUpdate构造的返回值抓取到的是渲染后的dom状态,通过snopshot拿到dom渲染前的状态
销毁时
即将卸载 componentWillUnmount
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异步)
同vue
正向代理隐藏真实客户端,反向代理隐藏真实服务端,正向代理实现翻墙,反向代理实现跨域,客户端代理指的就是代码写在客户端,不过实现的是跨域
方案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
}
}
}
}
JSON-Server 是一个 Node 模块,运行 Express 服务器,你可以指定一个 json 文件作为 api 的数据源。
yarn add json-server -S
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 [options] <source>
参数 | 简写 | 默认值 | 说明 |
---|---|---|---|
–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 | 显示版本号 | [布尔] |
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')
});
页面模式 | 多页面模式(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
属性 | 类型 | 作用 |
---|---|---|
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
<Route component={Error}/> 总是会匹配
参数数据传递
let {history,location,match}=props
<Link to={match.url+'/001'}/>
<Link to={`${match.url}/002?a=1&b=2`}/>
<Link to={{pathname:match.url+'/003',search:'?a=11&b=12'}}
<Route path={match.path+'/:aid'} component={Detail}
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)
非路由跳转组件
不是所有组件会通过路由跳转,也需要抓取路由上下文时,解决方案
import {withRouter} from 'react-router-dom'
class 组件 extends Component{}
export default withRouter(组件)
前置授权路由
需要自定义路由,具体为,自定义一个组件,代替Route,其内部根据条件返回一个Route 组件指向目标组件,或者Route的render函数内部判断加载目标,最后组件树关系为:switch>自定义组件>Route>目标组件
<Auth path="/goods" component={Goods} />
<Auth path="/user" component={User} />
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 <Route render={props=>(//...props 目标组件需要用到的路由信息
this.state.auth ?
<Component {...props} data={this.state.data} /> :// 数据预载
<Redirect to="/login" />
)}/>
}
}
后置守卫
// reg.jsx
import { Prompt } from 'react-router-dom'
<Prompt
when={this.state.isBlocking}
message={location=>...}
/>
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)
资源引入
资源指向
相对路径 以src为根静态资源,绝对路径 以public为根动态资源, jsx前景图片默认都指向public, jsx里面行间样式链接图片资源指向了pubic,
布局方案
客户端代理
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
<jsx dangerouslySetInnerHTML={{__html:HTML字符的数据}}></jsx>
危险数据的信任和转换
全局方法过滤
|-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订阅, 组件求数据时发布 | 拦截器发布
安装
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和库的动态请求 |
方案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文件体,***
扩展
买服务器(机器)
使用finalShell连接服务器
給服务器安装环境
//安装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
买服务器(机器)
使用finalShell连接服务器
給服务器安装环境
curl --silent --location https://rpm.nodesource.com/setup_12.x | sudo bash -
sudo yum install -y nodejs
检测: node -v
上传代码
npm run build
-> buildnpm i serve -g
serve -s build -l 8080
问题 : 生产环境下 不能访问 3001
原因 : 生产环境下客户端代理是无效的,部署后的代码需要在服务端做代理
解决: 服务器端 安装ngnix 来完成代理
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
可以有多个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模块响应)
下载: 证书通过后->下载 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 实现