onClick
事件之后
document
上唯一的标识key
来对事件函数进行存储dispatchEvent
回调函数react会将click
这个事件统一存到一个对象中,回调函数的存储采用键值对(key/value)的方式存储在对象中,key 是组件的唯一标识 id,value 对应的就是事件的回调函数,通过组件的key就能回调到相应的函数了严格模式
,所以事件回调的时候会丢失this指向,指向undefined
,需要使用bind来给函数绑定上当前实例的this指向箭头函数
的this指向上下文,所以永久能拿到当前组件实例的this
指向,我们可以完美的使用箭头函数来替代传统事件处理函数的回调性能优化分为2个方面
setState 是修改其中的部分状态,相当于 Object. assign,只是覆盖,不会减少原来的状态; replaceState 是完全替换原来的状态,相当于赋值,将原来的 state 替换为另一个对象,如果新状态属性减少,那么 state 中就没有这个状态了
接收旧的 state 和 action,返回新的 state
受控组件就是可以被 react 状态控制的组件
在 react 中,Input textarea 等组件默认是非受控组件(输入框内部的值是用户控制,和React无关)。但是也可以转化成受控组件,就是通过 onChange 事件获取当前输入内容,将当前输入内容作为 value 传入,此时就成为受控组件。
如果项目体量较小,只是需要一个公共的store存储state,而不讲究使用action来管理state,那context完全可以胜任。反之,则是redux的优点。
使用场景:
如果你在组件间传递的数据逻辑比较复杂,可以使用redux;
如果组件层级不多,可以使用props;
如果层级较深,数据逻辑简单,可以使用context。
componentDidMount:useEffect(()=>{console.log('第一次渲染时调用')},[])
useEffect(()=>{console.log('任意状态改变')})
监听多个属性的变化需要将属性作为数组传入第二个参数。
useEffect(()=>{console.log('指定状态改变')},[状态1,状态2...])
useEffect(()=>{ ... return()=>{ //组件卸载前} })
setState( updater [,callback] )
updater:object/function - 用于更新数据
callback:function - 用于获取更新后最新的 state 值
构造函数是唯一建议给 this.state 赋值的地方
不建议直接修改 state 的值,因为这样不会重新渲染组件
自动进行浅合并(只会合并第1层)
由于 setState() 异步更新的缘故,依赖 state 旧值更新 state 的时候建议采用传入函数的方式
useState(initState)
const [ state , setState ] = useState(initState)
state:状态
setState(updater) :修改状态的方法
updater:object/function - 用于更新数据
initState:状态的初始值
当props发生变化时执行,初始化render时不执行,在这个回调函数里面,你可以根据属性的变化,通过调用this.setState()来更新你的组件状态,旧的属性还是可以通过this.props来获取,这里调用更新状态是安全的,并不会触发额外的render调用
//props发生变化时触发
componentWillReceiveProps(props) {
console.log(props)
this.setState({show: props.checked})
}
返回一个 memoized 值。
把“创建”函数和依赖项数组作为参数传入 useMemo,它仅会在某个依赖项改变时才重新计算 memoized 值。这种优化有助于避免在每次渲染时都进行高开销的计算。
传入 useMemo 的函数会在渲染期间执行。请不要在这个函数内部执行与渲染无关的操作,诸如副作用这类的操作属于 useEffect 的适用范畴,而不是 useMemo。
如果没有提供依赖项数组,useMemo 在每次渲染时都会计算新的值。
你可以把 useMemo 作为性能优化的手段,但不要把它当成语义上的保证。将来,React 可能会选择“遗忘”以前的一些 memoized 值,并在下次渲染时重新计算它们,比如为离屏组件释放内存。先编写在没有 useMemo 的情况下也可以执行的代码 —— 之后再在你的代码中添加 useMemo,以达到优化性能的目的。
useState:
useState 是用于声明一个状态变量的,用于为函数组件引入状态.
useState 只接收一个参数,这个参数可以是数字、字符串、对象等任意值,用于初始化声明的状态变量。也可以是一个返回初始值的函数,最好是函数,可在渲染时减少不必要的计算。
返回一个长度为2的读写数组,数组的第一项是定义的状态变量本身,第二项是一个用来更新该状态变量的函数,约定是 set 前缀加上状态的变量名.
useState Hook 中返回的 setState 并不会帮我们自动合并对象状态的属性;
setState 中接收的对象参数如果地址没变的话会被 React 认为没有改变,因此不会引起视图的更新
useReducer:
useReducer 是 useState 的升级版。在 useState 中返回的写接口中,我们只能传递最终的结果,在 setN 的内部也只是简单的赋值操作。
创建初始状态值initialState,
创建包含所有操作的 reducer(state, action) 函数,每种操作类型均返回新的 state 值
根据 initialState 和 reducer 使用 const [state, dispatch] = useReducer(reducer, initialState) 得到读写 API
调用写接口,传递的参数均挂在 action 对象上
useContext:
context 是上下文的意思,上下文是局部的全局变量这个局部的范围由开发者自己指
useEffect:
effect 是副作用的意思,对环境的改变就是副作用。副作用是函数式编程里的一个概念
在 React 中,useEffect 就是在每次 render 后执行的操作,相当于 afterRender, 接收的第一个参数是回调函数,第二个参数是回调时机。可用在函数组件中模拟生命周期。
如果同时出现多个 useEffect ,会按出现顺序依次执行
useLayoutEffect
useEffect 总是在浏览器渲染完视图过后才执行,如果 useEffect 里面的回调函数有对 DOM 视图的操作,则会出现一开始是初始化的视图,后来执行了 useEffect 里的回调后立马改变了视图的某一部分,会出现一个闪烁的状态。
为了避免这种闪烁,可以将副作用的回调函数提前到浏览器渲染视图的前面执行,当还没有将 DOM 挂载到页面显示前执行 Effect 中对 DOM 进行操作的回调函数,则在浏览器渲染到页面后不会出现闪烁的状态。
layout 是视图的意思,useLayoutEffect 就是在视图显示出来前执行的副作用。
useEffect 和 useLayoutEffect 就是执行的时间点不同,useLayoutEffect 是在浏览器渲染前执行,useEffect 是在浏览器渲染后执行。但二者都是在 render 函数执行过程中运行,useEffect 是在 render 完毕后执行,useLayoutEffect 是在 render 完毕前(视图还没渲染到浏览器页面上)执行。
因此 useLayoutEffect 总是在 useEffect 前执行。
一般情况下,如果 Effect 中的回调函数中涉及到 DOM 视图的改变,就应该用 useLayoutEffect,如果没有,则用 useEffect。
useRef
useRef Hook 是用来定义一个在组件不断 render 时保持不变的变量。
组件每次 render 后都会返回一个虚拟 DOM,组件内对应的变量都只属于那个时刻的虚拟 DOM。
useRef Hook 就提供了创建贯穿整个虚拟 DOM 更新历史的属于这个组件的局部的全局变量。
为了确保每次 render 后使用 useRef 获得的变量都能是之前的同一个变量,只能使用引用做到,因此,useRef 就将这个局部的全局变量的值存储到了一个对象中,属性名为:current
useRef 的 current 变化时不会自动 render
useRef 可以将创建的 Refs 对象通过 ref 属性的方式引用到 DOM 节点或者 React 实例。
useCallback
是将某个函数“放入到react底层原型链上,并返回该函数的索引”,而useMemo是将某个函数返回值“放入到react底层原型链上,并返回该返回值的索引”。一个是针对函数,一个是针对函数返回值。
useImperativeHandle
useImperativeHandle可以让父组件获取并执行子组件内某些自定义函数(方法)。本质上其实是子组件将自己内部的函数(方法)通过useImperativeHandle添加到父组件中useRef定义的对象中。
useMemo
useMemo可以将某些函数的计算结果(返回值)挂载到react底层原型链上,并返回该函数返回值的索引。当组件重新渲染时,如果useMemo依赖的数据变量未发生变化,那么直接使用原型链上保存的该函数计算结果,跳过本次无意义的重新计算,达到提高组件性能的目的。
Component 没有直接实现 shouldComponentUpdate 这个方法;但是 PureComponent通过浅层的Porps 和 state 的对比,内部实现了这个生命周期函数。
PureComponent会跳过整个组件子树的props更新,要确保全部的子组件也是 pure 的形式。
Component 中需要手动执行的 shouldComponentUpdate 函数,在PureComponent中已经自动完成了(自动浅对比)。
PureComponent不仅会影响本身,而且会影响子组件,所以PureComponent最好用在数据展示组件中
PureCoponent 如果是复杂数据类型,这里会造成错误的显示(setState浅复制更新,但是界面不会重新渲染)
类组件缺点一:复杂且不容易理解的“this”
Hooks解决方式:函数组件和普通JS函数非常相似,在普通JS函数中定义的变量、方法都可以不使用“this.”,而直接使用该变量或函数,因此你不再需要去关心“this”了。
类组件缺点二:组件数据状态逻辑不能重用
Hooks解决方式:
通过自定义Hook,可以数据状态逻辑从组件中抽离出去,这样同一个Hook可以被多个组件使用,解决组件数据状态逻辑并不能重用的问题。
类组件缺点三:组件之间传值过程复杂、缺点三:复杂场景下代码难以组织在一起
Hooks解决方式:
通过React内置的useState()函数,可以将不同数据分别从"this.state"中独立拆分出去。降低数据复杂度和可维护性,同时解决类组件缺点三中“内部state数据只能是整体,无法被拆分更细”的问题。
通过React内置的useEffect()函数,将componentDidMount、componentDidUpdate、componentWillUncount 3个生命周期函数通过Hook(钩子)关联成1个处理函数,解决事件订阅分散在多个生命周期函数的问题。
父组件使用 useRef
创建一个 ref
传入 子组件
子组件需要使用 useImperativeHandle
暴露 ref
自定义的实例值给父组件。这个需要用 forwardRef
包裹着。
1 父子通信, 使用的时候 在子组件上面自定义属性,子组件通过 this.props[累组件]或者props[函数组件]接收然后使用
子组件
class Son extends React.Component {
render(){
// this.props.til til 为父组件传递数据
return ...
}
}
父组件
class Home extends React.Component {
render(){
return <>
>
}
}
2 子父通信 ,使用的时候其实还是父传子,只不过这次将父组件的方法传递给子组件,然后子组件去调用父组件传递的方法,通过传递实参的我方式将子组件的数据传递过来
class Son extends React.Component {
render(){
// this.props.til til 为父组件传递数据
return this.props.onData('传递的数据')}>...
}
}
父组件
class Home extends React.Component {
getData=(data)=>{
//data就是接收的参数。可以在里面来使用
}
render(){
return <>
>
}
}
3 状态提升,这种情况将两个组件通过同一个父组件来包裹,将数据绑定到父组件上,然后通过子父和父子进行通信
4 context传递 , 利用Context对象来传递,Context 提供了 Provider 和 Consumer 俩个组件,Provider负责提供数据,Consumer负责使用数据 , Provider需要包裹父组件,那么所有它的子代组件都可以获取到共享值,实际上react-router,react-redux 等等都在使用context
const Context = React.Contexxt('这里是默认值');
// App组件里面
class App extends React.Component {
render(){
return <>
// 只要是home的组件的子代组件都可以获取到传餐
>
}
}
// Home 组件
class Home extends React.Component {
render(){
return <>
{
data=> data就可以拿到数据
}
>
}
}
// Son
class Son extends React.Component {
static contextType = Context. // 可以静态接收。接收完之后。可以使用 this.context来调用context的值
render(){
return <>
//Son组件里面可以获取到 context的值
{
data=> data就可以拿到数据
}
>
}
}
5 reudx. 可以使用redux 来进行组件通信,适用于多个组件使用同一个数据 ,可以更好的维护和使用
类组件修改数据的方法, 通过setState , 注意setState的修改方法有两种,而且它是异步的
函数组件修改方式通过自定义的方法。需要通过 useState , 例如。const [count,setCount] = useState(0)
这里面setCount 可以修改count参数
React native 基于 JavaScript 开发的一个 可以开发原生app的这么一个集成框架,它兼容开发 iOS 和 Android能够实现一套代码,两个系统都能使用,方便维护,相比于web前端的 react ,react-native更好的提供了一些调用手机硬件的API,可以更好的开发移动端,现在react-native它的生态环境也是越来越好,基本上可以完全实现原生开发,
但是现在好多的应用还是用它来套壳 (原生 + web前端),用它来做有些路由,和框架的搭建,然后里面内容来使用前端的react来实现,这样一来让维护更方便,开发更快捷
redux它是一个单独的状态管理工具,它是一个数据集中管理的方案,简单来说,就是将公用的数据,放在redux里面进行存储,修改的时候也是利用redux提供的方法来修改,让框架使用的数据的时候更方便,维护起来更容易,reudx提供了一下核心内容:
store是redux的核心内容,整个redux的仓库,所有的应用方法都是由store提供
createStore用老创建store的,方法里面有三个参数,有reducer,有中间件,还有初始值,最重要的就是reducer函数,它提供了整个数据管理的逻辑
reducer函数相当于数据加工厂,初始的数据,修改数据的 逻辑都统统的在这里完成,它让我们整个的数据走向更完成,维护更方便
action 本质是一个对象,它来告诉 redux 要执行什么任务
state它就是我们需要存储的redux的数据,所有的数据都将要在这里面存储
它就是用来获取数据的,每次修改前后的数据都可以用store.getState()来修改
用户通过dispatch出发要执行的任务,这个就是出发动作,然后reducer函数会执行,然后进行数据加工
会自动的监听state,一旦有state发生改变,store.subscribe就会还行,利用它可以用来更新视图
1 第一种是通过受控组件,可以获取到state里面的值,获取修改结果
class Home extends React.Component {
state = {
val:""
}
//这是一个通用的写法,然后注意 name的值一定要与state定义的一直
changeInput = (e) =>{
let {name,value} = e.target
this.setState({
[name]:value
})
}
render(){
return <>
>
}
}
2 第二种通过ref来获取里面的值
class Home extends React.Component {
render(){
// this.val 获取里面的真是dom
return this.val} />
}
}
react生命周期有三个阶段。 两个阶段都会执行 render
主要从更新和挂载两个阶段来讲,挂载阶段都顺序,更新阶段一定要说 shouldComponentUpdate true 和false 分别对应后边是否执行render
1) 挂在阶段
constructor(){}
static getDerivedStateFromProps(){
return {}
}
render(){}. 挂载阶段会执行一次
componentDidMount(){}
2 ) 更新阶段
static getDerivedStateFromProps(props, state){rerturn {}}
shouldComponentUpdate(nextProps, nextState).{ return Boolean. 注意如果 false 则 不向下执行 ,true的时候会执行render}
return。true
render() ..
useEffect的依赖为饮用类型的时候,可能会导致监听不出发,原因就是监听的统一个地址的时候,对象本身地址没变,所以监听的结果就是认为数据并没有改变从而不直径调用
解决方案
1 如果数据是对象的话,可以监听对象的里面的值,值是基本类型,如果值改变了,那么可以监听执行
2 在去修改对象和数据的时候,使用参拷贝或者浅拷贝,这样地址发生改变可以监听执行
3 可以转成字符串,通过JSON.stringify() ,监听字符串这样的,这样改变也会执行
1 首先注意到就是 useState 里面方法是异步的,所以不要在后面连续调用,由于react方法是批量异步调用,并不是每次调用修改方法都执行,所以需要用到callback写法
const [count, setCount] = useState(0);
const add = () => {
setCount(count + 1);
setCount(count + 1);
setCount(count + 1);
setCount(count + 1); // 就算执行多次 其实还是会只执行一次
//class组件是用一样的
};
修改后
const add = () => {
setCount(count => count + 1); //这种回调函数的写法
setCount(count => count + 1);
setCount(count => count + 1);
setCount(count => count + 1);
// class组件
this.setState(prev=>({count: prev.count}))
};
2 由于 useState的是异步的,不要在修改后直接使用数据 , 可以先修改数据,判断数据,也可以利用useEffec,useMemo等等通过监听数据执行
const [count, setCount] = useState(0);
const add = () => {
setCount(count + 1);
if(count>=10) { // 一个逻辑 这样的写的话 会执行上一次吃结果}
};
正确使用 1
const add = () => {
let n = count +1
if(n){
//逻辑
}
setCount(n)
};
正确2 :
const add = () => [
setCount(count+1)
]
useEffect(()=>{
// 这里是逻辑
},[count])
3 useEffect 这个hook的使用,每一个消耗性能的内容都可以通过return 来消除
useEffect(()=>{
// 逻辑1
return ()=>{
// 清楚逻辑1的副作用
}
},[监听的值])
4 useRef 可以获取组件的数据,也可当常量的值来使用,注意获取数据使用的时候函数组件特别需要注意,如果子组件是函数组件需要利用 useImperativeHandle ,forWard
1 react的key值给组件作为唯一标识,类似身份证,当数组发生增删改查的时候可以通过这个 标识来去对比虚拟dom的更改前后,如果相同标识的数据或者属性没有改变的话,讲直接略过,对比有改变的然后直接改变此项
2 如果给组件添加key,如果key值改变的时候,组件将会重新出创建会执行 挂在阶段的生命周期
constructor
static getDerivedStateFromProps()
render(){}
componentDidUpdate
react.createPortal 这个方法是来使用做弹窗Modal组件的,在没有这个组件之前我们可以自己定义组件,然后去实现Modal效果
Modal.js
const styles = {
modal: {
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(0,0,0,0.3)',
display: 'flex',
justifyContent: 'center',
alignItems: 'center'
}
}
class Modal extends Component {
constructor(props) {
super(props);
}
render() {
return (
{this.props.children}
);
}
}
react.createPortal 这个来制作弹窗组件,它 在Modal 组件位置进行 fixed
定位,可以任意的挂载到某个dom元素上,使用后的管理更方便,但是注意需要预留html的挂载元素
真正意义上的react全家桶,其实指的是react,react-dom,react-native。
因为react核心库只是vDom的操作,无关dom(dom操作在react-dom中)——这意味着它天生就可以做到跨平台。
注意这里有误区,react-router,react-router-dom,react-dux只是社区的一些使用较多的解决方案,事实上它们各有缺陷。
截止到当前版本(react17.0.2),react尚存在一个明显的缺陷——这是大多数vDom框架都需要面对的——“diff”。
要知道,render前后的两颗vDom tree进行diff,这个过程是不可中断的(以tree为单位,不可中断),这将造成当diff的两颗tree足够庞大的时候,js线程会被diff阻塞,无法对并发事件作出回应。
为了解决这个问题,react将vDom节点上添加了链表节点的特性,将其改造成了fiber节点(其实就是vdom节点结合了链表节点的特性),目的是为了后面的Fiber架构的实现,以实现应对并发事件的“并发模式”。
事实上,原计划是17版本上的,但最终定期在了18版本。
fiber节点的改造是为了将diff的单位从不可中断的tree降级到可中断的单一vDom。这意味着每次一个vDom节点比对过后,可以轮训是否发生了优先级更高的并发事件(react将用户想更快得到反馈的事进行了优先级排序,比如js动画等)。
每个vDom的比对是一个时间片单位,因此基于fiber节点的Fiber架构,存在这样的特性:
“时间分片,任务分炼,异步渲染。”
react-hooks提供给了函数式组件以业务逻辑抽离封装的能力,从单一组件单位进行UI与业务逻辑的分离,以此来保证函数式组件的UI纯净。
在此之前,我们只能通过状态提升的方式,将业务逻辑提升到容器组件的方式。
只可以在顶层调用栈调用hooks
只可以在react function或自定义hooks里调用hooks
hooks的引用记录,是用单链表结构实现的;(可在力扣/leetcode网找一下实现单链表的算法)
每一个链表节点(node)都有一个next的指针指向下一个node;
如果中间有缺失,那么就无法找到下一个next;
因此hooks不可以在条件里调用(有可能丢失链表node,导致hooks遍历中断);
useEffect的本质作用,其实是将函数式组件中的业务逻辑副作用进行抽离封装。模拟生命周期并不是它的核心思想,但它当然能做到这个事:
https://github.com/melodyWxy/melody-core-utils/blob/master/src/lifeHooks/index.ts
与useEffect的区别是,useLayoutEffect中副作用的执行时间在组件render之前,这意味着它适用于比组件render更紧急的副作用。
正因为如此,多数情况下我们不会选择使用它,因为我们并不想阻塞render。
变量值存储功能
首先,函数式组件每次render就会重新执行一次,这意味着我们在函数组件中定义的变量都会重置,无法保留变更。
useRef可以解决这个问题——它会形成一个闭包作用域来记录这个值。
ref的通用功能
当然,我们也可以用它来标记dom和类组件,从而获取对应的实例。
同样的,因为函数式组件每次render就会重新执行一次,因此我们在函数式组件中定义的方法都会重新定义————多数情况下这是不必要的,因此我们可以用useCallback来缓存它。
当然,我们也可以基于它第二个参数——一个监听数组,来控制它在何种时机进行重定义。
同样的,因为函数式组件每次render就会重新执行一次,因此我们在定义的一些变量或者计算过后的变量,都会重新定义————多数情况下这是不必要的,因此我们可以用useCallback来缓存它。
当然,我们也可以基于它第二个参数——一个监听数组,来控制它在何种时机进行重定义。
Flux 是一种 架构思想,简单来讲 就是:
状态提升到全局Store + Store内容分发给组件 + 组件交互updateStore然后更新组件。
就是这样一种 通过全局store同一管理数据(从而实现组件间数据共享)的架构思想。
基于这种思想,社区里已有各种各样成型的解决方案,其中,redux是通用性比较强的方案(兼容原生和绝大部分框架);
但它的使用较为麻烦,在react项目里,一般用 redux + react-redux + 配置中间件(根据项目需要配置) 这样的方式。
比如 dva = redux + react-redux + redux-saga + router (耦合了router也是dva的诟病所在);
在hooks出现之后,react提供了 useContext这样的钩子,实现了redux的功能。
react-redux 的核心:Provider 与 connect
Provider容器组件底层: 把store打入 contextconnect;
高阶组件底层: 从context拿到store,然后打入到参数组件的props;
高阶组件(HOC)是 React 中用于复用组件逻辑的一种高级技巧。HOC 自身不是 React API 的一部分,它是一种基于 React 的组合特性而形成的设计模式。
1. 属性代理是最常见的实现方式,它本质上是使用组合的方式,通过将组件包装在容器组件中实现功能。
2. 属性代理方式实现的高阶组件和原组件的生命周期关系完全是React父子组件的生命周期关系,所以该方式实现的高阶组件会影响原组件某些生命周期等方法。
操作 props
抽象 state
获取 refs 引用
获取原组件的 static 方法
通过 props 实现条件渲染
反向继承指的是使用一个函数接受一个组件作为参数传入,并返回一个继承了该传入组件的类组件,且在返回组件的 render() 方法中返回 super.render() 方法,最简单的实现如下:
非受控组件将数据存储在DOM中,而不是组件内,这比较类似于传统的HTML表单元素。
setState会重新执行render函数,如果添加setState会导致死循环
如果父组件中没有依赖子组件的状态不会
如果父组件中依赖了子组件的状态,因为要执行setState会重新触发生命周期
会,因为会重新执行render函数,如果不想更新子组件可以在shouldComponentUpdate中终止掉
跳转路由,并向路由组件传递params参数
{tabItem.title}跳转路由,并向路由组件传递search参数
{tabItem.title}跳转路由,并向路由组件传递state参数
{tabItem.title} ### 使用redux时,setState修改状态的过程是异步还是同步不能,props为只读状态 单向数据流
useState()
userContext()
userReducer()
useEffect()
如果想执行只运行一次的 effect(仅在组件挂载和卸载时执行),可以传递一个空数组([])作为第二个参数。这就告诉 React 你的 effect 不依赖于 props 或 state 中的任何值,所以它永远都不需要重复执行。
使用Redux的主要优势之一是它可以帮你处理应用的共享状态。如果两个组件需要访问同一状态,该怎么办?这种两个组件同时需要访问同一状态的现象称为“共享状态”。你可以将该状态提升到附近的父组件,但是如果该父组件在组件树中向上好几个组件的位置,那么将状态当做属性向下一个一个地传递,这项工作很快就会变得乏味。此外,在该父组件和该子组件之间的组件甚至根本不需要访问该状态!
在构建网络应用时,Redux不仅使我们能够以有条理的方式存储数据,而且使我们能够在应用的任何位置快速获取该数据。只需告诉Redux到底哪个组件需要哪个数据,它就会为你处理后续一切工作!
借助Redux,你可以控制状态改变的时间、原因和方式。
该 Hook 接收一个包含命令式、且可能有副作用代码的函数。
在函数组件主体内(这里指在 React 渲染阶段)改变 DOM、添加订阅、设置定时器、记录日志以及执行其他包含副作用的操作都是不被允许的,因为这可能会产生莫名其妙的 bug 并破坏 UI 的一致性。
使用 useEffect 完成副作用操作。赋值给 useEffect 的函数会在组件渲染到屏幕之后执行。你可以把 effect 看作从 React 的纯函数式世界通往命令式世界的逃生通道。
不可以。 useEffect 并不完全等同于 componentWillUnMount 。当 useEffect 模拟 componentDidMount 和 componentDidUpdate 的时候,返回的函数并不完全等同于 componentWillUnMount。
useEffect和useLayoutEffect作为组件的副作用,本质上是一样的。共用一套结构来存储effect链表。整体流程上都是先在render阶段,生成effect,并将它们拼接成链表,存到fiber.updateQueue上,最终带到commit阶段被处理。他们彼此的区别只是最终的执行时机不同,一个异步一个同步,这使得useEffect不会阻塞渲染,而useLayoutEffect会阻塞渲染。
React遵循从上到下的数据流向,即单向数据流。
1、单向数据流并非‘单向绑定’,甚至单向数据流与绑定没有‘任何关系’。对于React来说,单向数据流(从上到下)与单一数据源这两个原则,限定了React中要想在一个组件中更新另一个组件的状态
(类似于Vue的平行组件传参,或者是子组件向父组件传递参数),需要进行状态提升。即将状态提升到他们最近的祖先组件中。子组件中Change了状态,触发父组件状态的变更,父组件状态的变更,
影响到了另一个组件的显示(因为传递给另一个组件的状态变化了,这一点与Vue子组件的$emit()方法很相似)。
2、Vue也是单向数据流,只不过能实现双向绑定。
3、单向数据流中的‘单向’-- 数据从父组件到子组件这个流向叫单向。
4、绑定的单双向:View层与Module层之间的映射关系。
eact-router和vue-router实现原理大同小异,都是通过两种方式,hash和history,首先看一下什么是hash和history:
hash模式背后的原理是onhashchange事件。
window.onhashchange=function(){
let hash=location.hash.slice(1);
document.body.style.color=hash;
}
(localtion是js里管理地址栏的内置对象,是window对象的一部分,可通过window.localtion访问,在w3cshool里的详细介绍)
由于hash发生变化的url都会被浏览器记录下来,使得浏览器的前进后退都可以使用了,尽管浏览器没有亲求服务器,但是页面状态和url关联起来。后来人们称其为前端路由,成为单页应用标配。
比如http://www.abc.com/#/index,hash值为#/index。hash模式的特点在于hash出现在url中,但是不会被包括在HTTP请求中,对后端没有影响,不会重新加载页面。
2.history模式
当使用history模式时,url就像正常的url,例如http://abc.com/user/id相比hash模式更加好看。特别注意,history模式需要后台配置支持。如果后台没有正确配置,访问时会返回404。
其中一事件触发用到了 onpopstate。
通过history api,我们丢弃了丑陋的#,但是有一个缺点,当刷新时,如果服务器中没有相应的相应或者资源,会分分钟刷出一个404来(刷新需要请求服务器)。所以history模式不怕前进,不怕后退,就怕刷新。
下述案例为实现的react-router的代码,来了解router大致实现原理
原理: 在dom渲染完成之后,给window 添加 “hashchange” 事件监听页面hash的变化,并且在state属性之中添加了 route属性,代表当前页面的路由。
1、当点击连接 页面hash改变时,触发绑定在window 上的 hashchange 事件,
2、在 hashchange 事件中改变组件的 state中的 route 属性,(react组件的state属性改变时,自动重新渲染页面)
3、页面 随着 state 中的route属性改变自动 根据 不停的hash 给 Child 变量赋值不通的组件,进行渲染
核心代码:
import React from 'react'
import { render } from 'react-dom'
const About = function () {
return <div>111</div>
}
const Inbox = function () {
return <div>222</div>
}
const Home = function () {
return <div>333</div>
}
class App extends React.Component {
state = {
route: window.location.hash.substr(1)
}
componentDidMount() {
window.addEventListener('hashchange', () => {
this.setState({
route: window.location.hash.substr(1)
})
})
}
render() {
let Child
switch (this.state.route) {
case '/about': Child = About; break;
case '/inbox': Child = Inbox; break;
default: Child = Home;
}
return (
<div>
<h1>App</h1>
<ul>
<li><a href="#/about">About</a></li>
<li><a href="#/inbox">Inbox</a></li>
</ul>
<Child />
</div>
)
}
}
render(<App />, document.body)
其实这个问题问的是axios请求封装种的请求拦截器和响应拦截器,比如如下的封装代码案例
import axios from 'axios'
import router from './../router'
// development 开发环境 production 生产环境
const isDev = process.env.NODE_ENV === 'development'
// baseURL 会自动加到所有的请求之前
const request = axios.create({
// http://121.89.205.189/api/pro/list ==> /pro/list
baseURL: isDev ? 'http://121.89.205.189/api' : 'http://121.89.205.189/api',
timeout: 6000
})
// 请求拦截器
request.interceptors.request.use(config => {
// 传入token验证登录状态
// 如果需要设置其他的信息也可以在这里
config.headers.common.token = localStorage.getItem('token')
return config
}, err => Promise.reject(err))
// 响应拦截器封装
request.interceptors.response.use(response => {
// 如果验证未登录,可以跳转到登录页面
if (response.data.code === '10119') {
router.push('/login')
}
return response
}, err => Promise.reject(err))
export default request
这个其实是支付宝整合的一套框架,集成了 (redux + react-router + redux-saga 等)的一层轻量封装。dva 是 framework,不是 library,类似 emberjs。
他最核心的是提供了 app.model 方法,用于把 reducer, initialState, action, saga 封装到一起。
app.model({
namespace: 'products',
state: {
list: [],
loading: false,
},
subscriptions: [
function(dispatch) {
dispatch({type: 'products/query'});
},
],
effects: {
['products/query']: function*() {
yield call(delay(800));
yield put({
type: 'products/query/success',
payload: ['ant-tool', 'roof'],
});
},
},
reducers: {
['products/query'](state) {
return { ...state, loading: true, };
},
['products/query/success'](state, { payload }) {
return { ...state, loading: false, list: payload };
},
},
});
namespace - 对应 reducer 在 combine 到 rootReducer 时的 key 值
state - 对应 reducer 的 initialState
subscription - [email protected] 的新概念,在 dom ready 后执行,这里不展开解释
effects - 对应 saga,并简化了使用
reducers - 相当于数据模型
// 如何创建一个dva项目
$ npm install dva-cli -g
$ dva new myapp && cd myapp
$ npm start
可以在样式的外部添加:global
:global{
.account-form{
background-color: red;
}
}
可以参考链接https://ant.design/docs/react/customize-theme-cn
我们以 webpack@4 为例进行说明,以下是一个 webpack.config.js
的典型例子,对 less-loader 的 options 属性进行相应配置。
// webpack.config.js
module.exports = {
rules: [{
test: /\.less$/,
use: [{
loader: 'style-loader',
}, {
loader: 'css-loader', // translates CSS into CommonJS
}, {
loader: 'less-loader', // compiles Less to CSS
+ options: {
+ lessOptions: { // 如果使用less-loader@5,请移除 lessOptions 这一级直接配置选项。
+ modifyVars: {
+ 'primary-color': '#1DA57A',
+ 'link-color': '#1DA57A',
+ 'border-radius-base': '2px',
+ },
+ javascriptEnabled: true,
+ },
+ },
}],
// ...other rules
}],
// ...other config
}
注意:
node_modules
下的 antd 包。lessOptions
的配置写法在 [email protected] 里支持。hooks 模拟 componentDidMount
useEffect 拥有两个参数,第一个参数作为回调函数会在浏览器布局和绘制完成后调用,因此它不会阻碍浏览器的渲染进程。
第二个参数是一个数组
useEffect(()=>{console.log('第一次渲染时调用')},[])
没有第二个参数代表监听所有的属性更新
useEffect(()=>{console.log('任意属性该改变')})
监听多个属性的变化需要将属性作为数组传入第二个参数。
useEffect(()=>{console.log('n变了')},[n,m])
通常,组件卸载时需要清除 effect 创建的诸如订阅或计时器 ID 等资源 useEffect函数返回的函数可以表示组件卸载了
useEffect(()=>{
const timer = setTimeout(()=>{
...
},1000)
return()=>{
console.log('组件卸载了')
clearTimerout(timer)
}
})
在 React 16.8 之前,函数组件只能是无状态组件,也不能访问 react 生命周期。hook 做为 react 新增特性,可以让我们在不编写 class 的情况下使用 state 以及其他的 react 特性,例如生命周期。接下来我们便举例说明如何使用 hooks 来模拟比较常见的 class 组件生命周期。
hooks 模拟 componentDidMount
useEffect 拥有两个参数,第一个参数作为回调函数会在浏览器布局和绘制完成后调用,因此它不会阻碍浏览器的渲染进程。
第二个参数是一个数组
useEffect(()=>{console.log('第一次渲染时调用')},[])
hooks 模拟 shouldComponentUpdate
React.memo 包裹一个组件来对它的 props 进行浅比较,但这不是一个 hooks,因为它的写法和 hooks 不同,其实React.memo 等效于 PureComponent,但它只比较 props。
const MyComponent = React.memo(
_MyComponent,
(prevProps, nextProps) => nextProps.count !== prevProps.count
)
hooks 模拟 componentDidUpdate
没有第二个参数代表监听所有的属性更新
useEffect(()=>{console.log('任意属性该改变')})
监听多个属性的变化需要将属性作为数组传入第二个参数。
useEffect(()=>{console.log('n变了')},[n,m])
hooks 模拟 componentWillUnmount
通常,组件卸载时需要清除 effect 创建的诸如订阅或计时器 ID 等资源 useEffect函数返回的函数可以表示组件卸载了
useEffect(()=>{
const timer = setTimeout(()=>{
...
},1000)
return()=>{
console.log('组件卸载了')
clearTimerout(timer)
}
})
1.render里面尽量减少新建变量和bind函数,传递参数时尽量减少传递参数的数量
第一种是在构造函数中绑定this,会在 构造函数 实例化的时候执行一次
第二种是在render()函数里面绑定this,在每次render()的时候都会重新执行一遍
第三种就是使用箭头函数,每一次render()的时候,都会生成一个新的箭头函数,即使两个箭头函数的内容是一样的。
2.shouldComponentUpdate是决定react组件什么时候能够不重新渲染的函数,但是这个函数默认的实现方式就是简单的返回一个true。也就是说,默认每次更新的时候都要调用所用的生命周期函数,包括render函数,重新渲染。
为了不做不必要的渲染,需要使用shouldComponentUpdate 加以判断
最新的react中,react给我们提供了React.PureComponent
3.使用key进行组件的唯一标识
虚拟DOM可以看做一棵模拟了DOM树的JavaScript对象树。在传统的 Web 应用中,我们往往会把数据的变化实时地更新到用户界面中,于是每次数据的微小变动都会引起 DOM 树的重新渲染。虚拟DOM的目的是将所有操作累加起来,统计计算出所有的变化后,统一更新一次DOM。
React会先将你的代码转换成一个javascript对象,然后再把这个javascript对象转换成真实的DOM。而这个javascript对象就是所谓的虚拟DOM。
1.基于babel-preset-react-app把JSX变成React.createElement(...)
。可以理解为JSX只是React.createElement的语法糖。
123
456
经过babel转换成下面的样子:
React.createElement("div", {
className: "box",
style: {
color: '#f00'
},
index: 0
}, "123", React.createElement("p", null, "456")
);
2.执行React.createElement(...)
会返回一个javascript对象,这个对象就是所谓的虚拟DOM。
createElement会返回一个虚拟对象 let VitrualDomObj = React.createElement(...)
3.把这个虚拟DOM通过render函数转换成真实的DOM
myrender( VitrualDomObj , document.getElementById('root'));
1.共同点:
2.不同点:
Redux每一次的dispatch都会从根reducer到子reducer嵌套递归的执行,所以效率相对较低;而Mobx的内部使用的是依赖收集,所以不会有这个问题,执行的代码较少,性能相对更高;
Redux核心是不可变对象,在Reducer中的操作都要比较小心,注意不能修改到state的属性,返回时必须是一个全新的对象;而Mobx采用不存在这个问题,操作比较随意;
Redux中写法固定,模板代码较多,Mobx中写法比较随意,但是因为写法随意的原因,如果没有规范性的话,维护性则不会像Redux那么高;
正因为Redux中的reducer更新时,每次return的都是不可变对象,所以时间旅行操作相对容易,而Mobx在这方面不占优势
Redux更加的轻量,但是一般来说都会配合中间件进行使用
1、Context目前还处于实验阶段,可能会在后面的发行版本中有很大的变化,事实上这种情况已经发生了,所以为了避免给今后升级带来大的影响和麻烦,不建议在app中使用context。
2、尽管不建议在app中使用context,但是独有组件而言,由于影响范围小于app,如果可以做到高内聚,不破坏组件树之间的依赖关系,可以考虑使用context
3、对于组件之间的数据通信或者状态管理,有效使用props或者state解决,然后再考虑使用第三方的成熟库进行解决,以上的方法都不是最佳的方案的时候,在考虑context。
4、context的更新需要通过setState()触发,但是这并不是很可靠的,Context支持跨组件的访问,但是如果中间的子组件通过一些方法不影响更新,比如 shouldComponentUpdate() 返回false 那么不能保证Context的更新一定可以使用Context的子组件,因此,Context的可靠性需要关注。
其他的解决方案可以使用
顶层Router订阅history,history变化时,Router调用setState将location向下传递,并设置到RouterContext。Route组件匹配context中的location决定是否显示。Switch选择最先匹配到的显示,利用props children。Link组件阻止a标签默认事件,并调用history.push。NavLink通过匹配context中的location决定是否为active状态。Redirect组件匹配context里的location决定是否调用history.push(to),Switch组件会匹配location和from决定是否发起Redirect。
单向数据流就是:数据在某个节点被改动后,只会影响一个方向上的其他节点。数据只会影响到下一个层级的节点,不会影响上一个层级的节点。
在react中Props 为只读的,而不是可更改的。这也就是数据更新不能直接通过 this.state 操作,想要更新,就需要通过 React 提供的专门的 this.setState() 方法来做。
单向数据流其实就是一种框架本身对数据流向的限制。保证数据的可控性。
JSX 是 JavaScript 的一种扩展,为函数调用和对象构造提供了语法糖,特别是 React.createElement()。JSX 看起来可能更像是模板引擎或 HTML,但它不是。JSX 生成 React 元素,同时允许你充分利用 JavaScript 的全部功能。
JSX 是编写 React 组件的极好方法,有以下优点:
改进的开发人员体验:代码更易读,因为它们更加形象,感谢类 XML 语法,从而可以更好地表示嵌套的声明式结构。
更具生产力的团队成员:非正式开发人员可以更容易地修改代码,因为 JSX 看起来像HTML,这正是他们所熟悉的。
更少的语法错误:开发人员需要敲入的代码量变得更少,这意味着他们犯错的概率降低了,能够减少重复性伤害。
在React组件复用与组合 中我们会提到,应当避免在底层的展示性组件中混入对于状态的管理,而应该将状态托管于某个高阶组件或者其他的状态容器中。利用函数式声明组件可以彻底保证不会在组件中进行状态操作。
同步的情况:
React控制之外的事件中调用setState是同步更新的。比如原生js绑定的事件,setTimeout/setInterval,ajax,promise.then内等 React 无法掌控的 APIs情况下,setState是同步更新state的
redux
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NQnPCpdb-1655685209074)(./pic/1.png)]
hooks
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GdD9566E-1655685209076)(./pic/2.png)]
简单分析
redux 的数据流程图画得比较简单,理解大概意思就好,毕竟它不是我要说的重点,和 hooks 的数据流程相比其实是大同小异。
从 hooks 数据流能大致看出来,我们设计好 store 后,通过对应的 hooks 函数生成每个 store 的 Provider 和 Context。我们把所有的单个 Provider 糅合为一个整体的 Providers,作为所有 UI 的父组件。
在任何一个子 UI 组件内部,通过 hooks 函数得到对应 store 的 state、dispatch。UI 组件内,通过主动调用 dispatch 发送 action,然后经过 store 的数据处理中心 reducer,就能触发相应的数据改变。这个数据流程和 redux 几乎一模一样。
相同点
统一 store 数据管理
支持以发送 action 来修改数据
支持 action 处理中心:reducer
异同点
hooks UI 层获取 store 和 dispatch 不需要用 HOC 依赖注入,而是用 useContext
redux 在 action 之后改变视图本质上还是 state 注入的方式修改的组件内部 state,而 hooks 则是一对一的数据触发
hooks 的 reducer 来自于 useReducer
hooks 还没有 middleware 的解决方案
react旧版生命周期包含三个过程
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-98ZYyCG7-1655685209079)(./pic/3.png)]
1、挂载过程
constructor()
componentWillMount()
componentDidMount()
2、更新过程
componentWillReceiveProps(nextProps)
shouldComponentUpdate(nextProps,nextState)
componentWillUpdate (nextProps,nextState)
render()
componentDidUpdate(prevProps,prevState)
3、卸载过程
componentWillUnmount()
其具体作用分别为:
1、constructor()
完成了React数据的初始化。
2、componentWillMount()
组件已经完成初始化数据,但是还未渲染DOM时执行的逻辑,主要用于服务端渲染。
3、componentDidMount()
组件第一次渲染完成时执行的逻辑,此时DOM节点已经生成了。
4、componentWillReceiveProps(nextProps)
接收父组件新的props时,重新渲染组件执行的逻辑。
5、shouldComponentUpdate(nextProps, nextState)
在setState以后,state发生变化,组件会进入重新渲染的流程时执行的逻辑。在这个生命周期中return false可以阻止组件的更新,主要用于性能优化。
6、componentWillUpdate(nextProps, nextState)
shouldComponentUpdate返回true以后,组件进入重新渲染的流程时执行的逻辑。
7、render()
页面渲染执行的逻辑,render函数把jsx编译为函数并生成虚拟dom,然后通过其diff算法比较更新前后的新旧DOM树,并渲染更改后的节点。
8、componentDidUpdate(prevProps, prevState)
重新渲染后执行的逻辑。
9、componentWillUnmount()
组件的卸载前执行的逻辑,比如进行“清除组件中所有的setTimeout、setInterval等计时器”或“移除所有组件中的监听器removeEventListener”等操作。
react16.4后使用了新的生命周期,使用getDerivedStateFromProps代替了旧的componentWillReceiveProps及componentWillMount。使用getSnapshotBeforeUpdate代替了旧的componentWillUpdate。
使用getDerivedStateFromProps(nextProps, prevState)的原因:
旧的React中componentWillReceiveProps方法是用来判断前后两个 props 是否相同,如果不同,则将新的 props 更新到相应的 state 上去。在这个过程中我们实际上是可以访问到当前props的,这样我们可能会对this.props做一些奇奇怪怪的操作,很可能会破坏 state 数据的单一数据源,导致组件状态变得不可预测。
而在 getDerivedStateFromProps 中禁止了组件去访问 this.props,强制让开发者去比较 nextProps 与 prevState 中的值,以确保当开发者用到 getDerivedStateFromProps 这个生命周期函数时,就是在根据当前的 props 来更新组件的 state,而不是去访问this.props并做其他一些让组件自身状态变得更加不可预测的事情。
使用getSnapshotBeforeUpdate(prevProps, prevState)的原因:
在 React 开启异步渲染模式后,在执行函数时读到的 DOM 元素状态并不总是渲染时相同,这就导致在 componentDidUpdate 中使用 componentWillUpdate 中读取到的 DOM 元素状态是不安全的,因为这时的值很有可能已经失效了。
而getSnapshotBeforeUpdate 会在最终的 render 之前被调用,也就是说在 getSnapshotBeforeUpdate 中读取到的 DOM 元素状态是可以保证与componentDidUpdate 中一致的。
这里再次提及下Redux的Store上的3个api,
store.getState() // 获得 store 上存储的所有状态
store.dispatch() // View 发出 Action 的唯一方法
store.subscribe() // 订阅 store上State 发生变化
虽然 Redux 应用全局就一个 Store 这样的直接导入依然有问题,因为在组件中直接导人 Store 是非常不利于组件复用的 。
所以,一个应用中,最好只有一个地方需要直接导人 Store ,这个位置当然应该是在调用最顶层 React 组件的位置 。 但是这样需要把Store,从顶层一层层往下传递,首先我们想到的就是props(父子组件通信方案)。这种方法有一个很大的缺陷,就是从上到下,所有的组件都要帮助传递这个 props 。
设想在一个嵌套多层的组件结构中,只有最里层的组件才需要使用 store ,但是为了把 store 从最外层传递到最里层,就要求中间所有的组件都需要增加对这个 store prop 的支持,即使根本不使用它,这无疑增加程序的耦合度,复杂度和不可维护性 。
React 提供了一个 叫 Context 的功能,能够完美地解决这个问题 。
什么是thunk?
thunk可以理解为函数的另一种称呼,但不是以前我们说的简单函数。Thunk代表的是一个被其他函数返回的函数。
那么它是怎么作用在redux上的呢?
Redux中有几个概念:”actions”,”action creators”,”reducer”,“middleware”
Actions就是普通对象,并且一定有一个type属性,除了这个,你可以在对象中自定义你想要描述该action的数据。
// 1. plain object
// 2. has a type
// 3. whatever else you want
{
type: "USER_LOGGED_IN",
username: "dave"
}
每次都要手写action对象会很麻烦,redux有一个“action creators”的概念来简化。
function userLoggedIn() {
return {
type: 'USER_LOGGED_IN',
username: 'dave'
};
}
你可以通过调用userLoggedIn函数来生成这个action,如果你在app中多次dispatch同样的action,使用action creators会大大简化工作。
如果我们希望action做些什么,就要把代码放进一个函数中去,这个函数就是thunk。
Action creators返回一个函数而不是一个action对象。
function getUser() {
return function() {
return axios.get('/current_user');
};
}
传递给thunk函数的就是两个参数:
Dispatch,如果它们需要,可以dispatch新的action。
getState,它们可以获取最新的state。
function logOutUser() {
return function(dispatch, getState) {
return axios.post('/logout').then(function() {
// pretend we declared an action creator
// called 'userLoggedOut', and now we can dispatch it
dispatch(userLoggedOut());
});
};
}
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import rootReducer from './reducers/index';
const store = createStore(
rootReducer,
applyMiddleware(thunk)
);
通常情况下,store 状态数据发生改变,会刷新页面这个问题是由于设计导致的,可能是组件定义时的问题。建议不要将页面中使用的组件都包装到一个大的容器组件中(如图1所示),而是将各相关的组件包装到一个较小的容器组件中(如图2所示):
图1 图2
图1中,当容器组件中订阅的 store 发生变化时,所有组件都会重新渲染。在图2中,由于将相关组件包装到了较小的容器组件中,当 container2 容器中订阅的 store 发生变化时,仅相关的组件重新渲染,其它组件不会导致重新渲染。
在遇到如下情况时,可以考虑使用 redux:
优点:
缺点:
switch case
分支判断,造成不小的负担类组件有 state,但 React 推荐组件间传递数据使用 props 自上而下流动,如果涉及到跨组件层级的通信,涉及到全局组件状态数据的共享及更新等,在各组件中都添加 props 来传递数据会显得非常繁琐,利用 redux 可以简化状态更新的繁琐度,提供统一的状态更新方式,使得状态更新可预测,也方便调试与维护。
此方法仅作为性能优化的方式而存在,根据 shouldComponentUpdate()
的返回值,判断 React 组件的输出是否受当前 state 或 props 更改的影响,返回 false
以告知 React 可以跳过更新。
示例:
这是一个组件的子树。每个节点中,SCU
代表 shouldComponentUpdate
返回的值,而 vDOMEq
代表返回的 React 元素是否相同。最后,圆圈的颜色代表了该组件是否需要被调停。
节点 C2 的 shouldComponentUpdate
返回了 false
,React 因而不会去渲染 C2,也因此 C4 和 C5 的 shouldComponentUpdate
不会被调用到。
对于 C1 和 C3,shouldComponentUpdate
返回了 true
,所以 React 需要继续向下查询子节点。这里 C6 的 shouldComponentUpdate
返回了 true
,同时由于渲染的元素与之前的不同使得 React 更新了该 DOM。
最后一个有趣的例子是 C8。React 需要渲染这个组件,但是由于其返回的 React 元素和之前渲染的相同,所以不需要更新 DOM。
显而易见,你看到 React 只改变了 C6 的 DOM。对于 C8,通过对比了渲染的 React 元素跳过了渲染。而对于 C2 的子节点和 C7,由于 shouldComponentUpdate
使得 render
并没有被调用。因此它们也不需要对比元素了。
ref 是 React 提供的用来操纵 React 组件实例或者操作 DOM 元素的技术。
适合使用 ref 的几种情况:
示例1:
class CustomTextInput extends React.Component {
constructor(props) {
super(props);
// 创建一个 ref 来存储 textInput 的 DOM 元素
this.textInput = React.createRef();
this.focusTextInput = this.focusTextInput.bind(this);
}
focusTextInput() {
// 直接使用原生 API 使 text 输入框获得焦点
// 注意:我们通过 "current" 来访问 DOM 节点
this.textInput.current.focus();
}
render() {
// 告诉 React 我们想把 ref 关联到
// 构造器里创建的 `textInput` 上
return (
);
}
}
这个例子是为 DOM 元素添加 ref
,使用 ref
去存储 DOM 节点的引用,React 会在组件挂载时给 ref
对象的 current
属性传入 DOM 元素,并在组件卸载时传入 null
值。ref
会在 componentDidMount
或 componentDidUpdate
生命周期钩子触发前更新。
示例2:
class AutoFocusTextInput extends React.Component {
constructor(props) {
super(props);
this.textInput = React.createRef();
}
componentDidMount() {
this.textInput.current.focusTextInput();
}
render() {
return (
);
}
}
这个示例是包装上面的 CustomTextInput
,来模拟它挂载之后立即被点击的操作,我们可以使用 ref 来获取这个自定义的 input 组件并手动调用它的 focusTextInput
方法。
Redux工作流程:
由上图我们可以看到,redux 数据流向大致步骤(以按钮点击修改状态为例):
$0
元。state
和接收到的 action 经 reducer 函数处理,并将返回值保存为新的 state
redux 使用流程:
如果我们要在 View 中实现 store 中的状态更新,则需要 dispatch(action),这个 dispatch()
方法是从创建的 store 中解构出来的,在 store 中除了 dispatch 外,还可以解构诸如 getState、subscribe 等属性。
对于框架之间我们并不是特别在意谁强谁弱, 而是要搞清楚框架真正值得我们学习的点:
像React框架, 它在 架构上融合了数据驱动视图、组件化、函数式编程、面向对象、Fiber 等经典设计“哲学”, 在底层技术选型上涉及了 JSX、虚拟 DOM等经典解决方案,在周边生态上至少涵盖了状态管理和前端路由两 大领域的最佳实践。此外,它还自建了状态管理机制与事件系统,创造性地在前端框架中引入了 Hooks 思 想… React 十年如一日的稳定输出背后,有太多值得我们去吸收和借鉴的东西.
DOM
更新); 减少手动操作DOM
操作(不用再像以前写jQuery
那样,先获取DOM
元素,再设置属性)在结构和样式方面,小程序提供了一些常用的标签与控件,比如:
view,小程序主要的布局元素,类似于html标签的div,你也完全可以像控制div那样去控制view。
scroll-view,你要滚动内容的话,没必要用view去做overflow,scroll-view提供了更为强大的功能,通过参数的调整,你可以控制滚动方向,触发的事件等等
配置文件app.json平级的还有一个app.js文件,是小程序的脚本代码。我们可以在这个文件中监听并处理小程序的生命周期函数、声明全局变量,在每个page目录里的js做当前页面的业务操作。但是小程序的页面的脚本逻辑是在JsCore中运行,JsCore是一个没有窗口对象的环境,所以不能在脚本中使用window,也无法在脚本中操作组件,所以我们常用的zepto/jquery 等类库也是无法使用的。
另一个app.wxss文件,这个是全局的样式,所有的页面都会调用到,每个项目目录下面的wxss是局部样式文件,不会和其他目录产生污染,可以放心使用样式名。
他提供的WXSS(WeiXin Style Sheets)是一套样式语言,具有 CSS 大部分特性,可以看作一套简化版的css。
同时为了更适合开发微信小程序,还对 CSS 进行了扩充以及修改,直接帮我们把适配的一部分工作都做了,比如他的rpx(responsive pixel),可以根据屏幕宽度进行自适应,规定屏幕宽为750rpx。如在 iPhone6 上,屏幕宽度为375px,共有750个物理像素,则750rpx = 375px = 750物理像素,1rpx = 0.5px = 1物理像素。
在调用微信生态系统功能时,微信小程序提供了相应的api,比如你要修改一个头像,可以使用wx.chooseImage等
以微信小程序为例,可以分成容器组件、基础组件、表单组件、媒体组件、开放能力组件等
小程序开发基于html、css、javascript,与web开发一样具有跨平台特性,一次开发即可在安卓和iOS等平台访问,但与普通web开发不同,小程序运行环境并不是浏览器,而是依附于各自的软件App,如微信小程序必须在微信中访问,支付宝小程序必须在支付宝中访问等,小程序的开发流程也有所不同,需要经过申请小程序帐号、安装小程序开发者工具、配置项目、开发、调试、上线发布等过程方可完成
如果弹窗被别的内容覆盖,且设置很大的z-index也无法解决,这种情况多半是被一些如map
、video
、textarea
、canvas
等原生组件遮盖,因为原生组件层级高于前端组件,我们可以使用cover-view
组件解决
onReady 生命周期函数–监听页面初次渲染完成
onShow 生命周期函数–监听页面显示
onHide 生命周期函数–监听页面隐藏
onUnload 生命周期函数–监听页面卸载
onPullDownRefresh 页面相关事件处理函数–监听用户下拉动作
onReachBottom 页面上拉触底事件的处理函数
onShareAppMessage 用户点击右上角转发
onPageScroll 页面滚动触发事件的处理函数
onTabItemTap 当前是 tab 页时,点击 tab 时触发
通过组件navigator跳转,设置url属性指定跳转的路径,设置open-type属性指定跳转的类型(可选),open-type的属性有 redirect, switchTab, navigateBack
// redirect 对应 API 中的 wx.redirect 方法
在当前页打开
// navigator 组件默认的 open-type 为 navigate
跳转到新页面
// switchTab 对应 API 中的 wx.switchTab 方法
切换 Tab
// reLanch 对应 API 中的 wx.reLanch 方法
//关闭所有页面,打开到应用内的某个页面
// navigateBack 对应 API 中的 wx.navigateBack 方法
关闭当前页面,返回上一级页面或多级页面
通过api跳转,wx.navigateTo() , wx.navigateBack(), wx.redirectTo() , wx.switchTab(), wx.reLanch()
wx.navigateTo({
url: 'page/home/home?user_id=1' // 页面 A
})
wx.navigateTo({
url: 'page/detail/detail?product_id=2' // 页面 B
})
// 跳转到页面 A
wx.navigateBack({
delta: 2 //返回指定页面
})
// 关闭当前页面,跳转到应用内的某个页面。
wx.redirectTo({
url: 'page/home/home?user_id=111'
})
// 跳转到tabBar页面(在app.json中注册过的tabBar页面),同时关闭其他非tabBar页面。
wx.switchTab({
url: 'page/index/index'
})
// 关闭所有页面,打开到应用内的某个页面。
wx.reLanch({
url: 'page/home/home?user_id=111'
})
遇到的如下:
new Date跨平台兼容性问题
在Andriod使用new Date(“2018-05-30 00:00:00”)木有问题,但是在ios下面识别不出来。
因为IOS下面不能识别这种格式,需要用2018/05/30 00:00:00格式。可以使用正则表达式对做字符串替换,将短横替换为斜杠。var iosDate= date.replace(/-/g, ‘/’);。
wx.getUserInfo()接口更改问题
微信小程序最近被吐槽最多的一个更改,就是用户使用wx.getUserInfo(开发和体验版)时不会弹出授权,正式版不受影响。现在授权方式是需要引导用户点击一个授权按钮,然后再弹出授权。
开发uni-app遇到的坑
小程序时必须要写header:{“Content-Type”: “multipart/form-data”}, h5是必须省略
uni-app h5端 ios只能加载https的图片
需要在methods同级下加一个 :
options: { styleIsolation: ‘shared’ },
在开发中我们接口上传图片是post请求 无法传递一个数组 解决方法如下
我们可以把数据转换成字符串 然后拼接到请求地址后后面
拼接字符串格式:image[]=arr[0]&image[]=arr[1]
imgURlClick(imgArr){
return '?images[]='+imgArr.join('&images[]=')
}
准备一个button组件, 将 button 组件 open-type
的值设置为 getPhoneNumber
,当用户点击并同意之后,可以通过 bindgetphonenumber
事件回调获取到动态令牌code
;
<button open-type="getPhoneNumber" bindgetphonenumber="getPhoneNumber">button>
Page({
getPhoneNumber (e) {
console.log(e.detail.code)
}
})
接着把code
传到开发者后台,并在开发者后台调用微信后台提供的 phonenumber.getPhoneNumber 接口,消费code
来换取用户手机号。每个code
有效期为5分钟,且只能消费一次。
getPhoneNumber: function (e) {
var that = this;
console.log(e.detail.errMsg == "getPhoneNumber:ok");
if (e.detail.errMsg == "getPhoneNumber:ok") {
wx.request({
url: 'http://localhost/index/users/decodePhone',
data: {
encryptedData: e.detail.encryptedData,
iv: e.detail.iv,
sessionKey: that.data.session_key,
uid: "",
},
method: "post",
success: function (res) {
console.log(res);
}
})
}
}
注:getPhoneNumber
返回的 code
与 wx.login
返回的 code
作用是不一样的,不能混用.
注:从基础库 2.21.2 开始,对获取手机号的接口进行了安全升级, 需要用户主动触发才能发起获取手机号接口,所以该功能不由 API 来调用,需用 button 组件的点击来触发。另外,新版本接口不再需要提前调用wx.login
进行登录.
登陆流程:
首次登录
调用小程序api接口 wx.login()
获取 临时登录凭证code ,这个code是有过期时间的.
将这个code
回传到开发者服务器(就是请求开发者服务器的登录接口,通过凭证进而换取用户登录态信息,包括用户的唯一标识(openid)及本次登录的会话密钥(session_key)等)
拿到开发者服务器传回来的会话密钥(session_key)之后,前端需要保存起来.
wx.setStorageSync('sessionKey', 'value')
再次登录的时候,就要判断存储的session_key是否过期了
wx.getStorageSync('sessionKey')
wx.checkSession()
来判断登录态是否过期,回调成功说明当前 session_key 未过期,回调失败说明 session_key 已过期。登录态过期后前端需要再调用 wx.login()获取新的用户的code,然后再向开发者服务器发起登录请求.参考官网: 小程序登陆流程 登陆态过期检测
当小程序发布新的版本后,用户如果之前访问过该小程序,通过已打开的小程序进入(未手动删除),则会弹出提示,提醒用户更新新的版本。用户点击确定就可以自动重启更新,点击取消则关闭弹窗,不再更新.
核心步骤:
打开小程序, 检查小程序是否有新版本发布
updateManager.onCheckForUpdate(function (res) {})
小程序有新版本,则静默下载新版本,做好更新准备
updateManager.onUpdateReady(function () {})
新的版本已经下载好,调用 applyUpdate
应用新版本并重启小程序
updateManager.applyUpdate()
更新版本的模拟测试
微信开发者工具上可以通过「编译模式」下的「下次编译模拟更新」开关来调试.
点击编译模式设置下拉列表,然后点击“添加编译模式”,在自定义编译条件弹窗界面,点击下次编译时模拟更新,然后点击确定,重新编译就可以了.
注: 需要注意的是,这种方式模拟更新一次之后就失效了,后边再测试仍需要对这种编译模式进行重新设置才可以.
核心代码如下:
App({
onLaunch: function(options) {
this.autoUpdate()
},
autoUpdate:function(){
var self=this
// 获取小程序更新机制兼容
if (wx.canIUse('getUpdateManager')) {
const updateManager = wx.getUpdateManager()
//1. 检查小程序是否有新版本发布
updateManager.onCheckForUpdate(function (res) {
// 请求完新版本信息的回调
if (res.hasUpdate) {
//2. 小程序有新版本,则静默下载新版本,做好更新准备
updateManager.onUpdateReady(function () {
wx.showModal({
title: '更新提示',
content: '新版本已经准备好,是否重启应用?',
success: function (res) {
if (res.confirm) {
//3. 新的版本已经下载好,调用 applyUpdate 应用新版本并重启
updateManager.applyUpdate()
} else if (res.cancel) {
//不应用
}
}
})
})
updateManager.onUpdateFailed(function () {
// 新的版本下载失败
wx.showModal({
title: '已经有新版本了哟~',
content: '新版本已经上线啦~,请您删除当前小程序,重新搜索打开哟~',
})
})
}
})
} else {
// 如果希望用户在最新版本的客户端上体验您的小程序,可以这样子提示
wx.showModal({
title: '提示',
content: '当前微信版本过低,无法使用该功能,请升级到最新微信版本后重试。'
})
}
}
})
webview 指向网页的链接。可打开关联的公众号的文章,其它网页需登录小程序管理后台配置业务域名。
具体实现步骤:
登陆小程序管理后台, 配置服务器域名( h5页面所在的域名 )
在小程序里面嵌入h5
<navigator url="/page/navigate/navigate" hover-class="navigator-hover">跳转到新页面navigator>
<web-view src="{{url}}" bindmessage="getMessage">web-view> block>
注: 实际开发中在h5页面中有可能需要向小程序发送消息, 实现h5页面和小程序页面的通信
需要使用postMessage向小程序发送消息, 在h5中postMessage 注意,key必须叫做data,否则取不到.
小程序的生命周期函数大体分为三类:
小程序应用的生命周期
属性 | 说明 |
---|---|
onLaunch | 监听小程序初始化, 全局只触发一次 |
onShow | 监听小程序启动或切前台。 |
onHide | 监听小程序切后台。 |
参考官网: 应用的生命周期
小程序页面的生命周期
属性 | 说明 |
---|---|
onLoad | 监听页面加载, 获取其他页面传过来的参数, 发起网络请求 |
onShow | 监听页面显示 |
onReady | 监听页面初次渲染完成 |
onHide | 监听页面隐藏 |
onUnload | 监听页面卸载 |
参考官网: 页面的生命周期
小程序组件的生命周期
定义段 | 描述 |
---|---|
created | 在组件实例刚刚被创建时执行,注意此时不能调用 setData ) |
attached | 在组件实例进入页面节点树时执行) |
ready | 在组件布局完成后执行) |
moved | 在组件实例被移动到节点树另一个位置时执行) |
detached | 在组件实例被从页面节点树移除时执行) |
主要配置5个核心文件
1. mode:通过选择 `development`, `production` 或 `none` 之中的一个,来设置 `mode` 参数,你可以启用 webpack 内置在相应环境下的优化。
2. entry:**入口起点(entry point)** 指示 webpack 应该使用哪个模块,来作为构建其内部 [依赖图(dependency graph)](https://webpack.docschina.org/concepts/dependency-graph/) 的开始。进入入口起点后,webpack 会找出有哪些模块和库是入口起点(直接和间接)依赖的。
3. output:**output** 属性告诉 webpack 在哪里输出它所创建的 *bundle*,以及如何命名这些文件。主要输出文件的默认值是 `./dist/main.js`,其他生成文件默认放置在 `./dist` 文件夹中。
4. loader:webpack 只能理解 JavaScript 和 JSON 文件,这是 webpack 开箱可用的自带能力。**loader** 让 webpack 能够去处理其他类型的文件,并将它们转换为有效 [模块](https://webpack.docschina.org/concepts/modules),以供应用程序使用,以及被添加到依赖图中。
5. plugin:loader 用于转换某些类型的模块,而插件则可以用于执行范围更广的任务。包括:打包优化,资源管理,注入环境变量。
初始化参数
:解析webpack配置参数,合并shell传入和webpack.config.js文件配置的参数,形成最后的配置结果;
开始编译
:上一步得到的参数初始化compiler对象,注册所有配置的插件,插件 监听webpack构建生命周期的事件节点,做出相应的反应,执行对象的run方法开始执行编译;
确定入口
:从配置的entry入口,开始解析文件构建AST语法树,找出依赖,递归下去;
编译模块
:递归中根据文件类型和loader配置,调用所有配置的loader对文件进行转换,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理;
完成模块编译并输出
:递归完事后,得到每个文件结果,包含每个模块以及他们之间的依赖关系,根据entry或分包配置生成代码块chunk;
输出完成
:输出所有的chunk到文件系统;
开放式题目
打包优化的目的
1、优化项目启动速度,和性能
2、必要的清理数据
3、性能优化的主要方向
cdn加载
-压缩js
减少项目在首次加载的时长(首屏加载优化)
4、目前的解决方向
cdn加载不比多说,就是改为引入外部js路径
首屏加载优化方面主要其实就两点
第一:
尽可能的减少首次加载的文件体积,和进行分布加载
第二:
首屏加载最好的解决方案就是ssr(服务端渲染),还利于seo
但是一般情况下没太多人选择ssr,因为只要不需要seo,ssr更多的是增加了项目开销和技术难度的。
1、路由懒加载
在 Webpack 中,我们可以使用动态 import语法来定义代码分块点 (split point): import(’./Fee.vue’) // 返回 Promise如果您使用的是 Babel,你将需要添加 syntax-dynamic-import 插件,才能使 Babel 可以正确地解析语法。
结合这两者,这就是如何定义一个能够被 Webpack 自动代码分割的异步组件。
const Fee = () => import(‘./Fee.vue’)
在路由配置中什么都不需要改变,只需要像往常一样使用 Foo:
const router = new VueRouter({
routes: [
{ path: '/fee', component: Fee }
]
})
2、服务器和webpack打包同时配置Gzip
Gzip是GNU zip的缩写,顾名思义是一种压缩技术。它将浏览器请求的文件先在服务器端进行压缩,然后传递给浏览器,浏览器解压之后再进行页面的解析工作。在服务端开启Gzip支持后,我们前端需要提供资源压缩包,通过Compression-Webpack-Plugin插件build提供压缩
需要后端配置,这里提供nginx方式:
http:{
gzip on; #开启或关闭gzip on off
gzip_disable "msie6"; #不使用gzip IE6
gzip_min_length 100k; #gzip压缩最小文件大小,超出进行压缩(自行调节)
gzip_buffers 4 16k; #buffer 不用修改
gzip_comp_level 8; #压缩级别:1-10,数字越大压缩的越好,时间也越长
gzip_types text/plain application/x-javascript text/css application/xml text/javascript application/x-httpd-php image/jpeg image/gif image/png; # 压缩文件类型
}
// 安装插件
$ cnpm i --save-dev compression-webpack-plugin
// 在vue-config.js 中加入
const CompressionWebpackPlugin = require('compression-webpack-plugin');
const productionGzipExtensions = [
"js",
"css",
"svg",
"woff",
"ttf",
"json",
"html"
];
const isProduction = process.env.NODE_ENV === 'production';
.....
module.exports = {
....
// 配置webpack
configureWebpack: config => {
if (isProduction) {
// 开启gzip压缩
config.plugins.push(new CompressionWebpackPlugin({
algorithm: 'gzip',
test: /\.js$|\.html$|\.json$|\.css/,
threshold: 10240,
minRatio: 0.8
}))
}
}
}
3、优化打包chunk-vendor.js文件体积过大
当我们运行项目并且打包的时候,会发现chunk-vendors.js这个文件非常大,那是因为webpack将所有的依赖全都压缩到了这个文件里面,这时我们可以将其拆分,将所有的依赖都打包成单独的js。
// 在vue-config.js 中加入
.....
module.exports = {
....
// 配置webpack
configureWebpack: config => {
if (isProduction) {
// 开启分离js
config.optimization = {
runtimeChunk: 'single',
splitChunks: {
chunks: 'all',
maxInitialRequests: Infinity,
minSize: 20000,
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name (module) {
// get the name. E.g. node_modules/packageName/not/this/part.js
// or node_modules/packageName
const packageName = module.context.match(/[\\/]node_modules[\\/](.*?)([\\/]|$)/)[1]
// npm package names are URL-safe, but some servers don't like @ symbols
return `npm.${packageName.replace('@', '')}`
}
}
}
}
};
}
}
}
// 至此,你会发现原先的vender文件没有了,同时多了好几个依赖的js文件
4、启用CDN加速
用Gzip已把文件的大小减少了三分之二了,但这个还是得不到满足。那我们就把那些不太可能改动的代码或者库分离出来,继续减小单个chunk-vendors,然后通过CDN加载进行加速加载资源。
// 修改vue.config.js 分离不常用代码库
// 如果不配置webpack也可直接在index.html引入
module.exports = {
configureWebpack: config => {
if (isProduction) {
config.externals = {
'vue': 'Vue',
'vue-router': 'VueRouter',
'moment': 'moment'
}
}
}
}
// 在public文件夹的index.html 加载
<script src="https://cdn.bootcss.com/vue/2.5.17-beta.0/vue.runtime.min.js">script>
<script src="https://cdn.bootcss.com/vue-router/3.0.1/vue-router.min.js">script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.22.2/moment.min.js">script>
5、完整vue.config.js代码
const path = require('path')
// 在vue-config.js 中加入
// 开启gzip压缩
const CompressionWebpackPlugin = require('compression-webpack-plugin');
// 判断开发环境
const isProduction = process.env.NODE_ENV === 'production';
const resolve = dir => {
return path.join(__dirname, dir)
}
// 项目部署基础
// 默认情况下,我们假设你的应用将被部署在域的根目录下,
// 例如:https://www.my-app.com/
// 默认:'/'
// 如果您的应用程序部署在子路径中,则需要在这指定子路径
// 例如:https://www.foobar.com/my-app/
// 需要将它改为'/my-app/'
// iview-admin线上演示打包路径: https://file.iviewui.com/admin-dist/
const BASE_URL = process.env.NODE_ENV === 'production'
? '/'
: '/'
module.exports = {
//webpack配置
configureWebpack:config => {
// 开启gzip压缩
if (isProduction) {
config.plugins.push(new CompressionWebpackPlugin({
algorithm: 'gzip',
test: /\.js$|\.html$|\.json$|\.css/,
threshold: 10240,
minRatio: 0.8
}));
// 开启分离js
config.optimization = {
runtimeChunk: 'single',
splitChunks: {
chunks: 'all',
maxInitialRequests: Infinity,
minSize: 20000,
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name (module) {
// get the name. E.g. node_modules/packageName/not/this/part.js
// or node_modules/packageName
const packageName = module.context.match(/[\\/]node_modules[\\/](.*?)([\\/]|$)/)[1]
// npm package names are URL-safe, but some servers don't like @ symbols
return `npm.${packageName.replace('@', '')}`
}
}
}
}
};
// 取消webpack警告的性能提示
config.performance = {
hints:'warning',
//入口起点的最大体积
maxEntrypointSize: 50000000,
//生成文件的最大体积
maxAssetSize: 30000000,
//只给出 js 文件的性能提示
assetFilter: function(assetFilename) {
return assetFilename.endsWith('.js');
}
}
}
},
// Project deployment base
// By default we assume your app will be deployed at the root of a domain,
// e.g. https://www.my-app.com/
// If your app is deployed at a sub-path, you will need to specify that
// sub-path here. For example, if your app is deployed at
// https://www.foobar.com/my-app/
// then change this to '/my-app/'
publicPath: BASE_URL,
// tweak internal webpack configuration.
// see https://github.com/vuejs/vue-cli/blob/dev/docs/webpack.md
devServer: {
host: 'localhost',
port: 8080, // 端口号
hotOnly: false,
https: false, // https:{type:Boolean}
open: true, //配置自动启动浏览器
proxy:null // 配置跨域处理,只有一个代理
},
// 如果你不需要使用eslint,把lintOnSave设为false即可
lintOnSave: true,
css:{
loaderOptions:{
less:{
javascriptEnabled:true
}
},
extract: true,// 是否使用css分离插件 ExtractTextPlugin
sourceMap: false,// 开启 CSS source maps
modules: false// 启用 CSS modules for all css / pre-processor files.
},
chainWebpack: config => {
config.resolve.alias
.set('@', resolve('src')) // key,value自行定义,比如.set('@@', resolve('src/components'))
.set('@c', resolve('src/components'))
},
// 打包时不生成.map文件
productionSourceMap: false
// 这里写你调用接口的基础路径,来解决跨域,如果设置了代理,那你本地开发环境的axios的baseUrl要写为 '' ,即空字符串
// devServer: {
// proxy: 'localhost:3000'
// }
}
1、HtmlWebpackPlugin
包名:html-webpack-plugin
该插件将为你生成一个 HTML 文件, 在 body 中使用 script 标签引入你所有 webpack 生成的 bundle。
官方传送门
2、CleanWebpackPlugin
包名:clean-webpack-plugin
用于在打包前清理上一次项目生成的
bundle
文件。默认情况下,此插件将删除webpack的Output.Path
目录中的所有文件,以及在每次成功重建后所有未使用的webpack资源(assets)。如果使用的webpack版本是 4 以上的,默认 清理下的文件。
/dist/
官方传送门
3、MiniCssExtractPlugin
包名:mini-css-extract-plugin
将 css 成生文件,而非内联 。该插件的主要是为了抽离 css 样式,防止将样式打包在 js 中引起页面样式加载错乱的现象。支持按需加载 css 和 sourceMap
官方传送门
4、HotModuleReplacementPlugin
包名:HotModuleReplacementPlugin
由
webpack 自带
。在对CSS / JS文件进行修改时,可以立即更新浏览器(部分刷新)。依赖于webpack-dev-server
官方传送门
5、ImageminPlugin
包名:imagemin-webpack-plugin
批量压缩图片。
官方传送门
6、PurifyCSSPlugin
包名:purifycss-webpack
从CSS中删除未使用的选择器(删除多余的 css 代码)。和
extract-text-webpack-plugin
一起使用
官方传送门
7、OptimizeCSSAssetsPlugin
包名:optimize-css-assets-webpack-plugin
压缩css文件。
官方传送门
8、CssMinimizerPlugin
包名: css-minimizer-webpack-plugin
压缩css文件。**用于 webpack 5 **。
官方传送门
9、UglifyJsPlugin
包名:uglifyjs-webpack-plugin
压缩js文件。
官方传送门
10、ProvidePlugin
包名:ProvidePlugin
由
webpack 自带
。自动加载模块,而不必在任何地方import
或require
它们。例如:new webpack.ProvidePlugin({$: ‘jquery’,React: ‘react’})
官方传送门
11、SplitChunksPlugin
包名:-
在
webapck配置
中的optimization
字段中配置。cacheGroups
是关键,将文件提取打包成公共模块,像 抽取node_modules
里的文件。
官方传送门
12、CompressionPlugin
包名:compression-webpack-plugin
启用
gzip
压缩。
官方传送门
13、CopyWebpackPlugin
包名: copy-webpack-plugin
将已存在的单个文件或整个目录复制到构建目录中。多用于 将静态文件 因在打包时 webpack 并不会帮我们拷贝到 dist 目录 拷贝到 dist 目录
官方传送门
14、DefinePlugin
包名:DefinePlugin
由
webpack 自带
。设置全局变量。如:new webpack.DefinePlugin({‘process.env.NODE_ENV’: JSON.stringify(‘production’)})
官方传送门
1、DllPlugin
包名:DllPlugin
由
webpack 自带
。dllplugin
和dllreferenceplugin
提供了拆分捆绑包的方法,这些方式可以大大提高构建时间性能。
官方传送门
2、DLLReferencePlugin
包名:DLLReferencePlugin
由
webpack 自带
。它引用了仅需要预先构建的依赖项的DLL-only-Bundle
。
官方传送门
3、ParallelUglifyPlugin
包名:webpack-parallel-uglify-plugin
开启多个子进程,把对多个文件压缩的工作分别给多个子进程去完成。减少构建时间。
官方传送门
4、HappyPack
包名:happypack
让 webpack 把任务分解给多个子进程去并发的执行,子进程处理完后再把结果发送给主进程。提升 构建 速度
官方传送门
开放式题目
Gulp强调的是前端开发的工作流程。我们可以通过配置一系列的task,定义task处理的事务(例如文件压缩合并、雪碧图、启动server、版本控制等),然后定义执行顺序,来让Gulp执行这些task,从而构建项目的整个前端开发流程。通俗一点来说,“Gulp就像是一个产品的流水线,整个产品从无到有,都要受流水线的控制,在流水线上我们可以对产品进行管理。”
Webpack是一个前端模块化方案,更侧重模块打包。我们可以把开发中的所有资源(图片、js文件、css文件等)都看成模块,通过loader(加载器)和plugins(插件)对资源进行处理,打包成符合生产环境部署的前端资源。 Webpack就是需要通过其配置文件(Webpack.config.js)中 entry 配置的一个入口文件(JS文件),然后在解析过程中,发现其他的模块,如scss等文件,再调用配置的loader或者插件对相关文件进行解析处理。
虽然Gulp 和 Webpack都是前端自动化构建工具,但看2者的定位就知道不是对等的。Gulp严格上讲,模块化不是他强调的东西,旨在规范前端开发流程。Webpack更明显的强调模块化开发,而那些文件压缩合并、预处理等功能,不过是他附带的功能。
ts是一种基于静态类型检查的强类型语言那当然js就是一种弱类型语言
由于我们在浏览器中不能编译ts语言所以我们需要安装编译器
安装下载
使用npm install -g typescript进行下载
使用tsc进行检测是否安装成功
在文件中间一个js文件,然后在文件中见一个ts文件,但是直接去使用的时候会报错,需要在终端中使用tsc ./js/hello.ts,这样之后可以在当前的就是文件中自动编译一个同名js文件。
let num:number=20
console.log(num)
console.log("str")
ts支持的数据类型
数组
let arr:number[]=[1,2,3,4,5]
//将let定义为一个数组,每一项都是number
let arr:number[]=[1,2,3,4,5,"str"] //报错不能将类型string分配给类型number
let arr1:Array<number|string>=[1,2,3,4,5,"str"]//这样写就不会报错
//通过给范型添加多类型,让数组支持多种数据格式
元组Tuple
规定元素类型和规定元素数量和顺序的数组
特点:不要越界访问
定义的是什么类型写的就是什么类型,可以使用数组的下标取值,但是如果使用数组的push方法的话,虽然输出的数组中有,但是取值的话会报错可以打印出来但不建议这样写,这就说了元组的一个越界问题
let tu:[number,string]
tu=[1,"str"]
枚举
1.有限的可列举出来的命名和索引对应的类型
2枚举类型的优势:语义化可维护性
3原理:反向映射,互相指向
//定义了一个枚举
enum user{
admin,
guest,
develoment,
pm
}
console.log(user)
//使用user类型来定义枚举变量any
代表任意类型:
let t:any=10
t="str"
t=true
接口
跟另一个事物之间的一个媒介
interface userInfo{
name:string;
age:number;
address?:string//问号代表该属性可添加可不添加
}
function getUserInfo(u:userInfo){
console.log(u.name) //张三
}
let user1 = {name:"张三",age:24,address:"北京"}
getUserInfo(user1)
使用{useName:string, password:number}约束传入的userData参数(并将password置为可选参数)
class User{
useName:string;
password?:number|undefined;
//使用{useName:string, password?:number|undefined}约束传入的userData参数
constructor(userData:{useName:string, password?:number|undefined}){
this.useName=userData.useName;
if(userData.password)
this.password=userData.password;
}
}
let u1=new User( {useName:"小猪猪", password:12233} );
let u2=new User( {useName:"大猪猪"} )
console.log(u1);
console.log(u2);
泛型(Generics)是指在定义函数、接口或类的时候,不预先指定具体的类型,而在使用的时候再指定类型的一种特性。
通俗理解:泛型就是解决类 接口 方法的复用性、以及对不特定数据类型的支持
function getData(value:string):string{
return value //只能返回string类型的数据
}
//想要同时返回string和number类型
//1.通常: 代码冗余
function getData1(value:string):string{
return value
}
function getData2(value:number):number{
return value
}
//2.用any(可以解决,但是放弃了类型检查)
function getData(value:any):any{
return value //什么类型都可以
}
//传入什么返回什么。比如传入number类型,必须返回number类型。传入string必须返回string类型。用any就可以不一致。
//泛型,可以支持不特定的数据类型
//要求:传入参数和返回的参数一致
//这里的T指泛型,也可以用任意字母取代,但是前后要一致
function getData<T>(value:T):T{
return value
}
function getData<T>(value:T):T{
return 'xxxx' //错误写法。不能将任何任性分配给T
}
//调用
getData<number>(123); //123
getData<number>('xxx'); //错误写法
//可以调用的时候传一个泛型,返回的时候返回其他的类型(用的不多)
function getData<T>(value:T):any{
return 'xxx'
}
//调用
getData<number>(123); //xxx
getData<string>('xxx'); //xxx
//定义泛型是什么类型,就要传入什么类型的参数
ts中函数参数的类型定义
函数的参数可能是一个,也可能是多个,有可能是一个变量,一个对象,一个函数,一个数组等等。
1.函数的参数为单个或多个单一变量的类型定义
function fntA(one, two, three) {
// 参数 "two" 隐式具有 "any" 类型,但可以从用法中推断出更好的类型。
return one + two + three
}
const aResult = fntA(1, '3', true)
修改后:
function fntA(one: number, two: string, three: boolean) {
return one + two + three
}
const aResult1 = fntA(1, '3', true)
// 如果函数的参数为单个或者多个变量的时候,只需要为这些参数进行静态类型下的基础类型定义就行
2.函数的参数为数组的类型定义
function fntB(arr) {
//参数 "arr" 隐式具有 "any" 类型,但可以从用法中推断出更好的类型。
return arr[0]
}
const bResult = fntB([1, 3, 5])
修改后:
function fntB(arr: number[]) {
return arr[0]
}
const bResult1 = fntB([1, 3, 5])
// 如果参数是数组时,只需要为这些变量进行对象类型下的数组类型定义
3.函数的参数为对象的类型定义
function fntC({ one, two }) {
return one + two
}
const cResult = fntC({ one: 6, two: 10 })
修改后:
function fntC({ one, two }: { one: number, two: number }) {
return one + two
}
const cResult1 = fntC({ one: 6, two: 10 })
// 如果参数是对象,只需要为这些变量进行对象类型下的对象类型定义
4.函数的参数为函数的类型定义
function fntD(callback) {
//参数 "callback" 隐式具有 "any" 类型,但可以从用法中推断出更好的类型
callback(true)
}
function callback(bl: boolean): boolean {
console.log(bl)
return bl
}
const dResult = fntD(callback)
修改后:
function fntD(callback: (bl: boolean) => boolean) {
callback(true)
}
function callback(bl: boolean): boolean {
console.log(bl)
return bl
}
const dResult = fntD(callback)
// 如果参数是函数,只需要为参数进行对象类型下的函数类型定义即可
ts中函数返回值的类型定义
当函数有返回值时,根据返回值的类型在相应的函数位置进行静态类型定义即可
返回数字:
function getTotal2(one: number, two: number): number {
return one + two;
}
const total2 = getTotal(1, 2);
// 返回值为数字类型
返回布尔值
function getTotal2(one: number, two: number): boolean {
return Boolean(one + two);
}
const total2 = getTotal(1, 2);
// 返回值为布尔类型
返回字符串
function getTotal2(one: string, two: string): string{
return Bone + two;
}
const total2 = getTotal('1', '2');
// 返回值为字符串
返回对象
function getObj(name: string, age: number): { name: string, age: number } {
return {name,age}
}
getObj('小红',16)
// 返回值为对象
返回数组
function getArr(arr: number[]) :number[]{
let newArr = [...arr]
return newArr
}
getArr([1,2,3,4])
// 返回值为数组
函数返回值为underfinde,仅仅时为了在内部实现某个功能,我们就可以给他一个类型注解void,代表没有任何返回值,
function sayName() {
console.log('hello,world')
}
修改后:
function sayName1(): void {
console.log('无返回值')
}
当函数没有返回值时
// 因为总是抛出异常,所以 error 将不会有返回值
// never 类型表示永远不会有值的一种类型
function error(message: string): never {
throw new Error(message);
}
Rx 中强大的地方在于两处
在 Rx 中,我们先预装好管道,通过管道流通数据 。这些管道的来源多种, create ,from, fromEvent, of …, 通过操作符将管道 拼接,合并,映射…形成最终的数据模型 。
对于管道来说,有两点非常重要
. 管道是懒执行的,只有订阅器 observer subscribe了 数据管道,这个管道才会有数据流通 。
. 整个节点组成一个完整的管道,订阅了后面的管道节点,也会同时订阅之前的管道节点 ,每个节点接受之前的值,并发出新值
Rx 如此高效和强大,得益于其强大的操作符 。
主要包含下面几类
创建操作符: create, range, of, from, fromEvent, fromPromise, empty …
组合 contact ,merge, startWith, zip …
时间 delay , throttle, dobounceTime, interval …
过滤: filter, first, last, skip, distinct, take …
转换: buffer,map, mapTo, mergeMap, switch, switchMap, reduce, scan .
var express = require('express')
var app = express()
app.get('/', function (req, res) {
res.send('hello world')
})
1、常见的HTTP方法
2、GET/POST的区别
1、三次握手
2、四次挥手
关闭连接时,当服务器端收到FIN报文时,很可能并不会立即关闭链接,所以只能先回复一个ACK报文,告诉客户端:”你发的FIN报文我收到了”,只有等到服务器端所有的报文都发送完了,我才能发送FIN报文,因此不能一起发送,故需要四步握手。
1、HTTP1.0和HTTP1.1的一些区别
2、HTTP2.0和HTTP1.X相比的新特性
生成报表并下载是作为web应用中的一个传统功能,在实际项目中有广范的应用,实现原理也很简单,以NodeJS导出数据的方法为例,就是拿到页面数据对应id,然后根据id查询数据库,拿到所需数据后格式化为特点格式的数据,最后导出文件。
在nodejs中,也提供了很多的第三方库来实现这一功能,以node-xlsx
导出excel文件为例,实现步骤如下:
node-xlsx
npm install node-xlsx
以下代码使用
express
编写,调用接口实现下载功能
const fs = require('fs');
const path = require('path');
const xlsx = require('node-xlsx');
const express = require('express');
const router = express.Router();
const mongo = require('../db');
router.post('/export', async (req, res) => {
const { ids } = req.body;
const query = {
_id: { $in: ids }
}
// 查询数据库获取数据
let result = await mongo.find(colName, query);
// 设置表头
const keys = Object.keys(Object.assign({}, ...result));
rows[0] = keys;
// 设置表格数据
const rows = []
result.map(item => {
const values = []
keys.forEach((key, idx) => {
if (item[key] === undefined) {
values[idx] = null;
} else {
values[idx] = item[key];
}
})
rows.push(values);
});
let data = xlsx.build([{ name: "商品列表", data: rows }]);
const downloadPath = path.join(__dirname, '../../public/download');
const filePath = `${downloadPath}/goodslist.xlsx`;
fs.writeFileSync(filePath, data);
res.download(filePath, `商品列表.xlsx`, (err) => {
console.log('download err', err);
});
})
浏览器缓存主要分为强强缓存(也称本地缓存)和协商缓存(也称弱缓存)。浏览器在第一次请求发生后,再次发送请求时:
Cache-Control
和Expires
来判断是否过期。若没过期则直接从缓存中获取资源信息,包括缓存的header的信息,所以此次请求不会与服务器进行通信。这里判断是否过期,则是强缓存相关。后面会讲Cache-Control
和Expires
相关。If-None-Match
头将先前服务器端发送过来的Etag发送给服务器,服务会对比这个客户端发过来的Etag是否与服务器的相同,若相同,就将If-None-Match
的值设为false,返回状态304,客户端继续使用本地缓存,不解析服务器端发回来的数据,若不相同就将If-None-Match
的值设为true,返回状态为200,客户端重新机械服务器端返回的数据;客户端还会通过If-Modified-Since
头将先前服务器端发过来的最后修改时间戳发送给服务器,服务器端通过这个时间戳判断客户端的页面是否是最新的,如果不是最新的,则返回最新的内容,如果是最新的,则返回304,客户端继续使用本地缓存。强缓存是利用http头中的Expires
和Cache-Control
两个字段来控制的,用来表示资源的缓存时间。强缓存中,普通刷新会忽略它,但不会清除它,需要强制刷新。浏览器强制刷新,请求会带上Cache-Control:no-cache
和Pragma:no-cache
Expires
是http1.0的规范,它的值是一个绝对时间的GMT格式的时间字符串。如我现在这个网页的Expires
值是:expires:Fri, 14 Apr 2017 10:47:02 GMT
。这个时间代表这这个资源的失效时间,只要发送请求时间是在Expires
之前,那么本地缓存始终有效,则在缓存中读取数据。所以这种方式有一个明显的缺点,由于失效的时间是一个绝对时间,所以当服务器与客户端时间偏差较大时,就会导致缓存混乱。如果同时出现Cache-Control:max-age
和Expires
,那么max-age
优先级更高。如我主页的response headers部分如下:
cache-control:max-age=691200
expires:Fri, 14 Apr 2017 10:47:02 GMT
Cache-Control是在http1.1中出现的,主要是利用该字段的max-age值来进行判断,它是一个相对时间,例如Cache-Control:max-age=3600,代表着资源的有效期是3600秒。cache-control除了该字段外,还有下面几个比较常用的设置值:
no-cache:不使用本地缓存。需要使用缓存协商,先与服务器确认返回的响应是否被更改,如果之前的响应中存在ETag,那么请求的时候会与服务端验证,如果资源未被更改,则可以避免重新下载。
no-store:直接禁止游览器缓存数据,每次用户请求该资源,都会向服务器发送一个请求,每次都会下载完整的资源。
public:可以被所有的用户缓存,包括终端用户和CDN等中间代理服务器。
private:只能被终端用户的浏览器缓存,不允许CDN等中继缓存服务器对其缓存。
Cache-Control与Expires可以在服务端配置同时启用,同时启用的时候Cache-Control优先级高。
协商缓存就是由服务器来确定缓存资源是否可用,所以客户端与服务器端要通过某种标识来进行通信,从而让服务器判断请求资源是否可以缓存访问。
普通刷新会启用弱缓存,忽略强缓存。只有在地址栏或收藏夹输入网址、通过链接引用资源等情况下,浏览器才会启用强缓存,这也是为什么有时候我们更新一张图片、一个js文件,页面内容依然是旧的,但是直接浏览器访问那个图片或文件,看到的内容却是新的。
这个主要涉及到两组header字段:Etag
和If-None-Match
、Last-Modified
和If-Modified-Since
。上面以及说得很清楚这两组怎么使用啦~复习一下:
Etag
和If-None-Match
Etag/If-None-Match返回的是一个校验码。ETag可以保证每一个资源是唯一的,资源变化都会导致ETag变化。服务器根据浏览器上送的If-None-Match值来判断是否命中缓存。
与Last-Modified不一样的是,当服务器返回304 Not Modified的响应时,由于ETag重新生成过,response header中还会把这个ETag返回,即使这个ETag跟之前的没有变化。
浏览器第一次请求一个资源的时候,服务器返回的header中会加上Last-Modify,Last-modify是一个时间标识该资源的最后修改时间,例如Last-Modify: Thu,31 Dec 2037 23:59:59 GMT。
当浏览器再次请求该资源时,request的请求头中会包含If-Modify-Since,该值为缓存之前返回的Last-Modify。服务器收到If-Modify-Since后,根据资源的最后修改时间判断是否命中缓存。
如果命中缓存,则返回304,并且不会返回资源内容,并且不会返回Last-Modify。
你可能会觉得使用Last-Modified已经足以让浏览器知道本地的缓存副本是否足够新,为什么还需要Etag呢?HTTP1.1中Etag的出现主要是为了解决几个Last-Modified比较难解决的问题:
Last-Modified与ETag是可以一起使用的,服务器会优先验证ETag,一致的情况下,才会继续比对Last-Modified,最后才决定是否返回304。
常用的状态码: 200表示成功,301表示重定向,404表示资源未找到,500表示服务器内部错误
状态码分类:
分类 | 分类描述 |
---|---|
1** | 信息,服务器收到请求,需要请求者继续执行操作 |
2** | 成功,操作被成功接收并处理 |
3** | 重定向,需要进一步的操作以完成请求 |
4** | 客户端错误,请求包含语法错误或无法完成请求 |
5** | 服务器错误,服务器在处理请求的过程中发生了错误 |
http状态码列表
状态码 | 状态码英文名称 | 中文描述 |
---|---|---|
100 | Continue | 继续。客户端应继续其请求 |
101 | Switching Protocols | 切换协议。服务器根据客户端的请求切换协议。只能切换到更高级的协议,例如,切换到HTTP的新版本协议 |
200 | OK | 请求成功。一般用于GET与POST请求 |
201 | Created | 已创建。成功请求并创建了新的资源 |
202 | Accepted | 已接受。已经接受请求,但未处理完成 |
203 | Non-Authoritative Information | 非授权信息。请求成功。但返回的meta信息不在原始的服务器,而是一个副本 |
204 | No Content | 无内容。服务器成功处理,但未返回内容。在未更新网页的情况下,可确保浏览器继续显示当前文档 |
205 | Reset Content | 重置内容。服务器处理成功,用户终端(例如:浏览器)应重置文档视图。可通过此返回码清除浏览器的表单域 |
206 | Partial Content | 部分内容。服务器成功处理了部分GET请求 |
300 | Multiple Choices | 多种选择。请求的资源可包括多个位置,相应可返回一个资源特征与地址的列表用于用户终端(例如:浏览器)选择 |
301 | Moved Permanently | 永久移动。请求的资源已被永久的移动到新URI,返回信息会包括新的URI,浏览器会自动定向到新URI。今后任何新的请求都应使用新的URI代替 |
302 | Found | 临时移动。与301类似。但资源只是临时被移动。客户端应继续使用原有URI |
303 | See Other | 查看其它地址。与301类似。使用GET和POST请求查看 |
304 | Not Modified | 未修改。所请求的资源未修改,服务器返回此状态码时,不会返回任何资源。客户端通常会缓存访问过的资源,通过提供一个头信息指出客户端希望只返回在指定日期之后修改的资源 |
305 | Use Proxy | 使用代理。所请求的资源必须通过代理访问 |
306 | Unused | 已经被废弃的HTTP状态码 |
307 | Temporary Redirect | 临时重定向。与302类似。使用GET请求重定向 |
400 | Bad Request | 客户端请求的语法错误,服务器无法理解 |
401 | Unauthorized | 请求要求用户的身份认证 |
402 | Payment Required | 保留,将来使用 |
403 | Forbidden | 服务器理解请求客户端的请求,但是拒绝执行此请求 |
404 | Not Found | 服务器无法根据客户端的请求找到资源(网页)。通过此代码,网站设计人员可设置"您所请求的资源无法找到"的个性页面 |
405 | Method Not Allowed | 客户端请求中的方法被禁止 |
406 | Not Acceptable | 服务器无法根据客户端请求的内容特性完成请求 |
407 | Proxy Authentication Required | 请求要求代理的身份认证,与401类似,但请求者应当使用代理进行授权 |
408 | Request Time-out | 服务器等待客户端发送的请求时间过长,超时 |
409 | Conflict | 服务器完成客户端的 PUT 请求时可能返回此代码,服务器处理请求时发生了冲突 |
410 | Gone | 客户端请求的资源已经不存在。410不同于404,如果资源以前有现在被永久删除了可使用410代码,网站设计人员可通过301代码指定资源的新位置 |
411 | Length Required | 服务器无法处理客户端发送的不带Content-Length的请求信息 |
412 | Precondition Failed | 客户端请求信息的先决条件错误 |
413 | Request Entity Too Large | 由于请求的实体过大,服务器无法处理,因此拒绝请求。为防止客户端的连续请求,服务器可能会关闭连接。如果只是服务器暂时无法处理,则会包含一个Retry-After的响应信息 |
414 | Request-URI Too Large | 请求的URI过长(URI通常为网址),服务器无法处理 |
415 | Unsupported Media Type | 服务器无法处理请求附带的媒体格式 |
416 | Requested range not satisfiable | 客户端请求的范围无效 |
417 | Expectation Failed | 服务器无法满足Expect的请求头信息 |
500 | Internal Server Error | 服务器内部错误,无法完成请求 |
501 | Not Implemented | 服务器不支持请求的功能,无法完成请求 |
502 | Bad Gateway | 作为网关或者代理工作的服务器尝试执行请求时,从远程服务器接收到了一个无效的响应 |
503 | Service Unavailable | 由于超载或系统维护,服务器暂时的无法处理客户端的请求。延时的长度可包含在服务器的Retry-After头信息中 |
504 | Gateway Time-out | 充当网关或代理的服务器,未及时从远端服务器获取请求 |
505 | HTTP Version not supported | 服务器不支持请求的HTTP协议的版本,无法完成处理 |
很难通过技术手段完全避免 XSS,但我们可以总结以下原则减少漏洞的产生:
<%= data %>
而不是 <%- data %>
; 在 doT.js 中,尽量使用 {{! data }
而不是 {{= data }
; 在 FreeMarker 中,确保引擎版本高于 2.3.24,并且选择正确的 freemarker.core.OutputFormat
。onLoad="onload('{{data}}')"
、onClick="go('{{action}}')"
这种拼接内联事件的写法。在 JavaScript 中通过 .addEventlistener()
事件绑定会更安全。createElement
、setAttribute
之类的方法实现。或者采用比较成熟的渲染框架,如 Vue/React 等。存放静态资源文件到服务器
但这种形式在性能上也有缺陷:
受地理环境影响,离服务器越远资源加载越慢
频繁请求资源对服务器压力较大
存放静态资源文件到CDN
为了进一步提升性能,可以把动态网页(index.html)和静态资源(js、css、image…)分开部署。静态资源被放置于 CDN 上.
但是 CDN 也有缓存策略:新的静态资源发布后,需要一定的时间去覆盖各个边缘站点的旧资源。若某客户端获得了新的动态网页,但是附近的 CDN 节点尚未更新最近发布的静态资源,客户端即便放弃本地缓存,它加载的依旧是位于 CDN 上的“脏数据”。怎么办呢?干脆把文件名也给改了——让摘要信息成为文件名的一部分!具体实现可以仰仗 webpack,将 output.filename
设为 [name].[contenthash].js
,输出文件和 html 模版都会帮你更改好.
用摘要信息重命名后的资源文件,与旧资源就不同名了,不再需要以覆盖旧文件的形式主动更新各个地区的边缘站点。新版本发布后,浏览器首次请求资源,若 CDN 不存在该资源,便会向就近的边缘站点加载该文件,同时更新 CDN 缓存;这就彻底避免了 CDN 脏数据的问题.
我们从以下几方面来看nodejs.
什么是nodejs?
Node.js
是一个开源与跨平台的 JavaScript
运行时环境, 在浏览器外运行 V8 JavaScript 引擎(Google Chrome 的内核),利用事件驱动、非阻塞和异步输入输出模型等技术提高性能.
可以理解为 Node.js
就是一个服务器端的、非阻塞式I/O的、事件驱动的JavaScript
运行环境
非阻塞异步
Nodejs
采用了非阻塞型I/O
机制,在做I/O
操作的时候不会造成任何的阻塞,当完成之后,以时间的形式通知执行操作
例如在执行了访问数据库的代码之后,将立即转而执行其后面的代码,把数据库返回结果的处理代码放在回调函数中,从而提高了程序的执行效率
事件驱动
事件驱动就是当进来一个新的请求的时,请求将会被压入一个事件队列中,然后通过一个循环来检测队列中的事件状态变化,如果检测到有状态变化的事件,那么就执行该事件对应的处理代码,一般都是回调函数
优缺点
优点:
因为Nodejs
是单线程,带来的缺点有:
应用场景
借助Nodejs
的特点和弊端,其应用场景分类如下:
I/O
,不善于计算。因为Nodejs是一个单线程,如果计算(同步)太多,则会阻塞这个线程具体场景可以表现为如下:
json
的API产生代码库
新建一个git代码库
git init
下载远程项目和它的整个代码历史
git clone 远程仓库地址
配置
显示配置
git config --list [--global]
编辑配置
git config -e [--global]
设置用户信息
git config [--global] user.name "名"
git config [--global] user.email "邮箱地址"
暂存区文件操作
增加文件到暂存区
# 1.添加当前目录的所有文件到暂存区
git add .
# 2.添加指定目录到暂存区,包括子目录
git add [dir]
# 3.添加指定文件到暂存区
git add [file1] [file2] ...
在暂存区中删除文件
# 删除工作区文件,并且将这次删除放入暂存区
git rm [file1] [file2] ...
# 停止追踪指定文件,但该文件会保留在工作区
git rm --cached [file]
重命名暂存区文件
# 改名文件,并且将这个改名放入暂存区
git mv [file-original] [file-renamed]
代码提交
# 提交暂存区到仓库区
git commit -m [message]
分支操作
# 列出所有本地分支
git branch
# 列出所有远程分支
git branch -r
# 列出所有本地分支和远程分支
git branch -a
# 新建一个分支,但依然停留在当前分支
git branch [branch-name]
# 新建一个分支,并切换到该分支
git checkout -b [branch]
# 新建一个分支,指向指定commit
git branch [branch] [commit]
# 新建一个分支,与指定的远程分支建立追踪关系
git branch --track [branch] [remote-branch]
# 切换到指定分支,并更新工作区
git checkout [branch-name]
# 切换到上一个分支
git checkout -
# 建立追踪关系,在现有分支与指定的远程分支之间
git branch --set-upstream [branch] [remote-branch]
# 合并指定分支到当前分支
git merge [branch]
# 选择一个commit,合并进当前分支
git cherry-pick [commit]
# 删除分支
git branch -d [branch-name]
# 删除远程分支
git push origin --delete [branch-name]
git branch -dr [remote/branch]
信息查看
# 显示有变更的文件
git status
# 显示当前分支的版本历史
git log
# 显示commit历史,以及每次commit发生变更的文件
git log --stat
# 搜索提交历史,根据关键词
git log -S [keyword]
# 显示某个commit之后的所有变动,每个commit占据一行
git log [tag] HEAD --pretty=format:%s
# 显示某个commit之后的所有变动,其"提交说明"必须符合搜索条件
git log [tag] HEAD --grep feature
# 显示过去5次提交
git log -5 --pretty --oneline
同步操作
# 增加一个新的远程仓库,并命名
git remote add [shortname] [url]
# 取回远程仓库的变化,并与本地分支合并
git pull [remote] [branch]
# 上传本地指定分支到远程仓库
git push [remote] [branch]
# 强行推送当前分支到远程仓库,即使有冲突
git push [remote] --force
# 推送所有分支到远程仓库
git push [remote] --all
撤销操作
# 恢复暂存区的指定文件到工作区
git checkout [file]
# 恢复某个commit的指定文件到暂存区和工作区
git checkout [commit] [file]
# 恢复暂存区的所有文件到工作区
git checkout .
# 重置暂存区的指定文件,与上一次commit保持一致,但工作区不变
git reset [file]
# 重置暂存区与工作区,与上一次commit保持一致
git reset --hard
# 重置当前分支的指针为指定commit,同时重置暂存区,但工作区不变
git reset [commit]
# 重置当前分支的HEAD为指定commit,同时重置暂存区和工作区,与指定commit一致
git reset --hard [commit]
# 重置当前HEAD为指定commit,但保持暂存区和工作区不变
git reset --keep [commit]
# 新建一个commit,用来撤销指定commit
# 后者的所有变化都将被前者抵消,并且应用到当前分支
git revert [commit]
冲突合并一般是因为自己的本地做的提交和服务器上的提交有差异,并且这些差异中的文件改动,Git不能自动合并,那么就需要用户手动进行合并
如我这边执行git pull origin master
如果Git能够自动合并,那么过程看起来是这样的:
拉取的时候,Git自动合并,并产生了一次提交。
如果Git不能够自动合并,那么会提示:
这个时候我们就可以知道README.MD
有冲突,需要我们手动解决,修改README.MD
解决冲突:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-F5DEAiz1-1655685209086)(https://storage.lynnn.cn/assets/markdown/91147/pictures/2021/12/7d37e41669c2572905b80640702251df80004482.png?sign=d0a69bfe47903bbe41f03166db81a88a&t=61c3e0b2)]
可以看出来,在1+1=几的这行代码上产生了冲突,解决冲突的目标是保留期望存在的代码,这里保留1+1=2,然后保存退出。
退出之后,确保所有的冲突都得以解决,然后就可以使用:
git add .
git commit -m "fixed conflicts"
git push origin master`
即可完成一次冲突的合并。
整个过程看起来是这样的:
关于Git
分支管理,每个团队在不同阶段都有自己的管理策略。我们团队以前采用的是版本分支管理策略,也就是每次上线新版本都会创建一个新的版本分支,而新的需求开发也从当前最新的版本分支迁出一个新的需求分支开发,线上bug
则在版本分支上修改然后发布。
现在的远程仓库都提供了基于SSH协议的Git服务,在使用SSH协议访问仓库之前,需要先配置好账户/仓库的SSH公钥。
① 产生公私钥对
ssh-keygen -t rsa -C [email protected]
产生完毕后,公私玥对位于c/Users/用户名/.ssh/
:
② 将公钥上传至远程仓库个人中心的设置里
以gitee为例,添加公钥请访问:https://gitee.com/profile/sshkeys
注:如果想用现在的免密登录,请使用ssh协议去关联本地与线上仓库。
Lodash 是一个一致性、模块化、高性能的 JavaScript 实用工具库。
首先要明白的是lodash的所有函数都不会在原有的数据上进行操作,而是复制出一个新的数据而不改变原有数据。类似immutable.js的理念去处理。
Lodash 通过降低 array、number、objects、string 等等的使用难度从而让 JavaScript 变得更简单。 Lodash 的模块化方法 非常适用于:
常用语法:cloneDeep深拷贝,uniq数组去重,compact 去除假值,filter和reject 过滤集合传入匿名函数, join 将 array 中的所有元素转换为由 separator 分隔的字符串等
1.原生开发(NativeApp开发):像盖房子一样,先打地基然后浇地梁、房屋结构、一砖一瓦、钢筋水泥、电路走向等,原生APP同理:通过代码从每个页面、每个功能、每个效果、每个逻辑、每个步骤全部用代码写出来,一层层,一段段全用代码写出来
此种APP的数据都保存在本地,APP能及时调取,所以相应速度及流畅性有保障
2.混合开发(HTML5开发):这个就相当于一种框架开发,说白了就是网页;该模式通常由“HTML5云网站+APP应用客户端”两部份构成,APP应用客户端只需安装应用的框架部份,而应用的数据则是每次打开APP的时候,去云端取数据呈现给手机用户。
混合APP还有一种是套壳APP,套壳APP就是用H5的网页打包成APP,虽然是APP能安装到手机上,但是每个界面,全部是网页
混合APP开发优势:
时间短:基本都是模版拿来直接套上或打包成APP,会节省很大一部分时间。
价格便宜:代码不需要重新写,界面不用重新设计,都是固定的,可替换的地方很少,自己随便都能换上,所以价格相对便宜。
可以使用第三方组件库以及自定义组件
vue项目中使用组件库为 vant / element-ui / iview / ant-design-vue
react常用组件库为 Ant Design / Ant Design Mobile
开发者也可以通过自定义实现组件的使用
1、常用的目录操作命令
2、常用的文件操作命令
3、常用的文件内容操作命令
echarts是一个基于 JavaScript 的开源可视化图表库,可以流畅的运行在 PC 和移动设备上,兼容绝大部分的浏览器(IE9/10/11,Chrome,Firefox,Safari等),底层依赖矢量图形库 ZRender,提供20 多种图表和十几种组件,支持Canvas、SVG 双引擎并能一键切换,让移动端渲染更加流畅
echarts源自百度,现由Apache 开源社区维护,官方提供了完善的文档( https://echarts.apache.org ),使用也非常简单,步骤如下:
下载echarts
https://github.com/apache/echarts
npm install echarts --save
https://www.jsdelivr.com/package/npm/echarts
https://echarts.apache.org//builder.html
引入echarts
DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<script src="echarts.js">script>
head>
html>
添加一个div容器并设置宽高
注意:不设置宽高无法显示效果
<body>
<div id="main" style="width: 600px;height:400px;">div>
body>
初始化一个 echarts 实例并通过 setOption
方法生成一个图表(以柱状图为例)
<script type="text/javascript">
// 基于准备好的dom,初始化echarts实例
var myChart = echarts.init(document.getElementById('main'));
// 指定图表的配置项和数据
var option = {
title: {
text: 'ECharts 入门示例'
},
tooltip: {},
legend: {
data: ['销量']
},
xAxis: {
data: ['衬衫', '羊毛衫', '雪纺衫', '裤子', '高跟鞋', '袜子']
},
yAxis: {},
series: [
{
name: '销量',
type: 'bar',
data: [5, 20, 36, 10, 10, 20]
}
]
};
// 使用刚指定的配置项和数据显示图表。
myChart.setOption(option);
script>
hash与history一般指Vue-router或React-router中的路由模式,Vue-router中通过mode
配置选项设置,React-router中通过HashRouter
与BrowerRouter
组件设置,区别如下:
#
,而history是没有pushState()
和 replaceState()
方法,在已有 back()
、forward()
、go()
的基础上,提供了对历史记录进行修改的功能。调用pushState()
和 replaceState()
时,虽然改变了当前的 URL,但浏览器不会向后端发送请求,但如何用户刷新页面,会导致浏览器向服务器发起请求,如后端没有做出适当的响应,则会显示404页面要理解宏任务(macrotask)与微任务(microtask),就必须了解javascript中的事件循环机制(event loop)以及js代码的运行方式
要了解js代码的运行方式,得先搞懂以下几个概念
JS是单线程执行
单线程指的是JS引擎线程
宿主环境
JS运行的环境,一般为浏览器或者Node
执行栈
是一个存储函数调用的栈结构,遵循先进后出的原则
JS引擎常驻于内存中,等待宿主将JS代码或函数传递给它执行,如何传递,这就是**事件循环(event loop)**所做的事情:当js执行栈空闲时,事件循环机制会从任务队列中提取第一个任务进入到执行栈执行,优先提取微任务(microtask),待微任务队列清空后,再提取宏任务(macrotask),并不断重复该过程
在实际应用中,宏任务(macrotask)与微任务(microtask)的API分别如下:
Javascript是单线程的,那么各个任务进程是按照什么样的规范来执行的呢?这就涉及到Event Loop的概念了,EventLoop是在html5规范中明确定义的;
何为eventloop,javascript中的一种运行机制,用来解决浏览器单线程的问题
Event Loop是一个程序结构,用于等待和发送消息和事件。同步任务、异步任务、微任务、宏任务
javascript单线程任务从时间上分为同步任务和异步任务,而异步任务又分为宏任务(macroTask)和微任务(microTask)
宏任务:主代码块、setTimeOut、setInterval、script、I/O操作、UI渲染
微任务:promise、async/await(返回的也是一个promise)、process.nextTick
在执行代码前,任务队列为空,所以会优先执行主代码块,再执行主代码块过程中,遇到同步任务则立即将任务放到调用栈执行任务,遇到宏任务则将任务放入宏任务队列中,遇到微任务则将任务放到微任务队列中。
主线程任务执行完之后,先检查微任务队列是否有任务,有的话则将按照先入先出的顺序将先放进来的微任务放入调用栈中执行,并将该任务从微任务队列中移除,执行完该任务后继续查看微任务队列中是否还有任务,有的话继续执行微任务,微任务队列null时,查看宏任务队列,有的话则按先进先出的顺序执行,将任务放入调用栈并将该任务从宏任务队列中移除,该任务执行完之后继续查看微任务队列,有的话则执行微任务队列,没有则继续执行宏任务队列中的任务。
需要注意的是:每次调用栈执行完一个宏任务之后,都会去查看微任务队列,如果microtask queue不为空,则会执行微任务队列中的任务,直到微任务队列为空再返回去查看执行宏任务队列中的任务
console.log('script start')
async function async1() {
await async2()
console.log('async1 end')
}
async function async2() {
console.log('async2 end')
}
async1()
setTimeout(function() {
console.log('setTimeout')
}, 0)
new Promise(resolve => {
console.log('Promise')
resolve()
})
.then(function() {
console.log('promise1')
})
.then(function() {
console.log('promise2')
})
console.log('script end')
最终输出 :
script start
VM70:8 async2 end
VM70:17 Promise
VM70:27 script end
VM70:5 async1 end
VM70:21 promise1
VM70:24 promise2
undefined
VM70:13 setTimeout
解析:
在说返回结果之前,先说一下async/await,async 返回的是一个promise,而await则等待一个promise对象执行,await之后的代码则相当于promise.then()
async function async1() {
await async2()
console.log('async1 end')
}
async function async2() {
console.log('async2 end')
}
//等价于
new Promise(resolve =>
resolve(console.log('async2 end'))
).then(res => {
console.log('async1 end')
})
整体分析:先执行同步任务 script start -> async2 end -> await之后的console被放入微任务队列中 -> setTimeOut被放入宏任务队列中 ->promise -> promise.then被放入微任务队列中 -> script end -> 同步任务执行完毕,查看微任务队列 --> async1 end -> promise1 -> promise2 --> 微任务队列执行完毕,执行宏任务队列打印setTimeout