一款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= 强壮
语法要求
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,插入点,回调)
传递属性
<组件名 属性名=值 属性名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
事件绑定
修正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
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
留言板
引用
定义
选择器冲突解决方案
import 变量 from './css/xx.module.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'
//使用
引入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]
参数 | 简写 | 默认值 | 说明 |
---|---|---|---|
–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
总是会匹配
参数数据传递
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)
非路由跳转组件
不是所有组件会通过路由跳转,也需要抓取路由上下文时,解决方案
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)
资源引入
资源指向
相对路径 以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
危险数据的信任和转换
全局方法过滤
|-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
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 实现