My Little World

learn and share


  • 首页

  • 分类

  • 标签

  • 归档

  • 关于
My Little World

一些jQuery知识

发表于 2018-12-08

1.jquery对象与Element对象转换

1
2
3
4
5
6
7
//Element对象转jquery对象
var domObj = document.getElementById('id')
var $obj = $(domObj)//jQuery对象

//jquery对象转Element对象
var $box = $('.box')
var box = $box[0]

2.常用jQuery选择器接口
//传入对象 , 例$(document),把传入的对象包装成jQuery对象
$(this}

//传入函数 , 这个是在页面DOM文档加载完成后加载执行的,等效于在DOM加载完毕后执行了${document).ready()方 法。
$ (function (){})

//传入字符串 ,査询D0M节点包装成jQuery对象
$(.box)

//传入HTML字符串 创建DOM节点包装成jQuery对象
$(<div>)

//空
$()创建jQuery对象

3.如何把创建的Dom结点包装成jQuery对象
contetx.createElement创建DOM节点存储在数组中,调用merge方法把数组中存储的DOM节点的成员添加到jQuery实例对象上

4.jQuery实例对象length属性作用
存储DOM节点的数组对象平滑地添加到jQuery实例对象上

5.merge方法应用场景有哪些
合并数组
把数组成员合并在有length属性的对象上

6.$(document).ready()与$(function(){})的关系
$(document).ready()是对document.DOMContentLoaded事件的封装
$(function(){})每次调用$()传入的参数会收集在readyList数组中,
当document.DOMContentLoaded事件触发时依次执行readyList中收集的处理函数

My Little World

js 知识点 一

发表于 2018-12-08

1.< script > 标签几个属性

async :立即异步下载,因此不保证能够按指定的先后顺序执行,但会在load事件前,DOMContentLoad前后执行
defer :延迟执行,也不保证按顺序执行,在DOMContentLoad前后执行
src :外部文件链接
charset :src指定代码的字符集,防止出现乱码
language :已废弃,表示脚本语言,
type :值为脚本语言内容类型(MIME),language替代属性,服务器传递过来类型为application/x-javascript需要注意,一个是可能导致脚本被忽略,另一个是可能触发浏览器XHTML标准模式;另外外链文件不是js文件时,一定要设置该值

2.js 代码执行过程

包括两个部分一个是编译,或者叫预处理过程,一个是执行
编译过程 首先会创建一个全局作用域,然后进行词法分析,
词法分析过程会涉及到变量提升和函数声明提前处理,
即词法分析到变量时会先判断当前域到全局域有没有该变量声明,如果有,就会将该声明的变量挂到该作用域的对象上,
如果到全局域都没有则以全局为背景,新建该变量,并为其分配内存,
对于基本类型,变量中会保存它的值,对于对象,变量中会保存值所在内存的内存地址,
在一定场景下,这里就相当于发生了变量提升,把变量从局部提升到了全局,或者将声明提前到了使用之前,
避免变量提升可以使用es6的let 关键字或者使用立即执行实现变量声明使用,但要注意使用let重复声明会报错,var 会同名覆盖;
当遇到函数声明时,会直接按声明顺序,将声明提前到代码顶端,
重复声明 的函数会被后者覆盖,
如果变量名与函数名相同,则无论原来词法位置如何,先将函数声明提前到顶端,不再进行变量声明,变量名在哪赋值,则原函数在哪被覆盖,
覆盖之前调用使用原函数
提前之后会为函数创建作用域,
这时创建的作用域就是函数的 【【scope】】属性,保存着指向父域到全局域的指针
(指针就是指向活动对象的一个地址,全局域浏览器环境就是windows,nodejs环境就是global)
将来执行时会根据此指针建立作用域链,作用域链就是指针列表,指向不同层级作用域的内存,在里面取值
以上就基本上是编译过程,除了一些特定的编译规则,编译过程还会会使用大量技巧进行优化提升编译速度
但在使用js时,需要注意try-catch,with和eval三个语法关键字的使用,因为在使用它们时,会创建块级作用域,
破坏掉编译器原有的创建管理作用域的规则,在编译时,编译器也就不会对其中的代码进行优化,最终导致性能变差

运行过程,基本就是在运行函数,函数在运行时会首先根据【【scope】】属性拿到父域到全局的指针,创建指针列表即作用域链,
然后创建函数执行的局部作用域,将函数中声明的变量挂到局部作用域活动对象上,然后将局部作用域指针推向作用域链前端,
在用到变量值时,会根据作用域链依次查找取值
函数执行结束后,局部活动对象被销毁,作用域链被销毁,进行垃圾回收
但是对于声明在一个函数内部的函数来说,这个声明的函数在声明时,就会包含其父域的活动对象,
因此在外部使用该函数时,可以访问到其父域的变量,实现了父域访问子域变量,延长了变量的使用范围,也就是常见的闭包现象,
缺点就是闭包的函数执行完毕后,因为本身【【scope】】会指向父域的活动对象,所以父域的活动对象始终不会被回收,除非闭包函数被销毁
从而有可能造成内存泄露,为什么说有可能,因为父活动对象占的内存其实是比较小的,造成内存泄露的真正原因是使用闭包易造成循环引用,
尤其是闭包作用域中保存dom结点时,如果是在IE中,dom和bom都是以C++的COM对象保存的,垃圾回收机制是计数策略,
因此循环引用的话,永远都不会被回收

补充,对于未以分号结尾的语句,会采用能合就合的原则,即,会尝试和下一行语句合并执行,如果能合并执行就合并执行,
不能合并执行就在中间加分号再执行,如果再执行不了就会报错
好处就是对于条件判断语句可进行一行一行判断
坏处就是如果下一行以大括号开头,上一行语句就会变成函数,导致出现意想不到的结果
两个例外,
对于return,break,continue语句不会做尝试合并;
对于++,–自增自减符后没有分号,上一行也没分号情况,自增自减会和下一行合并,上一行自行添加分号

以上即是我所了解的js运行机制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
//补充1
每个值都有内存地址,变量名指向内存地址/保存内存地址
变量名携带内存地址存储在栈中
保存变量值的内存空间在堆中(数据类型:object,function,函数缓存区,函数加载区),相同模样的值,内存地址不同,(即不同变量名,赋值为看似一样的对象时,会不相等;更改变量的值,即更改指向的地址)
常量值保存在池中(数据类型:string,number,boolean),相同值,内存地址相同

变量是个引用,常量是个值
== 比较值
=== 比较地址

//补充2
function func(a,b,c){
/*
* 逻辑代码执行前
* step1 创建AO对象
* AO ={}
* step2 初始化AO对象
* AO = {
* this:undefined,//内置
* arguments:undefined,//默认
* a:undefined,//参数
* b:undefined,//参数
* c:undefined,//参数
* inner:undefined//函数内声明
* }
* step3 赋值
* AO = {
* this:window,//内置
* arguments:[length:2,0:1,1:2],//默认
* a:1,//参数
* b:2,//参数
* c:undefined,//参数
* inner:undefined//仅声明还未执行
* }
* step4 处理函数声明,有相同变量名的函数覆盖赋值已有变量名,新函数名直接挂到AO上
* AO = {
* this:window,//内置
* arguments:[length:2,0:1,1:2],//默认
* a:function a(){},//有同名函数声明进行覆盖
* b:2,//参数
* c:undefined,//参数
* inner:undefined,//仅声明还未执行
* innerFunc:function(){}//添加函数声明
* }
*/
console.log(arguments);
console.log(global)
console.log(inner)
////////////////
var inner = 20;//执行到该步,inner被赋值为20
function inner(){

}
function innerFunc(){

}
console.log(inner)
}

var global=20
func(1,2)

3.关于对象继承

3.0 new 操作符调用构造函数 执行过程
1.创建或者构造一个全新的对象
2.将构造函数的作用域赋给这个新的对象,
3.执行构造函数 (为这个新对象添加属性) ,第二,三步相当于func.call(newobj)
4.如果构造函数没有返回值或返回值非对象,则返回新对象,否则返回构造函数return的对象

3.1 继承的6种方式及优缺点
第一种原型链方式,子类的原型对象指向父类的实例
缺点
给原型添加方法必须要在替换原型的语句之后;
不能实现多继承(原型指向多个父对象);
所有属性共享
无法传递参数到父类进行初始化
第二种借助构造函数,在子类构造函数中使用call或者apply执行父类构造函数
优点
可以实现多继承
可以传递参数
属性不共享
缺点
只能继承构造函数中属性,不能继承原型上方法,不能复用方法
实例仅为子类实例,不是父类实例
第三种组合继承,以上两组结合在一起,两种方式都执行
缺点
要调用两次父类构造函数,影响性能
实例属性和原型属性各占一份,同名覆盖机制,重复占内存,没必要
第四种原型式继承,借助object()或者object.create()函数,缺点同原型链方式

1
2
3
4
5
function object(o){
function F(){}
F.prototype = o;
return new F();
}

第五种寄生式继承,利用原型式继承和增强对象过程的封装函数创建子类,缺点同原型链方式
第六种寄生组合式继承,保留借助构造函数部分,原型部分功能借助中间函数处理,
中间函数借助object拿到父类prototype,给到子类的prototype,从而完美解决组合继承的缺点

1
2
3
4
5
6
function inheritPrototype(subType,superType){
var prototype = object(superType.prototype)
prototype.constructor = subType
subType.prototype = prototype
}
subType.prototype.__proto__ = superType

3.2 es6 使用class extend实现继承
使用class声明构造函数时,本来挂在this上的属性,放在constructor()函数里面执行初始化,
挂在prototype上的方法直接放在class里面声明

使用extend继承时,需要在constructor里面先执行super函数才能使用this关键字,
增强方法同样直接在class中和constructor并列声明,相当于挂在prototype上

注意super函数只能在consructor中运行;super用作对象在constructor中使用时,相当于this,
添加属性会添加在当前对象上,运行函数时会调用继承的父类的方法

还有就是class extend继承null和 什么也不继承是,相当于继承Function.prototype
继承Object时,子类就相当于object 的复制,子类实例就是object实例

4.关于this 四种使用场景

普通函数执行,指向全局作用域
对象属性方法调用,指向调用对象
call,apply中强制绑定对象,this执行绑定的对象
构造函数中this指向构造的新对象
(箭头函数中this,指向词法作用域的父域)

5.js文件位置

放html底部原因:
下载解析执行过程是阻断式的,会停止页面渲染,造成白屏,影响用户体验;
另外JS中如果有对dom的操作,页面中还没有dom,获取不到,是不符合逻辑的,会报错

哪些js功能文件可以放顶部:
与css相关的js,比如rem单位的换算,需要根据根结点进行设置
使用的框架需要在根结点将浏览器支持的样式罗列出来

6.什么情况下用内部文件,内部文件比外部文件好处在哪?

内部文件相比外部文件最大好处就是性能提升,因为访问外部文件时,不管文件大小都会造成一次网络请求链接,请求服务器,下载文件,
解析文件,除了文件本身代码外,还要处理文件头文件尾的请求,增加了链接数,从而造成性能影响
对于功能短小精悍,不会被到处复用的js代码不适合采用外部文件,应该采用内部文件写法,尤其对手机端页面有性能提升

7.开发过程避免缓存方法

在浏览器开启禁用缓存的模式
手动清缓存 ctrl+shfit+del
引用的文件名添加随机数,浏览器根据文件名不同,就会重新获取资源更新缓存

8.使用严格模式弊端及解决办法

在代码压缩时,’use strict’ 这一行可能不在位于第一行,
或者后续代码不需要在严格模式下执行,被压缩在了严格模式范围内,导致了全部以严格模式执行

解决办法就是,将需要严格模式执行的代码放在匿名函数中,形成代码块,在函数中使用严格模式

9.立即执行函数好处

避免变量污染:有些变量仅在小功能内使用,将小功能封装起来,就可以避免这些变量暴露到全局,
另外函数本身没有函数名,不会增加全局变量
提升性能:功能所需变量全都在函数内时,查找变量快
有利于压缩:一部分代码执行需要用到的变量变量名太长,将这段代码封装成立即执行函数,将长变量名以参数形式传进去
避免全局命名冲突:一段代码需要用到两个代表不同功能但名称相同的变量,可以将该段代码封装成立即执行函数,
将其中一个变量以参数形式传递进来,达到换名的目的
保存闭包状态:循环执行异步代码时,将异步代码用立即执行函数包裹,函数内可保存本次循环的状态
改变代码运行顺序:umd,通用模块规范,function(fn){fn()}()

10.变量类型

根据当前变量的值的类型确定
如果想要进行标记,方法有三种
初始化时指定相应类型值
使用匈牙利标记法,用单字母表示类型,添加到变量命中,例sName 代表string类型
使用注释,变量声明时,在旁边添加注释

11.null vs undefined

转数字时,null –>0;undefined–>NaN
undefined可以当做windows对象上的一个属性,null不行

12. 类型转换

基本类型包括,undefined,null,boolean,string,number

基本类型 —> boolean Boolean(someval) 相当于 !!someval
undefined,null,NaN,’’,+0,-0 —> false
其他 —> true

基本类型 —> string String(someval)相当于 someval+’’
一般情况直接加引号
[] —> ‘’
{} —> ‘[object object]’

基本类型—>number + someval 相当于 Number(someval)
undefined,{} —> NaN
false, null,’’,[],—> 0
true —> 1
‘123’ —> 123
‘abc’ —> NaN
[]—> toString —> ‘’ —> 0
{} —> valueOf —> {} —> toString —> ‘[object object]’ —> NaN

基本类型 —>对象
undefined,null —> {}
true/123 —> {initval:true/123}
‘abc’ —> {initval:’abc’,length:3,0:’a’,1:’b’,2:’c’}

对象 —>基本类型

关于–对象分类:
内部对象(错误对象;常用对象:Boolean,Number,String,Object,Array,Function,Date,Exec;内置对象:Math,global,Json)
宿主对象:windows,document
自定义对象

关于–属性访问
obj.someprop—>检查obj是不是null/undefined—>是|不是—>报错|是不是Object,不是的话转object—>取值
obj[someval]—>检查obj是不是null/undefined—>是|不是—>报错|是不是Object,不是的话转object—>计算someval拿到string—>取值

Boolean —>true
Number —>基本类型转的|{}/{a:1}|Date—>相应值|NaN|时间戳
Array.toString:数组每一项转字符串
Function.toString:原代码字符串形式
Date.toString:日期和时间组合形式
Exec.toString:正则字符串形式

运算转换

Number()参数不能有非数字
parseInt()参数只能为字符串,非字符串会进行强制转化字符串,可含非数字,但不能以非数字开头
一元操作符对任何数据类型有效,得到数字

+二元运算侧重转字符串,然后进行字符串拼接
其中一个为字符串或者两个都是对象时,转字符串拼接
undefined,null,Boolean,数字混合运算+时,会被转数字再计算
{}为第一个运算值时,会被当做函数结束,不参与计算

1
2
3
4
5
6
7
8
9
10
11
12
null + null --> 0
undefined + null --> NaN
[] + [] --> ''
1 + [1,2] --> '11,2'
1 + {a:1} --> '1[object object]'
{a:1} + 1 --> 1
{} + {} --> NaN
({}) + {} --> '[object object][object object]'
[] + {} --> '[object object]'
{} + [] --> 0
var a = {}
a + a -->'[object object][object object]'

*/- 转数字,进行相减,如果不能转数字,返回NaN

>/< 侧重转数字
string < string 字典比较
非字符串<非字符串/字符串 都转数字然后比较

string == number 字符串转数字
boolean == 其他类型 二者转数字然后比较
null == undefined —>true undefined和null只与对方和自己==比较时为true,其他均为false
null === undefined —>false
非对象 == 对象 与数字比较object转数字,与字符串比较二者转数字再比较

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
'0' == null  //false
'0' == undefined //false
'0' == false //true
'0' == NaN //false
'0' == 0 //true
'0' == '' //false

false == null //false
false == undefined //false
false == NaN //false
false == 0 //true
false == '' //true
false == [] //true
false == {} //false

'' == null //false
'' == undefined //false
'' == NaN //false
'' == 0 //true
'' == [] //true
'' == {} //false

0 == null //false
0 == undefined //false
0 == NaN //false
0 == [] //true
0 == {} //false

[] == ![] //true

&&和|| 计算结果根据短路原则判断到了哪里,返回最后一个进行判断的值

运算符优先级
属性访问.
一元操作符(+,-,++,–)
二元操作符(+-*/)
比较(>,<)
相等(==,===,!=)
与&&,或||
三目运算符
赋值

运算符结合性
除了一元操作符,三目运算符和赋值是右结合,其他都是左结合

减少小数计算误差
尽量不使用小数进行比较或运算
转整数计算,再转回相应位数小数
使用toFixed()四舍五入
两个情境需要重复计算时,保证前后书写顺序

13. 数组相关方法

arrmethod

1
2
let arr = []
let iterator = arr[Symbol.iterator]() //可以获取数组迭代器

遍历对象/数组的方法
for
while(do…while)
forEach
map
reduce
for…of
for…in
iterator
generator

14.字符串相关方法

strmethod

15.日期相关方法

一共 33个
set/get(UTC)FullYear,Month,Date,Hours,Minutes,Seconds,Millseconds
set/get Time
get(UTC) Day
getTimezoneOffset

16. Math三个舍入方法

Math.ceil:比值大的最小整数
Math.round:四舍五入
Math.floor:比值小的最大整数

17. setInterval 注意问题

累积效应
代码执行时间大于间隔时间,后续调用会进行累积,累积会在短时间内连续触发

当用于动画时,因与显示器刷新频率不统一会造成视觉卡顿,解决办法如下:
一种是使用CSS3创建动画,根据显示器刷新执行动画
另一种是使用 requestAnimationframe(function(){})函数,也是在显示器刷新时执行
可以解决CSS3无法实现的,例如滚动控制等效果

18. 关于DOM

document 三个属性
url:地址栏中URL
domain:域名,跨域域名 X;父域名到子域名 X;
referer:上一页URL,可在跳转时,判断与当前页是否在同一域,是的话,就可以back,否则 location.href其他页面
dom.getAttribute(‘style’),得到样式字符串
dom.style 得到到样式对象
dom.getAttribute(‘onclick’) ,得到代码字符串
dom.onclick 得到函数对象

动态合集
使用时可能会造成死循环
使用框架如Jquery获取的合集不具有动态性
使用 queryselect(all)()获取到的也是静态合集

动态合集生成原理:
浏览器通过DOM树预先缓存起来,获取时,浏览器会通过缓存直接注册,创建一个变量返回

静态合集生成原理:
获取标识是一个CSS选择器,浏览器会先去解析字符串,判断是一个CSS选择器,
分析选择器并创建一个选择器结构,浏览器根据DOM树创建一个静态文件,即DOM树的一个快照
然后拿选择器和快照进行对比,符合选择器就放到合集中,知道整个快照对比完,再把合集的快照返回

19. 事件

绑定方式

直接通过HTML属性绑定
缺点
有可能响应函数还未被解析就被触发
事件名和回调1对1,修改繁琐
响应执行时,作用域为全局,不同浏览器解析规则不同,造成某些对象无法访问

使用dom属性绑定
冒泡阶段执行
缺点
只能绑定一个响应,后续赋值会被覆盖

IE attachEvent/detachEvent绑定/解绑
冒泡阶段执行
绑定多个时按按绑定顺序的逆序执行

addEventListener()/removeEventListener()绑定/解绑
第三个参数为对象时,passive属性为true时,会针对touchstart,touchend等特定事件通过开启两个线程,
一个执行浏览器默认行为,一个执行JS进行优化

事件优先级
浏览器在绑定事件而不是JS在绑定事件

1.html标签绑定事件会被dom属性事件覆盖
2.html标签优先执行,即使被属性事件覆盖,则执行被覆盖的属性事件
3.仅有监听事件和属性事件时,按绑定顺序执行,即事件对象的冒泡事件可能会在捕获事件之前执行

event对象属性
curentTarget:响应函数绑定的对象
target:发生事件的对象

My Little World

拖拽

发表于 2018-12-04

使用 React 实现拖放的技术要点

1.如何使用 React 的鼠标事件系统
2.如何判断拖放开始和拖放结束
3.如何实现拖放元素的位置移动 (可分为两种,一种是直接拖着具体要被移动的dom移动;另外一种是具体dom留在原位,拖着具体dom的影子移动,确定位置后,再将具体的dom放过去)
4.拖放状态在组件中如何维护

鼠标移动可能会超出要移动的组件和他的父组件,除了在document上监听,还可以在一个全局透明遮罩层上监听MouseMove和MouseUp好处:拖放过程不会选中其他任何元素,防止点击到其他组件

其他情景思考:
每个条目高度不一致,如何确定移动位置?
条目所在的列表有折叠,存在滚动条,如何根据滚动条确定位置?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100

import React, { Component } from "react";

require("./DndSample.css");

const list = [];
for (let i = 0; i < 10; i++) {
list.push(`Item ${i + 1}`);
}

const move = (arr, startIndex, toIndex) => {
arr = arr.slice();
arr.splice(toIndex, 0, arr.splice(startIndex, 1)[0]);
return arr;
};

const lineHeight = 42;
class DndSample extends Component {
constructor(props) {
super(props);
this.state.list = list;
}
state = {
dragging: false,
draggingIndex: -1,
startPageY: 0,
offsetPageY: 0,
};

handleMounseDown = (evt, index) => {
this.setState({
dragging: true,
startPageY: evt.pageY,
currentPageY: evt.pageY,
draggingIndex: index,
});
};
handleMouseUp = () => {
this.setState({ dragging: false, startPageY: 0, draggingIndex: -1 });
};
//如果往下滑,就一次把下一条数据交换位置,如果往上移动,就一次把上一条数据交换位置,
handleMouseMove = evt => {
let offset = evt.pageY - this.state.startPageY;
const draggingIndex = this.state.draggingIndex;
if (offset > lineHeight && draggingIndex < this.state.list.length - 1) {
// move down
offset -= lineHeight;
this.setState({
list: move(this.state.list, draggingIndex, draggingIndex + 1),
draggingIndex: draggingIndex + 1,
startPageY: this.state.startPageY + lineHeight,
});
} else if (offset < -lineHeight && draggingIndex > 0) {
// move up
offset += lineHeight;
this.setState({
list: move(this.state.list, draggingIndex, draggingIndex - 1),
draggingIndex: draggingIndex - 1,
startPageY: this.state.startPageY - lineHeight,
});
}
this.setState({ offsetPageY: offset });
};

getDraggingStyle(index) {
if (index !== this.state.draggingIndex) return {};
return {
backgroundColor: "#eee",
transform: `translate(10px, ${this.state.offsetPageY}px)`,
opacity: 0.5,
};
}

render() {
return (
<div className="dnd-sample">
<ul>
{this.state.list.map((text, i) => (
<li
key={text}
onMouseDown={evt => this.handleMounseDown(evt, i)}
style={this.getDraggingStyle(i)}
>
{text}
</li>
))}
</ul>
{this.state.dragging && ( //在一个遮罩层上监听MouseMove和MouseUp
<div
className="dnd-sample-mask"
onMouseMove={this.handleMouseMove}
onMouseUp={this.handleMouseUp}
/>
)}
</div>
);
}
}

export default DndSample;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
.dnd-sample ul {
display: inline-block;
margin: 0;
padding: 0;
background-color: #eee;
}

.dnd-sample li {
cursor: default;
list-style: none;
border-bottom: 1px solid #ddd;
padding: 10px;
margin: 0;
width: 300px;
background-color: #fff;
}

.dnd-sample-mask {
position: fixed;
left: 0;
right: 0;
top: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.1);
}
My Little World

对话框

发表于 2018-12-04

使用React Portals

React 16.3 新引入的 API
可以将虚拟 DOM 映射到任何真实 DOM 节点
解决了漂浮层的问题,比如Dialog,Tooltip 等

主要思路使用ReactDOM.createPortal将自定义dialog利用id挂到根标签上,dialog样式(包括悬浮)自己需要通过css定义,弹窗的显示和隐藏通过state操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
mport React from "react";
import ReactDOM from "react-dom";
import { Button } from "antd";
import "./PortalSample.css";

export default class PortalSample extends React.PureComponent {
state = { visible: false };
renderButton() {
return (
<Button type="primary" onClick={() => this.setState({ visible: true })}>
打开对话框
</Button>
);
}
renderDialog() {
return (
<div className="portal-sample">
<div>这是一个对话框!</div>
<br />
<Button
type="primary"
onClick={() => this.setState({ visible: false })}
>
关闭对话框
</Button>
</div>
);
}
render() {
if (!this.state.visible) return this.renderButton();
return ReactDOM.createPortal(
this.renderDialog(),
document.getElementById("dialog-container"),//在APP组件中定义
);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
.portal-sample {
position: absolute;
padding: 20px;
width: 500px;
height: 300px;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
background-color: #fff;
border-radius: 10px;
border: 1px solid #ddd;
box-shadow: 0px 0px 20px 2px #ddd;
}

使用UI组件库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import React from "react";
import ReactDOM from "react-dom";
import { Button, Modal } from "antd";
import "./PortalSample.css";

export default class PortalSample extends React.PureComponent {
state = { visible: false };
renderButton() {
return (
<Button type="primary" onClick={() => this.setState({ visible: true })}>
打开对话框
</Button>
);
}
renderDialog() {
return (
<Modal
visible={this.state.visible}
onCancel={() => this.setState({ visible: false })}
>
<div>这是一个对话框!</div>
</Modal>
);
}
render() {
return (
<div>
{this.renderButton()}
{this.renderDialog()}
</div>
);
}
}
My Little World

布局

发表于 2018-12-04

实现布局的几种方式

1.从0开始用 CSS实现
2.使用 CSS Grid 系统 (通过class实现,无需关心布局如何实现,同时可以适应不同屏幕的尺寸)
3.使用组件库,例如 antd(通过组件标签实现)

布局常见场景:侧边栏宽度可调整
1.手动实现拖放逻辑
2.使用 local storage 存储宽度位置

上中下结构,中间高度随父级高度自适应,头部和底部高度固定

1
2
3
4
5
<div className="app-layout1">
<div className="header">Header</div>
<div className="content">content</div>
<div className="footer">Footer</div>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<style>
.app-layout1 {
width: 500px;
height: 400px;
position: relative;
background-color: #eee;
text-align: center;
}

.app-layout1 .header {
line-height: 60px;
border-bottom: 2px solid #fff;
}
.app-layout1 .content {
position: absolute;
bottom: 60px;
top: 60px;
left: 0;
right: 0;
}
.app-layout1 .footer {
border-top: 2px solid #fff;
line-height: 60px;
bottom: 0;
left: 0;
right: 0;
position: absolute;
}
</style>

导航布局,左边是导航栏,右边上中下结构

1
2
3
4
5
6
<div className="app-layout2">
<div className="header">Header</div>
<div className="sider">Sider</div>
<div className="content">Content</div>
<div className="footer">Footer</div>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
.app-layout2 {
width: 500px;
height: 400px;
position: relative;
background-color: #eee;
text-align: center;
padding-left: 150px;
line-height: 60px;
}

.app-layout2 .header {
border-bottom: 2px solid #fff;
}
.app-layout2 .content {
position: absolute;
bottom: 60px;
top: 60px;
left: 150px;
right: 0;
}
.app-layout2 .sider {
width: 150px;
position: absolute;
border-right: 2px solid #fff;
top: 0;
left: 0;
bottom: 0;
}
.app-layout2 .footer {
border-top: 2px solid #fff;
bottom: 0;
left: 150px;
right: 0;
position: absolute;
}

导航栏随鼠标滑动调整宽度

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
import React from "react";
import { Button } from "antd";
import "./LayoutResize.css";

export default class LayoutResize extends React.PureComponent {
state = {
dragging: false,
startPageX: 0,
siderWidth: 150,
};

handleMouseDown = evt => {
this.setState({
dragging: true,
startPageX: evt.pageX,
});
};
handleMouseUp = () => {
this.setState({
dragging: false,
});
};
handleMouseMove = evt => {
let siderWidth = this.state.siderWidth + evt.pageX - this.state.startPageX;
if (siderWidth < 20 || siderWidth > 300) return;
this.setState({
siderWidth,
startPageX: evt.pageX,
});
};
render() {
const { dragging, siderWidth } = this.state;
const pxWidth = `${siderWidth}px`;
return (
<div className="app-layout-resize" style={{ paddingLeft: pxWidth }}>
<div className="header">Header</div>
<div className="sider" style={{ width: pxWidth }}>
Sider
</div>
<div className="content" style={{ left: pxWidth }}>
Content
</div>
<div className="footer" style={{ left: pxWidth }}>
Footer
</div>
<div
className="sider-resizer"
style={{ left: pxWidth }}
onMouseDown={this.handleMouseDown}
/>
{dragging && (
<div
className="resize-mask"
onMouseMove={this.handleMouseMove}
onMouseUp={this.handleMouseUp}
/>
)}
</div>
);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
.app-layout-resize {
width: 500px;
height: 400px;
position: relative;
background-color: #eee;
text-align: center;
padding-left: 150px;
line-height: 60px;
}

.app-layout-resize .header {
border-bottom: 2px solid #fff;
}
.app-layout-resize .content {
position: absolute;
bottom: 60px;
top: 60px;
left: 0;
right: 0;
}
.app-layout-resize .sider {
width: 150px;
position: absolute;
border-right: 2px solid #fff;
top: 0;
left: 0;
bottom: 0;
}
.app-layout-resize .footer {
border-top: 2px solid #fff;
bottom: 0;
left: 150px;
right: 0;
position: absolute;
}

.app-layout-resize .sider-resizer {
position: absolute;
left: 148px;
width: 6px;
top: 0;
bottom: 0;
cursor: col-resize;
}
.app-layout-resize .resize-mask {
background: rgba(0, 0, 0, 0);
position: fixed;
left: 0;
top: 0;
right: 0;
bottom: 0;
cursor: col-resize;
}
My Little World

表单 & 列表

发表于 2018-12-04

表单功能点

  1. 初始数据,提交和跳转

表单各项配置的优化,可以通过对UI框架的API进行封装成高阶组件,然后在使用时,通过配置高阶组件的属性,将需要的配置项传递给UI框架API,减少代码冗余,配置清晰

提交后状态通过中间件返回promise判断接下来操作

2.
错误处理(可依靠UI框架进行判断)
动态表单元素(在传入高阶组件属性时采用函数返回值形式动态加载表单元素,在函数中处理要不要显示表单元素)
内容动态加载(在componentDidMount中进行数据请求,以及初始状态处理)

列表页功能点

搜索,数据缓存和分页

开发列表也要考虑的技术要点
1.如何翻页 (数据来自服务器还是客户端缓存)
2.如何进行内容搜索 (当前页面数据搜索还是服务器端数据搜索)
3.如何缓存数据
4.何时进行页面刷新

列表页数据通过store拿数据渲染,翻页,查找,刷新通过触发action然后从服务器拿到数据后,更新store,从而组件重新渲染

store 模型
listItems:当前页id集合数组[id1,id2]
byId:{id1:{id:id1,name:xxx},id2:{id:id2,name:xxx}}
keyword:关键字string
page:number 页数
fetchListPending:bool是否正在加载数据
FetchListError:OBJECT数据加载出错
listNeedReload:bool是否需要重新加载(做判断条件,例如编辑之后,设为true,在列表页拿到判断为true则重新获取列表数据)

URL设计
将页数和关键字当做路由参数传递在componentDidupdated中获取参数判断是否重新获取数据

缓存更新,加载状态,错误处理

通过store模型中的相关数据进行判断展示

页面数据需要来源多个请求的处理

页面数据来自多个请求
1.请求之间无依赖关系,可以并发进行
2.请求有依赖,需要依次进行 (promise)
3.请求完成之前,页面显示 Loading 状态
loading数据状态由当前数据推导确定

内容页的加载和缓存

内容页和列表页的数据关系
1.简单业务:列表页数据包含内容页的数据 (注意页面刷新情况数据的获取)
2.复杂业务:内容页数据需要额外获取 (每次进来都重新获取)
3.内容页数据的缓存(将数据放在store中,在store中拿数据)

基于React Router 实现分步操作

向导页面需要考虑的技术要点
1.使用 URL进行导航的好处 (可以直接刷新其中一个步骤)

  1. 表单内容存放的位置
  2. 页面状态如何切换

将表单元素放在一个统一的form中,通过路由判断第几步,进而显示不同的表单元素,需要注意的是,切换下一步时,当前组件消失,配置的数据也会消失,需要将当前所填数据进行保存,以便在返回上一步时有数据

集成第三方 JS 库的技术要点

1.使用 ref 获取原生 DOM 节点引用
2.手动将组件状态更新到 DOM 节点
(对于数据驱动UI展示的情况,第一次渲染和之后更新需要手动操作DOM的过程,将数据注入过程隔离出来,单独处理成一个函数,再当react 部分操作导致需要重新渲染时,再调用,将react状态映射到第三方DOM中)
3.组件销毁时移除原生节点 DOM 事件

基于路由实现菜单导航

1.菜单导航只是改变 URL 状态
2.根据当前 URL 显示菜单的当前状态(使用navLink标签实现,注意react-router和redux一起使用时,url发生变化,组建要重新render时,必须要让组件绑定到router的store上,即让react-router也一起渲染)

如何避免应用出现性能问题

1.了解常见的性能问题场景 (键盘输入,鼠标移动是否卡顿)
2.时刻注意代码的潜在性能问题 (何时拆分组件,有优化空间;能否高效更新,组件拆分是否够细,足够细的话,组件越接近纯函数,就越可能减少参与到diff操作的可能性,从而提高渲染速度)
3.注重可重构的代码 (代码耦合性低,几乎不依赖外界,也不被外界依赖,对于有重构可能性的代码保留重构空间,当其他优先级较高的性能问题解决后,再进行代码重构)
4.了解如何使用工具定位性能问题

网络性能优化:自动化按需加载

1.什么是按需加载 (切换页面时才加载相应页面,而不是一开始就将所有页面加载进来)
2.使用 Webpack 的 import API
3.使用 react-loadable 库实现 React 异步加载
利用分包加载,在配置路由时实现,在总路由中删除,防止打包到总包中

1
2
3
4
5
import loadable from 'react-loadable'
const listPage = loadable({
loader:()=>import ('./listPage'),
loading:()=><div>loading...</div>
})

listPage页面在加载该页时,会自动加载listPage的独立打包文件

使用 reselect 避免重复计算
reselect 可以创建自动缓存的数据处理流程,通过保存计算结果,避免重复计算

异步渲染的两个部分

时间分片
DOM操作的优先级低于浏览器原生行为,例如键盘和鼠标输入,从而保证操作的流畅。
(setstate导致的重新render等页面行为(滚动,输入)结束后再执行)
1.虚拟 DOM 的 diff 操作可以分片进行 (对操作进行序列化,然后进行合并或者优先级处理)
2.React 新 API: unstatble_deferredUpdates(低级别的setstate)
3.Chrome 新 API:requestIdleCallback(浏览器级别的api,告诉代码内存空闲(不再滚动或敲击键盘),可以进行一些之前优先级低的UI更新操作,时间分片基础)

渲染挂起
虚拟DOM节点可以等待某个异步操作的完成,并指定timeout后,才完成真正的渲染
(可以不再注意什么时候显示loading状态,取消loading状态,虚拟dom节点可以自己等待异步操作的完成,只要虚拟dom节点返回一个promise,渲染引擎就会等promise结束后再去render)

  1. 新内置组件:Timeout

  2. unstatble_deferUpdate

借助工具发现性能问题

1.使用 React DevTool 找到多余渲染
chrome 插件,可查看react应用的状态,帮助找到不必要的render,从而进行调优—> [highlight update]
2.使用 Chrome DevTool 定位性能瓶颈
什么样的组件花了多长时间做了什么样的渲染—>performance

My Little World

Rekit

发表于 2018-12-04

背景
一个独立功能通常需要多个文件组成 (组件,reducer,action,路由等文件)
代码模板很复杂 (reducer,action需要代码模板,实际上填写的逻辑只是一小部分)
重构极为困难
(需要改动的地方很多)
项目复杂后很难理解和维护(无法直观的看到他们之间的关系)

思路

Rekit:更好的代码导航

  1. 语义化的组织源代码文件
  2. 使用子 Tab来展示项目元素的各个部分
  3. 直观的显示和导航某个功能的所有依赖

Rekit: 一键生成项目元素(它是一个IDE)

  1. 直观的 UI 用于生成组件,action,reducer 等
  2. 模板代码遵循最佳实践(只需填入具体逻辑)
  3. 支持命令行方式创建项目元素

Rekit: 重构非常容易

  1. 右键菜单重命名或者删除某个项目元素
  2. 所有相关代码都会一次性重构从而保证一致性
  3. 详细的 log信息显示重构的完整修改

Rekit: 可视化的项目架构

  1. 项目总体架构的可视化图表
  2. 项目依赖关系的图表

Rekit 是如何工作的?

  1. 定义了基于feature 的可扩展文件夹结构(文件夹结构有一定规则,可以进行解析)
  2. 基于最佳实践生成代码和管理项目元素
  3. 提供工具和 IDE 确保代码和文件夹结构遵循最佳实践
    (减少工作量)

遵循最佳实践

1.以 feature 方式组织代码 (功能如果是必须的就放在已有模块底层实现,如果是原有功能增强的实现,则新拆分成功能模块)
2.拆分组件,action 和 reducer
3.拆分路由配置

通过代码自动生成保持一致性
1.文件夹结构一致性
2.文件名一致性
3.变量名一致性
4.代码逻辑的一致性

使用 React Router 管理路由授权

1.实现基础:React Router 的动态路由机制 (
不采用各个页面判断是否登录再redirect的方式跳转,路由配置(添加属性,那些可以在未登录下访问,或者哪些在未登录下不能访问)时就去动态监测当前用户是否登录,如果登录则全部路由生效;反之某些路由配置就不生效===>不生效处理,在解析router JSON数据时根据store中登录状态和路由配置的属性判断,未登录则跳转403,其子路由清空
)
2.区分受保护路由和公开路由
3.访问未授权路由时重定向到登录页面

My Little World

Redux

发表于 2018-12-04

为什么需要redux

原有组件间通信方式错综复杂,逐层传递,不明了,方便组件间通信
用store管理所有组件的状态

redux 特性

single source of truth:状态来源唯一;

可预测性:state +action = new state

纯函数更新Store:输出取决于输入,内部不会依赖任何除参数以外外部元素产生副作用

store 三个函数

getState()

dispatch(action)

subscribe(listener)

action 描述行为的对象集合

reducer 更新state的具体行为

几个工具函数

combineReducers 把多个reducer 合成一个新的reducer,并定义每个reducer在store中的对应节点

bindActionCreators 封装actionCreator和store.dispatch调用过程

1
2
3
4
5
6
7
8
9
10
function actionCreator(){
return {
type:'addItem',
text:'......'
}
}
store.dispatch(actionCreator())
===>
actionCreator = bindActionCreators(actionCreator,store.dispatch)
actionCreator()

connect 创建高阶组件,对store 特定属性进行监听和绑定,从而在这些属性变化时自动刷新组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
import React from "react";
import { bindActionCreators, createStore } from "redux";
import { Provider, connect } from "react-redux";

// Store initial state
const initialState = { count: 0 };

// reducer
const counter = (state = initialState, action) => {
switch (action.type) {
case "PLUS_ONE":
return { count: state.count + 1 };
case "MINUS_ONE":
return { count: state.count - 1 };
case "CUSTOM_COUNT":
return { count: state.count + action.payload.count };
default:
break;
}
return state;
};

// Create store
const store = createStore(counter);

// Action creator
function plusOne() {
// action
return { type: "PLUS_ONE" };
}

function minusOne() {
return { type: "MINUS_ONE" };
}

export class Counter extends React.Component {
render() {
const { count, plusOne, minusOne } = this.props;//这三者通过connect关联store后获得
return (
<div className="counter">
<button onClick={minusOne}>-</button>
<span style={{ display: "inline-block", margin: "0 10px" }}>
{count}
</span>
<button onClick={plusOne}>+</button>
</div>
);
}
}
//组件需要数据,state是store.getState()获得的数据集合根节点
//性能问题:如果将store中的整个state绑定到组件上,那state上任何一个属性发生变化都会引起组件更新,所以向组件传递数据时,将数据绑定到尽可能的最小范围,实现只有绑定的数据变化时才引起组件更新
function mapStateToProps(state) {
return {
count: state.count
};
}
//组件中需要用到的触发函数
function mapDispatchToProps(dispatch) {
return bindActionCreators({ plusOne, minusOne }, dispatch);
}
//使用connect将redux和react 组件结合起来,形成高阶组件
const ConnectedCounter = connect(mapStateToProps, mapDispatchToProps)(Counter);

export default class CounterSample extends React.Component {
render() {
return (
<Provider store={store}> //根节点定义Provider,挂上store,其所有子节点均可以访问到
<ConnectedCounter />
</Provider>
);
}
}

主要流程

1.定义redux store中需要的东西:reducer,actionCreator
2.使用createStore 根据reducer创建store
3.定义组件,确定需要哪些数据和触发函数
4.根据组件需要,创建数据获取函数,封装触发函数相应actionCreator
5.使用connect结合redux和组件形成高阶组件在Provider 中使用

异步Action

异步 action不是特殊action而是多个同步 action的组合使用

一个AJAX的Action发出后,到达中间件,进行预处理,根据预处理的结果dispatch不同的action出去,给reducer,然后更新store

异步Action不是redux的一个概念而是action的一种设计模式

不同的action还是同步的action,只是异步action把这些同步的action放在了不同阶段去dispath

中间件
1.截获某种特定类型的action,进行特殊处理
2.发出action

如何组织action和reducer

‘标准’形式redux action的问题
1.所有Action放一个文件,会无限扩展
2.Action,Reducer分开,实现业务逻辑时需要来回切换
3.系统中有哪些Action不够直观
新的方式
单个action和reducer放在同一个文件,一个文件一个action
然后将所有action导入同一文件,所有reducer导入同一文件
1.易于开发:不用在action和reducer文件间来回切换
2.易于维护:每个action文件都很小,容易理解
3.易于测试:每个业务逻辑只需要对应一个测试文件
4.易于理解:文件名就是action名字,文件列表就是action列表
a sample
问题:拆分这么细的话,文件会多很多,对包的大小也会有影响

答:webpack 确实会为每个文件生成一小段固定的元代码,同样的代码行数,文件越多 bundle 确实会越大。但这个并不会成为性能瓶颈,两个原因:1. 拆分后代码数量不会显著增多,增加的元代码部分相比整个包的大小基本可以忽略;2. 生产环境普遍会启用 gzip,对于重复的元代码信息会被有效压缩,对比拆分前,gzip 后的包大小基本没有差异

不可变数据

redux在修改state时采用新对象替换老对象的思路,不在原来对象上做修改,在新对象中复制老对象,包含要修改的部分,通过对比新老对象的引用不同,引起节点更新—-redux运行基础

为何需要不可变数据
1.性能优化(通过对比新旧state不是同一个引用即可判断store发生了变化,从而不用进行深层遍历,比较具体的值,redux中的store都是不可变数据,每个节点都是不可变数据,当一个组件绑定在一个节点上,只需判断前后状态的引用是否相等,从而判断store是否发生变化,进而决定是否要更新组件)
2.易于调试和跟踪(store变化时可以看到前后状态,便于调试)
3.易于推测(任何时刻可以知道是什么引起store发生变化,根据action前后的状态判断,action是否被进行了正确处理)

如何操作不可变数据
1.原生写法{…},Object.assign() 性能最好

1
2
3
4
5
6
const state = {
filter:'completed',
todos:['learn react']
}
const newState1 = {...state,todos:[...state.todos,'learn redux']}
const newState = Object.assign({},state,{todos:[...state.todos,'learn redux']})

2.immutability-helper 适合需要深层次节点处理,需要引入类库,熟悉相应语法

1
2
3
4
5
6
import update from 'immutability-helper'
const state = {
filter:'completed',
todos:['learn react']
}
const newState = update(state,{todos:{$push:['learn redux']})

3.immer 可以像操作原对象一样生成新对象,性能最差(当节点层数多,结构复杂时,需要为每个属性建立代理),适合应用程序小,场景简单的情况

1
2
3
4
5
6
7
8
import produce from 'immer'
const state = {
filter:'completed',
todos:['learn react']
}
const newState = produce(state,draftState =>{
draftState.todos.push('learn redux')
})

My Little World

路由

发表于 2018-12-04

为什么需要路由

1.单页应用需要进行页面切换
2.通过URL可以定位到页面
3.更有语义的组织资源

基本原理

在组件容器里根据URL决定显示什么样的组件

REACT router特性(和后端路由对比)

1.声明式路由定义

通过react组件标签进行声明,可以放在任何地方,不需要具体的路由表进行声明

2.动态路由

传统路由一旦配置了它就是一个配置文件,成为一个静态文件
而react router的路由是页面在render的时候才会被解析的,有相应路由的标记标签就是有相应的配置,没有标签就没有相应的配置

三种路由实现方式

  1. url路径:通过改变URl更改视图

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    import React from "react";
    import { BroswerRouter as Router, Route, Link } from "react-router-dom";

    const Home = () => <h1>Home</h1>;
    const Hello = () => <h1>Hello</h1>;
    const About = () => <h1>About Us</h1>;

    export default class RouterSample extends React.PureComponent {
    render() {
    return (
    <Router>
    <div>
    <ul id="menu">
    <li>
    <Link to="/home">Home</Link>
    </li>
    <li>
    <Link to="/hello">Hello</Link>
    </li>
    <li>
    <Link to="/about">About</Link>
    </li>
    </ul>

    <div id="page-container">
    <Route path="/home" component={Home} />
    <Route path="/hello" component={Hello} />
    <Route path="/about" component={About} />
    </div>
    </div>
    </Router>
    );
    }
    }
  2. hash路由:使用Router 的HashRouter组件 进行路由容器包裹,切换路由时,
    具体的路径会被放在URl的#后面,通过改变hash更变视图《兼容低版本浏览器》

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    import React from "react";
    import { HashRouter as Router, Route, Link } from "react-router-dom";

    const Home = () => <h1>Home</h1>;
    const Hello = () => <h1>Hello</h1>;
    const About = () => <h1>About Us</h1>;

    export default class RouterSample extends React.PureComponent {
    render() {
    return (
    <Router>
    <div>
    <ul id="menu">
    <li>
    <Link to="/home">Home</Link>
    </li>
    <li>
    <Link to="/hello">Hello</Link>
    </li>
    <li>
    <Link to="/about">About</Link>
    </li>
    </ul>

    <div id="page-container">
    <Route path="/home" component={Home} />
    <Route path="/hello" component={Hello} />
    <Route path="/about" component={About} />
    </div>
    </div>
    </Router>
    );
    }
    }
  3. 存路由:使用react-router 的MemoryRouter组件进行路由容器包裹,路由信息放在内存中管理,URL不变的情况下,即可进行视图切换

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    import React from "react";
    import { HashRouter as Router, Route, Link } from "react-router-dom";
    import { MemoryRouter } from "react-router";

    const Home = () => <h1>Home</h1>;
    const Hello = () => <h1>Hello</h1>;
    const About = () => <h1>About Us</h1>;

    export default class RouterSample extends React.PureComponent {
    render() {
    return (
    <MemoryRouter>
    <div>
    <ul id="menu">
    <li>
    <Link to="/home">Home</Link>
    </li>
    <li>
    <Link to="/hello">Hello</Link>
    </li>
    <li>
    <Link to="/about">About</Link>
    </li>
    </ul>

    <div id="page-container">
    <Route path="/home" component={Home} />
    <Route path="/hello" component={Hello} />
    <Route path="/about" component={About} />
    </div>
    </div>
    </MemoryRouter>
    );
    }
    }

基于路由配置进行资源组织 好处

1.实现业务逻辑的松耦合
2.易于扩展,重构和维护
3.路由层面实现Lazy Load

REACT Router API

1.:普通链接,会触发浏览器刷新
类似a标签,但是不会触发浏览器的刷新,点击时router会接管导航,对切换进行处理不会传递到浏览器,让其进行真正的页面切换
to属性代表链接到的URl的地址
2.:类似 Link但是会添加当前选中状态
可以添加activeClassName属性,当当前链接符合to属性值时,显示相应的样式 还有其他属性,详情请看react-router文档

1
<NavLink to="/faq" activeClassName='selected'>FAQs</NavLink>

3.:满足条件时提示用户是否离开当前页面
切换页面时,跟用户提供一个确认的操作

1
2
3
4
5
import { Prompt } from "react-router";
<Prompt
when={formIsHalfFilledOut}
message='Are you sure you want to leave'
/>

4.重定向当前页面,例如登录判断

1
2
3
4
5
6
7
8
import { Redirect,Route } from "react-router";
<Route exact path='/' render={()=>(
loggedIn?(
<Redirect to='/dashboard' />
) : (
<PublicHomePage/>
)
)}/>

5.:路由配置的核心标记,路径匹配时显示对应组件
path:路由
component:相应要显示的组件
exact:是否精准配置path
多个route path都符合当前路由时,那相应组件都会进行显示

1
<Route exact path="/" component={Home} />

6.:只显示第一个匹配的路由
找到一个匹配的路径就只显示这个路径相应的组件,其他组件不显示

1
2
3
4
5
6
7
import { Switch,Route } from "react-router";
<Switch>
<Route path="/home" component={Home} />
<Route path="/hello" component={Hello} />
<Route path="/about" component={About} />
<Route component={Nomatch} />
</Switch>

参数传递

通过URL传递参数 :
获取参数:this.props.match.params
页面状态尽量通过URl参数定义,方便页面间跳转时数据传递,否则需要将数据转化成组件内部state进行渲染,过程复杂

套嵌路由

1.每个React组件都可以时路由容器
2.React Router的声明式语法可以方便的定义嵌套路由

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
import React from "react";
import {
BrowserRouter as Router,
Route,
Link
} from "react-router-dom";

const Category = ({ match }) => (
<h1>Sub Category {match.params.subId}</h1>
);

const SubCategory = ({ match }) => (
<div>
<h1>Category {match.params.id}</h1>

<ul id="menu">
<li>
<Link to={`/category/${match.params.id}/sub/1`}>
Sub Category 1
</Link>
</li>
<li>
<Link to={`/category/${match.params.id}/sub/2`}>
Sub Category 2
</Link>
</li>
<li>
<Link to={`/category/${match.params.id}/sub/3`}>
Sub Category 3
</Link>
</li>
</ul>
<div id="page-container-2">
<Route
path="/category/:id/sub/:subId"
component={Category}
/>
</div>
</div>
);

export default class NestedRoute extends React.PureComponent {
render() {
return (
<Router>
<div>
<ul id="menu">
<li>
<Link to="/category/1">Category 1</Link>
</li>
<li>
<Link to="/category/2">Category 2</Link>
</li>
<li>
<Link to="/category/3">Category 3</Link>
</li>
</ul>

<div id="page-container">
<Route
path="/category/:id"
component={SubCategory}
/>
</div>
</div>
</Router>
);
}
}

My Little World

构建

发表于 2018-12-04

UI库

ant design 适合企业级应用,复杂交付,密集数据展示
material UI:时尚,花哨,具有的美观性,更适合开发面向消费者的应用(google版原始react实现)
sematic UI:把UI当做一种language来描述(jquery版原始react实现)

选择UI库的考虑因素

  1. 组件库是否齐全(齐全的话可以减少很多工作量,不用自己实现)
  2. 样式风格是否符合业务需求(企业级应用一般要求简洁明了,对于密集性数据展示比较合理;对于移动端面向消费者一般要求好看,button,字体比较大)
  3. API 设计是否便捷和灵活(使用起来是否方便,从而会影响开发效率)
  4. 技术支持是否完善(技术文档是否齐全;github上提issue时,能不能得到快速解答)
  5. 开发是否活跃(是否有稳定团队在进行快速迭代和维护,在遇到BUG或者需要新功能时可以得到满足)

使用next.js 构建react 同构应用

同构应用:浏览器第一次向服务器请求页面时,服务器返回解析好的页面,不需要浏览器执行js来进行渲染(这样可以加快页面首次打开的速度),之后页面的操作/切换,像单页面一样,不需要浏览器刷新,均由前端完成,包括UI渲染,页面路由切换等,不需要再向服务端请求页面

next.js的几个规则
1.页面就是pages目录下的一个组件
所有页面放在pages文件夹下,一个页面即一个组件,文件名和文件路径对应路由路径
2.static目录映射静态文件(图片)
3.page具有特殊静态方法 getInitialProps
next.js提供给react组件初始化props的方法

命令行装包:

1
npm install --save react react-dom next

在页面中使用其它 react 组件
1.页面也是标准的node模块,可使用其它 react组件
2.页面会针对性打包,仅包含其引入的组件
,不会加载其他额外的资源

所有组件放在components文件夹下,在pages文件夹下的页面js中使用时,直接用路径import进去

使用Link实现同构路由

1.使用 next/link 定义链接

1
2
3
4
5
import Link from 'next/link'
export default ()=>
<div>
<Link href='/about'><a>here</a></Link>
</div>

2.点击链接时页面不会刷新
如果使用a标签会产生刷新

3.使用 prefatch预加载目标资源
如果不添加该属性,则链接对应的组件,在切换的时候再加载相应页面打包的内容,有该属性,则next.js 打包时就会获取所有链接对应的组件,会预加载所有的js内容,从而提高页面切换速度,但是并不会预加载需要从服务器端API请求的数据

4.使用 replace 属性替换 url
替换当前页在路由中的history,无法后退回当前页

动态加载页面

1
2
3
4
5
6
import dynamic from 'next/dynamic'
const DynamicComponet=dynamic(import('../components/hello'))
export default ()=>
<div>
<DynamicComponet/>
</div>

页面在加载时,除了加载整个页面的main.js,还会加载动态加载的组件的js包,当这个动态组件被render的时候才会被加载

测试

react 让前端单元测试变得容易
1.react 应用很少需要访问浏览器 API
2.虚拟 Dom 可以在 NodeJs 环境运行和测试 (不需要浏览器环境,在内存中render虚拟DOM即可完成测试)
3.Redux隔离了状态管理,纯数据层单元测试

单元测试涉及的工具
1.Jest: Facebook 开源的 JS 单元测试框架 (零配置,可以直接开始测试)
2.JS dom浏览器环境的 NodeJS 模拟 (一个在nodejs中可以模拟浏览器的library)
3.Enzyme:React组件渲染和测试 (在nodejs中渲染虚拟DOM进行测试,通过进行不同程度的render(shallow Rendering,Full Rendering,Static Rendering,进行不同程度的测试,比如利用shallow Rendering浅渲染测试渲染结果是否符合预期的DOM结构)
4.Nock: 模拟 HTTP请求 (模拟得到请求返回数据)
5.Sinon: 函数模拟和调用跟踪 (例如测试组件UI时,跟踪响应函数有没有被调用,调用几次,而不是去测试响应函数是否符合预期,测试响应函数是否符合预期属于响应函数测试范畴。集成在JEST中)
6.istanbul:单元测试覆盖率(通过对已有代码的修改和埋点去判断相关代码有没有被执行到)

开发调试工具

eslint
1.使用 .eslintrc 进行规则的配置
2.使用 airbnb 的 JavaScript 代码风格

检查语法风格,拼写错误,会报错,保证代码的一致性

Prettier
vscode 的一款插件,根据一定规则做代码格式化 .eslintrc
1.代码格式化的神器
2.保证更容易写出风格一致的代码
不会报错,会直接根据规则将代码格式化

React DevTool chrome插件
定位组件,可以以组件树的形式查看DOM结构,
勾选highlight updates可看到需要更新的组件

Redux DevTool chrome插件
可查看action和action引起的变化
time machine 功能,可以回溯action,方便观察loading过程,进而对loading进行优化
可自动生成测试代码

理想架构

易于开发(但可能会不容易扩展和维护)
1.开发工具是否完善 (采用的技术栈是否有相应的开发工具来支持)
2.生态圈是否繁荣 (是否有类似项目遇到过相同问题可以借鉴)
3.社区是否活跃(提问有人回答)

易于扩展
1.增加新功能是否容易
2.新功能是否会显著增加系统复杂度(系统放大之后会难以扩展和维护,考虑引进新功能时,如何架构能保证减少系统复杂度的提高)

易于维护
1.代码是否容易理解 (基于一定的最佳实践规范去编写,保证多人开发有一定的规则)
2.文档是否健全(通过注释,架构图,具体文档描述项目中的一些关键点,方便后续工作人员查看,也方便日后回头开发能快速理解关键点,来维护已有项目)

易于测试
1.功能的分层是否清晰 (UI,数据层之间依赖少)
2.副作用少 (模块高内聚,对外部依赖少)
3.尽量使用纯函数(输入决定输出,确定)

易于构建
1.使用通用技术和架构 (保证现有项目不需要定制化,就可以打包部署)
2.构建工具的选择(webpack ,roolup,使用常用技术栈,减少额外配置)

大型前端应用需要拆分复杂度原因
当项目增加更多功能的时候,项目复杂度并不会指数级的上升,始终保持在一个可控的范围之内

拆分复杂度技术架构

从功能上进行区分,将业务逻辑拆成高内聚松耦合的模块,每个模块负责一个功能,拥有自己的componet,action和reducer,这样即使当一个功能需要删除时,只要删除相应的模块即可,也保证其他模块不受影响;路由不再由一个文件统一定义,每个feature自己定义一个路由,然后由route的loader把每个模块的路由文件加载进根结点的routing文件

技术上的component,路由,reducer是按照功能的方式组织在模块上

如何组织 component,action 和reducer
文件夹结构
• 按 feature 组织源文件
• 组件和样式文件同一级
• React 单独文件夹
• 单元测试保持同样目录结构放在 tests 文件夹

组件和样式组织
一个功能的组件js和样式文件放在一个模块中,再由一个index.js引入各个模块的组件js文件,一个style文件引入各个模块的样式文件,进行统一管理输出,打包时就只是打包index.js和style文件
注意:
如果组件中引入样式文件,那么当这个组件被重复使用时,样式文件会被多次引入,打包时就会多次进行打包,造成不必要的代码冗余
使用上述加载方式,样式文件独立加载,js文件只需要加载它所需要的资源,就不会发生多次加载

action 和 reducer组织
一个模块的,action 和 reducer 一一对应放在同一文件,再由一个actions.js引入所有action,一个reducers.js引入所有reducer,这两个文件只是充当loader的角色
好处:action 和 reducer 非常小,不会被无限的去扩展,更容易理解

CONSTANTS.js中命名的常量一功能名开头,避免与其他模块常量命名冲突

再由rootreducer加载所有模块下的reducer,进行combineReducers挂载,实现全局使用

优点:一个模块内部是高内聚的,各个模块之间是松耦合的

主要思想:使用 root loader 加载 feature的各个资源

路由配置
每个模块(页面级)路由通过export JSON格式数据 ,自己配,再loader到根路由,由根路由统一处理解析json格式为react Router的声明式形式

每个模块都有都有自己的专属路由配置
顶层路由使用JSON配置更易维护和理解

1…121314…27
YooHannah

YooHannah

264 日志
1 分类
23 标签
RSS
© 2025 YooHannah
由 Hexo 强力驱动
主题 - NexT.Pisces