react-router可以让你向应用中快速地添加视图和数据流,同时保持页面与 URL 间的同步。
react-router 路由的实现原理
history.pushState 实现浏览器页面不刷新修改url链接
HTML5 History对象,Javascript修改地址栏而不刷新页面
react-router的实现原理
history对象的方法能够做到修改URL地址而不刷新页面;
通过pushState/replaceState方法,搭配popState事件
// 三个参数分别是:
/**
1.状态对象(stateObject)--stateObject是一个JavaScript对象,通过pushState方法可以将stateObject内容传递到新页面中。
2.标题(title)--几乎没有浏览器支持该参数,但是传一个空字符串会比较安全。
3.地址(url)--新的历史记录条目的地址(可选,不指定的话则为文档当前URL);浏览器在调用pushState()方法后不会加载该地址;传入的URL与当前URL应该是同源的,否则,pushState()会抛出异常。
*/
window.history.pushState({
key,state},null,href);
location.hash的切换,搭配hashChange事件
但它的问题是修改后页面会刷新,故不是我们前端路由的商讨范围
import {
Component } from 'react';
import createHistory from 'history/createHashHistory';
const history = createHistory(); //创建 history 对象
/**
* 配置路由表
* @type {
{"/": string}}
*/
const router = {
'/': 'page/home/index',
'/my': 'page/my/index'
}
export default class Router extends Component {
state = {
page: null }
async route(location) {
let pathname = location.pathname;
let pagePath = router[pathname];
// 加 ./的原因 https://webpack.docschina.org/api/module-methods#import-
const Page = await import(`./${
pagePath}`); //获取路由对应的ui
//设置ui
this.setState({
Page: Page.default
});
}
initListener(){
//监听路由切换
history.listen((location, action) => {
//切换路由后,更新ui
this.route(location);
});
}
componentDidMount() {
this.route(history.location);
this.initListener();
}
render() {
const {
Page } = this.state;
return Page && <Page {
...this.props} />;
}
}
其他:默认情况下必须是经过路由匹配渲染的组件才存在this.props,才拥有路由参数,才能使用编程式导航的写法,执行this.props.history.push(’/detail’)跳转到对应路由的页面
然而不是所有组件都直接与路由相连(通过路由跳转到此组件)的,当这些组件需要路由参数时,使用withRouter就可以给此组件传入路由参数,此时就可以使用this.props
withRouter的作用和一个简单应用
虽然在react学习笔记系列(一),学习笔记(2)中已经提过路由包括dva以及react的路由跳转:但其实自己在用的时候发现并没有真正的理解,以下系统的讲一下路由:
React Router 基础组件一览
路由器组件:对于每一个 React Router 应用来说,都应该有一个路由器组件,它们会为应用创建一个专用的 history
对象。针对 Web 项目,react-router-dom
提供了
和
。
路由匹配组件:React Router 提供了两种路由匹配组件:
和
。
Route 组件:路由匹配是通过比较
组件的 path
属性和当前地址的 pathname 实现的,如果匹配则渲染当前路由组件的内容,如果不匹配则渲染 null。注意:没有提供 path
属性的
组件将总是匹配。
Switch组件:
组件用于给
组件分组,可以将一组
组件嵌套在
组件中。
那么,
组件仅是为了给
组件分组而诞生的吗?我们知道
组件通过比较它的 path
属性和当前地址来判断是否渲染它的内容,所以就会存在多个
匹配成功且都渲染它们内容的情况。为了避免这样的情况,
组件就起了作用,它会迭代当中的
组件,并选出第一个匹配的
来渲染。
import {
Switch, Route } from 'react-router-dom';
<Switch>
<Route exact path='/' component={
Home} />
<Route path='/about' component={
About} />
<Route path='/contact' component={
Contact} />
{
/* when none of the above match, will be rendered */ }
<Route component={
NoMatch} />
</Switch>
在使用时,如果我们想渲染的内容已经有对应的组件,则可以直接使用component的方法;
render方法直接使用一个内联函数来渲染内容。
注意:(这里有一个notice,虽然没有遇到过,但是最好能notice一下)
不要将component属性设置为一个函数,然后在其内部渲染组件。这样会导致已经存在的组件被卸载,然后重写创建一个新组件,而不是仅仅对组件进行更新。
// convenient inline rendering
<Route path="/home" render={
() => <div>Home</div>}/>
React Router 提供了 组件用于为应用提供链接,当渲染
组件的时候,会在应用中渲染为 HTML 的
标签。
另外还有一个特殊的 组件就是
,如果当前的地址与它的属性相匹配,它就会使用 “active” 样式。
任何时候如果您想要强制一个导航,就可以使用
组件,当它被渲染的时候,就会导航到它的属性。
- link组件传参方式有两种:
- title url传参
- title 参数可以放在query中,可以放在state中 ,导航到的组件中可以通过this.props.location.state
react router路由传参
注意query和state传参异同点:
同:
react-router路由匹配原理
记住一点是:深度优先做匹配
// renderRoutes源码
function renderRoutes(routes, extraProps, switchProps) {
if (extraProps === void 0) {
extraProps = {
};
}
if (switchProps === void 0) {
switchProps = {
};
}
return routes ? React.createElement(Switch, switchProps, routes.map(function (route, i) {
return React.createElement(Route, {
key: route.key || i,
path: route.path,
exact: route.exact,
strict: route.strict,
render: function render(props) {
return route.render ? route.render(_extends({
}, props, extraProps, {
route: route
})) : React.createElement(route.component, _extends({
}, props, extraProps, {
route: route
}));
}
});
})) : null;
}
//或者
import React from "react";
import Switch from "react-router/Switch";
import Route from "react-router/Route";
const renderRoutes = (routes, extraProps = {
}, switchProps = {
}) =>
routes ? (
<Switch {
...switchProps}>
{
routes.map((route, i) => (
<Route
key={
route.key || i}
path={
route.path}
exact={
route.exact}
strict={
route.strict}
render={
props => (
<route.component {
...props} {
...extraProps} route={
route} />
)}
/>
))}
</Switch>
) : null;
export default renderRoutes;
可以看出它返回的是一个路由匹配组件,所以我们使用的时候
<BrowserRouter>
{/* kick it all off with the root route */}
{renderRoutes(routes)}
BrowserRouter>
https://www.npmjs.com/package/react-router-config
<利用react-router4的react-router-config做路由鉴权>这篇文章提供了一种改造renderRoutes来实现鉴权,但事实上完全可以不用这么麻烦去增加额外参数,完全可以在renderRoutes的extraProps里面传进去我们的用户信息,注意传进去的一定是一个对象,所以无论是不是一个对象都额外需要用{}包起来,使用时直接通过this.props拿出来判断是否有值来决定是否重定向等;
[ADD]:
一般来讲,这里所说的用户信息之类的需要传递的信息实则最初是从store中读取的(store中的数据其实是根组件compontDidMout中从后端读取的),而我们在子路由组件里想通过用户信息获取服务端数据一般是从componentDidMount生命周期里面发起请求,而因为嵌套组件树的原因,父组件遇到子组件就先去渲染子组件并且执行完子组件的componentDidMount才去执行父组件componentDidMount,这就导致子组件其实获取不到这个props,此时解决方案同样是:在父组件里做一个拦截,如果用户信息为空那么就return null如果有(因为登录不登录都会有这个用户信息,里面有字段标识是否登录)则renderRoutes传值
仔细看这篇文章,其实是一样的意思:
在dva.js中如何拦截路由(做登录认证),没有类似vue中的beforeEach钩子函数
let {
left, right: R} = {
left: 10, right: 10 }
console.log(R)
别名在后
虽然已经再学习笔记(1)中总结过,这里强调的是react16生命周期钩子的改变:以下为新的生命周期图
https://www.npmjs.com/package/react-loadable
动态引入组件的HOC(按需加载),简单用法可以看上面npm;
react-loadable做按需加载/代码分割:
React Loadable is a small library that makes component-centric code splitting incredibly easy in React.
问题引入场景:
import Bar from './components/Bar';
class Foo extends React.Component {
render() {
return <Bar/>;
}
}
Right now we’re depending on Bar
being imported synchronously via import
, but we don’t need it until we go to render it. So why don’t we just defer that?
因而使用react-loadable做按需加载:
npm上介绍了一些其他属性:比如
http://es6.ruanyifeng.com/?search=export&x=0&y=0#docs/module#import-命令
import()
返回一个 Promise 对象。所以可以链式调用;其实是dva的开发者做的一个命令行工具,类似于webpack-cli命令行工具
https://github.com/sorrycc/roadhog/blob/master/README_zh-cn.md#define这是readme
此次使用import函数,它是es6语法,我们必须对其进行转化才能让运行环境识别,所以需要引入babel的插件,
webpack4 动态导入文件 dynamic-import 报错的解决方法参考这个文章可以在webpack.config.js里面配置这个plugin,但是roadhog配置方式有所不同,其实很简单:在.roadhogrc中放入:“dynamic-import-webpack”
{
"entry": "src/index.js",
"less":true,
"disableCSSModules": false,
"disableDynamicImport":false,
"env": {
"development": {
"extraBabelPlugins": [
"dva-hmr",
"transform-runtime",
"dynamic-import-webpack"
]
},
"production": {
"extraBabelPlugins": [
"transform-runtime"
]
}
}
}
https://github.com/easonyq/easonyq.github.io/blob/master/学习记录/others/babel.md
从这篇文章中摘出来几点:
简单来说把 JavaScript 中 es2015/2016/2017/2046 的新语法转化为 es5,让低端运行环境(如浏览器和 node )能够认识并执行
总共存在三种方式:
其中后面两种比较常见。第二种多见于 package.json 中的 scripts
段落中的某条命令;第三种就直接集成到构建工具中。
这三种方式只有入口不同而已,调用的 babel 内核,处理方式都是一样的,所以我们先不纠结入口的问题。
很简单的几条原则:
preset 的逆向顺序主要是为了保证向后兼容,因为大多数用户的编写顺序是 ['es2015', 'stage-0']
。这样必须先执行 stage-0
才能确保 babel 不报错。因此我们编排 preset 的时候,也要注意顺序,其实只要按照规范的时间顺序列出即可。
简略情况下,插件和 preset 只要列出字符串格式的名字即可。但如果某个 preset 或者插件需要一些配置项(或者说参数),就需要把自己先变成数组。第一个元素依然是字符串,表示自己的名字;第二个元素是一个对象,即配置对象。
最需要配置的当属 env,如下:
"presets": [
// 带了配置项,自己变成数组
[
// 第一个元素依然是名字
"env",
// 第二个元素是对象,列出配置项
{
"module": false
}
],
// 不带配置项,直接列出名字
"stage-2"
]
比如 es2015 是一套规范,包含大概十几二十个转译插件。如果每次要开发者一个个添加并安装,配置文件很长不说,npm install 的时间也会很长,更不谈我们可能还要同时使用其他规范呢。
为了解决这个问题,babel 还提供了一组插件的集合。因为常用,所以不必重复定义 & 安装。(单点和套餐的差别,套餐省下了巨多的时间和配置的精力)
preset 分为以下几种:
官方内容,目前包括 env, react, flow, minify 等。这里最重要的是 env,后面会详细介绍。
stage-x,这里面包含的都是当年最新规范的草案,每年更新。
这里面还细分为
此外,低一级的 stage 会包含所有高级 stage 的内容,例如 stage-1 会包含 stage-2, stage-3 的所有内容。
stage-4 在下一年更新会直接放到 env 中,所以没有单独的 stage-4 可供使用。
这些是已经纳入到标准规范的语法。例如 es2015 包含 arrow-functions,es2017 包含 syntax-trailing-function-commas。但因为 env 的出现,使得 es2016 和 es2017 都已经废弃。所以我们经常可以看到 es2015 被单独列出来,但极少看到其他两个。
latest 是 env 的雏形,它是一个每年更新的 preset,目的是包含所有 es201x。但也是因为更加灵活的 env 的出现,已经废弃。
因为 env 最为常用也最重要,所以我们有必要重点关注。
env 的核心目的是通过配置得知目标环境的特点,然后只做必要的转换。例如目标浏览器支持 es2015,那么 es2015 这个 preset 其实是不需要的,于是代码就可以小一点(一般转化后的代码总是更长),构建时间也可以缩短一些。
如果不写任何配置项,env 等价于 latest,也等价于 es2015 + es2016 + es2017 三个相加(不包含 stage-x 中的插件)。env 包含的插件列表维护在这里
下面列出几种比较常用的配置方法:
{
"presets": [
["env", {
"targets": {
"browsers": ["last 2 versions", "safari >= 7"]
}
}]
]
}
// PC 端 browsers: ['Explorer >= 9', 'Edge >= 12', 'Chrome >= 49', 'Firefox >= 55', 'Safari >= 9.1']
//手机端 browsers: ['Android >= 4.4', 'iOS >=9']
'Explorer >= 9',
'Edge >= 12',
'Chrome >= 49',
'Firefox >= 55',
'Safari >= 9.1',
==如上配置将考虑所有浏览器的最新2个版本(safari大于等于7.0的版本)的特性,将必要的代码进行转换。==而这些版本已有的功能就不进行转化了。这里的语法可以参考 browserslist
{
"presets": [
["env", {
"targets": {
"node": "6.10"
}
}]
]
}
如上配置将目标设置为 nodejs,并且支持 6.10 及以上的版本。也可以使用 node: 'current'
来支持最新稳定版本。例如箭头函数在 nodejs 6 及以上将不被转化,但如果是 nodejs 0.12 就会被转化了。
另外一个有用的配置项是 modules
。它的取值可以是 amd
, umd
, systemjs
, commonjs
和 false
。这可以让 babel 以特定的模块化格式来输出代码。如果选择 false
就不进行模块化处理。
名称 | 作用 | 备注 |
babel-cli | 允许命令行使用 babel 命令转译文件 | |
babel-node | 允许命令行使用 babel-node 直接转译+执行 node 文件 | 随 babel-cli 一同安装 babel-node = babel-polyfill + babel-register |
babel-register | 改写 require 命令,为其加载的文件进行转码,不对当前文件转码 |
只适用于开发环境 |
babel-polyfill | 为所有 API 增加兼容方法 | 需要在所有代码之前 require ,且体积比较大 |
babel-plugin-transform-runtime & babel-runtime | 把帮助类方法从每次使用前定义改为统一 require ,精简代码 |
babel-runtime 需要安装为依赖,而不是开发依赖 |
babel-loader | 使用 webpack 时作为一个 loader 在代码混淆之前进行代码转换 |
已经同步整合到学习笔记(3)
已经同步到日常tips中;
【单一职责原则】
描述:一个类只负责一项职责
优势:降低复杂度、提升可读性和可维护性、降低类变更带来的风险
降低代码冗余度
建议放在utils文件夹中
– 之前的一个疑惑是:dva中没有用connect高阶组件返回的组件是不能使用dispatch函数的,但是我又不想把一个UI组件变成容器组件,所以这个时候就用到了组件间传值:
父组件传给子组件一个函数,父组件里这个函数handleClick(param)写dispatch,参数空出来,子组件调用this.props.handleClick(childParam),其实就可以dispatch action了,这个过程其实是子向父组件传值。
React子组件向父组件传值
此处实际上并没有牵扯到state的改变,或者说我们并没有必要取更新state,因为此处子组件给父组件传值,没有必要更新一下父组件的视图,但是一般来讲如果我们想要在父组件中显示子组件传过来的值的话那么一定要用到state了,此时需要深刻理解一下state:react中只有state发生了改变才会引起重新render。
React setState的一些思考与心得