我们看下动画效果如下:
如果我们打开控制台去看元素上面的属性,我们会发现,这样的效果其实主要是两种动画效果叠加:卡片3D旋转和后面的辉光层的效果,我们逐一来看下实现的逻辑。
本文的实现代码实现基于Vue3,对Vue3语法不了解的可以先看一下这篇文章;想看最终的实现效果的小伙伴可以直接戳这里查看
在实现效果之前,我们先来回顾一下,我们在打印鼠标事件的时候,经常会看到事件对象event上面有各种属性,offsetX、clientX、pageX和screenX等等,那么这些属性有什么区别呢?我们在使用时到底该用哪个属性呢?
为了简化流程,下面主要介绍X轴方向上的属性,Y轴方向属性同理。
我们按照从小范围到大范围的原则,先来看下offsetX属性,根据MDN上的介绍,只读属性offsetX
规定了事件对象与目标节点的内填充边(padding edge)在X轴方向上的偏移量;从offset单词也能看出,它是相对于目标div的偏移量,我们用图形通俗的表示如下:
然后是clientX,client有客户端的意思,因此它表示事件发生时的浏览器客户端区域的水平坐标,是相对于浏览器窗口
的边距,会随页面滚动而改变,用图形表示如下:
注:clientY是不包含浏览器的书签、地址栏和标签栏等的高度。
再是pageX属性,它是相对于整个文档
的水平坐标,不随着页面滚动而改变;它和上面clientX看似是相同的,大多数情况下它们的值也是相同的;但pageX会考虑页面的水平方向上的滚动,因此有下面的公式:
ev.pageX = ev.clientX + window.scrollX
通过图形我们也能看出,当文档大小超出浏览器区域时,pageX明显是要大于clientX的值:
最后是screenX,表示范围是最大的,使用频率也是最低的,表示距离电脑屏幕边缘的水平坐标偏移量:
除此之外,还有一个属性是movementX,它提供了当前鼠标移动事件
和上一次鼠标移动事件
之间鼠标在水平方向的移动值;在处理移动元素等场景时就不需要我们记录上次的位置数据了,因此它的计算公式如下:
currentEvent.movementX = currentEvent.screenX - previousEvent.screenX
首先我们来看下第一个实现效果,首先是让卡片实现3D的旋转效果,我们先在页面上将卡片的布局排列好:
1 |
|
我们给每个卡片设置一个固定的宽高和位置,使用绝对布局让其堆叠排列;
接下来,我们就需要在dom节点上监听鼠标事件后对5个卡片进行样式的修改;但是这里,如果写五遍监听函数显然是不合适的;想要在多个节点元素上复用相同的逻辑,可以把这个逻辑以组合式函数
的形式提取,我们单独新建一个useMouse.js
文件:
1 |
|
导出我们需要用到的styles样式、mouseMove鼠标移动函数、mouseOut鼠标移出函数;使用方式也很简单,导入组合式函数,传入宽高以及卡片的节点,卡片节点在后面要用到。
1 |
|
接下来就是最最最最核心的useMouse的代码了;我们想象一下,需要在鼠标移动的时候,让卡片旋转向前倾斜对应的角度,而在鼠标离开的时候,则清空角度样式:
1 |
|
我们想象一下,鼠标某一时刻的位置假设在卡片的左上方某个点,获取鼠标距离卡片左上角的offsetX和offsetY,旋转角度需要计算出蓝色框的长宽:
绕着Y轴旋转的角度就是蓝色框的长度除以一半的宽度,绕着X轴旋转的角度就是蓝色框的高度除以一半的宽度。
我们定义一个最大的旋转角度MAX_DEG,然后用一个笨办法,在四个区域分别计算出不同的角度:
1 |
|
这里的CSS3的perspective指定了观察者与平面的距离,使具有三维位置变换的元素产生透视效果,如果不设置就没有透视的效果;我们看下鼠标的效果基本就可以实现3D倾斜的效果了。
继续对代码进行优化,四个区域太麻烦了,我们只考虑X轴和Y轴方向上的rotateX和rotateY的计算方式,上面代码最终可以优化成两行:
1 |
|
接下来就是鼠标移动时候的背景辉光效果了,我们首先向卡片下插入一个dom节点,用来渲染辉光:
1 |
|
给hover-element元素
设置样式,在卡片悬浮的时候才让元素显示出来:
1 |
|
在鼠标移动时,去设置悬浮元素的位置偏移:
1 |
|
但是我们这样设置之后会发现,后面的hover-element元素在鼠标移动的时候,一直在不断的闪现:
这是因为鼠标在卡片上移动的时候,我们给hover-element元素设置了偏移,偏移的hover-element元素阻断了mousemove事件,让hover-element元素又回到了原点,在鼠标不断移动时,导致了辉光呈现出了闪现效果。
我们可以在卡片下面再嵌套一层div作为卡片内容,让它始终位于hover-element元素的上方:
1 |
|
这样我们的辉光效果就很流畅了。
我们将文案和图片添加到卡片内容后会发现,当鼠标悬浮到图片元素上时,辉光元素会出现偏差;由于笔者这边是将图片元素设置成absolute的,而offsetX/offsetY是相对偏差,当悬浮到目标图片上是,offset是相对于图片的位移,而不是相对于外层元素,这样就会导致错位。
我们在mouse移动时,获取图片的left和top,将其数据加到offsetX/offsetY上,即可修正偏差:
1 |
|
这里getComputedStyle实时获取图片的left和top会比较耗费性能,这里的优化点,可以在mounted的时候提前获取图片的left/top,提前将其存储起来。
本文最终的实现效果可以戳这里查看。
本文首先总结了鼠标事件中offsetX/clientX/pageX/screenX每个属性的用法,了解了每个属性的用法和区别;然后实现了3D倾斜旋转和辉光元素的效果,将具体的实现逻辑抽取到了独立的js文件中,方便复用逻辑;最后我们发现某些元素下鼠标悬浮的效果会有偏差,排查原因后对偏差进行修正。
]]>本文所有源码敬请关注公众号【前端壹读】,后台回复关键词【辉光卡片】即可获取。
首先我们搭建Threejs的基本环境,我们将初始化的元素都封装到一个类中;在使用时,直接初始化类即可:
1 |
|
代码比较多,这里主要就是搭建了场景、相机、渲染器、轨道控制器等基本的Threejs元素,实现一个Three画布中该有的元素;然后把我们的类放到页面中初始化;在vue中,我们可以放到onMounted钩子函数中执行:
1 |
|
这个时候,我们只要一改变页面的宽高大小,我们的画布由于没有及时更新,就会出现空白的区域;我们在构造函数中绑定页面大小监听事件,重新更新renderer和相机:
1 |
|
在vue中,页面unmount时调用beforeDestroy函数,解除监听事件:
1 |
|
环境初步搭建好后,我们就可以向画布上添加物体了,这里我们具体来学习水Water、天空Sky等物体的使用方式。
这里我们需要从Three.js源码中获取一个法线贴图,拷贝到我们项目public目录:
1 |
|
所谓的法线贴图(Normal Map)是一种纹理映射技术,用于在渲染过程中模拟物体表面的细节和几何形状。它通过使用RGB颜色值来存储每个像素点的法线方向信息。
法线贴图也广泛应用于游戏开发、动画制作、虚拟现实等领域,以提供更逼真和优化的视觉体验。
我们打开复制法线贴图看一下,是一张偏蓝紫色的图片;
通过水面的法线贴图,我们就可以模拟水面的波纹效果以及太阳光的照射效果了;我们从threejs中引入Water
类,构建水平面物体:
1 |
|
这里我们Water构造函数接收两个参数,第一个是物体,我们直接使用一个较大的PlaneGeometry作为水平面;第二个参数是WaterOptions
,其中主要的是waterNormals属性
,就是我们的法线贴图,通过TextureLoader加载,加载完成后我们让它在S和T方向上都重复平铺开来;还有一个属性是waterColor
,就是水面的基本颜色,我们选一个接近海水的蓝色即可。
完整的WaterOptions参数如下,其他属性的含义后面会进行调试:
1 |
|
我们先打开页面看一下效果,海水的纹理也呈现出来了:
海水纹理加载后,我们就可以通过海水材质的uniforms属性
来让纹理动起来:
1 |
|
那么这里的uniforms是什么?为什么我们更改了time的value就可以让波纹动起来呢?我们悬浮查看一下Water的材质,发现它是一个ShaderMaterial
材质:
ShaderMaterial在定义顶点着色器和片元着色器之外,还会声明uniforms属性,可以给顶点着色器和片元着色器中的变量传值,达到不同的渲染效果,比如我们查看Water.js的源码就能看到它的uniforms属性里面有一个time
参数,初始化的value是0.0,因此我们改变这个值就可以控制水面波纹的渲染效果了。
1 |
|
在后面天空的材质中我们会看到uniforms中更多参数的用法。
海水做完了,我们来实现天空中的效果;这里的太阳和天空是一体的,Three.js都集成到Sky类中,因此我们不需要去单独做一个太阳的物体,只要初始化一个太阳的位置,后续传入即可:
1 |
|
这里Water的sunDirection是一个Vector3向量,我们使用copy
函数将传入的太阳xyz位置进行赋值。我们在后面调试的时候,只要更改太阳的位置,就可以同时更改阳光在海水和天空的效果;
1 |
|
这里实例化了一个天空,setScalar设置了一个放大的倍数,我们跟海平面设置成一样大小;uniforms中的sunPosition
属性也是太阳的位置,我们传入sun位置即可;其他的一些属性也是调节天空的参数,我们在下面调试的时候会详细分析每个参数的意义。
加上天空后,我们看一下页面的效果,现在整体的效果就更加的真实了,有种海上落日余晖的场景了。
但是这里加了太阳之后,水面显示会有点泛白,是因为太阳的位置的向量长度太长;我们上面初始化太阳位置是一个Vector3三维向量,不过这并不表示太阳实际在天空中的真实位置,只是通过向量的角度方位来模拟太阳的位置;而向量是有长度的,向量越长,太阳光就越强烈,水面也就更白了。
因此,这里需要介绍一下归一化的概念
,归一化在机器学习中也有着广泛的应用,就是将所有的数据压缩到0到1之间的范围;Three.js中的归一化,其实就是将向量的xyz等比例缩放,将整个向量的长度缩放到长度为1。
更多向量的学习,可以参考这篇文章:向量方向(归一化.normalize)
比如下面的向量p1,根据初中学的两点之间计算公式,它的长度是√(10*2+20*2+30*2)
,算出来长度大概是37多,而通过normalize函数
之后,我们再去获取length,得到的就是单位1:
1 |
|
因此,回到太阳位置的设置,我们传到sunDirection.value中后,copy函数会将传入的sun位置复制到sunDirection向量,并返回自身向量;最后再调用一下normalize函数
就可以将向量设置成单位向量了:
copy函数作用将所传入Vector3的x、y和z属性复制给这一Vector3,并返回自身。
1 |
|
我们通过查看Water.js源码中sunDirection初始值,发现是Vector3( 0.70707, 0.70707, 0 ),也是一个归一化的向量。
再次看页面效果,我们发现海水也更加的柔和了,仿佛你女朋友看你的眼神一样的柔和:
在Three.js的Demo中,我们可以看到一个多面体在不停的上下浮沉,外面的材质被海水和天空所浸染,就像生活在大城市的我们一样,沾染着世俗气息,随波逐流。。。。
我们首先构建一个二十面体的球形物体:
1 |
|
在渲染的时候,控制y轴方向做sin函数运动,同时绕着x轴和z轴不断的旋转:
1 |
|
这时,由于我们使用的MeshStandardMaterial材质,因此我们会看到一个灰色的,光滑的球体在水面上下漂浮。
如果我们想让环境的光照射到圆球的表面,可以使用PMREMGenerator
,它的全称叫预计算辐射度环境贴图(pre-computed radiance environment map,PMREM)生成器,PMREMGenerator可以根据当前场景和光照计算出辐射度环境贴图,并将其缓存在内存中,方便后续使用
1 |
|
它的用法也很简单,构建一个类,然后调用fromScene从当前场景中生成辐射环境贴图,赋值给scene.environment
。
上面我们创建了蓝天、海水等物体,我们会看到材质的uniforms属性中有很多参数,但对每个参数的用法却并不清楚;本节我们就看实际看下每个参数的实际效果。
我们在创建物体之后,先来添加太阳位置的调试看下效果:
1 |
|
通过addFolder
单独创建一个单独展开的文件菜单,然后像里面添加对应的变量设置;由于这里我们已经有了全局的this.sun变量,它里面也有xyz属性,因此我们直接拿来用即可。更新参数后,我们需要同步更新water和sky的uniforms,因此这里我们抽离一个单独的函数updateSunPosition。
我们改变太阳方位后,发现小球表面的光照辐射强度还是没有改变,这是因为我们在初始化的时候调用了pmremGenerator.fromScene
生成了辐射环境贴图;因此在updateSunPosition函数中,我们再次调用,给小球表面进行重新渲染:
1 |
|
我们还记得海水的WaterOptions参数中有很多的属性字段,我们看下不同属性的效果,首先是time属性,控制海水波浪起伏的速度,我们单独创建一个参数变量:
1 |
|
在render函数渲染的时候,将固定变量1.0替换成我们的参数变量,我们可以查看效果。
除了time,还有两个属性alpha和distortionScale可以加到我们的调试面板调试,alpha控制透明通道的值,色值越小越泛白;而distortionScale控制水面波纹的扭曲程度,数值越大,波纹越扭曲。
1 |
|
我们将属性添加到gui中调试时默认展示属性的英文名称,比如这里的distortionScale,很多时候会不知道这个属性的作用;因此我们加个name函数
给它一个中文的名称,在调试时更容易知道其作用。
这里就不截图展示具体效果了,大家可以点击这里自己手动调试查看效果。
下面就来调试天空的参数,我们看下最重要的两个参数,turbidity浑浊度和rayleigh锐利值;还是和上面一个,我们在gui里给天空单独创建一个折叠的菜单:
1 |
|
浑浊度turbidity大概的效果就是太阳被云层遮挡的光晕的浑浊程度,数值越小,太阳的轮廓就越清晰;锐利值rayleigh则更像是太阳被乌云遮住的感觉,数值越大越有日落西山的感觉。
最终所有调试效果可以点击这里查看。
学习Three.js很痛苦的一点就是很多时候不知道这里调用这个函数有什么用,还找不到资料解释,很多函数里面都会涉及到了数学或者图形学方面的知识,调试的不方便也极大的增加了我们学习的成本;同时网络上也充斥着各种版本的代码,质量也都参差不齐;比如笔者在学习water和sky的uniforms设置时,water后面调用了一个normalize函数,而sky没有,让人很费解,一开始并不了解其中的原理;不过通过更深层的学习查资料,加上不断的尝试,最终透彻理解了就有种恍然顿悟的感觉。
]]>本文所有源码敬请关注公众号【前端壹读】,后台回复关键词【海天一色】即可获取。
我们先来看一下最终的实现效果:
面对这种复杂的(奇奇怪怪)动画需求,作为老前端人了,上来肯定就是质问设计师:
你能不能做个GIF图?
直接贴个GIF图不就完事了,不过,设计给出了自己理由,最后出图的文件太大以及GIF图容易模糊,另一个原因其实是我们的设计做动画不专业;因此,在得到肯定不能的答复之后,设计给出的方案是:使用蒙版动画
。
所谓的蒙版动画,也就是;类似遮罩层动画,将一个遮罩层首先覆盖在想要动画的物体上方,比如我们这里的箭头;然后随着时间的推移,逐渐的抽离遮罩层,实现动画的目的。
不过这种的动画效果用在这里感觉有点low,不是很合适;因此我们还是来一点点实现吧。
就在不知怎么做的时候,好在上网查资料,一眼看到了CSS新的属性offset-path
给了我一丝希望;传统的CSS3动画只能实现平移、缩放、拉伸等规则的路径动画。
而offset-path这个属性就比较强大了,可以让元素沿着指定的路径实现不规则路径,可以是任何的形状;我们看下其浏览器的支持程度:
它的语法很强大,支持画circle(圆)、ellipse(椭圆)或者polygon(折线)等多种图形的函数,不过我们这里就使用自定义的path函数即可,传入一个path路径:
1 |
|
对路径语法不熟悉的小伙伴可以学习一下MDN路径的这篇文章;此外它还有两个重要的属性,一个是offset-distance
,表示元素移动的距离,可以是px单位,也可以是百分比单位:
1 |
|
我们可以控制offset-distance属性来实现元素的动画效果,一般动画时设置从0%到100%。
另一个属性就是offset-rotate
,定义了元素运动时的角度和方向,可以是某个具体的角度,也可以是auto,也可以组合起来一起使用,auto表示让元素运动时自己根据轨迹调整角度即可:
1 |
|
因此有了上面的属性,我只要画一个div,给它一个线性过渡的背景CSS,然后让它沿着指定路径移动不就行了,说干就干。
1 |
|
上面的path路径看着很复杂,其实就是一个简单的S型曲线的运动,这里我们让div沿着S型曲线运动;虽然想法很美好啦,不过现实也十分的现实,最后的效果如下:
因为我们的div是长条的,并不是流体,所以我们看到的效果就是一长条沿着固定的轨道晃晃悠悠的移动,这肯定不行了,不能用div了。
那怎么办呢,笔者又思考了很久,突然一个想法又冒出来了,想起了微积分的思路;同理,既然一个div太长了,那我把它切割成多个不就行了,只要我们的刀把div切得够细,再拼接起来,用户就看不出来是拼接的,说干就干。
1 |
|
这里代码看着很复杂,其实很简单,首先我们循环了20个div,然后用scss的@for
语法给20个div设置样式,这里我们给每个div一个背景颜色的从右到左线性过渡,逐渐透明,最后使用animation-delay设置一个延迟时间,效果如下:
最后的效果就像一条贪吃蛇一样,从头连到尾;不过这样的实现方式可能会有问题,如果delay的间隔太久,div之间就会有空隙,如果间隔太小,就会导致div之间有颜色重叠的部分,导致颜色排布不是很均匀。
不过我们可以通过将div设置成宽只有几个px的长方形条状,同时把div切割的数量拉大,切成100或者200的div,这样就会好很多。
箭头拐弯的效果我们已经实现的差不多了,只要再给它加上合适的路径即可;下面那么我们再来考虑一下,如何把箭头动画结合到背景中去;在切图时,我们肯定是需要将背景中的建筑元素切到单独的一张图片,但是箭头的轨道如果我们自己用代码实现会非常棘手。
因此笔者的想法是将轨道单独让设计切图出来,导出成svg,我们就可以参考svg中的代码,能够知道轨道实现的逻辑了;因此我们将整体放到SVG中实现起来会比较方便一点。
首先我们看一下轨道的实现方式,轨道环的实现很简单,直接使用path,设置stroke颜色和一个opacity就可以:
1 |
|
我们重点来看下轨道中间的点,这里就不得不说到svg的stroke-dasharray
属性,这个属性可以用来控制实现描边的点的图案样式;它的语法很简单,就是传入一个数列:
1 |
|
这个数列中的数用逗号和空格间隔开,比如"5, 3, 2"
这种形式,那么每个数字代表什么意思呢?我们先从简单的一个数字和两个数字开始:
1 |
|
stroke-dasharray中的每个数字依次来表示短划线和缺口的长度,当一个数字10的时候,表示短划线和缺口的长度都是10,因此我们就会看到它的距离是比较均匀的;而两个数字的时候,第一个数字表示短划线,第二个数字表示缺口,因此我们看到短划线较长,而缺口较短。
理解了上面数字的含义,我们再扩展到三个数字和四个数字来看一下;如果是奇数数字的话会比较特殊,根据MDN文档上的解释:
如果提供了奇数个值,则这个值的数列重复一次,从而变成偶数个值。因此,5,3,2 等同于 5,3,2,5,3,2。
由于奇数个数字在循环的时候会有一个位置衔接不上,因此这个属性定义的时候就将奇数个自动扩展到了偶数个,我们看下具体代码理解一下:
1 |
|
我们在每个短划线和缺口处用数字标记一下:
我们发现,3个数字的时候,在第一次排列之后,第二次排列的时候,60所在的位置自动变成了缺口位,而不是短划线,这样自动进行了一次顺序扩展,这就是这个属性将奇数个自动扩展到了偶数个的效果;而4个数字则是照常循环排列。
理解了stroke-dasharray
属性,我们的轨道也可以在svg下来最终完成了。
下面的轨道有了,我们就需要将切好的箭头放到svg中,在svg中,我们使用rect来代替div;那么,如何让rect动起来呢?
svg也有自己的动画元素,这里使用animateMotion元素
,它的作用就是让一个元素如何沿着运动路径进行移动。它的用法也很简单,在元素下层嵌入animateMotion元素,最重要的属性就是path
,相当于CSS3中的offset-path
属性:
1 |
|
rotate属性也相当于CSS3中的offset-rotate,因此我们理解了上面CSS中的offset-*
等一系列属性,animateMotion也就很好理解了。
通过width/height属性,我们可以设置svg画布的固定大小:
1 |
|
但是,在实际的场景中,我们经常需要让画布自适应外部div的宽高,以实现画布呈现大小适应页面的缩放;这里就要用到svg另一个属性viewBox
了,我们看下mdn上对这个属性的介绍:
viewBox属性允许指定一个给定的一组图形伸展以适应特定的容器元素。
它的属性值是一个包含四个参数的列表:min-x,min-y,width,height,四个值可以用空格或者逗号分隔开;
1 |
|
viewBox顾名思义就是视图盒子,把它理解成截图工具呈现的效果就行;简单理解,min-x和min-y就是截图的右上角的x和y坐标,width和height就是截图区域的宽度。
我们看下它的具体效果,首先,不带viewBox的情况下展示svg下的内容:
1 |
|
元素正常大小显示,这时候我们给它加一个viewBox:
1 |
|
我们会发现两个元素放大显示了,这个也很好理解,我们在200200的画布上截出一个6060的区域,然后就会等比例放大呈现出来。
因此回到我们的箭头svg,为了实现自适应的效果,我们去掉svg的宽高,加上viewBox等于我们的画布宽高即可:
1 |
|
点击查看本文的实现效果。
我们从CSS3的offset-*
属性入手,了解了如何让一个元素实现非规则路径下的动画效果;然后我们为了方便,将动画效果迁移到了svg中去实现,对svg中的关键属性stroke-dasharray
进行了详细的学习;最后为了实现自适应效果,使用了viewBox属性。
大多数Linux发行版都提供了Node.js的安装包,我们可以直接使用包管理器进行安装:
1 |
|
但是包管理器安装的Nodejs版本比较老旧,有些是10或者12版本的,因此我们可以通过去官网通过下载源码包的方式进行安装。
首先打开Node.js中文网,官网提供了不同系统平台的安装包,我们下载对应平台的即可,例如这里的node-v18.18.0-linux-x64.tar.gz
:
我们可以选择这里Linux(x64)版本的,如果想要其他版本的,也可以选择全部安装包
;这里我们用wget命令下载:
1 |
|
解压压缩包并且移动到/usr/local目录下:
1 |
|
创建软链接:
1 |
|
验证node是否安装成功:
1 |
|
首先看一下笔者的系统环境,这里由于之前一篇文章中笔者建议使用Linux,而有一些评论说笔者没用过Linux,这里手头正好有一台Ubuntu22系统就用来折腾一下吧:
1 |
|
下面我们就需要准备仓库了,首先我们新建一个文件夹用来存放克隆的仓库:
1 |
|
然后克隆众多的Echarts仓库,有些仓库文件比较大,受限于不同的网络环境,可能会有失败的情况,需要多次尝试。
1 |
|
最后我们看到的目录结构就是这样的:
1 |
|
文件夹准备好了,之后就可以对我们的克隆下来的仓库进行构建了;这里需要说一下的是echarts-website
这个文件夹,这个仓库主要用来其他仓库存放构建出来的静态资源文件,因此最后我们的服务器也是在这里启动的;如果不构建,直接在这里启动服务器,我们会发现浏览器自动跳转到Echarts的官方地址;因此下面我们对各个仓库进行构建操作。
进入echarts-handbook,首先我们npm i
安装依赖,然后打开configs/config.localsite.js
,修改配置;这里我们需要确定最后的服务器的IP和端口端口,比如这里笔者就使用8070端口:
1 |
|
最后执行npm run build:localsite
构建,如果出现下面的日志就说明构建成功了:
echarts-examples是示例的目录,我们进入目录,npm i
安装依赖,然后执行npm run localsite
构建,如果出现下面的日志就说明构建成功了:
进入echarts-doc,还是npm i
安装依赖,修改config/env.localsite.js
,还是改成需要部署的主机IP和端口号
1 |
|
打开echarts-doc/build.js
,在public后添加zh
,否则会提示找不到public/js/doc-bundle.js
文件。
1 |
|
然后执行npm run localsite
进行构建,如果出现下面的日志就说明构建成功了:
进入echarts-www目录,执行npm i
安装依赖,打开config/env.localsite.js
,将下面三个地方的地址更换成自己服务器的IP和端口
1 |
|
然后执行npm run localsite
构建,本次构建过程比较长,如果出现下面的日志就说明构建成功了:
上面所有仓库构建完成后,我们回到echarts-website
目录,这里我们可以通过一个简易方便的node服务器工具来启动服务器,-p
参数可以用来指定端口号:
1 |
|
我们打开服务器的地址localhost:8070
,这边可以换成你自己的服务器IP+端口,就可以看到Echarts文档了,很多示例和手册都是可以正常查看的,打开某一个具体的示例也能看到效果,和Echarts官网简直是一毛一样,内网访问基本就是秒开了:
笔者已经将本文打包好的echarts-website目录整理上传,公众号【前端壹读】后台回复关键字【Echarts官网】
即可获取。
Stable Diffusion是一种通过模拟扩散过程,将噪声图像逐渐转化为目标图像的文生图模型,具有较强的稳定性和可控性,可以将文本信息自动转换成高质量、高分辨率且视觉效果良好、多样化的图像。
首先需要安装python3的环境,SD(Stable Diffusion的简称,下面都用SD进行指代)推荐的版本是python3.10,这里看下笔者的电脑环境:
1 |
|
然后我们执行git clone https://github.com/AUTOMATIC1111/stable-diffusion-webui.git
,将项目克隆到本地:
项目克隆下来后,我们需要切换到SD项目里面去安装依赖:
1 |
|
在安装过程中我们会看到有下面这种报错的情况,一般都是某个依赖安装不上,我们可以查看具体的报错信息,然后尝试手动安装:
比如pytorch,下载安装比较慢,会导致报错,我们可以尝试下面的命令进行手动安装:
1 |
|
pytorch分为CPU版本和GPU版本,其中GPU版本的安装需要先安装CUDA,可以参考CUDA和cuDNN安装教程。所有依赖安装完成后,如果我们直接执行启动命令:
1 |
|
有可能会如下报错:
1 |
|
我们需要使用python创建一个虚拟环境:
1 |
|
看到以下提示,就说明启动成功了,我们在浏览器打开http://localhost:7860
:
如果我们启动时想要加一些参数,比如,启动时不想要cuda的校验,打开webui.sh
,添加以下命令行参数:
1 |
|
这样启动后我们服务器只能通过localhost访问,如果我们想要服务器可以通过远程访问,添加参数:
1 |
|
我们还可以为SD设置访问密码的限制:
1 |
|
SD还有一些不错的插件和扩展可以配合使用。
反向提示词(Negative prompt),就是我们不想出现什么的描述,例如一些容易变形的身体部位的描述等。EasyNegative是目前使用率极高的一款反向提示词embedding模型,可以有效提升画面的精细度,避免模糊、灰色调、面部扭曲等情况,适合动漫风大模型。
它的用法也是最简单的,下载easynegative.safetensors文件
,并且放到stable-diffusion-webui/embeddings/
目录下,然后在反向输入词中,输入触发关键词easynegative
,就可以使用了;除此之外,还有以下一些可以用的Embedding模型:
我们将Embedding模型放入文件夹后,重启SD,在webui中找到Embedding面板
,点击相应的模型就可以使用了。
相信全是英文界面的webui肯定没有几个小伙伴有耐心一个个的看下去,因此汉化插件是必备的;官方有好几种安装方式,我们通过最简单的下载zip的方式;打开stable-diffusion-webui-localization-zh_CN,下载zip文件
解压zip文件,并把文件夹放到stable-diffusion-webui/extensions/
文件夹中,放好之后目录如下:
重启webui,点击Extensions => Installed
,可以看到我们的插件已经安装;确保已经勾选插件,如果没有,勾选后点击Apply and restart UI
按钮启用:
然后就需要切换界面的语言,找到Settings => User interface => Localization
选项,在下拉菜单中选中zh_CN
,然后点击上面的两个按钮`Apply settings和
Reload UI``,等待一段时间后就可以看到我们的界面切换中文了。
sd-webui-prompt-all-in-one也可以算是SD的必备实用插件之一了,它的作用在于提高提示词/反向提示词输入框的使用体验。它拥有更直观、强大的输入界面功能,它提供了自动翻译、历史记录和收藏等功能,它支持多种语言,满足不同用户的需求。
它的安装也非常简单,将插件克隆下来到extensions目录即可:
1 |
|
重启服务器,我们在界面上看到有各种密密麻麻的提示词,插件就生效了。
我们来看下插件的主要功能,首先就是实时翻译功能了,由于SD只支持英文,对我们来说,怎么让它知道我们想要画什么图案就不是那么友好了,因此翻译工具是必备的;在插件的输入框中(注意,是插件的输入框,不是SD原始的输入框)输入内容后,插件会自动帮我们翻译。
同时,我们也不需要一个一个的输入翻译,可以在一次性输入完成后,点击一键翻译
的按钮,将输入框中的内容统一进行翻译。
对于密密麻麻的提示词,我们可以在下面可视化的快速调整某一提示词的权重或者顺序,不会因为各种符号问题而烦恼了。
而且在使用一段时间后,肯定积累了一些高频常用的提示词,使用收藏工具,可以把这些词收藏起来,后面使用就很方便了。
adetailer是一款可以修复面部崩坏以及更换脸部标签的插件,操作比较简单,首先我们通过本地命令行来进行安装:
1 |
|
安装完成后重启我们的SD进行,打开浏览器后发现文生图的左下方多了一个Adetailer选项,点击展开可以看到有一些参数调整,点击Enable ADetailer
的选项就可以启用插件了:
我们选择mediapipe_face_full
模型,在下面提示词中,我们可以更改最后生成的面部表情,比如这里我们写smile,可以让生成的人物表情更加的微笑;最后的运行效果,面部对比之前也更加的自然。
我们知道,setup函数是vue3中的一大特性函数,是组合式API的入口,我们在模板中用到的数据和函数都需要在里面定义,并且最后通过setup函数导出后才能在template中使用:
1 |
|
但是setup函数使用起来比较臃肿,所有的逻辑都写在一个函数中定义;我们发现这样简单的变量和函数,需要频繁的定义导出,再次定义导出,在实际项目开发中会很麻烦,我们写的时候也是需要不断的来回切换,而且变量一多还容易搞混。
于是更好用的setup语法糖出现了,将setup属性添加到<script>
标签,上面的变量和函数可以通过语法糖简写成如下:
1 |
|
通过上面的一个简单的小案例,我们就发现setup语法糖不需要显示的定义和导出了,而是直接定义和使用,使代码更加简洁、高效和可维护,使代码更加清晰易读,我们接着来看下还有哪些用法。
上面的案例我们已经知道了在setup语法糖中,不需要再繁琐的进行手动导出;不过setup语法糖不支持设置组件名称name,如果需要设置,可以使用两个script标签:
1 |
|
如果设置了lang属性,script标签和script setup标签需要设置成相同的属性。
Vue3中取消了create的生命周期函数,在其他的生命周期函数前面加上了on,例如onMounted、onUpdated;同时新增了setup函数替代了create函数,setup函数比mounte函数更早执行,因此我们可以在代码中导入函数钩子,并使用它们:
1 |
|
和vue2的8个生命周期函数相比,在setup函数中,排除了beforeCreate和created,加上onActivated和onDeactivated2个在keep-alive中使用的函数钩子,和一个onErrorCaptured异常捕获钩子,一共有9个生命周期的函数钩子可供使用。
响应式是vue3和vue2比较大的一处不同之处,vue2在data中定义的数据会自动劫持成为响应式,而vue3默认返回的数据不是响应式的,需要通过ref和reactive来定义数据,ref定义简单的数据类型,而reactive定义复杂数据类型,使之成为响应式:
1 |
|
虽然ref是用来定义简单数据类型,不过对于对象和数组的复杂数据类型也能使用,不过使用时都需要加上.value:
1 |
|
ref和reactive看起来用法是相同的,但使用ref时,操作变量值的时候需要用.value
,因此适用零散的单个变量;如果是多个相关联的变量,比如用户的一系列信息,姓名、性别、住址等,使用ref定义单个变量较为麻烦,就可以使用reactive组合成对象。
如果我们想要用到复杂数据类型中的某个属性,还想要和原来的数据保持关联,比如person中的name或者age,只通过解构的方式,数据响应性会丢失,页面并不会改变:
1 |
|
这个时候,我们就可以使用toRef()
函数来关联两个变量,这个函数的功能相当于创建了一个ref对象,并将其值指向对象中的某个属性:
1 |
|
这样,我们更改person中的属性或者直接更改name变量,两者都会随对方的改变而改变;我们发现toRef一次只能创建一个ref对象,如果同时有数个变量,效率不够高,就需要用到toRefs()
:
1 |
|
toRef只是创建一个ref变量,而toRefs则是创建了一堆ref变量,它的作用是将响应式对象上所有的属性都转换为ref,然后再将这些变量组合成一个对象,因此我们可以打印出来看下,发现toRefs后的数据也只是一个普通的对象,只不过对象中有很多的ref变量:
虽然toRef可以将响应式数据的属性转换成ref对象,不过当toRef和props结合使用的时候,是不允许修改ref对象的值的,因为这样等于直接修改props的数据,这种情况下可以使用下面介绍的带有get/set的computed函数。
1 |
|
我们可以将title改成computed的形式:
1 |
|
此外,对于一些复用性高的数据和业务逻辑,我们可以将其封装到组合函数中,所谓的组合式函数,官方的解释如下:
在Vue应用的概念中,“组合式函数”(Composables) 是一个利用 Vue 的组合式 API 来封装和复用有状态逻辑的函数。
比如对于分页请求的列表数据tableList和页码等多数页面会用到的复用性高的数据,我们可以选择将其提取到组合式函数中来,这个时候就可以利用toRefs
函数将响应式数据转换成多个ref,同时也不失去响应性:
1 |
|
我们在页面上引入usePage函数,同时解构出其中的数据和函数:
1 |
|
computed是基于依赖进行缓存的一种属性,用于派生出或者计算出一个值;我们在setup中使用时,需要先引入computed
1 |
|
我们给computed函数传入一个箭头函数,箭头函数的返回值作为computed的计算返回;不过此时的double是一个只读属性,在setup中通过.value
获取其值,如果强行改变其值会报错;computed也可以接收一个options,动态设置依赖值:
1 |
|
在Vue3中,watch和watchEffect都是用来侦听数据源并执行相应操作的函数;其中watch
函数是用来侦听特定的数据源,并在数据源改变时执行回调函数:
1 |
|
对于reactive对象中的属性,很多小伙伴理所应当的认为这样写就可以了:
1 |
|
如果按照上面写法,则会报以下告警信息:
A watch source can only be a getter/effect function, a ref, a reactive object, or an array of these types.
这是因为person.name变量存放的是一个固定的字符串值,watch拿到的参数只是一个字符串,但是字符串并不具备任何响应式的属性;因此上述的报错信息提示了,可以传入一个getter函数、ref值、reactive对象或者以上类型的数组,因此我们可以有以下两种修改方式:
1 |
|
同样的,我们如果要监听多个属性,也可以传入一个数组:
1 |
|
那么,有趣的事情来了,如果我们将情况变得更加复杂一些,person中的属性是多层嵌套的复杂对象:
1 |
|
如果使用watch监听person中的属性,还是能监听到改变,因为watch会自动对reactive对象开启深度监听;但是用getter函数包裹的嵌套属性,还能吗?
1 |
|
很遗憾,这样并不能监听到,我们需要对多级的属性手动开启深度监听:
1 |
|
watchEffect函数
则是vue3新增的一个api,用于侦听响应式数据源,发送改变后自动重新运行函数;watchEffect可以观察到函数中所有的响应式数据,并且在这些数据发送改变后自动重新运行函数:
1 |
|
watchEffect监听的任意数据发生变化都会触发函数。
在有些情况下,我们需要获取元素的dom节点或者子组件的实例对象,比如canvas画图传入dom节点或者调用子组件内部的函数等等,都需要获取节点;在vue2中是通过this.$refs
的方式,vue3中需要通过ref:
1 |
|
我们发现组件import导入后,在模板中就可以直接使用了,不需要再进行注册;给需要操作的节点绑定ref属性,名称和下面ref定义的保持一致;不过需要注意的是,操作dom元素需要在页面mounted之后。
对于for循环中的多个节点,我们可以将ref属性接收一个函数,函数的参数代表了当前循环的元素,将其存储下来,就可以获取多个节点的列表:
1 |
|
对setup的基础用法有了一定了解,我们来看看setup语法糖的更多用法;首先就是父子组件传数据,子组件需要定义props,通过defineProps
指定props的数据类型,主要有三种写法方式:
1 |
|
接收到的props可以直接在模板中使用;对于复杂数据类型,比如对象和数组,我们在为其设置默认值的时候,如果只写一个空数组,就会报错:
1 |
|
正确的方式是通过函数的方式返回:
1 |
|
对于组合类型的props,可以通过中括号,使用逗号进行分割:
1 |
|
上面的写法,根据官方的说明,称为运行时声明
,也就是在项目运行时才会校验参数的类型是否正确;而使用了typescrip,可以基于类型声明
,这样我们在IDE中传入参数时,立刻就能进行类型推断和检查:
运行时声明和基于类型声明不可同时使用。
1 |
|
使用defineProps进行基于类型声明的缺点就是不能给props提供默认值,这里还需要用到一个withDefaults
函数进行默认赋值:
1 |
|
每次用到defineProps
,都需要从vue中引入,这样比较麻烦;很多文章中都会说这是一个宏函数,不需要导入,直接使用;所谓的宏函数也叫编译宏函数,是在作用域内没有定义,而在编译过程中自动注入的工具函数;实际项目中eslint会校验失败,我们需要在eslint配置中开启编译宏:
1 |
|
修改完后需要重启服务器,这样,下面的defineEmits、defineExpose等函数都可以直接使用。
defineEmits
函数是一个用于定义组件的自定义事件的API,通常用于子组件中;它接受一个参数,可以是一个数组或对象,用于指定需要定义的自定义事件。
如果传入的是一个数组,数组的每个元素就是一个字符串,表示一个自定义事件的名称:
1 |
|
在父组件中我们就可以定义使用@add
和@sub
的回调函数了。而如果我们传入一个对象,对象的键就是自定义事件的名称,值可以是一个函数,用于验证自定义事件的参数类型。
1 |
|
在上面的代码中,我们定义了自定义事件customEvent
;当该事件被触发时,就会调用customEvent后面定义的函数,打印出负载数据,同时,我们可以在customEvent函数中返回一个Boolean类型,对响应数据进行校验,如果返回false,数据校验不通过,会在控制台进行提示:
[Vue warn]: Invalid event arguments: event validation failed for event “customEvent”.
defineEmits写法也分为运行时声明和基于类型声明,使用基于类型声明同样需要在函数后面跟上数据类型,使用e声明函数的名称:
1 |
|
不过,这样的写法不是很友好,而vue3.3引入了一种更符合人体工程学
的声明方式,写法更加友好:
1 |
|
在vue2中,如果父组件需要调用子组件的方法,直接使用this.$refs.child.getData(),就可以调用;但是在vue3中,子组件默认都不会暴露任何数据和方法,需用通过defineExpose
函数定义后才能拿到:
1 |
|
父组件通过上面ref的方式获取组件实例,即可调用子组件暴露的方法;
1 |
|
同样的,defineExpose也支持基于类型声明:
1 |
|
我们回顾一下,在vue2中,挂载全局指令通过directive
函数,直接挂载到Vue对象:
1 |
|
而在vue3中,通过createApp
创建实例,因此通过app.directive
函数进行挂载全局指令:
1 |
|
而在setup语法糖中引入自定义指令,我们需要将引入的指令名称定义成v
为前缀的小驼峰形式,引入后不用注册,直接在模板中通过小写的中划线连接使用即可:
1 |
|
Vue中插槽slot是一种特殊的内置标签,它允许父组件向子组件内部插入自定义的html内容,使得父组件可以在不修改子组件的情况下,非常灵活向子组件中动态的添加修改内容;在vue2使用this.$slots
对象来获取插槽,而在setup语法糖中,我们就要用到useSlots
函数。
useSlots
函数可能很多小伙伴比较陌生,大部分场景下我们直接使用<slot />
标签即可;而在一些特殊的渲染场景下,就需要useSlots在JSX中渲染插槽数据;比如一些组件的属性支持JSX代码,我们可以用来渲染一些插槽:
1 |
|
我们通过useSlots获取slots
对象,默认会有一个default属性,就是我们的默认插槽;如果我们向子组件中插入其他命名插槽,slots对象会有相应的属性,比如这里我们在父组件使用title插槽,
1 |
|
打印slots对象查看,我们发现有两个属性:
1 |
|
回到上面的案例代码,我们可以判断slots.title
属性是否存在,也就是插槽是否存在,然后通过h函数渲染slots.title()
。
另外一个有些类似的属性就是attrs,可以用来捕获任何我们没有在组件中声明的参数,我们在setup语法糖中也是使用useAttrs
来获取它:
1 |
|
1 |
|
如果我们在Child.vue将title定义到props中后,attrs就不会出现title属性。
本文整理总结了setup语法糖的一些用法,主要包括响应式、props、emit、expose和slot,由于篇幅的限制,响应式中还有很多函数,包括isRef、unref、toRaw等这里不再详细介绍;setup语法糖的优势在于能够使得代码更简洁,可读性强,同时可以将复杂的逻辑和状态管理通过组合式函数拆分为小的、可复用模块,使得代码更加模块化。因此在vue3中,掌握并合理的利用setup语法糖可以帮助我们更好的组织和管理代码,提高开发效率。
]]>CUDA(Compute Unified Device Architecture,统一计算架构)是由NVIDIA推出的基于GPU的并行计算平台和编程模型。它允许开发者使用NVIDIA GPU进行高性能的计算,从而加快了深度学习等领域的计算速度。
cuDNN(CUDA Deep Neural Network library)是一个由NVIDIA开发的深度神经网络库,它基于CUDA,可以与GPU结合使用,以实现深度学习应用的加速。cuDNN可以用于深度神经网络的训练和推理,如卷积神经网络(CNN)、循环神经网络(RNN)等。
CUDA和cuDNN的结合使用,使得开发者可以使用NVIDIA GPU进行深度学习应用的开发。这种结合不仅可以提高计算速度,而且还可以降低内存开销,使得深度学习应用可以在更少的硬件资源下运行。
在实践中,CUDA和cuDNN被广泛应用于图像识别、语音识别、自然语言处理等人工智能领域。此外,它们还被用于科学计算、大数据分析、虚拟现实等领域。本文在Windows10环境下进行CUDA和cuDNN的安装。
首先查看电脑有没有安装NVIDIA的显卡驱动,鼠标在桌面空白处右击NVIDIA控制面板
,或者在右下角双击NVIDIA控制面板的绿色图标:
两种方式都能打开控制面板界面,可以看到我们的显卡型号以及驱动程序的版本:
如果上述方式都没有找到,则需要下载安装,打开NVIDIA官方驱动https://www.nvidia.cn/Download/index.aspx?lang=cn,选择和自己显卡适配的驱动,下载进行安装。
显卡驱动安装完成后,我们就来安装CUDA了,需要注意的是显卡驱动的版本决定了CUDA的版本,并不可以无限安装最新版本,我们还是在控制面板中查看,打开帮助 => 系统信息 => 组件
:
可以看到我电脑最高能安装12.2.79的版本,然后就可以去下载我们需要的CUDA了,打开CUDA下载页面:
默认是最新的版本,如果我们需要低版本,可以点击页面下方的Previouse Release
,打开https://developer.nvidia.com/cuda-toolkit-archive,找到我们对应的版本下载即可:
完成后双击我们下载的cuda_12.0.0_527.41_windows.exe
安装包,出现安装界面,默认安装即可;这里需要记住安装目录,后面配置环境变量时需要:
安装过程中可能出现闪屏现象,属于正常。
安装成功后我们运行nvcc -V
就能看到CUDA的版本号。
打开cuDNN下载页面,这里需要注册一个NVIDIA的账号,填写邮箱很方便:
我们也选择适合自己版本的进行下载,解压后我们得到以下的目录结构:
我们将文件夹覆盖到上面的CUDA安装目录下,比如我的CUDA的安装目录是C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v12.0
,将压缩包内对应的文件夹复制到bin、include、lib目录下即可
然后添加环境变量,鼠标右键此电脑 => 属性 => 高级系统设置 => 环境变量
,将CUDA的安装目录添加到CUDA_PATH
变量中
然后在PATH中添加以下路径:
1 |
|
命令行输入nvidia-smi
,能够正确显示各种信息就安装完成了。
CSS框架发展到现在,主要经历了四个阶段,第一个阶段是以CSS2.0和CSS3.0为主的,原生CSS阶段,需要用到什么样式就写什么样式,也会有一些简单的复用;第二个阶段是将CSS组件化,将具有相同视觉效果的元素封装成同一个组件类,比如07年Twitter推出的Bootstrap框架,后面以及React和Vue等框架涌现出来的Element UI、Antd等各种各样的组件库,都对自身的组件进行了封装,提供相当丰富的预设组件。
第三个阶段是出现以Sass、Less和Stylus为代表的的CSS预处理器,弥补了常规CSS语法不够强大,没有变量和样式复用的机制;使得我们在开发样式时可以使用样式嵌套、循环、变量、条件控制等更高级的语法,更加灵活方便的开发样式。
但是丰富的组件库在面对高度定制化的UI设计界面时,有时候也无可避免的需要自己写一些样式,不同组件中也会有重复封装的样式;同时高度封装的组件,还需要一定的学习成本,知道组件样式如何来控制;因此第四个阶段以Tailwind CSS为主的CSS原子化,直接将CSS样式打散,就像一个个原子一样,将每个CSS的样式应用到对应的类名。
比如我们最常用的flex布局,如果封装到组件中,我们会在很多组件中不断重复的写flex样式,比如下面的组件,我们对下面每个层级的flex布局都需要来写对应的样式:
1 |
|
但是,如果使用了Tailwind CSS,你只需要写下面几行代码即可:
1 |
|
怎么样?是不是比上面的代码简洁多了;有些小伙伴可能会问,这些CSS类名都是什么意思?还是有一定的学习成本啊;别着急,Tailwind CSS的语义化CSS,再结合VS Code强大的插件功能,可以让我们很轻松的记住这些类名;因此,笔者在做一些管理后台类的项目时,由于对页面样式要求不高,在实际项目中大多数页面都不用写样式,直接用类名即可,极大的提高了工作效率。下面,我们继续来看Tailwind CSS还有哪些强大的功能吧。
首先我们在项目中安装Tailwind CSS,将其作为PostCSS的一个插件,这样我们就能和webpack、rollup、vite以及parcel等打包工具集成了;因此通过npm安装相关的依赖:
1 |
|
tailwindcss init
在项目中自动创建了一个配置文件tailwind.config.js
,一些主题、插件等就可以在这里配置,我们将项目中的模板文件路径添加进来:
1 |
|
别忘记把tailwindcss配置到postcss.config.js中:
1 |
|
创建一个/src/tailwind.css
文件,通过@tailwind
指令来添加每一个用到的功能模块:
1 |
|
最后在我们的项目入口文件中引入tailwind.css
:
1 |
|
这样我们就能启动查看项目了;项目启动后,我们在写class类名时,肯定记不住那么多繁杂的类名,就需要用到编辑器插件了;打开VS Code的扩展面板,搜索Tailwind CSS IntelliSense
:
这里可以看到很多扩展,选择第一个官方扩展进行安装,就可以增强Tailwind的开发体验;我们在写类名时,模糊写一个flex,就会带出flex相关的类名,并且每个旁边都会有对应的类名的详细属性;甚至鼠标放在现有类名上也会呈现具体样式细节,这样就不用担心用错类名了。
我们来看下Tailwind CSS有哪些特性。
Tailwind CSS设计思路是优先考虑如何来满足实际需求(Utility-First Fundamentals),因此提供了大量使用的CSS类名,可以款速构建常见的界面元素,我们以官网的一个案例来理解它的实用主义优先的原则:
1 |
|
我们看下呈现的效果,也比较简单。
在这个案例中,我们使用了下面几种样式:
这里margin和padding比较特殊,有多种方式来设置;我们知道margin: 24px是设置上下左右四个方向的边距,在Tailwind CSS就可以简写成m-6
;如果是margin: 24px 12px,Tailwind CSS就可以设置成X轴方向和Y轴方向,对应的类名就是:mx-3 my-6
,因此上面的mx-auto就非常好理解了;而上下左右对应四个字母t、b、l、r,加上margin(m)和padding(p),就可以分别对应不同方向的设置了,比如pb-4
。
更多的样式缩写,如果小伙伴不知道怎么写的话,可以查看官网的文档,查一两次语法基本就能记住了,还是比较语义化的。
我们发现这样的写法,会让页面显得比较臃肿,喜欢这种写法的人会非常喜欢,不喜欢它的人会觉得页面很混乱,难以接受;但是笔者用下来发现确实有以下优势:
相较于常规的类名,我们页面上更多的是鼠标悬浮、聚焦等状态,还有很多的伪元素和伪类修饰符,我们看下他们是如何通过类名的方式来实现的。
我们可以给按钮元素设置悬浮、聚焦状态的改变,在CSS中是通过:hover
,:focus
等实现的,Tailwind CSS添加hover:
前缀来实现,比如下面
1 |
|
这样鼠标悬浮后,背景颜色就会加深;还可以使用active:
激活和focus:
聚焦:
1 |
|
对于a链接,visited:
修饰符表示链接已经访问过。
1 |
|
还有一些first-of-type:
、last-of-type:
、empty:
、disabled:
、checked:
修饰符,这里就不再赘述。
对于第一个和最后一个元素,使用first:
和last:
来选择元素:
1 |
|
对于奇偶元素,可以使用odd:
和even:
修饰符来选择元素:
1 |
|
对于一些特殊的子元素,比如选择第几个元素:nth-child,我们通过[&:nth-child(n)]
前缀:
1 |
|
还有一些子元素的样式依赖于父级元素,我们通过给父级元素标记group
类名,并且使用group-*
的修饰符来标记目标元素,比如下面的例子:
1 |
|
除了group-hover
,还支持group-active
、group-focus
或者group-odd
等修饰符;如果有多个组嵌套的情况,我们可以使用group/{name}
来标记该父级元素,其中的子元素使用group-hover/{name}
修饰符来设置样式:
1 |
|
比如上面的案例中,最外层的元素使用了group/item
,而下面的按钮使用了group/opt
单独变成一个组,用来控制该组下面的元素样式。
当我们需要根据同级元素的状态对目标元素进行样式设置时,使用peer
标记同级的元素,使用peer-*
修饰符对目标元素进行样式设计,比如下面的案例:
1 |
|
我们给同级的输入框标记为peer,而p标签就是我们需要设计样式的目标元素,使用peer-invalid:visible
让p标签当输入框输入内容无效时进行内容的显示,效果如下:
和group
的使用一样,peer也可以使用peer/{name}
来标记某个具体的元素,然后使用peer-*/{name}
来设计目标元素的样式。
todo
对于::after和::before等伪元素,我们也可以使用before:
和after:
修饰符:
1 |
|
当使用before:
和after:
修饰符这类修饰符时,Tailwind CSS会默认添加content: ''
样式,除非我们需要在content中添加其他内容,否则不需要额外的声明。
对于输入框input的placeholder,我们可以使用placeholder:
修饰符很方便的更改占位符样式:
1 |
|
我们在浏览有些网站或者App时都会看到有夜间模式的功能,启用夜间模式可以让网站展示不同的风格样式,Tailwind CSS可以通过类名很容易的控制;要开启夜间模式,我们先在tailwind.config.js
配置darkMode:
1 |
|
这样我们就可以通过全局根节点上控制类名来控制整体的页面风格是否呈现夜间模式了,比如在html节点或者App.vue上添加/移除dark
类名;下面就来对页面进行改造,对于夜间模式下的背景颜色或者文字颜色,使用dark:
修饰符,
1 |
|
在上面代码中,我们使用了bg-white
在默认模式下的背景颜色为白色,以及dark:bg-black
夜间模式下背景颜色为黑色,通过isDark变量来实现控制根节点开启/关闭夜间模式;效果如下:
对于一些全局的样式,比如颜色模式、自适应缩放模式、间距等等,我们可以添加到tailwind.config.js
配置文件中:
1 |
|
比如觉得全局的red红色,红的不够鲜艳,我们可以在colors中重新设置一个色值;或者设置一个全局的主要色值main-color
,在页面中使用bg-main-color
或者text-main-color
就可以设置一个全局的颜色。
在使用边距值时,我们发现只有mt-6
这种模糊的数据,使用的单位也是rem;如果设计稿需要比较精确的还原,我们可以使用大括号来将精确的数值进行呈现:
1 |
|
对于色值、字体大小等,这种使用方式也是有效的:
1 |
|
甚至对于CSS变量,也可以直接使用大括号,都不需要var()
,只需要提供变量名称:
1 |
|
在Tailwind CSS中,tailwind指令
是用于快速生成基于配置的样式代码的工具。``tailw数生成相应的样式代码,这些参数可以是任何有效的CSS属性和值。它可以根据 Tailwind CSS的配置文件中的设置来生成相应的样式代码。
1 |
|
这里对应参数的作用如下:
@layer
指令是Tailwind CSS中一个重要的指令,它用于将 CSS 类分层,从而更好地组织和控制样式。我们可以使用@layer指令
来创建不同的层(layers)。层是CSS类的分组,这些组可以用于将 CSS 规则封装为独立的、可重用的模块。通过将样式规则组织到不同的层中,这样就可以更好地控制样式的作用范围和优先级。
1 |
|
这里的@layer base
指令用于创建基础层base。基础层包含了通用的、基础的样式规则,例如颜色、字体、间距等。这些基础样式是整个网站中普遍适用的,通常不需要进行修改或定制。通过将基础样式规则分离到基础层中,可以确保它们在整个网站中保持一致,并且不会受到其他样式规则的影响。
而@layer components
指令用于创建组件层components。组件层包含了与具体组件相关的样式规则。组件可以是任何自定义的HTML元素或页面组件,例如按钮、卡片、表单等。组件层中的样式规则通常与具体的UI组件有关,并且可能需要进行频繁的修改和定制。
@layer utilities
指令用于创建实用层。实用层包含了高度定制化的、短小精悍的样式规则。这些规则通常是用来实现某些特定的设计效果。实用层中的样式规则通常是单一用途的,并且可以根据需要进行精确地控制和定制。
在Tailwind CSS中,我们可以使用@apply指令
将现有的CSS类应用于已经定义的样式规则,以实现更灵活的样式控制:
1 |
|
比如在上面的案例中,我们定义了我们自己的类名my-custom-class
,然后使用@apply指令
将text-center、bg-blue和border-2应用于我们自定义的样式,这样就可以根据具体的需求,封装一系列我们需要的样式规则。
本文整理了Tailwind CSS的不同特性和用法,它的核心思想是实用性,使用方式也非常简单,提供了一系列预设的CSS类名,同时可以根据不同的需求,自定义样式组合,应用于不同的场景中;通过Tailwind CSS,我们可以很方便的构建一致性和可维护性的页面,而且也无需编写大量的CSS样式。
总之,Tailwind CSS是一个非常实用的CSS框架,已经被应用于大量的网站和框架中,如果你现在还在为写样式而发愁,那么它是一个非常值得尝试的选择。
]]>上一篇文章写的时间比较早,使用的还是V2版本的插件,而现在Chrome最新的插件版本也来到的V3,而且V2插件也不能继续在Chrome商店里面发布上架了;因此很多朋友吐槽得比较多的就是,上一篇文章中介绍的插件版本太老了;因此本文我们先来看下如何从V2升级到V3,以及两个版本存在着哪些区别。
首先Chrome浏览器是从88版本开始支持V3,因此开发之前,首先确定一下自己的浏览器版本是否高于这个版本;第一步,就是修改manifest.json
文件,将我们的插件版本号从2改到3。
1 |
|
注意:这里改的是
manifest_version
,而不是version
字段。
在V2版本中,host权限和其他的权限配置一般都统一的放在permissions
字段中,而其他一些可选权限则在optional_permissions
:
1 |
|
permissions
列出的权限是插件被安装前
所需要的;而optional_permissions
列出的一些权限,是插件在安装时不需要的,在安装之后
可能会要求的权限。
在V3版本中,权限配置更加精细化,我们需要把主机权限独立到单独的host_permissions
和optional_host_permissions
字段中:
1 |
|
web_accessible_resources
字段用来控制外部访问插件中的资源,比如content-script脚本或者popup页面中需要展示展示图片资源;在V2版本中,直接定义一个资源列表,那么所有网站都能访问这些资源了:
1 |
|
而来到V3版本,我们需要配置一个对象数组,对象中通过resources和matches更加精细化的配置了哪些外部网站可以访问哪些资源文件。
1 |
|
假设我们有一张图片资源在以下插件目录下:
1 |
|
我们想让content-script.js来在页面呈现图片的地址,需要在manifest.json声明可以被访问到:
1 |
|
然后在content-script.js中调用Chrome插件的chrome.runtime.getURL函数
来获取图片的地址,图片的地址看起来可能是这样的:
1 |
|
这里的
extension-UUID
并不是插件的ID,而是一个随机生成的唯一id。
我们在匹配资源文件的路径时,面对多个文件匹配,也可以使用通配符:
1 |
|
background后台的升级也是Chrome插件更新的重要特性之一,使用了Service Worker替代了原来的Background page。在V2版本中,我们使用background.scripts可以配置多个js,或者使用background.page配置一个后台页面:
1 |
|
persistent: true
指定了脚本一直在后台运行,直到插件被禁用或者卸载,这样就导致占用了大量的内存;因此V3废弃了scripts和page;如果我们还是指定这两者,Chrome就会报下面错误,直接就不让我们运行插件了,
1 |
|
V3版本升级改用了service_worker
字段代替原来scripts和page,确保插件不会一直占用浏览器的资源,仅在需要时才运行,从而节省资源:
1 |
|
service_worker
字段不是一个数组,只支持字符串格式。
同时V3版本升级也让background.js支持了模块化开发,我们可以在里面直接import本地的方法,让我们能够不用依赖打包的方式进行模块化开发,使用方式也很简单,在background添加type
属性即可:
1 |
|
我们在background.js中就可以使用import导入本地模块:
1 |
|
同时,由于background不再支持page页面配置background.html,因此也无法调用window对象上的XMLHttpRequest
来构建ajax请求;也就是说我们不能像V2版本一样,在background.html中使用jQuery的$.ajax来发送请求了,而是需要使用fetch函数
来获取接口数据。
由于service workers是短暂的,在不使用时会终止,这意味着它们在整个浏览器插件运行期间会不断的启动、运行和终止,也就是不稳定的;因此我们可能需要对V2中background.js的代码逻辑进行一些改造,以往我们会习惯将一些数据直接存储到全局变量,比如像下面这样:
1 |
|
当我们运行项目时发现,全局变量saveUserName在某些情况下获取到的数据变成空字符串,存储的数据直接消失了;笔者在项目调试中刚开始经常会遇到这种神奇的问题,调试的值跟实际的值不一样,随之消失的还有笔者的信心。
因此在V3中,需要对这种全局存储的变量数据进行改造,改造的方式也很简单,就是将数据持久化保存到storage中,需要用到的地方随用随取:
1 |
|
有小伙伴也许发现了,我们上面使用了chrome.action.onClicked
来注册点击事件,而不是原来的chrome.browserAction.onClicked
。
由于历史原因,之前将插件的图标分为pageAction
和browserAction
,两者的区别在于browserAction始终都显示,更像我们现在的插件图标逻辑;而pageAction则比较特殊,只有当某些特定的页面打开时才会显示图标。
而V2版本两者的区分界限已经较为模糊了,区别不是很大;但是在manifest.json中配置还是有区分,常用的就是browser_action:
1 |
|
升级到V3版本,直接统一为同一个action,不需要再区分:
1 |
|
需要注意的是:如果注册了popup.html的页面,则
chrome.action.onClicked
点击事件注册后并不会被执行。
我们在绑定chrome.action
事件的地方也需要进行统一:
1 |
|
内容安全策略(Content Security Policy,简称CSP),是在manifest.json中配置的,用于限制扩展可以从哪些源加载代码,比如script标签可以从哪些域名地址加载CDN,或者禁止eval()
等可能不安全的函数;在V2版本中,默认是一个字符串配置:
1 |
|
升级到V3版本,content_security_policy
字段依然被保留,支持另外两个属性:extension_pages和sandbox:
1 |
|
default-src 'self'
表示默认所有类型的引用文件(js文件、html文件)都是应该在插件包内的;如果我们想要支持从某个域名地址引入js文件,在V2中我们会看到下面的写法:
1 |
|
但V3中不支持这样的写法,不允许从某个域名地址引入文件。
我们在调用chrome API的地方,也有一些需要进行升级改造的,比如上面的chrome.action:
1 |
|
在获取资源地址的时候,也需要将chrome.extension.getURL
替换成chrome.runtime.getURL
:
1 |
|
V3中,执行script-content的api函数executeScript也从tabs
,升级到了scripting
;因此我们还需要在manifest.json中添加scripting
权限才能调用;同时,执行的脚本也从原来的单个文件,变成可以接收多个文件:
1 |
|
insertCSS()
和removeCSS()
也从tabs
升级到了scripting
。
1 |
|
我们在实际项目中,有时候会需要service worker异步返回一些数据,比如请求接口后返回一些接口数据等:
1 |
|
上面的代码中在content-script.js发送消息到background中,虽然这里我们虽然是在then中返回了res,或者使用async/await;但是很遗憾,在content-script.js接收到的res还是undefined,我们需要对background代码进行改造
1 |
|
在onMessage回调函数里返回true,告诉Chrome我们想要异步发送响应。
我们有时候会遇到需要插件去和原生Web页面进行通信情况,这里的原生Web页面页面指的并不是content-script.js或者popup.html页面,一般也是我们开发的网站页面;比如在原生Web页面页面中,需要判断是否安装了插件,没有安装插件的话显示下载插件的跳转链接;或者点击原生页面上的某一个按钮,将数据保存到插件中来等等,就需要涉及插件和原生Web页面页面的通信问题。
这里有几种实现通信的方式,第一种最简单的方式就是通过隐藏的dom节点,比如安装插件后,通过content-script.js在页面上放置一个隐藏的dom,将插件信息放到放到dom节点上,这样的缺点也很明显,只能传输一些简单的数据,且不能进行双向通信。
第二种方式,通过插件的id,从原生Web页面想插件发送消息,首先需要配置在manifest.json中配置externally_connectable
字段,来声明哪些Web页面可以通过这种方式,和插件建立链接:
1 |
|
externally_connectable还可以指定ids
字段,用来指定需要通信的其他Chrome插件;配置完成然后就可以在我们的Web页面里添加发送消息的代码了:
1 |
|
这里如果我们没有配置上面的externally_connectable字段,浏览器是不会在我们的页面上注入chrome.runtime.sendMessage
方法的,因此我们需要对这个函数进行异常判断,否则页面就会报错。
1 |
|
第三种方式,我们可以通过window.postMessage
进行通信,window.postMessage
一般用在多个页面之间通信,当然,我们的content-script.js和原生Web界面是同源的,更能直接通信了;两者的发送方式和接收方式在代码上都是一样的,这里也不再进行区分:
1 |
|
这样我们不需要获取插件的ID也能通信了,不过我们在监听message消息时会看到各种各样插件或者页面之间传递的消息,因此我们对传输数据的命名方式上差异化,可以定义一些独特的前缀,避免和其他页面产生不必要的冲突。
]]>阅读本文需要一定的Threejs基础。
首先什么是全景图呢?全景图是一种实景360°全方位的平面图像,需要用特殊的工具来查看才能达到360°环绕的效果,比如用我们的Threejs。
比如上面的这张照片,就是一张全景照片,将整个房间前后左右上下都拍摄进来,如果用肉眼看的话,整个房间弯弯曲曲的,没有什么特别的地方;那么,这样的照片是如何来拍摄的呢?
打开你的手机相机,点击更多,里面一般会有一个全景相机的选项,然后站起身来,转一圈,你就能得到一张完整的全景图片了。
当然开个玩笑,这样拍摄,除非你的手稳定得如同工厂里的机械臂一样,并且能够保持身体的平衡;一般拍摄出来的相片只能粗略的看看即可;专业的拍摄设备需要用到单反+三脚架+云台,拍摄后还需要专业的后期处理和拼接,比较麻烦;当然我们也可以从网上一些资源下载:
近些年,随着个人vlog的兴起,各种消费级的拍摄设备和工具也迅速发展,全景相机、运动相机等产品不断更新迭代,已经能够带来很多非常好玩的拍摄体验了;比如这款Insta360 ONE X2在拍摄时就可以选择360°全景照片模式,轻轻一按,就可以得到一张满意的全景图了。
要实现全景图,首先我们来对Threejs的场景布局进行一个简单的封装,引入Threejs中所需要用到的组件:
1 |
|
将一些用到的默认配置定义到config中,这里将div的id设置为webgl-output
,因此我们只需要在页面添加一个div并将id设置即可。
1 |
|
我们定义一个Stage父类,将业务逻辑定义到子类,继承该类即可:
1 |
|
这里render函数每次都会周期性的执行,一些周期性的渲染都会放到这里进行处理;如果子类也需要渲染,避免命名重复,我们将子类的渲染定义到beforeRender
函数中。
渲染器相当于是一个画布,我们对其进行更精细的设置,开启阴影、设置背景颜色等。
1 |
|
在使用时Stage时,由于render函数已经被占用了,我们重新设置一个beforeRender函数,在每次render调用时调用。
1 |
|
子类必须放到dom元素渲染完成后实例化。
将环境贴图设置到场景scene的背景也能实现全景图的功能,这种方式实现起来也是最简单的;将相机放在整个场景的中心,当相机移动时,背景图片也随之移动。
构建一个CubeTextureLoader加载器实例,将六个面的贴图通过加载器载入,顺序为[right,left,up,down,front,back],然后直接设置到背景即可。
1 |
|
上面的顺序大家可能记不住,我们仔细查看px,nx的顺序会发现,是按照x、y、z轴的顺序,p表示positive正面,n表示negative反面,因此px也就是右侧了,nx就是左侧。
这种方式相较于下面两种盒子的好处,就是实现起来简单,而且鼠标缩放不会暴露盒子的原型,不会出圈;但是缺点也明显,不能缩放查看背景的细节之处,也不能控制视角的距离,
全景球则是首先创建一个球形,直接将一张全景图直接作为素材贴上。
1 |
|
这里我们将球的半径设置为500,如果太小了,缩放时也会出圈;这里的sphereGeometry.scale(-1, 1, 1)
其实相当于sphereGeometry.scale.x = -1
,将贴图沿着x轴进行翻转。
全景球的方式只能用全景图,一般全景图的大小都达到几兆甚至几十兆,因此实际使用时需要对图片进行压缩;或者通过先加载一张模糊图再加载高清图的方式,加载两张图片来避免用户长时间等待,因此这种方式对网络有一定的要求。
天空盒的思路和上面全景球相同,只不过将SphereGeometry球形换成了BoxGeometry正方形;贴图也由一张全景图,变成了盒子六个面的贴图;六个面的贴图相较于一张全景图,可以利用浏览器的并行加载能力,提高加载速度。
1 |
|
整体的思路和上面两种方式有些类似,只不过Mesh传参的材质由原来的一个材质变成了材质数组;然后还是将mesh对象沿X轴进行翻转。
环境搭建好之后,我们会看到有一些网站上,物体周围有一些可点击的悬浮标签,或者鼠标悬浮后有一些提示的文案,这种导航标签是如何来实现的呢?这里就需要介绍一下Threejs的精灵对象Sprite,它是一个永远面向相机的平面,我们通常用它来显示一些标签;我们看下他的构造函数:
1 |
|
这里的material是SpriteMaterial的一个实例,也就是将材质传进来,我们来看下具体的用法
我们首先定义好锚点的数据,在哪些位置需要标注的,这一步也可以利用dat.gui
不断调整XYZ轴的位置:
1 |
|
从上面的构造函数看出Sprite的构建不需要几何图形,如果是没有任何文案的简单锚点,我们可以将一张png图片设置为精灵材质SpriteMaterial的贴图,然后循环构建多个Sprite添加到场景中去。
1 |
|
这样就能看到多个锚点固定漂浮在某个位置了。
我们还会看到一些锚点旁边有文案标识,事情就开始变得复杂起来了;由于Threejs中不能添加div,因此我们有两种方式可以来实现这样的效果;首先是使用canvas绘制文字,并作为纹理设置为SpriteMaterial的map属性。
1 |
|
我们创建一个canvas画布,设置画笔的颜色粗细,然后绘制文案,将canvas传入Texture,然后作为map属性构建了SpriteMaterial。
canvas的方式一般用来画图形简单、内容格式较为固定的Sprite标签。
另一种方式就是使用CSS2DRenderer(CSS 2D渲染器),这个组件听着非常高深,其实来说很简单,就是在页面最外层插入一个div,然后将我们所需要的渲染dom节点插入到这个div中,渲染我们熟悉的HTML元素;当相机旋转时,实时更新每个dom节点的位置即可;同时场景放大缩小时,不会缩放标签的大小,可以触发DOM点击事件;我们如果打开开发者工具查看,可以看到body最下面有一个div,嵌套了多个子标签。
1 |
|
CSS2DRenderer是Threejs提供的扩展库,我们需要额外从渲染器的包中引入CSS2DRenderer和它的模型对象CSS2DObject;由于要新建一个渲染器,我们对封装的Stage类进行改造,
1 |
|
CSS2DRenderer渲染器和WebGLRenderer有些类似,也都有setSize和render方法,我们需要把实例化的domElement添加到body中来;CSS2DRenderer也需要实时更新,因此我们也需要在render函数中对其进行渲染
我们还是和上面的Sprite锚点一样,循环创建div标签,添加到场景scene中;
1 |
|
我们发现,上面创建完div后,通过div标签创建了CSS2DObject的实例对象,然后设置XYZ轴坐标;这个对象的作用就是将Threejs中的坐标与屏幕的坐标进行转换,进行实时的渲染。
锚点加上后,我们需要对其进行触发事件的绑定,如果使用CSS2DRenderer渲染器的方式,由于是dom元素,我们直接给元素绑定原生事件即可:
1 |
|
我们可以进行后续的业务逻辑,比如场景切换了,或者弹框展示详细信息等;而使用Sprite,由于所有的物体都在Threejs的场景中,我们不能简单的利用绑定点击事件来触发,Sprite也没有addEventListener事件。
因此用到一个光线投射类:Raycaster,这个类用于进行鼠标拾取,也就是在三维空间中计算出鼠标移动或点击时,划过了什么物体。
它的原理是从鼠标处发射一条射线,穿过场景中的物体,通过计算,找出与射线相交的物体,因此这种方法也叫射线追踪法
;我们来看下它的一个用法,
1 |
|
我们实例化Raycaster,新建一个二维的点mouse,这个点下面会用来存储鼠标移动的二维坐标;然后将Sprite放到数组list中,用于Raycaster检测照射到了场景中的哪些物体;当然我们下面也能照射整个场景scene.children下的所有物体,但是有一些物体不是我们想要的,需要额外的判断,因此可以将需要照射物体存到数组中来。
1 |
|
我们详细看下这里的逻辑,首先通过event来计算mouse的XY轴坐标,由于setFromCamera需要归一化的坐标值,因此我们计算时将其处理为[-1, 1]范围内的值。setFromCamera方法通过摄像机和鼠标位置更新射线,接收mouse和camera对象;更新射线后就可以使用intersectObject函数来拾取对象了,接收的intersects数组就是拾取到的Mesh合集。
我们发现Sprite和CSS2DRenderer两种方式各有利弊,Sprite虽然添加元素方便,但是canvas绘制图形比较麻烦,同时触发事件也繁琐;而CSS2DRenderer添加元素较为复杂,需要用到一系列原生属性,但是触发事件方便,具体使用哪种方式,还需要结合业务场景,选择合适的方式。
我们有时候会看到这样的俯视效果的场景动画,那么这种效果是如何来实现的呢?
首先这种效果就需要用到全景球的SphereGeometry球形,然后将摄像机的位置放到球体的最顶部。
1 |
|
我们这里将initPosition
,也就是摄像机的初始位置,放到了Y轴500的位置,由于球体的半径也是500,因此就位于球体内部的最顶上,接下来我们只需要将摄像机的位置缓慢下移就可以。
这里引入一个补间动画库tween.js,让我们可以用平滑的方式更改对象的属性,只需要告诉它初始值、最终值以及所需要花费的时间,在这段时间里,tween.js会帮我们自动计算出每个时间点,应该设置为什么样的值。
1 |
|
它的用法也很简单,这里通过链式调用,创建一个Tween对象,设置y的结束位置和动画时间4000毫秒;在onUpdate函数中,设置每次更新后的y实时数值,最后调用start函数激活tween。需要注意的是,还要在render函数中调用Tween.update
更新。
1 |
|
当然这样的镜头最后会比较生硬,我们还可以调用摄像头的lookAt,当镜头下降时,逐渐看向远方;初始化也设置一个大的广角fov为100,当镜头下降时,缩小fov的值,这样效果过渡的更加自然。
]]>
我们先来欣赏一下页面的效果,每一幕如同电影开场一样缓缓的呈现效果,大家可以点击这个链接来欣赏效果:
在整体布局上,我们发现,它是通过多个section来划分每一屏的;这里的一屏,可以理解为一个动画效果的划分,每一屏的高度大致等于100vh。
大多数的section再嵌套一层.section-wrapper
来包裹内部的元素,同时使用margin: 0 auto;
来让wrapper左右居中:
1 |
|
样式上,将很多屏公共的、通用样式抽离出来,放到.magic-os
中,比如给section添加黑色的背景.section-dark
,.section-headline
是主标题,.section-intro
是介绍性的文字,.section-link
是跳转链接等等;不同屏有相同的布局和呈现效果,样式上也可以通用,比如.section-start
呈现svg画图和.section-card-view
呈现卡片式布局等等。
1 |
|
而每一屏特有的样式则在下面独立出来。
首屏是整个网站的门面,体现出整个网站的特色与风格;我们看到首屏的设计还是比较简洁明了的,一个logo、主标题和slogan;随着屏幕宽度不断的缩放,文字的宽度和图片的大小也在随之缓慢的等比缩放,适配了各尺寸的屏幕。
缓慢的效果主要是通过transition
属性来实现的,常见的用法是:transition: 1s
表示过渡效果需要1秒来完成;这里我们发现后面还带有一个时间值:transition: 1s 0.5s
;我们回顾一下transition的语法:
1 |
|
不难猜出来1s表示完成时间duration,0.5s表示延迟时间delay;因此上面的就相当于下面的省略写法:
1 |
|
不知道大家有没有遇到多个属性需要使用transition的情形,笔者一般会偷懒,使用all让它们的完成时间差不多;但是如果几个属性的完成时间差距较大,就需要使用逗号将多个属性复合使用:
1 |
|
通过transition属性我们能够实现很多意想不到的动画效果。
我们发现在.section-wrapper
外层还有一个比较特殊的类名,就是.aspect-ratio
,这就涉及到了如何通过CSS来实现固定宽高比。
首先,可替换元素(replaced element)
实现固定宽高比就比较简单了,和其他元素不同,它们本身有像素宽度和高度的概念;这里说到了一个概念:可替换元素,其实就是浏览器根据元素的标签和属性,来决定元素的具体显示内容;可替换元素的内容不受当前文档的样式的影响。
CSS可以影响可替换元素的位置,但不会影响到可替换元素自身的内容
比如iframe也是可替换元素,可能有自己的样式表,CSS不能影响其内部的样式;常见的可替换元素有iframe、video、img、embed;与之相对应的就是不可替换元素了,它们内容可以受CSS渲染控制;我们常见的div、p、span等大多数都是不可替换元素。
我们就来看下img固定宽高比,只需要设置width或者height为一个具体值,另一个属性设置为auto即可:
1 |
|
虽然上面的方式实现了可替换元素的固定宽高比,但是不适用于div、span等不可替换元素,因为它们本身是没有尺寸的,默认的高度都是0。
对于不可替换元素,我们能想到一种方式是通过js来实现,页面加载时获取宽度,根据宽高比rate计算出高度然后赋值style属性即可;别忘了,还需要监听resize,这样的方式也能实现。
另一种就是我们下面介绍的纯CSS的实现方式了,我们使用padding来撑大div的高度:
1 |
|
我们看到div元素的宽高比也是固定的了,大致相当于4/3,也就是75%。
很多小伙伴肯定会好奇,为什么加了padding就能实现这样的效果;我们从mdn上来找答案,看下mdn对于padding属性的解释,当取百分比值的时候,是相当于包含块的宽度来计算的:
通过这种方式,div的高度实际上是被padding给撑开的;我们可以将上面的样式抽离成一个通用的样式.aspect-ratio
;在需要用到固定宽高比的地方直接使用类名即可,给wrap元素设置一个padding-bottom样式。
1 |
|
这样wrap盒子就被before元素撑开了,如果我们想要在里面放入内容,还需要将div内部元素使用绝对定位充满整个内容;这种方式虽然能够实现,但是只能高度随着宽度改变而改变,缺点是并不能反过来,宽度随着高度改变。
W3C提出一个保持纵横比的规范属性:aspect-ratio,我们看到目前大部分主流的浏览器也已经支持了,支持率已经有90%;但是IE还是全版本不支持,如果你不需要考虑支持IE,可以考虑使用该属性。
那么aspect-ratio如何使用呢?我们就不需要像上面的padding那样来套娃了,只需要在CSS添加一行代码:
1 |
|
第二屏也是使用.aspect-ratio
来实现视频元素宽高的固定比例,这里就不再赘述了。
我们往下继续看,第三屏是滚动渐显的效果,这里就用到了GSAP的滚动触发,我们先欣赏一下页面的效果:
这一屏的页面布局也比较简单,一个section-headline标题,section-content内容包裹四个section-item模块展示。
1 |
|
我们发现,这里section-headline标题
和section-content内容
都加了两个特殊的样式fade-copy和fade-trigger,fade-copy的样式比较简单,初始化通过opacity: 0
进行隐藏,同时使用transform让它在原始位置Y轴偏下方;触发时,再加上active样式就可以实现从底部滑动上来,实现渐显的效果。
1 |
|
CSS的样式实现了,那么最最最关键的问题来了,如何在滚动时触发给fade-copy元素添加active类名呢?这里我们就用到了ScrollTrigger滚动触发了:
1 |
|
这里hook参数用来设置滚动触发起始的位置,默认是在距离屏幕顶部70%的高度;我们在写代码的时候,很多时候不知道元素滚动到什么时候会触发,因此可以给ScrollTrigger添加markers: true
添加页面上的标记,来调试滚动条触发的位置;还不了解ScrollTrigger用法的小伙伴可以点击这里。
我们通过forEach循环来遍历页面上所有的.fade-trigger
元素,每个元素都绑定了滚动触发的事件;因此在下面的很多地方,我们发现都是使用该类名来实现的效果。
svg绘制的动画效果,图形可以进行无限缩放,也不会失真,相较于图片也更加的灵活;说到失真就不得不提荣耀Magic5的超动态臻彩显示技术,让HDR照片和视频栩栩如生,结合荣耀鹰眼精彩抓拍,让图像永不失真。
本文不对svg的具体使用教程进行深入的探讨,我们简单看下gsap是如何结合svg实现强大的动画效果。
在第四屏、十一屏、十五屏和十九屏都有类似的svg动画效果,我们以第四屏为例,首先欣赏一下页面的效果:
页面上通过前面三个ellipse元素绘制椭圆形描边,设置transform让每个旋转一定的角度,形成对称的图案;最后一个ellipse是中心的圆形。
1 |
|
图形绘制后,现在需要的就是如何让他们动起来,这里借助stroke-dasharray
样式,让其实现描边的效果:
1 |
|
我们的图案就像下面一样动起来了:
很多小伙伴对stroke-dasharray这个样式可能不是很了解,我们先看下mdn上的用法:
它是由数值或者百分比组成的一个数列,数列中的数值,第一个表示点的大小,第二值表示两个点之间的空隙大小;一般的写法如:stroke-dasharray:10, 2
表示点10px,点空隙2px;上面样式中刚开始0 220% 0
其实相当于0 220%
,表示空隙占满全部的空间,也就是不显示了。
使用stroke-dashoffset也能实现类似的效果。
现在图案有了也动起来了,我们就不用CSS的动画了;我们需要结合GSAP来让它和滚动条实现互动了,还记得我们之前说过,GSAP也能控制svg的属性,让svg动起来么?
1 |
|
我们发现,很多动画结束后,都有相同的效果,主标题headline渐隐展示、副标题subhead和链接link都从下方滚动展示出来;这里就需要介绍一个新的函数:gsap.registerEffect
,可以让我们在全局注册想要的效果,直接调用,不用每次都重复造轮子。
1 |
|
这样注册后我们每次都需要手动调用gsap.effects,或者我们还设置extendTimeline: true
,在任意时间线之后都可以调用该效果。
1 |
|
这样rainbow效果就会在时间线上顺序调用;我们回到荣耀的页面注册函数上来,发现在全局注册了一个tech4的效果。
1 |
|
因此在动画效果结束后,都会调用这个tech4
效果来对标题、副标题等元素进行处理。
1 |
|
我们前面介绍过卡片式布局的通用样式是.section-card-view
,这种布局将两个或多个div如同卡片横向排列,随着滚动条而移动,首先也来欣赏一下页面的滚动效果:
页面结构看似很复杂,其实主要就三层结构:
1 |
|
我们仔细来看下它的层级结构,首先是.sticky-wrapper
设置高度108vw,用来撑开高度;中间的元素.sticky-content
设置position:sticky,就是我们用来实现粘性定位的主要元素了,这样页面在滚动时就能保证内容始终距离顶部悬浮一定高度;.section-wrapper
设置display:flex,是内部flex布局的容器。
我们发现第二个元素刚开始会有缩小并且毛玻璃的效果,弱化内容的展示,随着滚动逐渐清晰;初始化时可以通过css设置blur,来达到毛玻璃的遮罩效果。
1 |
|
页面有了,那如果让卡片滚动起来呢?又到了我们的GSAP开始大显身手的时候了;实现的逻辑其实也非常简单,粘性定位元素.sticky-content
在滚动时保持悬浮位置不变,让其内部的flex布局元素.section-wrapper
向右移动,这样就让我们有种错觉,滚动条向下时将卡片推着移动。
1 |
|
我们查找页面上所有的.section-card-view
,遍历元素将其绑定ScrollTrigger事件;scrub属性将滚动条和.sticky-wrapper
元素的x轴位移绑定;其实现的效果如下:
我们看到上面的代码中有一个swiperOffset
变量,猜测就是wrapper的位移距离,那它是如何来计算的呢?我们将整个flex布局元素.section-wrapper
内部的所有卡片想象成一个整体的div,它向左移动的距离就是整体的宽度减去页面的宽度,因此我们主要的工作就是计算它的宽度。
计算方式直接上代码:
1 |
|
那么现在wrapper也滚动起来了,我们就需要将第二个及其以后的卡片内容在滚动时逐渐放大清晰,去掉模糊效果。
1 |
|
最终实现的效果如下:
滚动效果丝滑的如同荣耀Magic5的悬浮流线四曲面屏一样,无界视域,自在掌控。
通过本文,我们结合实际的案例,对GSAP的使用方式有了更进一步的了解;但是由于篇幅和精力的限制,本文主要分析了滚动渐显、svg动画和卡片式布局的几个效果,实际页面中有非常丰富的效果,本文只是窥探了其中的极少一部分的效果,大家如果感兴趣可以自行在荣耀官网查看。
]]>本文所有源码敬请关注公众号【前端壹读】,后台回复关键词【GASP荣耀官网】即可获取。
当我们在谈论ChatGPT时,讨论的是聊天的欣喜,是失业的担忧,是未来的憧憬。
ChatGPT看似好像是在一夜之间突然火起来,但其实它并不是一个什么新鲜的概念,最早的人工智能可以追溯到1950年,艾伦·图灵(Alan Turing)发表了具有里程碑意义的论文《计算机器与智能》
,又名《机器能思考吗?》,在这篇论文里,第一次提出了很有哲学的概念:模仿游戏
,也正是我们所熟知的大名鼎鼎的图灵测试,正是这篇文章为图灵赢得了人工智能之父的桂冠。
图灵测试就是将测试人和被测试者(一台机器或者人),在没有面对面的情况下,让测试者通过一些测试装置(例如键盘)像被提问者发问,如果被测试者超过30%的答复,不能使测试人确认出哪个是人、哪个是机器的回答,那么这台机器就通过了测试;这样的机器也被称为图灵机,图灵机也只是一个设想,并不是真正的机器。
其实我们发现图灵测试并没有对人工智能进行直接的定义,什么样才算是人工智能;而是反其道而行之,并没有拘泥于繁杂的过程,以结果为导向,达到什么样的结果(欺骗到测试人),才算是人工智能;我们有时候在处理棘手问题时也可以换个思路,以结果为起点对问题进行拆解。
在传记电影《模仿游戏》
中,也对这位孤独的天才提出的设想进行了描绘,当图灵被警探关到审讯室时,警探提出了一个意味深长的问题:机器能思考吗?图灵就提出了玩一个游戏(正是图灵测试);最终图灵露出了笑容,他感觉到警探理解了游戏规则。
在图灵测试被提出后,很多科学家和实验室也向这个测试发起了冲击;在1966年,MIT实验室诞生了一个真正意义上的聊天机器人Eliza,它的定位是一名心理治疗师。
Eliza主要的策略就是提出问题,并重新表述用户说的话,引导用户多描述问题。比如你告诉它:我今天有点头疼(headache),它就会告诉你头疼就去咨询医生用药;如果你说今天有点很沮丧(depressed),然后它就会说听说你不开心,表示很难过;如果你又说我妈妈(mother)照顾我,它会问你家里还有谁来照顾你。
Eliza通过关键词匹配规则对输入进行分解,而后根据分解规则所对应的重组规则来生成回复;通俗点说就是抓取句子中的关键字,比如发现句子中有妈妈
这个关键词,她就会说:跟我说说你的家庭;其实它的背后就是很多的if/else代码。
到了1995年,受到Eliza的启发,理查德·华勒斯开发了一个聊天机器人Alice,并于1998年开源;Alice的技术原理主要是基于自然语言处理,它的目的是模仿人类的自然语言,与用户进行有意义的对话。它可以回答关于天气、新闻、体育等各种话题的问题,还可以与用户进行有趣的聊天。
不过目前阶段,无论是Eliza还是Alice,他们的原理都是基于模式匹配(Pattern Matching),通过提取关键词,调用预先设好的文本进行回复。
这些对话机器人虽然能够进行简单的语言交互,但是对语句缺乏深度的理解和推理能力,很难和人类的对话水平相比。
但这种模式也并不是一无是处,可以避免很多重复性的工作,反而在我们身边也很常见。比如常见的购物网站、银行网站或App等,进入聊天界面先给你来一个热情的聊天机器人,巴拉巴拉罗列一堆关键词咨询你想干嘛。
上面我们也说到了,模式匹配的方式就算if/else代码写的再多,但是机器还是不能理解句子的真正含义,只是简单的回复,因此出现了一个新的模式,也就是:机器学习。
顾名思义,就是不进行人为的规定问题和答案,而是给机器一堆现成的案例,让机器来进行学习,这种方式也更加符合人类学习的认知规律。
这个阶段比较出名的就是SmarterChild(更聪明的小孩),它是ActiveBuddy股份有限公司于2001年开发,该聊天机器人用在了老牌即时通讯AIM,能够进行有趣的对话并快速访问其他服务的数据,比如天气、股票和电影数据;你甚至可以责骂它、刁难它,它似乎总是知道如何来应对。
SmarterChild也是最早集成到即时通讯平台的聊天机器人之一,在当时迅速引起了轰动,有3000多万用户在使用它;后来于2006年被微软收购,也被应用到了MSN Messenger上。
不过SmarterChild后,聊天机器人被遗弃了好几年,微软也关闭了SmarterChild的高科技部门。
2010年随着机器学习中的一个领域人工神经网络(Artificial Neural Networks简称ANNs)的爆发,人工智能迎来了空前的发展。
它的灵感来源于生物学,初中生物课本中我们学到过,动物的神经网络能够处理大量复杂的信息,就是通过大约1000亿个神经元彼此连接来执行功能;人工神经网络要做的就是模拟大脑中的基本单元:神经元。
人工神经网络看起来很厉害,其实本质上就是一个不断的提取特征的过程
,跟我们小时候学会认知事物很相似,就是找不同事物的特征;人工神经网络在得到一串样本数据后,也是通过学习提取所观察事物各部分的特征,将特征之间进行关联,再经过反复的训练,最后输出得到正确的答案。
通过这个过程,我们也能发现,人工神经网络需要海量的数据训练和强大的计算能力支撑;随着互联网的快速发展,大量的数据集也不再是问题。
Siri成立于2007年,2010年被苹果以2亿美金收购,最初也是以文字聊天为主,随后与全球最大的语音识别厂商Nuance合作,Siri实现了语音识别的功能,并于2011年在iPhone 4S上首次亮相,在当时引发轰动,iPhone 4S也成为了一代神机。
Siri识别你的声音使用的就是深度卷积网络,也是人工神经网络的一种;iPhone中专门有一个低功耗的处理器来运行这个神经网络,当相似度达到一定的阈值,就会启动Siri。
Siri的推出也标志着聊天机器人技术进入了一个新的时代,
时间来到了2017年,谷歌在《Attention is all you need》
一文中发布了一个新的机器学习模型:Transformer模型
,该模型主要用于克服机器翻译中传统网络训练时间过长,难以较好实现并行计算的问题。
传统的自然语言处理(例如语音识别、语言建模、机器翻译)依赖于循环神经网络(Recurrent Neural Network, RNN),利用循环进行顺序操作,也就是一个字一个字的学习,有着训练时间过长、难以并行计算的缺点。
而Transformer模型抛弃了传统的时序结构,并行处理序列中的所有单词或符号,同时利用自注意力机制将上下文与较远的单词结合起来;这就相当于学渣还在一个字一个字看书时,学霸已经一目十行,几个段落都看完了,这学习效率自然就杠杠的。
微软的GPT模型和谷歌的BERT模型,其中的
T
都是代表Transformer模型的意思。
我们发现一个规律,科学技术的发展进步总是伴随着理论研究的提出和突破。
既然模型有了,那肯定就有公司来对其进行商业化了,我们先来说说ChatGPT的母公司OpenAI,它于2015年由一帮硅谷科技大佬成立,包括我们熟知的特斯拉创始人埃隆·马斯克等,成立之初就确认了公司的主要目标:
包括制造“通用”机器人和使用自然语言的聊天机器人。
2018年,OpenAI在Transformer的模型基础上,又发布了生成式预训练语言模型
(Generative Pre-trained Transformer,即GPT-1);不过老对手谷歌的BERT很快就出现了,并且性能上全面碾压了GTP。
那被对手超过了OpenAI自然就不甘心了,于是疯狂砸钱,增大了训练数据集,陆续又发布了GPT-2和GPT-3模型,模型的参数量也从GPT-1的1.25亿个迅速“狂飙”
到GPT-3的1750亿个;数量更庞大的参数量也就意味着模型具有更强大的表达能力和更小的预测误差,也就是可以生成更长、更自然的文本。
海量的模型参数就让GPT-3在一些比较复杂的问题上也能有很好的表现,比如代替人类写一些论文,甚至编写SQL语句、JavaScript代码等等。
2021年,OpenAI基于GPT-3模型进行修改和改进,调整了模型参数,添加了训练数据,年末发布了GPT-3.5,也就是目前很火的ChatGPT的原始模型。
2022年11月,ChatGPT上线仅仅几天就获得了100万用户,上线两个月,其月活就达到了惊人的1亿,成为历史上用户增长最快的消费应用。
大量用户涌入的背后,是需要庞大的算力成本和服务器的投入;根据某研究机构的测算,运行这么复杂的GPT-3.5模型,需要的GPU芯片的数量就高达2万枚,而专业级显卡一般使用A100;根据某购物网站的数据,10万人民币一块A100显卡的价格在国内还是有市无价;因此粗略计算下,单单显卡的投入就至少在20亿以上;还有其他如数据采集、人工标注、模型训练等软性成本更是难以统计。
聊了这么多的ChatGPT的发展历程,相信大家对这上百亿投入的“高级聊天机器人”肯定也都迫不及待、跃跃欲试了,那么这里笔者就分享一下注册的攻略;由于众所周知的某些原因,国内用户并不能流畅的直接访问,注册的过程也会有一些曲折,因此我们需要做好以下准备工作:
首先我们准备好一个接收短信验证码的手机号,打开sms-activate.org并注册账号,语言调整为中文,点击右上角充值
按钮,支付方式选择支付宝。
账号最低充值金额是1美元,按照当前汇率折算下来也就大概七块多人民币;我们可以先充值1美元。在左侧选择国家,然后搜索OpenAI
,租期默认4个小时,点击出租;这里不同国家的价格也不相同,一般东南亚国家如印尼、泰国、印度尼西亚价格会便宜,但是现在用的人比较多,有概率会被OpenAI屏蔽或者没有号码的情况;其他欧美国家相对贵些,1美元有可能下不来,比如笔者选择的西班牙,就砸下去整整2美元。
可以对不同的国家号码进行尝试,在四小时内没有收到短信是可以退款的。
手机号码准备完成后,我们就可以来注册了,打开signup页面,在输入邮箱地址、密码以及验证邮箱后,我们就来到了验证手机号码的页面,我们选择上面租用手机号码的相应国家,然后粘贴号码、点击发送即可。
粘贴手机号码时需要把前面的国际区号删掉。
回到sms-activate页面,我们在留言
中可以看到收到的OpenAI的code,粘贴即可。
注册完成后我们打开chat.openai.com/chat就可以使用了。
它的使用方式也很简单,在下面输入框中输入你的问题,点击发送按钮,就能喜提一个上知天文下知地理、无所不能的话痨机器人了。
所以,这么多科技大佬砸了几百亿研发出来的ChatGPT,仅仅是用来给我们聊天的吗?要想了解这个问题,我们不妨先来问问ChatGPT自己吧。
确实,让ChatGPT用作聊天机器人,确实有点杀鸡用牛刀了;但是,我们先来看看这把牛刀,用来杀鸡到底够不够快呢?我们要是想要搞点事情,就需要调用API,而OpenAI刚好在近期提供了API Keys的接入方式,可以将ChatGPT集成到我们的应用程序和服务中。访问platform.openai.com,点击API Keys => Create
按钮,在出现的弹框复制keys即可。
弹框隐藏后就看不到api key了,需要去重新生成。
复制成功后,这里推荐wechatbot这个项目为个人微信接入ChatGPT;有多种方式来运行项目,可以基于源码运行,也可以基于docker来运行,不过都需要用到上面复制出来的key,具体运行方式可查看项目说明,这里不再展开了;如果对docker不了解的小伙伴可以查看这篇文章。
项目运行后,使用微信扫码登录即可,然后我们的微信号就自动接入了ChatGPT的聊天了;使用方式也十分的简单,私聊这个微信号会直接回复,群里需要@
这个微信号。
重要提示:滥用有可能会被微信封禁危险,尽量用小号,本文不承担任何责任。
不过需要注意的是,每个账户的API的调用也是有限制的,目前是5美元,还有过期时间,大家娱乐玩玩就好,有条件的小伙伴可以进行充值。
除了用来聊天,在工作和学习中,也深受众多学生和职场人追捧。在国外一所大学哲学教授评分时,十分惊喜的读到了一篇“全班最好的论文”,论文以简洁的段落、恰当的举例和严谨的论据探讨了一个哲学问题;然而在教授的追问下,学生承认了论文是用ChatGPT写的。
在工作上,ChatGPT也挽救了不少职场人的发际线,用它生成了包括且不限于:领导讲话稿、媒体通稿、集团简介、颁奖词、祝酒词、宣传册等等,甚至连周报月报、请假理由这些微不足道的小事,它也能给你包圆咯。
在文字润色方面,ChatGPT丝毫也不输专业的编辑,在周报月报甚至是年报中,这就相当实用了,懂得都懂。
对于一些简单的工具函数,我们可以很方便的让ChatGPT直接生成即可;比如我需要一个隐藏手机号码的函数,描述这个函数的功能即可;甚至还能够联系上下文,这是以往的人工智能没有实现的。
ChatGPT不仅帮助我们解决了问题,还能有理有据的解释问题背后的逻辑;比如笔者上周在Vue3中使用KeepAlive组件就遇到了问题,在搜索百度后,虽然有很多的回答,但我们还需要在大量的网页中去进行二次筛选,最后可能筛选出来的解决方式都是千篇一律(互相抄袭);而且用下来也是错误百出,各种报错,用户体验十分不友好,估计花了大半天的时间才解决问题。
比如百度找到的这篇文章中,如果使用红色线框中的写法,vue-router就会出现各种奇怪的报错,而且出现的错误信息根本没法去搜索。
但是笔者如果使用ChatGPT提问,我们看到它逻辑清晰,还有具体的案例和注意的提示,我们只需要把组件的名称放到include属性下即可;在它的帮助下,笔者相信在当时能够跳出错误的逻辑,大大缩短解决问题的时间。
在文案生成方面,ChatGPT也是一把好手;当我们面对空白的文档苦苦思索的时候,不妨打开ChatGPT,描述我们的需求,轻松的生成一段粗略的文案,在此基础上进行再次编辑,节省时间和压力。
在之前办公软件一文中我们就介绍了ONLYOFFICE,在ONLYOFFICE官网免费下载桌面版或者免费注册在线个人版,在办公软件里面使用ChatGPT,快速的生成文案。
首先需要安装插件,我们在github克隆代码后,找到/sdkjs-plugins/content/openai/
,选择所有文件添加到ZIP文件,然后把文件格式改成plugin;打开文档界面,找到插件 => Setting => Add plugin
,选择我们的plugin文件,插件就成功安装了。
然后输入上面获取到的API Key,我们的插件就激活完成了;在文本字段中描述我们想要生成的文案内容,点击提交按钮即可;ChatGPT会对请求进行处理,在几秒钟内返回响应,并在文档中以纯文本的形式插入。
很多小伙伴看到这里肯定也不得惊叹:这ChatGPT确实太厉害了!笔者的很多程序员小伙伴也都在感叹:我们是不是要失业了,但是目前看来暂时还不会,毕竟它的训练成本确实高昂,应用落地起来不容易。
但有一些岗位,比如客服岗位,就比较容易受到冲击了;相信大家应该都接到过类型的机器人客服电话,都是识别特定的关键字,按照固定流程,一句话一句话的回复;随着ChatGPT的出现,相信未来机器人客服能够应对更加复杂的场景,更加精准的理解顾客的需求,从而灵活的应对。
ChatGPT会完全替代程序员的工作吗?
在编程方面,笔者觉得ChatGPT在目前阶段还不能完全的取代程序员;在生成代码片段虽然能够很好的实现,有点类似之前的Copilot;但是在复杂的项目中,需要去理解不同文件模块的含义,从而进行调用,ChatGPT就无法取代了;在代码bug修复、前后端联调、跨部门协调、出差对接等需要组织协调的工作上更是无法替代人工。
笔者认为ChatGPT带来更多的是编程效率的提升;比如原来我们需要一天做的工作量,有了它的协助,一些重复简单的模块我们直接丢给他就能生成了,最终可能不到半天就能够完成了。
商汤科技董事长:未来软件的代码可能80%都是由AI生成的。
虽然目前看来并不能完全的替代,随着硬件成本不断降低、机器学习能力提高;当机器训练的成本低于程序员的工资时,你觉得资本家在需要交五险一金、时不时还要摸鱼的你和三四毛一度电、24小时不停运行的机器之间如何抉择呢?相信未来原本可能需要数十人的研发部门,最终只需要三四个核心工程师维护就能保证业务的正常进行。
未来我们如何去选择就业?
未来ChatGPT虽然会替代一部分低端的岗位,但是肯定也会不断创造出新的职位;就像计算机的出现淘汰了打字员,但是创造了大量的程序员岗位一样,人工智能创造出很多新兴岗位,比如提示词顾问师
(笔者自己臆想),专业负责给ChatGPT提问生成相应文案或者素材;这是社会进步的必然趋势,也是逼着被历史大势裹挟前行的我们,不断的学习进步。
在前端方面肯定也会淘汰很多初级的程序员,程序员的门槛不断降低;因此我们需要在人工智能取代我们之前不断的学习进步;笔者认为在WebGL方面,人工智能对于复杂图形化和对美学的理解还是不能够替代人工,因此前端的小伙伴可以尝试进阶这块领域,同时高级架构师也是不错的方向。
相信在不久的未来,ChatGPT能够帮助我们极大的提升生产力和学习效率;面对新技术或新文章时,不需要再完整的阅读,让ChatGPT生成文章的大纲和主要内容,帮助我们快速学习;在写文档时,也能够让它快速生成一篇文字优美的内容。
我们正处在一个见证历史的时刻,没人能够在人工智能的历史洪流面前独善其身,保持傲慢不屑的态度只会加速被淘汰,正如小说《三体》中说的那样:
弱小和无知不是生存的障碍,傲慢才是。
在电影流浪地球2中,刘培强问Moss,人类能活下来吗?Moss说人类的命运取决于自己的选择;就像我们现在问ChatGPT会不会取代程序员一样,虽然它告诉我们还无法完全取代,但是如何做出选择,是关系着我们每个人的命运;而当历史的车轮缓缓驶过时,我们唯一要做的事,就是尽量跑在它的前面。
]]>首先这样的滚动效果和fullpage.js、Swiper.js全屏翻页滚动轮播的效果是不一样的,页面元素的位置极度的依赖于滚动条的位置,因此是需要监听滚动条事件;笔者在调研了better-scroll.js、scrollReveal.js和iScroll.js等一系列插件后,发现这些插件并不能满足需求。
笔者也曾一度想过不依赖库,自己来实现类似的效果,不就是监听页面滚动么;但是想了想滚动时这么多元素的动画效果导致的性能问题以及页面resize后如何来重新计算也是不小的问题,于是就打消了不切实际的念头。
在扒开很多网站的源代码之后,笔者找到了一个很多网站都在用的动画库:GSAP;但是很奇怪,网站搜索这个库,我们发现它的教程非常的少,这么好用的一个动画库不应该资料这么匮乏;但是看到官网全英文的教程和有时候无法访问demo教程后,以及有点难理解的各种概念后,我好像知道了原因。
由于我们要实现的很多动画效果都依赖于GSAP,因此我们先来看下GSAP的使用教程。
首先我们要知道这个库能做什么,The GreenSock Animation Platform (GSAP)
是一个功能十分强大的动画平台,可以帮助我们实现大部分的动画需求,构建高性能的、适用于所有主要浏览器的高性能动画;GSAP非常的灵活,可以在任何框架上处理页面能够所有通过js改变的元素,不仅可以对div的css属性进行动画,还是SVG、React、Vue、WebGL,甚至和Threejs一起使用。
除了GSAP核心库外,还有很多实用的插件,比如结合ScrollTrigger插件,我们可以实现非常震撼的滚动触发效果;同时也不需要担心响应式的问题,GSAP确保项目响应迅速、高效且流畅。
我们从一个简单的例子开始,先把一个.box
元素沿着X轴移动200px;
1 |
|
如果我们对.box
元素进行元素检查,我们会发现GSAP实际上是不停的修改transform
属性,直至最终停留在transform: translate(200px, 0px)
;我们继续回到上面的代码。
在上段代码中,我们发现这段代码包含有3层含义:函数、目标和变量;首先目标就是我们想要移动的元素,可以是CSS选择器,也可以使dom元素,甚至是一串数组:
1 |
|
然后是函数,有四种类型的动画函数:
我们直接看效果就能明白这几个函数的意义了。
1 |
|
查看demo3效果
1 |
|
查看demo4效果
1 |
|
查看demo5效果
最后是变量对象,这个对象可以包含的信息种类就比较丰富了,可以是想要动画的任意CSS属性,也可以是影响动画表现形式的特殊属性,比如duration持续时间、repeat重复次数。
1 |
|
查看demo6效果
GSAP可以动画任何属性,没有确定的列表,包括CSS属性、自定义对象属性甚至CSS变量和复杂的字符串,最常见的动画属性是transforms和透明度。transforms属性是动画中性能消耗最小的,可以用它来移动元素、旋转或者放大缩小,因为他们不会影响页面的布局,更不会使页面重排,因此有着较好的性能表现。
尽可能的使用transforms,而不是布局属性,例如top、left或者margin,有更平滑的动画体验。
我们可能比较熟悉以下的transforms属性:
1 |
|
GSAP提供了下面的缩写形式,上面的transforms属性可以直接缩写成下面的属性(yPercent表示百分比元素的高度):
1 |
|
GSAP支持CSS属性转为小驼峰形式,例如
background-color
变成backgroundColor
通过上面的例子我们也发现了,默认情况下GSAP会给transform属性使用px和degrees单位,比如{x: 10, rotation: 360}
就表示x轴10px,旋转360度;但是我们有时候想要使用其他的单位,比如vw,radians或者相对单位。
1 |
|
GSAP的神奇之处在于,不仅能够对dom元素动画,还能够对非dom元素,比如svg、js对象等进行动画操作;对于svg元素,我们添加attr属性
额外的处理一些svg的属性,像width、height、fill、stroke、opacity等。
查看demo8效果
1 |
|
甚至,我们对js对象进行动画时,不需要任何dom元素,针对任意js对象的任意属性进行动画,onUpdate函数用于监听动画的更新过程:
1 |
|
特殊属性用来调整动画的表现形式,我们在上面用到了repeat和duration,下面的文档中提供了一些常用的属性:
属性名 | 描述 |
---|---|
duration | 动画的持续时间(单位:秒)默认0.5秒 |
delay | 动画延迟时间 |
repeat | 动画重复的次数 |
yoyo | 布尔值,如果为true,每次其他动画就会往相反方向运动(像yoyo球)默认false |
stagger | 每个目标动画开始之间的时间(秒) |
ease | 控制动画期间的变化率,默认”power1.out” |
onComplete | 动画完成时的回调函数 |
repeat属性
就是重复的次数,会让动画执行多次;需要注意的是,如果我们填一个数值2,但实际动画的次数是3,因此我们总结出来公式:真实运动次数 = repeat属性 + 1
。
如果我们想让动画一直重复下去,使用
repeat: -1
。
repeat一般会和yoyo属性
一起使用,当yoyo为true时,在每次动画结束都会反向运动;需要注意的是,一个运动循环包含一个正向和反正运动,反向运动也计入运动的次数中。
1 |
|
我们这边repeat写的2,实际动画中,正好是3次运动,1.5次循环往复运动。
delay也非常好理解,动画开始延迟时间,如果后面是repeat重复的动画,则不会有延迟了;如果我们想要为后面的任何重复运动添加延迟,可以使用repeatDelay
属性。
1 |
|
查看demo11效果
我们发现同样是总计2次的重复旋转运动,绿的div动画开始前有停顿,而后面的重复运动就没有停顿了;而紫色的div动画开始前没有停顿,在后面的每次重复运动则会有停顿,就是repeatDelay的作用。
ease速度曲线也是动画效果的一部分,我们可以看到不同的速度曲线旋转效果也是不一样的。
1 |
|
stagger属性
也是比较有趣的属性,我们可以利用它控制多个目标之间动画的延迟差,形成奇妙又好看的交错效果。
1 |
|
查看demo13效果
比如这样,让div交错消失的场景;或者交错动画一个阵列,只需要告诉GSAP有多少行列。
1 |
|
查看demo14效果
我们动画经常会遇到多个对象的情况,虽然我们可以使用上面的delay进行简单的控制,延迟物体的动画开始时间;但是如果中间某个物体的动画执行时间突然延长了,那么其后面所有的动画时间需要进行手动进行延迟,这显得非常不方便;因此我们需要引入时间线timeline的概念。
时间线是GSAP最重要的概念之一
我们通过gsap.timeline()
创建一个时间线,然后通过时间线控制每一个动画顺序执行;这样即使我们修改中间某个动画的duration
,也不会影响后续时间线。
1 |
|
查看demo15效果
但是如果我们想要在一个动画开始的同时,执行另一个动画,除了再额外创建一条时间线,我们可以在to函数后面加一些小参数来进行精确的控制。
1 |
|
查看demo16效果
理解上面代码中的这些小参数可以帮助我们构建很多复杂精妙的动画效果,让我们能够在任意时间点来执行任意的动画效果;上面的例子乍一看可能不是那么好理解,不过没有关系,我们一点点来理解。
虽然我们上面都是以gsap.to
来为例,但是其他的函数比如from()、fromTo()、add()等也都适用;需要注意的是这些参数跟在变量对象
的后面,因此函数的代码结构如下:
.method( target, vars, position )
我们将这些参数简单的分一下类就好理解多了,其实主要有以下几种类型:
<符
和>符
:”<”在上个动画开始,”>”在上个动画结束。+=
在最后一个动画结束后,-=
在最后一个动画结束前。 绝对值就表示在某个绝对的秒数时执行动画,比如上面demo中的green元素,在1秒时执行动画;<符号
表示在上个动画开始,比如demo中的purple元素,就和green元素同时执行动画;我们还可以在后面加个数值,比如:<3和<=3,两种表达方式的含义相同,都表示在上个动画开始后的三秒执行。
1 |
|
查看demo17效果
在上面的gif效果中,我们看到,purple元素是在green元素开始的3秒后才开始执行,并不是结束的3秒后;>符号
则表示上个动画结束时,用法类似,这里就不再赘述了。
相对符则表示动画结束的时间点,+=1
表示上个动画结束1秒后,-=2
表示上个动画结束前2秒。
label值则很好理解了,在某个时间点插入一个label,在这个label前面或者后面的时间来执行,我们看下它的用法:
1 |
|
通过gsap.add
函数,我们在2秒处放置了一个myLabel的标识,在后面使用myLabel+=1和myLabel-=1相对这个标识的时间进行控制。
查看demo18效果
不同时间线中的动画可能会有相同的特殊属性,比如repeat和delay等,我们可以在时间线的创建函数中统一设置,避免重复:
1 |
|
如果你发现某个属性你重复使用了很多次,比如x、scale、duration等,我们就可以使用defaults属性
,任何加到defaults属性中的参数都会被下面的函数继承。
1 |
|
在有些情况下,我们需要对动画的开始、过程、结束的某个时间点进行回调操作,gsap提供了以下回调函数:
1 |
|
现在我们对gsap的基本用法有了一定的了解,下面我们来看下插件的用法;插件可以帮助我们扩展动画的高级功能,让动画的表现更丰富;我们主要来了解ScrollTrigger的使用。
我们先看下ScrollTrigger的一个简单用法,
1 |
|
使用前当然要对插件进行注册了,使用gsap.registerPlugin
将ScrollTrigger注册,否则我们在下面操作时会发现没有任何效果。
在to函数中我们新增了一个scrollTrigger属性
,trigger表示当前动画触发的元素,这个很好理解,我们使用当前元素;markers是否进行标记,scrub表示是否将动画效果链接到滚动条,随着滚动条平滑处理;如果是false(默认),随着元素出现在视窗内,直接触发动画,如果是true,则平滑动画,我们看下效果:
查看demo21效果
scrub还可以是某个具体的数值,表示延迟滚动条多少秒动画;比如这里的1,延迟1秒执行动画。
我们在滚动浏览器时,可以使用pin属性将某个元素固定在某个位置;pin可以是css选择器字符串、布尔值或者直接dom元素;如果是true,则直接固定当前的动画元素;我们这里使用pin将purple元素固定起始位置:
1 |
|
查看demo22效果
start和end属性用来决定滚动触发元素开始的位置,可以是字符串、数值或者函数,两者的用法类似,我们以start为例;start的值默认是"top bottom"
,它的含义是当触发物体(trigger)的顶部(top)碰到浏览器的底部(bottom)时;我们看下当开启标记marker时的触发位置。
我们看到scroller-start
的线就是浏览器视窗的边界线,当浏览器向下滚动时,这条线滚动到物体的start
线时,就触发了动画效果;同样的道理,向上滚动时,当scroll-end
的线触碰到end
时,动画结束。
start值看起来很怪异,不好理解,其实我们可以把它拆成两部分来看;第一个top值表示物体的上边界,同样的我们可以设为bottom(物体下边界)、center(物体中间)或者具体数值(100px、80%),即控制的是物体旁边的start线。
第二个值表示浏览器视窗滚动触发的scroller-start线,bottom表示视窗的底部,我们也设为top或者center或者数值,以及百分比(例如80%,表示整个视窗的80%高度),甚至是相对位置,比如bottom-=100px
。
有些情况下,我们不想要gsap的动画,而是想用我们自己自定义的css类名来实现某些动画效果,toggleClass属性
可以让我们在触发的元素上添加或者移除这些的类名,从它的名字也能看出来它是处理类名的;它可以是一个字符串,例如toggleClass: "active"
,就表示要新增/移除的类名。
toggleClass也可以是对象,可以在其他的元素上来新增/移除类名,比如:
1 |
|
查看demo24效果
我们可以将ScrollTrigger结合timeline创建动画。
1 |
|
查看demo25效果
本文GSAP所有的用法教程大致到这里就结束了,本文涉及到了一些动画方面的概念,有些不准确的地方欢迎指正;笔者也参考了官网很多demo和英文案例的翻译理解,工作量较大,望给个一键三连。
]]>上一篇文章我们介绍了Linux的历史和优势,笔者的很多文章也都是在Ubuntu系统上完成的;但是也有小伙伴在评论区说Linux下的办公软件比较匮乏,我们今天就来看下Linux下那些好用的Office办公软件。
相信很多小伙伴作为打工人,都被Office软件折磨过,各种开会需要写不完的PPT,整理各种Word文档资料等;有时候一不留神,可能文档就出点幺蛾子,一天的工作就白干了,甚至半夜还在改文档;明明Office软件的初衷是让我们的工作更方便,可是往往事与愿违。
Office是我们日常办公中必不可少的软件,一款好用的Office软件能够让我们在工作上事半功倍。
对于很多用户来说,没有微软Office(下面简称MS Office)的支持是他们迟迟不愿意切换到Linux的主要原因,甚至是唯一原因;是的,Linux不支持MS Office,但是这不影响我们在Linux下进行办公。
笔者安装了Linux系统下,市面常见的三款办公软件,今天我们从界面实用、功能丰富、与Office兼容以及在线协作等几个方面来体验以下这三款办公软件。
首先出战的是我们的老牌玩家WPS,WPS与MS Office的渊源也极深,两者在界面上也极为相似,很多人可能会觉得WPS是抄袭的,其实WPS才是国内办公软件元老,MS Office是后来者,这里能写一长篇历史文章来探讨,就不再展开了。这里顺便问大家一个问题:
金山文档和wps到底有什么区别?
相信很多小伙伴都分不清两者的关系,在社交App即刻中,金山文档还和WPS进行互动,置顶了两者关系的动态。
简单来说WPS对标的是微软的Office三件套办公软件,主打本地文档、深度编辑
,需要下载安装WPS软件的;而金山文档对标的则是谷歌文档,从它的slogan:一起办公才高效,可以看出来,主打协同办公、多人在线编辑
,因此它是不需要安装软件的,直接在web端操作,不过在一些功能上相较本地的WPS还是少了不少。
由于WPS和金山文档都是自家产品,两者在账号上是打通的,因此我觉得两者更像是微软自家的Office和Office Online的关系。WPS编辑的本地文档上传后,需要多人编辑的时候,可以在web端的金山文档上打开,同时分享给好友。
平台支持上,WPS更是做到了全平台支持
,不仅支持了常见的Windows和Mac平台,连Linux平台也能照顾,还有移动端的安卓和iOS,更是方便了移动办公的场景。
不过这里要吐槽一下我装的Ubuntu下的WPS,新建文档选择模板时,本地的模板数量还是比较少的,没有Windows平台下那么多,想要更多的模板要打开稻壳官网下载;不过看到这么多免费的模板,笔者也就忍了把。
在开始的主界面,我们看到整个工具栏,WPS和MS Office还是比较相似的,减少了上手的成本;不仅是Word,Excel和PPT基本都是相似的,这里就不再一一截图。
在日常使用习惯上,WPS也在Office的基础上进行了优化,更符合国人的工作场景。
举一个小栗子,比如在Excel中展示万元的单位,Office需要了解各种占为符、转义符,最后定义一个格式;但是在WPS中,你只要能看得懂汉字就可以了,直接在单元格格式设置即可。
在Word中清理空行比较麻烦,需要不断的进行查找和替换操作;而在WPS中,你也只要会操作鼠标就可以了,点击文字排版
,其中有删除空段和删除空格两项;直接点击删除空段,空行就没有了;此外还有一个智能格式整理
,点击后会一键帮你完成文章的排版(首行缩进、空行删除等),一下子省不少事。
还有小伙伴吐槽WPS卡、慢,尤其是打开大文件的时候:
某网友评论:我用了十几年的MS office,又开始用WPS,差距还是相当明显的。比如打开大文件,存储超过1000行的,WPS明显慢,还经常卡。文件越大越明显,打开的文件越多越明显。这是最大的缺点。
这里笔者准备了一个50多MB的、两千多页的一个PPT;相信两千多页应该已经不小了,一般写PPT也不会写这么多页。
这里笔者的电脑是8GB内存,属于一般的家用电脑,在刚打开的时候会有一个加载的过程,有一个卡顿的时间;加载完成后,就可以正常编辑、操作了,不影响使用。
还有被很多小伙伴吐槽的最多的就是WPS的兼容性问题了,这也是很多小伙伴担心的,但是在笔者同时打开多份文档后,并没有出现乱码的情况;WPS是全面兼容MS Office2003-2010版本,打开开始=>WPS=>配置工具=>高级=>兼容设置
,我们可以选择对应的Office版本来兼容。
笔者认为,主要是一些高级的功能,双方在争夺用户上都在暗自发力,因此高级功能的兼容较差;比如Office 2019新增的3D模型,在WPS上打开则显示为静态的图片;PPT的一些高级动画也是重灾区,很多VIP酷炫的动画在Office上直接没有效果。
总结一下,WPS适合那些对于高端商务办公没有太多的需求,也不愿意在办公软件上投入太多的小白用户,不会破解Office也不想付费使用;他们要的只是一款能够打开和简单编辑文档的软件,做到拿来就用,同时也没有太多的学习成本。而且随着WPS彻底关闭广告,相信让大家一直吐槽的卡慢等问题也会得到缓解,从而进一步提高用户体验。
是否开源:否
界面指数:★★★★☆
功能指数:★★★★★
协同指数:★★★★☆
兼容指数:★★★★★
推荐指数:★★★★☆
LibreOffice是Linux默认安装的一款开源的办公套件,至于说它是套件,因为它包含了Writer,Calc,Impress,Draw,Base以及Math等多个组件。
目前,截至本文发稿时,LibreOffice最新版本是7.4.4,长期支持版本则是7.3.7,支持Windows、Linux和macOS三大操作系统平台。相较于MS Office的高昂价格,LibreOffice显得十分亲民了,对于个人和企业用户,均不用支付任何费用即可使用。
LibreOffice是开源社区创造的项目,任何人都可以参与,我们可以从官网或者Github下载到源码。
与WPS兼容Office不同,LibreOffice使用的是一种叫开放文档的格式(OpenDocument Format, ODF);它是由Sun公司最先提出来的规范;根据笔者查到的资料,该格式主要是由OpenOffice和LibreOffice支持,Google Docs允许将 ODT文件作为Google Docs文档打开并直接进行编辑。
它包含了以下三种主要的文件格式,和我们常见的Word、Excel和PPT相对应。
和大家印象中很火的开源软件不同,比如Linux、VLC、Termux、VS Code等等,LibreOffice显得那么的低调平凡,相信很多小伙伴也没有听过它,笔者只是在装完Ubuntu系统后,在程序菜单里看到一系列图标,大概猜到这是一个办公软件,然后。。。。。就没有然后了。
造成LibreOffice不温不火的主要原因个人感觉有以下几点;首先就是经典的老一老二打架导致老三消失案例,国内办公软件阵营基本分为Office和WPS,两大阵营互相竞争,同时又互相合作,利用.doc、.xls、.ppt、.docx、.xlsx、.pptx、.pdf七个文件格式垄断办公领域。同时Office和WPS都有商业公司来运营推广,LibreOffice开源自带低调特性,加上ODF格式的不流行,主要依靠Linux预装来发展,造成知名度不高。
其次是不符合用户习惯(主要是国内用户),习惯了Office菜单和工具的布局方式,再来使用LibreOffice,需要一定的上手成本,很多操作按钮需要要去习惯;和WPS在按钮旁边展示按钮中文说明不同,LibreOffice需要在按钮上悬浮才能展示说明文字,这对笔者这样的懒癌用户就不是那么的友好。尤其是Impress(即PPT)的制作上,操作按钮让人看了一头的雾水,不知道是干嘛用的,这一点相信就劝退了很多用户。
最后是兼容性问题,LibreOffice使用的ODF格式和MS Office兼容性较差,在打开文档时容易出现格式问题;比如在.docx中行距会有偏差,在.pptx中的渐变背景、渐变文本填充、发光文本、切换动画几乎全部报废,兼容性没有WPS来的好。
参考链接中附录了微软整理OpenDocument和Office格式的差异。
总结一下,LibreOffice适合于那些重度的开源爱好者,经常使用Linux平台,有一定的技术基础及兴趣爱好,愿意花费一定的时间来折腾学习;同时身边最好有一定的使用氛围,比如老板和同事也都在使用(不然容易被喷)。
是否开源:是
界面指数:★★☆☆☆
功能指数:★★★★☆
协同指数:★☆☆☆☆
兼容指数:★☆☆☆☆
推荐指数:★★☆☆☆
OnlyOffice是一款免费开源
、无广告的在线文档编辑
套件,虽然主打在线编辑
,OnlyOffice也提供了Windows、Linux和macOS多平台的桌面编辑器以及移动端的安卓和iOS版。该套件包括主要三个办公软件:Word、Excel和PPT,以及表单模板,PDF查看器和文件转换器。
OnlyOffice文档是一款强大的在线编辑器,为您使用的平台提供文本文档、电子表格、演示文稿、表单和PDF查看器。
OnlyOffice主要分为桌面编辑器、文档服务器和连接器
三款软件,桌面编辑器就是文档的本地编辑软件,和WPS一样。
文档服务器则比较独特,这是在其他的办公软件中没有的,是一款可以部署在本地服务器的软件,在浏览器中处理Office文档的全功能在线办公套件,提供了企业版、开发者版和社区版三种不同版本,每种版本针对的用户群体不同;如果没有本地服务器,还可以使用官方的云服务器。
文档服务器提供了多种安装方式,Windows Server、Linux、Snap或Docker镜像等都可以下载部署。
连接器其实就是针对其他应用平台开发的一个插件,可以将文档服务器集成进去,方便协同办公;比如常见的Nextcloud、Confluence和ownCloud都有对应的连接器下载。
我们从官网找到Linux版本的桌面编辑器来下载,官方提供了DEB和RPM等常见的Linux发行版下载。
找到适合自己电脑的安装包,这里笔者使用dpkg来安装:
1 |
|
不出意外的话就要出意外了,有些同学电脑上可能会出现和笔者电脑一样的错误信息。
这是由于安装时缺少一些依赖关系,我们可以执行以下命令来安装缺失的依赖。
1 |
|
如果还是失败可以将服务器源更改为国内的源,例如:阿里云、腾讯云、华为云等;再次dpkg没有报错信息安装成功后,我们在应用程序列表就可以找到并打开。
我们看到欢迎界面上有一个OnlyOffice云
,这就是它的在线编辑功能,留个小彩蛋,我们后面会介绍到。
安装完成后,我们就来看下OnlyOffice的有哪些功能;首先打开常用的办公三件套,我们发现整体的界面呈扁平化风格,和Office还是相似的。
在基础编辑功能方面,OnlyOffice能够满足绝大多数的办公场景了,我们来看下它有哪些特点。
首先它本身支持多种文件格式,文件另存为,选择右下角的格式;我们看到它支持下面的格式,除了docx、表单格式、OpenDocument格式以及pdf、epub;是的,我们还可以将文档导出到电子书上进行查看。
然后一个非常实用的功能就是它的文档比对工具
,相信写过大学论文的小伙伴都会经历过好几个版本的论文迭代,1.0、2.0、2.1等等,最后可能连自己都傻傻分不清哪个版本修改了哪些内容。
而Beyond Compare只能对比文本文件的差异,对二进制的Word文件就无能为力了;但是在OnlyOffice中提供了比较的功能,点击协作=>比较
,可以选择文件中的文档和url中的文档;这里笔者在求职意向后面加了1,比对工具立刻出现弹框提示了。
OnlyOffice另外一个非常好用的功能就是表单了,这也是它特有的功能;说起表单,很多小伙伴肯定都会想到在网页上填写表单;是的,OnlyOffice的表单就是将现有的Word文档变成可填写的表单。
想象一下,你是一名hr同学,今天你联系了20个来应聘你们公司岗位的应聘者,你要给他们每人发一份文档填写个人信息;但是每个人填写的信息可能都不太对,有些人可能粗心,手机号多填一位或少填一位,每个人出生日期的格式可能也会填得五花八门,学历的内容你可能也会看到填写硕士或填研究生的。
面对这样的情况,hr同学就需要去和每个人沟通,重新填写或者帮他们改;很多的计划书、商业协议或者各种合同等也都会遇到类似的情况;OnlyOffice的表单就可以让我们创建这样的表单文件,分享给其他用户填写,同时在文档中就进行校验。
还记得上面导出多种格式中的表单格式吗,我们可以很方便的将自己电脑中的Word文档转为docxf的表单文件,然后添加我们所需要的字段:文本字段、图像、组合框、下拉列表、复选框、单选按钮、电子邮件、电话号码等等。
在右侧的文字字段浮框中,我们甚至还可以编写校验的正则表达式,docxf内置了邮箱和手机的正则;像学历这样的多字段列表,我们就可以使用下拉列表,设置好多个数值,就可以像网页上一样使用下拉列表了。
什么?你说你还是不会使用表单?没关系,OnlyOffice也提供了丰富的在线模板库,可以直接下载然后修改,模板全部都是免费的。
聪明的小明同学可能马上就会想了,这发docxf格式的文件,人家应聘者也打不开啊?诶,同学,这你就小看了docxf格式了,经过笔者在电脑上测试,docxf使用WPS和MS Office均能正常打开,打开方式选择Word即可,样式也并没有任何的错乱,保存时也可以正常保存为docx文件。
最最最厉害
的地方来了!!!通过OnlyOffice的在线协作功能,我们甚至都不用发文件了,直接发一个文档的链接,让对方直接在线填写;在docxf文件中,选择表单=>下载为oform
,将表单导出为一个oform文件,我们可以将这个文件放到自己的文档服务器(下面会介绍文档服务器)或者云服务器,然后将链接直接发送即可。
聪明的小明同学这时候可能又要说了,那分享出去的链接岂不是任何人都能填写和查看吗?还会造成信息泄露。不用担心,分享的链接也不用担心权限问题,数字表单权限管理上面非常的高效,我们可为需要填写表单的用户分配各种角色,简化文档工作流。这样,用户就能根据角色匹配的颜色,直观地识别他们应该填写哪些字段。
在兼容性方面,笔者在网上下载几个模板后,使用OnlyOffice打开,格式上也没有错乱。
既然OnlyOffice主打的是在线编辑功能,那么在网页上协同编辑肯定是有过人的特点了;不错,在线协作确实是它的优势,我们打开共享设置,设置分享权限;可以让朋友和同事完全访问权限,来和你一起编辑文档,也可以设置审阅和评论权限给甲方。
共享编辑是即时同步
的,对方的修改能立刻同步到你文档界面上,同时你也能看到谁修改了哪个地方;但这时如果有不靠谱的同事,手残删除了你苦心编辑的文案怎么办呢?
那也不用担心,协作编辑前打开协作=>跟踪变化=>对所有人启动
,这样就会把所有人编辑的历史都记录下来;想要回退到任意历史时刻的版本,点击对应版本下方的还原按钮即可。
我们在合作编辑时,经常需要来沟通交流,分配任务、修改文案等等,OnlyOffice提供了直接沟通聊天的渠道;点击左侧的聊天按钮,我们可以直接在页面里进行沟通,省去了在办公软件和聊天软件之间频繁切换的麻烦,提高了生产力。
OnlyOffice还支持插件扩展,具有非常高的可扩展性,并且插件的功能也非常强大;点击插件=>Plugin Manager
我们就能看到插件库中所有的插件,像draw.io插件能够编辑图形、OCR图像识别插件、Photo Editor图片编辑插件和实时聊天插件Telegram等;甚至还有Chess象棋插件,让我们在工作之余,能够和其他的合作者一起来一局象棋比赛。
多年之后,当我们在Word文档中玩游戏和聊天时,我们是否也会想起过第一次打开Office时懵懂、激动、好奇的那个遥远的下午,以及从不会安分地蹲在屏幕旁边,时不时吸引你注意力的那个小曲别针。
笔者觉得最实用的插件还是Google翻译插件;打开插件,选择它认识你而你不认识它的英文单词,翻译结果也立刻就出来了,省去了频繁切换翻译软件,同时我们点击Insert按钮,还能直接将翻译的结果插入到文档中去,非常的方便。
最近OnlyOffice发布了新版本(7.3)
,增加了高级表单、SmartArt图形插入、增强密码保护和公式计算、幻灯片特殊粘贴项等多项功能,可以参考官网这篇文章,了解一下。
首先我们来安装OnlyOffice的文档服务器(Document Server),它是一个免费开源的在线协作办公套件;这里笔者通过docker一键安装:
1 |
|
安装完成后,我们访问13300端口,看到我们安装的是一个社区版本。
在安装时,我们没有添加自定义秘钥,因此生成了一个随机的,我们执行Starting下面的docker命令,生成一段秘钥,这里记下这个秘钥【1】
,在后面集成到其他平台时会作为token使用。
在集成前,我们还需要对这个容器进行测试,执行Testing下面的两个命令,点击GO To TEST EXAMPLE
,跳到下面的welcome页面,我们的文档服务器就安装成功了。
在左侧,我们看到可以创建word,excel,ppt和表单,也可以通过Upload file
将本地文件上传到服务器。
我们的文档服务器就可以使用了,但是只能对Office文件进行编辑,界面也比较简单;我们可以去官方下载连接器。
这里我们以我们熟悉的Nextcloud网盘为例,还是通过docker来一键安装:
1 |
|
点击头像,找到+应用
,搜索ONLYOFFICE
,点击下载并启用。
连接器安装成功后,我们来配置文档服务器的地址,点击头像=>管理设置=>ONLYOFFICE
,填写文档服务器的ip地址,秘钥处填写上面安装文档服务器时生成的秘钥【1】
,点击保存,如果没有报错信息并提示已保存就安装完成了。
我们回到首页,点击+
按钮,可以看到出现了多个新建Office文档的按钮,说明文档服务器已经集成到我们的Nextcloud了。这里我们再次来打开一个文档查看,也是可以正常编辑的,在线协作和插件功能也都能正常使用。
总结一下,对于个人用户来说,OnlyOffice提供了全平台支持的免费开源且无广告的编辑器,能够让我们方便的编辑文档,提高工作效率;对于企业用户来说,可以在云端或者本地部署服务,方便团队协作,给自己的团队和企业赋能;对于开发者来说,可以将它集成到服务器或者App中,为其他用户提供文档服务。
是否开源:是
界面指数:★★★★☆
功能指数:★★★★☆
协同指数:★★★★★
兼容指数:★★★★☆
推荐指数:★★★★★
通过对三款办公软件的测试和使用,对于普通个人用户来说,WPS和OnlyOffice无论是在界面、兼容性还是在线协作方面,做的都还不错;对于Linux爱好者,LibreOffice自由开源,同时又简单实用的特性深深地吸引着他们;对于企业用户,OnlyOffice的在线协作赋能能够带来更多的便利,实为不错的选择;一款好用的办公软件能够让我们在工作上事半功倍,到底哪款软件适合自己,经过一段时间的磨合使用,相信你会有答案的。
2020 年了,现在 WPS 和 Office 哪个好用?
为什么会有很多人【觉得国产WPS】比不上微软的office?
OpenDocument 文本 (.odt) 格式与 Word (.docx) 格式之间的差异
OpenDocument 电子表格 (.ods) 格式与 Excel for Windows (.xlsx) 格式之间的差异
使用PowerPoint以 OpenDocument 演示文稿格式保存或打开演示文稿 (.odp) 格式
相信对Linux系统有一些了解的童鞋都听过这么一个故事,Linux是一名芬兰的学生Linus Torvalds在Unix系统的基础上开发的,并发布在学校论坛,最后火了起来。但是这么说并不十分的准确,Linux的故事缘起于更早的UNIX系统。
说到Linux,就不能提到大名鼎鼎的UNIX系统
,在上世纪60年代末的时候,那时候计算机系统还是批处理的,在又大又笨的大型机器上运行,要先将程序卡片装入设备,然后等1个小时后才能取运算的结果。不仅慢,还很废纸。
于是美国电话电报公司(下面简称AT&T公司)下面的贝尔实验室联合麻省理工学院及美国通用电气公司本来是打算开发一个大型机上的多人使用、多任务、多层次的操作系统multics
。但是multics
这个系统步子迈得太大了,贝尔实验室认为这个项目周期长、成本高,不久就撤资了,各方也陆续退出,项目于是凉凉了。
但是贝尔实验室下面的两个研究员肯·汤普森(Ken Thompson)和丹尼斯·里奇(Dennis Ritchie)对项目关闭很失望,因为他们在这个系统上开发了一个游戏太空旅行(Space Travel)
,实验室的其他人员对这个游戏玩得也很上头。
于是,为了能够继续玩游戏
,肯·汤普森和丹尼斯·里奇决定自己开发一个操作系统;是的,你没有看错,大神就是大神,出发点都异于常人。肯·汤普森找来了一台五年前老旧的PDP-7小型机,虽然称为小型机,但是也有一间屋子那么大!
正好在这段时间,他老婆带着孩子回娘家住了3个礼拜,也就是说他有整整3个礼拜没有人打扰他的工作。
这件事告诉了我们,一个男人想要搞大事,老婆不在家是多么的重要!
在这台机器上,他首先重写了游戏,然后想要开发一个全新的操作系统,由于有之前multics系统的经验,在一个月内他很快完成了内核系统、文件系统、编辑器、编译系统的工作。1969年第一版的UNIX系统正式诞生了。
在《UNIX传奇》一书中,提及了UNIX系统的高光时刻,上映于1993年的科幻冒险电影《侏罗纪公园》相信不少同学肯定对这部老电影仍旧印象深刻;其中一个名场面,哈蒙德教授的孙女就是操作UNIX系统,关闭了闸门,从而拯救了一行人,有兴趣可以重温B站视频片段。
这个故事告诉了我们,多学一门操作系统,在关键时刻说不定能够保命。
但是UNIX是由不少使用汇编语言完成的,汇编语言用来编程不够强大,也不具备很好的可移植性,于是1971年丹尼斯·里奇在B语言的基础上开发出了C语言,1973年UNIX也用C语言进行了重写,随后发布了4、5、6几个版本的UNIX。此后,UNIX被政府机关、研究机构、企业、大学纷纷注意,并逐步流行;随着UNIX系统的广泛流行,C语言也成为了最受欢迎的语言之一,一直延续至今。
一开始AT&T公司也没有把UNIX当回事,毕竟不是正式的项目,况且在操作系统上(multics)还吃过大亏,也就没想拿它卖钱,因此被免费提供给大学使用,甚至直接给V7版本的源码以做研究。
因此在后面的10年,UNIX在各个学术机构得到广泛应用,甚至许多机构在此源码基础上加以改进,其中最著名的变种之一是由加州大学柏克莱分校开发的BSD产品(Berkeley Software Distribution),在此基础上又诞生了三条分支:FreeBSD、OpenBSD和NetBSD,就连苹果电脑的内核Darwin所使用的NextSETP也是BSD的衍生版本。
AT&T公司一看,哟呵,UNIX居然这么火,于是意识到了UNIX巨大的商业价值,不再将UNIX源码授权给学术机构,并对之前的UNIX及其变种声明了版权,后面引发了各种旷日持久的版权纠纷,这是UNIX的后话。
时间来到了80年代,随着AT&T公司闭源UNIX系统,在学校里给学生用的操作系统很少;当时计算机主要使用的操作系统有UNIX、MS-DOS和MacOS这几种,UNIX已经开始商用,比较昂贵,仅局限于大型机;MS-DOS系统比较简陋,且源代码被软件厂商严格保密;而MacOS大家肯定也都知道,是专门用在苹果计算机上的系统,而且当时应该没有黑苹果一说。
1987年当时在荷兰阿姆斯特丹Vrije大学当教授的美国人安迪·塔内鲍姆(AndrewS.Tanenbaum)为了让学生更好的理解操作系统的原理,就仿照BSD的源代码,编写了一个类UNIX系统,取名为MINIX
,意为迷你的UNIX,并且开放全部代码给大学教学和研究用;既然是MINI,它的代码体量也是比较小的,全部代码共约12000行,而且只是一个教学工具,没有什么实际的应用价值。
1991年,我们的主人公Linus Torvalds(简称Linus)在芬兰赫尔辛基大学期间,开始对UNIX产生了浓厚的兴趣;在校期间,由于Linus经常要用他的终端仿真器(Terminal Emulator)去访问大学主机上的新闻组和邮件,他对MINIX只允许在教育上使用很不满,同时也为了方便读写和下载文件,他开始写属于自己的类UNIX系统;在一个暑假没日没夜的开发中,最终开发出了Linux的第一个内核(0.02版),并取名Linus' Minix
,后来改名为Linux。
1991年10月Linus在Minix新闻组发布消息,对外宣布Linux内核的诞生,并公开了内核源码;公开后Linux因为结构清晰、功能简洁,一经发布立即收受好评;后来在很多热心支持者的帮助下,经过多次版本升级迭代,终于在1994年3月,Linux1.0正式发布。
Linux的标志和吉祥物是一只叫做Tux的企鹅,它的由来是因为Linus之前在澳洲时,在动物园里曾被一只企鹅咬了一口,便选择了企鹅作为Linux的标志。
如同当初汤普森和里奇没有想到UNIX系统的成功一样,Linus也没有想到自己花了一个暑假做着玩的内核系统,竟然能以商品化操作系统的形态,运行在今天全球数十亿台设备上。
在Linux的官网,有这么一篇文章,什么是Linux,详细的介绍了Linux的功能、内核每个部分作用,以及为什么我们要使用Linux,感兴趣的童鞋可以看看。
我们上面提到了一个词:类UNIX系统
,那什么是类UNIX系统呢?
类UNIX系统是指继承UNIX的设计风格演变出来的系统。
类UNIX系统就是长得像UNIX、但实际不是UNIX的系统;其实本质上就是借鉴
了UNIX系统的界面、特性(多用户、多任务等),但是没有直接抄人家的源代码,毕竟人家是有版权限制的,因此更多是思想理念上的传承。上面提到的BSD、MINIX系统,以及Linux系统都属于类UNIX系统。由于UNIX标准认定价格昂贵,所以目前唯一获得UNIX标准认定的为苹果的MACOS系统。
Linux系统和UNIX系统主要有以下区别:
我们很多时候都能看到Linux发行版
这个词,或者又看到说Linux内核
怎么样,很多同学容易混淆这两个概念。其实当初Linus开发的Linux只是一个内核
,是一个提供设备驱动、文件系统、进程管理、网络通信等功能的系统软件,是硬件和软件之间进行通信的桥梁,内核并不是一套完整的操作系统;我们可以把内核理解为手机的芯片,有了芯片,手机的各个功能才能运行起来,因此内核是整个操作系统的核心。我们在The Linux Kernel Archives网站可以下载到各种版本的Linux内核,并且对其进行编译。
内核是操作系统重要组成部分,接近于物理硬件,不是操作系统。
我们常说的Linux系统
,其实更多说的是广义上Linux众多的发行版
,因为你并不会直接去操作系统的内核。发行版是指一些组织或厂商将Linux的内核与各种软件、软件包管理器等封装起来,并提供系统安装界面、系统配置和桌面环境等,构成了Linux的发行版。相当于小米、VIVO的手机厂商,将芯片集成到手机里,装上屏幕、外壳、扬声器、电池等等部件,然后把手机整个的卖给你。
Linux的各个发行版使用的是同一个Linux内核(内核版本可能有差异),因此在内核层不存在什么兼容性问题;每个发行版有不一样的感觉,只是在发行版的最外层(比如界面、包管理器)才有所体现。
uname -srm命令可以查看Linux系统的内核版本号。
Linux的发行版本有很多,其大体可以分为两类:
有些同学可能会开始疑惑了,上面不是说Linux是开源的吗?为什么还会有商业版?是的,Linux内核是开源的,但是开源不等于免费
,商业版收费的是它的商业服务和支持。
比如Red Hat虽然使用的都是开源软件,但是付出了很多人工将成千上万的开源软件整合成一个系统,并且保证软件间的兼容性稳定性,提供后续的支持、维护以及升级服务,因此它是收费的;如果你氪金氪了足够多,比如购买他们的高级服务,你甚至可以让Red Hat的工程师现场过来给你解决问题。
很多同学可能还是觉得开源收费不太能理解,但其实如果你去尝试编译多个开源软件,或者在操作系统时遇到一些莫名其妙的错误,然后你花费几天找遍github、stackoverflow和Google也没有找到问题而苦恼时,你会觉得如果有人能够帮你解决问题是一件非常高兴的事。更何况企业项目在运行时往往都会追求快速上线,计时按照天甚至小时,这个时候快速解决问题就显得非常的重要;这点费用对于企业来说是非常划算的。
这件事告诉了我们,天下没有免费的午餐,免费往往是最贵的。
下面我们简单介绍几个常见的Linux发行版本。
Red Hat(红帽公司)创建于1993年,是一家开源解决方案供应商,部位于美国北卡罗来纳州的罗利市。
1993年,Bob Young 成立了ACC公司,这家公司主要是做邮购业务,主营业务是出售Linux和Unix的软件附件。1994年,Marc Ewing创建了自己的Linux发行版,并将其命名为:红帽Linux,Ewing在就读卡内基·梅隆大学期间曾经戴着一顶红色的康奈尔大学长曲棍球帽子,这是他的祖父赠送给他的。Young在1995年收购了Ewing的企业,两者合并成为红帽软件公司,由Young担任首席执行官。
Red Hat公司的产品主要包括RHEL(Red Hat Enterprise Linux,收费版本)和 CentOS(RHEL 的社区克隆版本,免费版本)、Fedora Core(由 Red Hat 桌面版发展而来,免费版本)。
Fedora Linux是由Fedora项目社区开发、红帽公司赞助,目标是创建一套新颖、多功能并且自由的操作系统。
Fedora对于用户而言,是一套功能完备、更新快速的免费操作系统;而对赞助者Red Hat公司而言,它是许多新技术的测试平台,因此它的稳定性不如Centos。
CentOS可以理解为是基于Red Hat商业版系统的社区编译重发布版,完全开源免费,因此相较于其他一些免费的Linux发行版会更加稳定,也因此一般企业里常用作服务器操作系统。
Debian是目前世界最大的非商业性Linux发行版之一,是由世界范围1000多名计算机业余爱好者和专业人员在业余时间制做。
Ubuntu是基于Debian发展
而来,界面友好,容易上手,对硬件的支持非常全面,是目前最适合做桌面系统的Linux发行版,而且Ubuntu的所有发行版都免费提供,也是笔者个人非常喜欢的一个Linux发行版。
Ubuntu的创始人马克·沙特尔沃思(Mark Shuttleworth)是一名有传奇色彩的南非人,他在大学毕业后创建了一家安全咨询公司,后以5.75亿美元被收购,一跃成为南非本地的富翁。2002年马克自费乘坐罗斯联盟号飞船,在国际空间站中度过了8天的时光,之后创立了Ubuntu社区。他说太空的所见正是他创立Ubuntu的精神所在。Ubuntu这个词也是来源自非洲一个部落,意思是”人性””我的存在是因为大家的存在”,是非洲传统的一种价值观。
作为Linux发行版中的后起之秀,Ubuntu在短短几年时间里便迅速成长为从Linux初学者到实验室用计算机/服务器都适合使用的发行版。
Linux系统的发行版有很多,就不逐一介绍了,在《Linux从入门到精通》一书中整理了不同的发行版;我们如何来选择不同特性的版本呢?
相信计算机科班出身的同学在大学里都会接触一门课程:计算机操作系统,笔者在大学里,这门课老师让用过一段的时间的Ubuntu开发,做做作业,当时觉得命令行shell就像深不见底的黑洞,太麻烦了,完全没有图形化界面来的方便快捷;但是工作了一段时间,接触了一下Linux系统,熟悉命令行之后,哎,真香,比Windows好用多了。
简单介绍一下,笔者也算是Linux系统中度用户吧,个人桌面系统主力虽然是Windows 10,主要是由于之前系统存了很多文件资料等;目前转向使用Ubuntu 22.04系统;自己将家用闲置的一台电脑改造成为家用nas系统,搭载CentOS 7,因此改造的过程中接触了不少Linux系统的命令,于是就开始自学并喜欢上。
我相信很多童鞋开始学习和使用Linux系统应该和我一样,主要是在工作中开始的,因为毕竟Linux系统下娱乐、游戏、社交功能有限,全面使用Linux系统会带来一定的限制(主要是没有微信),我平时也是将Linux系统作为日常工作和编程开发的一个补充。
PS:steam平台也支持Ubuntu了,QQ推出了全新的Linux3.0版本。
下面简单的介绍几个觉得使用Linux系统的个人推荐看法吧,仅供参考。
常言道:始于颜值,陷于才华;看惯了Windows下千篇一律的图标,说实话,使用Ubuntu 22有一部分原因确实是被它的界面所吸引的。
打开Ubuntu系统,我们会发现,整体的风格非常简洁优雅。
很多刚从Windows转过来的小伙伴(包括我自己),一开始接触Ubuntu桌面,会常常感觉不习惯,经常会疑惑:
桌面的那些我的电脑、回收站等图标都去哪了?
包括在安装完很多应用后,我们发现这些应用也不会在桌面留下任何痕迹,没有Windows软件那种安装完后,还要死皮赖脸的请求你创建桌面快捷方式,还给你默认勾选;只要一不留神,你的桌面说不定就多了三四个不常用的图标。
因此在Windows系统,我们用过一段时间后会有各式各样繁杂的图标存在;但是Ubuntu就不会有这样的烦恼,用了几个月,我的桌面也仅仅只有刚开始的主目录文件夹存在,加上Foxit Reader创建的一个图标,仅此而已。
Ubuntu界面的设计者考虑到,大部分用户在工作时,桌面上的图标几乎都是被应用窗口遮住,把窗口移开来查找想要的应用是一件非常痛苦的事,因此停用了桌面图标,改用在应用程序中提供了入口;点击右下角的按钮,我们可以看到所有的程序。
对于一些常用的程序,我们可以将它固定到下方的程序坞中,方便随时访问。Ubuntu给了我们一个干净的环境,让我们能够更专注于当前的工作环境,更少被其他弹框打扰;因此更适合用来干活。
开源带来的一大显而易见的好处就是,你不用每次安装完系统去找各种Windows激活工具了,相信很多小伙伴都有装完系统被下面各种软件支配的恐惧,不装的话系统各种提示,装了又怕有风险。
开源意味着使用者可以免费自由使用、查看和修改系统的源代码,这种完全开放透明的架构对于政府机构或者特殊需求的组织等来说是非常重要的。
同时你会发现Linux系统有广泛的硬件支持,甚至可以拿出一台上个世纪老旧的intel奔腾3处理器来运行也能很流畅;正是得益于开源的特点,很多程序员不断地向Linux社区提供代码,使得Linux有着丰富的设备驱动资源,对主流硬件有着很好的支持,几乎能运行在所有主流的处理器上。
在超赞的Linux软件这篇文章中,作者整理了非常多Linux中开源的软件,也都是日常很实用的软件。
系统安全稳定之于电脑,如同法律对于人们,是最基本的要求和准则。大多数小伙伴应该也是从Windows开始接触计算机和网络的,因此觉得Windows也能满足日从的工作需求。但客观来说,在安全性、高性能方面,Windows相比Linux依然有不小的差距。
使用Windows过程中相信大家在日常中会遇到不少卡顿、蓝屏的情况发生;笔者在之前的公司就遇到IT装完系统,一段时间经常蓝屏死机的情况发生,然后数次重装系统,这在办公时是及其痛苦的。
但是Linux系统极少出现卡顿情况,除非你运行多个大型的软件。在我实际的体验中,在同一配置的电脑中,运行相同多软件的情况下,Linux系统的流畅度是明显优于Windows电脑的。
在Windows中,我们经常会遇到磁盘空间不够的情况,尤其是C盘空间,很多软件都会默认安装到C盘(比如Chrome),或者将缓存文件放到C盘,过一段时间就需要清理;但在Linux系统中不需要。
首先Linux系统安装完成后,本身不会占用太多的磁盘空间,占用较少的资源;其次Linux系统自身的树形目录结构
已经将每个文件的位置规范了,/home是用户目录,/usr软件目录等等进行划分,我们可以将硬盘格式成一个区,然后直接挂载根目录。
正是由于Linux系统的安全稳定高效,因此Linux天然适合用来做服务器;无论是企业级的大型服务器,还是最近流行的家用nas系统;无论是你用的手机操作系统,还是看家用大屏电视机,亦或是小巧的机顶盒,Linux系统出现在生活中的各个角落。
要想学好Linux,不能只记住几个命令,最好的方式是为自己搭建一个Linux的环境,在真实的环境下进行学习;Ubuntu就是一个比较适合初学者的发行版;如果怕装系统麻烦,最简单的方式是在Windows10下安装Ubuntu子系统体验,参考安装教程,不过有一些命令会被阉割。
如果手头有闲置移动硬盘,想要真实体验一下Ubuntu系统(虚拟机体验不好),又不想舍弃Windows系统,可以将Ubuntu环境安装到移动硬盘,打造自己的个人移动工作平台;这样你不管是在办公室还是回家干活,只需要随身携带一块小小的硬盘就能轻松将工作用到的所有资料打包带走,保持工作的进度和环境。这里推荐笔者自用的国产的致态1TB SSD固态和绿联M2移动硬盘盒组合,方便打造自己的Ubuntu To Go
环境。
刚装完系统,看着空荡荡的桌面,你可能会不知所措,可能会迷茫Ubuntu系统下可以做什么?;双击安装exe不再存在了,而是通过命令行来安装,甚至连接网络也要敲命令,当你熟悉命令行的环境后,你会发现这是一种高效的方式,也是另一种的体验。借用一位知乎前辈的话:
Windows为不知道自己正在做什么的人设计,Linux为知道自己要做什么,正在做什么的人设计。
在这里,你可以做任何事,你可以热衷于更换各种酷炫的桌面和主题,也可以享受学习带来的无穷乐趣;先是命令,再是shell脚本,搭建服务器,学习数据库,部署自己的网站等等;我相信,你也会喜欢上这个简洁而优雅的开源世界。
]]>很多人可能会觉得做个副业就是小打小闹,或者会耽误工作,或者平时工作忙好不容易有个休息的时间,还要干活,比较累。我问了下我周围的朋友,总结了可能就是下面几个原因让大家望而却步:
1、没有合适的渠道接触到副业
2、需要各种对接或者处理人际关系,不擅长
3、耽误工作,没有时间
4、技术不匹配
首先说说副业的可能性,经营副业和你上班打工会是两种完全不同的思考方式。引入副业后,最直观的就是明显对个人的财务状况带了很大的提升,降低了生活中的房贷车贷的风险,提高了生活的质量;一方面拓展客户,锻炼了自己的能力和技术,另一方面可以了解市场需要的技术要求,从客户的案例中提取重复的需求,将其模块化、产品化,从而提高自己的核心竞争力。
然后说说副业的渠道,确实,作为一名程序猿,身边认识的人有限,社交网络无外乎亲戚、朋友、同学、同事,或者外加上对象的亲戚、朋友、同学、同事。因此能够额外的拓展一些关系人脉,对我们是有很大的帮助的。比如我们一般都会有很多的技术群,可以在技术群里面咨询一些大佬,或者加一些志同道合的朋友,看看有没有可以一起合作的项目等。
这里推荐大家可以阅读几本非常不错的书,帮助大家提高沟通的技巧,《沟通的艺术》和《关键对话》。
其次是对接中的收款方案,一般笔者接触到的项目都是按照项目的结算的,因此可以设定按照项目的完成时间节点收取费用。
一般第一次合作的项目,笔者会预收10%~20%的费用作为项目订金,项目完成后,可以给用户部署到我们的测试服务器进行项目的演示,这时候可以收取到总费用的80%-90%,最后客户提出修改要求后可以收取最后的尾款。这样既帮助我们规避了款项收不回来的风险,又赢得客户的信任。
一般前期将价格谈妥后,可以将这套付款方案和客户沟通协商,基本都会认可的。后期合作次数增加,有一定信任基础后,可以适当的放宽方案。
是的,如果手头有一台公网IP的服务器,可以帮助我们做很多的事情。手头宽裕一些的同学,可以直接上阿里云服务器,新用户轻量级服务器50元/年;想要白嫖的可以使用免费的NATAPP进行内网穿透,免费隧道1M的小水管勉强凑合;动手能力强的的童鞋可以自己跟电信运营商申请公网IP,家里部署一台服务器,不过需要一定的Linux运维能力。
项目免不了会有改动,前期再怎么容易,客户形容得多么简单的项目,后面就越是会有改动;如果客户有好说话一点,协商一下就能加点钱。
如果协商不好就容易谈崩,而且大部分客户对自己的项目需求其实都只是一个大概的描述,很多都是描述一个简单的目标,但其中需要你对内部的技术细节和各种可能的坑都能了如指掌。因此这个时候就需要我们既能做程序员又能做产品经理了,前期根据客户的需求制定一个详细的产品规划,有哪些页面,每个页面需要什么功能;这样做的目的既是帮助客户将需求具体化,明确下来,尤其是避免后续的纠纷;又能够方便我们评估项目,下面给项目进行报价。
最后我想聊一下如何给项目报价,相信这也是让很多新手同学十分头疼的问题,常常不知道如何定价;报高了怕吓跑客户,也没有底气;报低了吧又怕自己吃亏,不划算;因此如何来报价就是一门平衡的艺术
,需要经过多次实践之后才能对这门技艺熟练掌握。
我们根据项目的工作繁杂程度进行一个简单的划分,当然每个人可能会有不同的适用范围(大神可以忽略):
再繁杂的项目,大于2W、3W的,除非人家对你有很深厚的合作基础和信任了解,不然一般不会偏向找个人开发者了;我们可以通过一个简单的公式进行粗略估算:
预估工时(天) * 自己每天的工资 + 预留修改费用
一个项目的开发往往不是一个人可以独立完成的,涉及到UI、后台等,我们也可以让合作的小伙伴报价后算上这些费用。
除此之外,还有一些额外隐形的费用
我们在充分调研后,需要和客户在报价中进行详细的说明,让客户了解;比如客户需要我们部署网站和域名,那每年的服务器费用就需要额外列出。
再或者在客户的需求中,有语音会议或者发送短信验证码等功能,这个我们自己肯定是不能开发的,也需要找额外的企业解决方案;好在很多的平台,阿里云腾讯云华为云等,都会有相应的解决方案,我们可以去这些平台调研,根据文档API来找到适合自己的方案。
下面就谈谈我接触的几个项目。
Chrome插件的项目也是笔者第一次接触的这样的需求,开发网站开发APP都普遍,开发插件确实不太常见,这样的需求也是少之又少。客户的需求描述也是比较简单,抓取某网站的数据,通过插件上传后关闭网站。
当时最最主要的难点就在于如何通过插件来关闭tab页,首先第一反应肯定是通过window.close()
方法来关闭,但是测试后失效了,根本没有反应,控制台还给出以下警告:
Scripts may close only the windows that were opened by them.
网上找了相关资料后,浏览器有相关规定:只有通过window.open(url)打开的窗口,才能够由window.close()关闭,为的是阻止恶意的脚本终止用户的浏览器。
又查了资料,可以先去一个空白的页面:
1 |
|
这样页面确实是没有了,但是跳转到了一个空白的页面,好像和客户的需求不太符合啊。于是我就放弃了window.close这条路,转而查找Chrome插件的API。
终于,在文档中找到了chrome.tabs.remove
,可以移除tab标签页,使用chrome.tabs.query
查找所有的标签页,第一个就是选中激活的标签页:
1 |
|
在开发中还有比较坑的就是谷歌升级了插件的版本,V2版本的插件不再支持,建议升级到V3版本:
在V2中,我们使用background
参数配置background.js在后台运行,并且设置persistent
来让脚本一直在后台运行,但是这样会占用系统资源;所以在V3中,改用service_worker
来智能化启动脚本。
V3中不能使用关键字persistent。
由于background的这些改动,因此我们的background.js脚本变得不可靠了,对于一些定制任务,只能使用alarms来实现。
小程序是当前被大家广泛接受和需要的,因此在做副业项目时也是接触比较多的。个人感觉小程序其实技术上没有什么复杂的,棘手的就是需要查找各种文档以及小程序兼容性问题。
小程序一般都需要用户登录和授权,登录和授权其实是两个不同的流程。整个技术难点在于整个流程步骤比较多,我们首先跟随官方给出的流程图,简单看下步骤(流程图会在下面反复被用到,大家可以在心里默记下来):
说到小程序的登录,绕不开的就是openid和unionid,这两者的区别也是面试时喜欢问的,简单来形容一下就是,unionid相当于你的身份证,在不同地方都是唯一的,而openid相当于你在公司和小区的唯一通行证,凭借这个才能进出。
首先来看下openid,它是微信提供给开发者的用户唯一标识,虽然称为唯一,但它在不同应用下是不一样的。
同一个用户在不同的公众号或小程序下的openid是不同的。
比如我在同一个公众平台的账号下面既开发了一个公众号,又维护了一个小程序,今天有一个人分别打开了这两个应用,但是我在两个应用的后端获取到这个人对应的openid是两个不同的值。
那么我们如何来获取openid呢?这个问题的回答就需要用到上面的登录授权流程图了,整个流程图其实就是获取openid的过程,没有记下来的小伙伴可以再回去看下。
我们结合后台接口来看下整个过程,首先在小程序中,我们调用wx.login
获取到一个临时的code,这个步骤一般放到app.js的onLaunch中,判断没有token的情况下去进行登录操作:
1 |
|
临时登录凭证
code
只能使用一次
拿到这个临时code后,我们就要发送给后端,让后端去调用微信的接口了:
1 |
|
后端拿到code后,来到了第二步,需要找微信服务器换取openid,我们看下node的简单实现:
1 |
|
后端拿到openid,存入数据库,唯一绑定用户,然后返回一个token给小程序,小程序拿到token后存入storage,发起业务在请求头携带就可以了。
说完了openid,我们来看看unionid,我们看下官方文档对unionid机制的描述:
如果开发者拥有多个移动应用、网站应用、和公众帐号(包括小程序),可通过 UnionID 来区分用户的唯一性,因为只要是同一个微信开放平台帐号下的移动应用、网站应用和公众帐号(包括小程序),用户的 UnionID 是唯一的。换句话说,同一用户,对同一个微信开放平台下的不同应用,UnionID是相同的。
简单来说,unionid可以唯一确定一个用户的身份,相当于用户的身份证信息。
同一个用户在不同的公众号或小程序下的unionid是相同的。
如果应用只限于小程序内,则不需要unionid,直接通过openid,如果要跨应用,则需要unionid作为身份标识。那么unionid如何来获取呢?若当前小程序已绑定到微信开放平台帐号下,后台调用jscode2session接口时就能获取到unionid,需要注意的是:
很多小伙伴就会有疑问了,既然unionid比openid更具有唯一性,那我直接用unionid不就完了,还要openid干嘛呢?
我们看完这两者的获取方式后会发现,unionid的获取其实有更多的条件,需要绑定微信开放平台,如果用户取消绑定了就获取不到了,多用于多个终端应用账号的打通。而openid不用授权弹框,静默的方式就获取了,更多可以看这篇文章:openid有什么用?为什么不直接用unionid?
上面我们拿到了用户的openid和unionid可以确定用户的身份了,但是为了给用户更好的体验,很多时候我们想要拿到用户的基本信息,比如昵称、头像,在用户详情页展示,或者拿到用户的手机号,避免用户验证时重复填写手机号,直接带入即可。
用户授权最重要的就是wx.getUserInfo
api了,在小程序登录、用户信息相关接口调整说明这篇官方公告中,对getUserInfo进行了调整,很多开发者一进入小程序就直接调用getUserInfo唤起弹框,这种方式让微信官方很恼火,因此对这个api进行了调整。
调整后,如果我们只是简单的展示用户头像昵称,可以使用<open-data />
组件:
1 |
|
我们也可以给这个组件加个类名,控制样式。如果我们需要存储用户信息,使用button
让用户主动触发弹框授权来获取:
1 |
|
1 |
|
在调用wx.getUserInfo前需要进行授权信息的判断:
1 |
|
我们在浏览网站或者App时经常会注册各种各样的账号,手机自带的的密码保险箱功能只能保存App的密码,不能保存网页的;不同网站的注册账号和密码的规则还不一样,因此我们的需求也很简单,记录下每个网站或App注册的账号密码即可;笔者之前在应用市场下载过某密码箱的App,如下:
但是将重要的密码保存在别人的App上,尤其是涉及到自己隐私的密码,总觉得有些不放心,其实主要是这破App还开始收费了;因此我们可以利用之前学习的RN知识来开发一款自用的密码保险箱,既安全可靠又物美价廉,说干就干。
我们首先来搭建项目:
1 |
|
这里RN的第一个小坑来了,RN的版本已经到了0.68以上,它强制使用JDK 11进行Android build;我们看下0.68版本最低要求:
但笔者装的版本比较早,是JDK1.8,因此我们搭建项目时需要留意自己的JDK版本;我们可以加上--version
来指定RN的版本
1 |
|
搭建后,我们加入常用的一些依赖,如图标和路由导航,这里不再赘述了,需要的小伙伴可以看下这篇文章:深入学习React Native之路由导航。组件库的话,我们选择了NativeBase@3.4.x,它的组件较为丰富且全面:
1 |
|
我们在项目下新建src目录作为我们代码的主要目录,然后建立以下结构:
1 |
|
router存放我们的路由组件,这里由于不需要复杂的选项卡导航,我们直接使用堆栈导航即可;我们首先改造入口的App.js
,加入NativeBase和导航的Provider容器:
1 |
|
给我们的路由router/index.js导入页面:
1 |
|
首先我们来看下登录页面,登录页面比较简单,我们只需要一个输入框和确定按钮(省略其他组件代码):
1 |
|
这里我们用到react-native-textinput-effects
组件,这是一个用纯js实现不同的炫酷效果的textinput组件库,实现的输入框效果如下:
其次是我们的首页,用来展示账号密码的列表:
1 |
|
fixedAddBtn按钮用来点击跳转到新增账号密码的页面;我们将list中的每个数据封装成ListItem组件,方便后面进行动画效果的展示。效果如下:
然后是修改入口密码页面Change,它分为两种情况,如果已经设置过了,就进行修改;如果没有设置,则可以设置新的密码。
1 |
|
最后是新增和编辑账号密码页面Passwd,它的部分代码如下:
1 |
|
这里除了密码类型,其他字段(用户名、密码、标题、网址)等都是输入框,直接输入即可;密码类型点击后呈现下拉框,我们这里使用native-base的Actionsheet
组件:
1 |
|
Actionsheet效果如下:
我们的页面框架已经基本搭建完成了,我们对路由组件进行一些改造,对登录状态进行区分;在登录成功后才能进入首页及后续页面,否则只能展示登录页面:
1 |
|
我们设置初始化页面为登录页,同时登录页不需要展示header;再给其他页面的header设置统一的背景颜色和字体颜色:
1 |
|
现在我们需要通过isLogin
变量来控制路由的变化,由于登录操作时在登录页面判断的,我们可以通过全局的event bus来进行监听,在页面销毁时不要忘记移除监听事件:
1 |
|
bus.js
的代码也很简单,直接调用events库:
1 |
|
数据的存和取是我们这个App的核心功能,我们用到Async Storage
这个库,它是一个给RN进行数据存储的依赖库,首先进行安装:
1 |
|
它的用法也很简单,和LocalStorage的API有点类似,都是以字符串形式存储键和值,我们把它封装到utils/storage.js
,方便调用:
1 |
|
我们封装了两组APi,一组是直接存取值的,另一组是可以存取对象的。这样,我们在登录页时,先把存储的入口密码entrance
取出来,如果不存在,则直接进入首页;如果存在,在下面点击登录按钮时就将用户输入的值进行比较即可:
1 |
|
在设置入口密码的Change页面,我们对新旧密码进行一系列校验,然后直接保存到storage中即可,这里不再赘述了。
在首页,我们在updateList
函数中将storage中保存的每一条密码取出来放到list中进行展示,但是在新增或者编辑storage中的密码之后,需要及时重新调用这个函数更新list数组,我们在RN路由导航中的导航的生命周期中说过,可以监听focus
事件来判断页面是否重新聚焦:
1 |
|
我们整个App的存储和读取的功能已经基本完成,基本功能也能够使用了,下面对界面和功能进行一些优化。
在原版的App中,新增按钮有一个阴影的效果,阴影效果也是一个很常见的需求;在CSS3中,我们可以直接通过box-shadow
属性实现,在RN中iOS平台支持以下shadow属性:
1 |
|
在安卓端可以通过elevation
属性,但是两者表现形式差别很大;因此我们引入第三方的react-native-shadow,它是利用react-native-svg画出svg阴影,因此两端效果比较一致;我们同时安装这两个依赖:
1 |
|
然后给fixedAddBtn套一层BoxShadow组件:
1 |
|
我们点击复制按钮可以直接复制账号或者密码到剪切板;RN已经将Clipboard功能从核心代码中分离出来,我们需要安装一下第三方的模块:
1 |
|
调用Clipboard.setString
,复制到剪切板,调用await Clipboard.getString()
获取剪切板的内容;我们这边使用setString即可:
1 |
|
现在的智能手机一般都带有指纹识别的传感器,我们可以利用指纹模块来方便用户登录;React Native Fingerprint Scanner是一个RN库,用于使用指纹对用户进行身份验证;它提供了一个默认视图,提示用户将手指放在传感器上进行扫描。
我们根据安装教程安装该依赖后,发现它的文档看起来很多,很唬人,但是核心的API只有两个,首先是isSensorAvailable
,用来判断传感器是否可用,如果手机没有设置指纹则不可用,该函数直接报错,因此通过try/catch进行包装:
1 |
|
在安卓端biometryType值如果可用的话为Biometrics,iOS端为’Touch ID’和’Face ID’,目前我们只考虑安卓端;判断后我们就真正可以调用指纹模块了,authenticate
函数自动唤起指纹识别模块的模态框,我们填入title和description一些描述文案来引导用户:
1 |
|
我们在页面初始化时,如果设置了入口密码,则进行指纹识别校验,自动弹出模态框,验证成功后跳转首页,同时不要忘了在页面销毁时调用release
释放指纹模块的资源:
1 |
|
指纹识别效果如下:
放错图了,应该是下面这张:
我们在首页的列表展开时,给小箭头一个旋转的动画效果;RN中提供了Animated API来实现动画,可以简洁的实现各种动画和交互方式,并且具备极高的性能,我们从RN中导出模块:
1 |
|
我们首先使用new Animated.Value创建一个值,在render函数中使用了一个interpolate()插值函数,用于将输入值范围转换为输出值范围,这里是将[0, 1]输入转为[“0deg”, “180deg”]输出。
1 |
|
当点击展开列表元素时,调用Animated.timing
,使得值按照过渡曲线随时间变化而变化,duration设置动画的执行时间,最终效果如下:
我们的App开发完成后,就需要进行打包了,这里简单的看下笔者遇到的一些坑,希望能够让大家在开发时规避类似的坑。
我们开发时没有问题,但打包成apk文件后,兴冲冲的安装,结果现实泼了一盆冷水,运行直接闪退;搜索一番后,说是index.android.bundle文件没正常生成所致,我们运行一下打包命令,首先进行js文件的打包,再运行./gradlew assembleRelease
打包:
1 |
|
打包时还会遇到Duplicate resources报错,翻译过来就是重复资源:
1 |
|
网上说是打包问题,打开node_modules/react-native/react.gradle文件,在doFirst代码段后面新增以下代码:
1 |
|
参考:
react-native-reanimated依赖报错:
1 |
|
打开babel配置文件babel.config.js,新增如下插件:
1 |
|
参考:https://github.com/software-mansion/react-native-reanimated/issues/3410
react-native-fingerprint-scanner打包时报如下错误:
1 |
|
build.gradle新增jcenter()
参考:https://github.com/hieuvp/react-native-fingerprint-scanner/issues/192
我们的自研App到这里基本就结束了,该有的功能也都有了;当然在开发过程中也遇到了不少坑,好几个依赖包安装和运行中都出现了大大小小不同的问题,上面的bug列表只是列举了一些典型的问题;只要我们利用好Github和Stack Overflow,总能找到解决方案。
有兴趣的小伙伴可以到Github给个Star,有好的想法和改进建议也欢迎提给我;本App只将数据保存在本地,没有上传服务器,因此大家可以放心使用;如果想要体验,请在公众号【前端壹读】后台回复关键词安全密码箱
即可获取App的安装包。
Navigator
组件来实现跳转;从0.44版本开始,Navigator
被官方从RN的核心组件库中剥离出来,主推的一个导航库就是React Navigation
,它性能也很接近原生,我们今天就来学习下它的用法。 React Navigation
库每个版本的改动还是挺大的,比如3.x创建堆栈导航和创建选项卡导航都是直接在react-navigation
库中导出create函数,而4.x中堆栈路由是从react-navigation-stack
这个库导出,5.x版本库名又改成了@react-navigation/stack
,6.x版本又双叒叕改成@react-navigation/native-stack
,因此对新手及其不友好,很容易让人看了头大。
不过好在导航方式主要是三种:堆栈导航(StackNavigator)、选项卡导航(TabNavigator)和抽屉导航(DrawerNavigator),而且导航方式、传参都大差不差,因此本文主要以目前最新的6.x为例。
堆栈导航是比较常见的导航方式,为应用程序在不同屏幕之间转换提供导航和管理的方式;其有些类似于web浏览器处理导航状态的方式。首先我们安装一些依赖:
1 |
|
要在项目里使用导航,我们首先要在项目的根组件创建一个路由导航容器,将我们的路由都包裹(一般是在App.js中),有点类似于Vue的<router-view />
:
1 |
|
我们导出createNativeStackNavigator
函数,用于配置堆栈路由的管理;它返回了包含两个组件的对象:Screen和Navigator,他们都是配置导航器所需的React组件,其中Screen组件是一个高阶组件,会增强props;在使用的页面中,会携带navigation
对象和route
对象,下面我们会介绍这两个对象的用法。
我们新建一个StackRouter.js
,将所有的堆栈导航配置统一在这个文件配置:
1 |
|
在根组件中引入我们的堆栈导航组件即可:
1 |
|
我们的程序很多时候都不止一个页面,我们可以在堆栈导航中继续加入其他的列表页、详情页等等;initialRouteName
配置初始化的路由,可以设置成非第一个Screen页面:
1 |
|
我们可以在每个路由上通过options
配置不同参数,比如标题、导航栏颜色等:
1 |
|
有时候,我们想要给一个页面传入额外的参数,我们可以把页面组件放到上下文中包裹,并传入props:
1 |
|
上面的用法在配置同一个页面不同路径时会很有用;比如我们的新建和编辑页面可以做成一个页面,配置不同路由通过传入额外的参数对两个页面进行区分。
在不同页面间,我们需要进行路由跳转;我们上面说过,在所有的页面组件中,都会携带一个navigation
对象,它是react-navigation注入的路由对象,它上面有很多的函数,可以进行不同形式的跳转。
如果我们跳转到未定义的路由,在开发版本中会报错,而在生产环境中不会发生任何事,
我们调用navigation.navigate()
函数来跳转,直接传入Stack.Navigator
中定义路由名name:
1 |
|
如果我们在List列表页也调用navigate('List')
,我们发现不会产生任何的效果,因为我们已经在列表页面了。navigate的含义是跳到这个页面,有点类似vue-router的router.replace
。
如果我们确实想要打开多个页面,可以将navigate改成push:
1 |
|
每次我们调用push
,都会在历史记录中新增一条记录,这也就是堆栈导航的由来;而调用navigate
,它会尝试在现在的路由堆栈中查找是否有这个路由,没有的话才会新增。
在堆栈导航的顶部有一个返回按钮,点击后可以返回上一个页面,我们也调用navigation.goBack()
来触发返回:
1 |
|
有时候堆栈导航的层级很深,我们需要穿越好几个页面才能返回到第一个页面;在这种情况下如果我们明确的知道我们想要回到的是Home页,我们可以直接调用navigation.navigate('Home')
,清除所有的路由并且回到Home。
另一种方式是调用navigation.popToTop()
,这样将清除路由堆栈并回到堆栈的第一个页面。
1 |
|
我们已经创建了多个页面并且在页面之间进行跳转,我们还需要传递不同的参数;例如从列表页到详情页需要传id,我们将需要传递的数据作为navigate函数的第二个参数。
1 |
|
route
对象是Screen组件增强的props,它里面包含一个属性params,就是用来接受传递过来的参数:
1 |
|
页面也能更新自己的参数,就像更新state状态一样;通过navigation.setParams
进行更新:
1 |
|
在传递参数时,虽然我们可以将整个数据传过去,例如下面的方式:
1 |
|
我们接收的时候也看似很方便的可以通过route.params.user
就能获取到数据;但是这是一种反模式,例如用户数据等,应该通过接口获取,或者放在全局的用户列表中,然后通过id进行获取。
1 |
|
我们在上面介绍了配置导航栏的标题,我们继续看下options
还有哪些用法;它除了接收一个对象,还可以接收一个返回对象的函数;函数的方式可以接收navigation和route两个参数,这种方式会很有用,例如我们在设置导航栏的组件时,获取这两个参数进行跳转操作。
1 |
|
headerTitle
可以覆写标题组件,替换成我们自定义的;headerRight
定义导航栏右侧的组件,可以是一些功能性的,比如设置、帮助等等,可能会涉及路由的跳转,因此我们可以将options设置成函数的形式。
我们可以调整导航栏标题的样式,通过下面三个参数:
backgroundColor
背景颜色。fontFamily
和fontWeight
。 我们肯定希望我们的App大部分的导航栏都是相同样式,有个别样式需要定制;我们将Options移动到Stack.Navigator的screenOptions
上:
1 |
|
这样,所有的导航栏的配置都是相同的了;有时候我们还需要和导航栏互动,修改导航栏的配置,我们可以调用navigation.setOptions
来重新设置options。
1 |
|
在上一小节中,我们在不同的页面中进行导航跳转,当我们从a页面去b页面时,我们怎么才能知道即将要离开a页面?从b页面返回时,如果我们需要更新a页面中的数据,那我们在a页面如何监听呢?
很多同学会理所当然的认为离开a页面时,我们直接在componentWillUnmount
处理可以了;但是实际上,a页面只是暂时的隐藏到后台了,它并没有被销毁,始终保持了挂载状态,因此它的componentWillUnmount并不会被调用;而b页面则是会进入时创建,返回时被销毁。
选项卡导航在操作时也会观察到类似的情况,由于它有多个tab页,我们可以将它想象成多个堆栈导航,它的每个tab切换时也只是将页面隐藏,并不会销毁。那我们怎么回到刚开始的问题,如何发现用户在进入它和离开它呢?
我们通过navigation
导航来订阅相关的事件,通过监听对应的事件来了解页面何时进入以及离开。
1 |
|
navigation支持以下五种事件:
navigation.addListener
返回一个函数,可以在组件销毁时调用来取消订阅的事件:
1 |
|
我们再来看下另一种常见的导航方式:选项卡导航;我们在常用的App中都能看到这种导航方式,如微信、知乎、某东、某猫底部导航,在屏幕底部显示三到五个App的主要板块,能够很方便的让用户在目标板块之间进行切换操作,避免路由层次过深。
React Natigation中主要使用的选项卡导航就是@react-navigation/bottom-tabs
,其他的还有下面这种material风格的:
material风格主要是@react-navigation/material-bottom-tabs
和@react-navigation/material-top-tabs
,使用方法都大同小异,只是风格不同,需要和App整体的风格协调。我们回到bottom-tabs,首先需要进行安装:
1 |
|
我们看下bottom-tabs
的简单使用案例:
1 |
|
这样我们看到底部多了两个tab按钮,但是没有icon,比较简陋;这里引入react-native-vector-icons
这个库,包含很多icon图标。遵循安装教程安装好后,我们在它的主页上,找到我们需要的icon,这里包含了AntDesign、FontAwesome、Ionicons、MaterialIcons等一众丰富的图标。
我们可以给每个tab设置单独的options,但是为了方便统一,我们在Tab.Navigator上的screenOptions
集中配置:
1 |
|
tabBarActiveTintColor和tabBarInactiveTintColor从名字我们就能看出,是设置激活状态和非激活的icon和label的颜色(注意,是两者的颜色);tabBarIcon
用来设置icon图标,接收一个函数,函数传入三个参数:focused(boolean)、color(string)和size(number)。
focused用来表示tab激活或者非激活,很好理解,但是这里的color和size就很奇怪了,我们打印color的值,发现和上面的active color和inactive color相同;这个色值是为了和icon下面的label色值保持统一而传入的,我们可以用它的色值,也可以和label不同;size则是导航预期icon的大小。
如果我们只需要展示图标,而不要label,将tabBarShowLabel
选项置为false即可。
1 |
|
有时候,我们想要在tab按钮上加一个徽章(Badge),来标识未读信息,可以使用tabBarBadge
选项:
1 |
|
上面堆栈导航我们介绍过navigate和push的用法,而选项卡导航就比较简单了,由于两个tab是同一级关系,直接调用navigate就能实现路由跳转:
1 |
|
选项卡导航中不能调用push函数。
想象一下,在选项卡导航中我们经常用到不止一个路由,在列表页后面我们需要跳转到详情页;但如果直接把详情页面放到Tab.Screen
中肯定是不行的,这样只会增加一个tab按钮。我们可以利用上面的堆栈导航,将两种导航方式进行嵌套使用。
1 |
|
我们创建了一个ListStack
堆栈导航,相当于在tab页面中嵌套了一层堆栈导航;我们很明显发现List页面会有两个头部的导航栏,我们将ListStack的headerShown
置为false即可将它的导航栏隐藏:
1 |
|
在iPhone X、iPhone XR等设备上,顶部的刘海设计和底部的小黑条都可能会遮住我们的App内容,因此需要进行适配;尽管RN提供了SafeAreaView
,但它有一些问题,React Navigation提供了更好用的react-native-safe-area-context
:
首先我们yarn安装,在iOS平台多一步pod安装:
1 |
|
首先在根组件使用SafeAreaProvider
,这是一个提供者,本身不会对布局产生影响,只有在该组件包裹下的子组件才能使用react-native-safe-area-context提供的功能,因此我们通常把它包裹在App组件:
1 |
|
在我们要适配的页面引入SafeAreaView
自动处理:
1 |
|
React Navigation内置了react-native-safe-area-context,一般如果使用了导航栏和底部的tab则无需处理。
抽屉式导航是从侧边栏划出抽屉一样的效果,抽屉式导航的核心是「隐藏」
,突出核心功能,隐藏一些不必要的操作,比如一些简报、新闻、栏目等等;在小屏幕时代,内容篇幅展示有限,会把一些辅助的功能放到侧边栏里;但是随着屏幕尺寸越来越大,通过新的交互方式,我们的App已经能够容纳足够多的内容,抽屉式导航的缺点也逐渐暴露:交互效率低下,处于操作盲区,单手操作不便。
因此抽屉式导航也逐渐衰落了,我们在大多App中也很难看到这种导航方式,仅有少数还保留,我们看下在虎嗅App上使用抽屉式导航的效果:
要在我们的App中使用这种导航,我们首先安装几个依赖:
1 |
|
和其他两种导航方式相同,我们需要把创建函数从依赖中导出:
1 |
|
我们可以点击左上角的按钮展开抽屉,或者在页面调用如下API进行手动打开或关闭:
1 |
|
还可以定义drawerPosition
,设置打开抽屉的位置,支持left(默认)和right:
1 |
|
我们可以设置打开抽屉的内容,通过drawContentView
很容易的覆写内容。默认情况下在ScrollView下渲染了一个DrawerItemList
元素,在这基础上我们可以进行自定义,也可以使用DrawerItem
元素:
1 |
|
经过前面几篇对RN的学习,我们基本已经掌握了RN的相关开发技巧,下面我们就开始进行实战环节,从零开始开发一款属于我们自己的App,敬请期待。
]]> 我们在启动docker容器的时候,经常需要向容器传递一些参数,以便容器进行一些特殊的配置,比如给mysql传入MYSQL_ROOT_PASSWORD
的root用户密码,或者我们想在自己的容器中传入一些数据库的配置等等。
第一种方式也最简单的,也是最常见的,在run容器时使用--env
,也就是我们在各个文档中经常见到的简写-e
:
1 |
|
我们在js代码中可以通过环境变量process.env
来获取
1 |
|
在python代码中调用os.getenv
获取:
1 |
|
第二种方式,也是我们在Docker进阶部署中介绍的,通过Dockerfile文件的ENV指令
:
1 |
|
第三种方式,run容器时,通过--env-file
指令加载env文件,首先我们把配置信息放在文件env.list中:
1 |
|
启动容器时传入文件,这样我们就不用传入一大堆的-e命令了:
1 |
|
经过测试,三种方式的优先级如下:
-e指令
大于>
–env-file大于>
ENV指令
我们查看容器的环境变量也很简单,通过inspect
命令,也可以加上grep过滤想要的字段:
1 |
|
也可以解析一下返回内容:
1 |
|
有时候我们部署docker容器会遇到问题,比如服务器在内网,不能连接外网的情况(一些具有较高保密性的企业),或者网络下载慢,不通畅;我们就可以编译、导出镜像后在内网服务器导入,就可以实现内网部署。
镜像的导入我们通常使用docker save
和``docker load`命令,save命令将镜像打包成tar文件:
1 |
|
导出镜像尽量使用
镜像名:标签
的形式,使用镜像id容易出现导入后镜像名出现<none>
的情况。
我们还可以将多个镜像打包到一个文件进行导出:
1 |
|
使用load命令就可以将导出的镜像包加载进来:
1 |
|
save和load的应用场景,就是我们上面说的内网部署的情况;同时如果我们的应用还是使用docker compose编排的多个镜像组合,就可以使用save将用到的多个镜像打包,然后拷贝到客户服务器上使用load载入。
首先我们查看本机的所有容器,使用export命令
将容器ID导出成文件:
1 |
|
导出到文件后,我们在本地目录可以看到该tar包文件,我们再使用import命令
将镜像文件导入进来:
1 |
|
导入后的容器文件会成为一个镜像,我们可以为它指定新的名称,如果存在同名镜像,原有的名称会被剥夺,赋给新的镜像。
export和import的应用场景主是要用来制作基础镜像,比如我们从一个ubuntu镜像启动一个容器,然后安装一些软件和进行一些设置后,使用docker export保存为一个基础镜像。然后把这个镜像分发给其他人使用,比如作为基础的开发环境。
总结一下docker save和docker export的区别:
在Docker中,我们执行pull XXX
某个镜像的时候,实际上它是从registry.hub.docker.com
官方的镜像仓库去拉取的;在实际工作中,我们不会把企业的项目push到公有仓库中管理;所以为了更好的管理,Docker不仅提供了公有仓库,也允许我们搭建私有仓库。
Docker Registry是一个无状态,高度可扩展的服务器端应用程序,它存储并允许您分发Docker映像;我们通过run命令启动:
1 |
|
Registry服务默认将上传的镜像保存在容器的/var/lib/registry
,我们可以将服务器本地的文件夹挂载到该目录,即可实现保存镜像;通过以下curl我们可以查看服务器是否启动,以及服务器上的镜像。
1 |
|
正常情况下,服务器推送镜像到仓库默认使用的是https,但是我们在企业内部使用,这里就不加https;需要在客户端配置可信的仓库地址为http,否则push时会报如下错误:
1 |
|
在windows的Docker客户端,我们可以直接修改Desktop的配置,在Setting中选择Docker Engine
,添加insecure-registries字段,完成后点击Apply & Restart
重启即可:
在linux客户端,我们修改/etc/docker/daemon.json
文件,我们同样也是加入insecure-registries
,然后执行命令sudo service docker restart
重启docker:
1 |
|
修改该文件必须符合JSON文件规范,否则会启动失败
Registry服务启动后,我们来看下如何推送镜像到仓库中;我们从Docker Hub下载一个ubuntu:18.04
镜像,然后将它推送到私有仓库;首先给这个镜像重新打上服务器的标签:
1 |
|
使用push命令
推送镜像:
1 |
|
我们可以在另外一台机器上拉取这个镜像,也可以将本地的192.168.0.1:5000/my-ubuntu:latest
和ubuntu:18.04
镜像删除后,再次拉取:
1 |
|
再次curl查看服务器,我们就能看到该镜像已经在服务器生效了
1 |
|
我们上面仓库搭建后,所有客户端都可以push、pull,这是我们不希望看到的,我们想要认证的用户才能够访问;将原有容器删除,创建一个保存账号密码的文件:
1 |
|
将上面的username和password替换成自己的账号密码,运行容器时我们绑定auth文件夹:
1 |
|
服务器开启认证后客户端再pull、push会提示no basic auth credentials
,我们需要先进行登录操作:
1 |
|
Docker Compose
是Docker官方的开源项目,负责实现对Docker容器集群的快速编排。在前两篇Docker中,我们都是介绍了单个容器的构建和使用方式;在日常开发中我们经常会遇到需要多个容器相互配合使用的情况,比如除了web项目本身,还要数据库支持,nginx负载均衡等等;如果所有容器都通过命令行的方式构建、启动、删除等操作会十分繁琐;就好像你每天下班回到家里,都需要进行开灯、关闭窗帘、打开电视、煮饭等等一系列简单且重复的操作。
Compose的出现就解决了这个问题,它通过定义一个模板文件docker-compose.yml
来管理一组相关联的容器,所有容器的配置、环境等都记录到文件中,通过一个命令就可以控制所有的容器;Compose就像智能家居的管家,我们只需要将开灯、关闭窗帘、打开电视、煮饭等操作在App中进行定义,我们下班回到家里只要对着它发出指令:回家啦!
,它就会自动帮你把所有的事情做了。
Docker Compose支持Windows、macOS和Linux平台,在Windows和macOS平台我们直接下载安装包安装后自带Compose,直接可以使用,我们通过version
查看安装情况:
1 |
|
Linux的安装也很简单,直接从官网下载编译好的二进制文件并赋予执行权限即可:
1 |
|
常见的项目就是web网站,包括web应用和数据库(mysql、mongodb或redis),我们尝试一个能够简单记录页面访问次数的web应用。新建一个express项目,编写app.js
文件:
1 |
|
这里我们连接redis的时候使用了容器名称,而不是ip,下面我们会使用容器名称和静态两种连接方式。再编写我们的Dockerfile
,构建镜像:
1 |
|
编写docker-compose.yml
文件,这是Compose的模板文件,我们用到2个服务:
1 |
|
在项目中允许docker-compose up -d
就在后台启动了Compose项目,访问8000端口,每次刷新页面,计数就会加1。
ps命令,列出所有的容器,以及运行状态和所有端口:
1 |
|
如果要查看某个服务的信息,ps命令带上某个服务的名称:
1 |
|
logs命令查看服务容器的输出。
1 |
|
启动已经存在的服务容器。
1 |
|
停止已经处于运行状态的容器,但不删除它。通过start
可以再次启动这些容器。
1 |
|
列出Compose文件中包含的镜像。
1 |
|
弹性设置服务运行的容器个数,通过service=num
;需要去掉在yaml文件指定的端口号,否则会导致端口占用问题
1 |
|
构建(重新构建)项目中的服务容器。
1 |
|
或者单独构建某个服务的容器。
1 |
|
此命令将会停止up命令所启动的容器,并移除网络。
1 |
|
此命令将自动完成包括构建镜像,(重新)创建服务,启动服务,并关联服务相关容器的一系列操作;可以直接通过该命令来启动一个项目。
1 |
|
默认情况下,启动的容器都在前台运行,使用-d
参数,将会在后台启动并运行所有的容器,一般在生产环境使用。
1 |
|
Compose通过配置文件docker-compose.yml
来管理多个Docker容器,模板文件主要分为3个部分:
下面介绍一些主要指令的使用。
yarm文件一般开头就是version字段,version指定了版本信息,关乎docker的兼容性,Compose文件格式有3个版本,分别为1,2.x和3.x。两者的版本要求如下表:
compose文件格式版本 | docker版本 |
---|---|
3.4 | 17.09.0+ |
3.3 | 17.06.0+ |
3.2 | 17.04.0+ |
3.1 | 1.13.1+ |
3.0 | 1.13.0+ |
2.3 | 17.06.0+ |
2.2 | 1.13.0+ |
2.1 | 1.12.0+ |
2.0 | 1.10.0+ |
1.0 | 1.9.1.+ |
从指定的镜像中启动容器,可以是存储仓库、标签以及镜像ID。
指定Dockerfile
所在文件夹的路径,可以是绝对路径,也可以是相对docker-compose.yml文件的路径
1 |
|
如果dockerfile文件名不是默认名,需要指定:
1 |
|
使用arg指令指定构建镜像时的变量,这里的var1
和var2
将被发送到构建环境。
注意:任何environment与args变量同名的env变量(使用块指定)将覆盖该变量。
覆盖容器启动后默认执行的命令。
1 |
|
将指定容器连接到当前连接,可以设置别名,避免ip方式导致的容器重启动态改变的无法连接情况。
1 |
|
我们在web容器中使用rd
就能访问redis,而不用ip。
链接到docker-compose.yml
外部的容器,甚至并非Compose管理的外部容器。
1 |
|
解决容器的依赖、启动先后的问题。以下例子中会先启动redis和db再启动web。
1 |
|
注意:web服务不会等待redis db「完全启动」之后才启动。
默认情况下,Compose会为我们的应用设置一个网络,服务的每个容器都加入默认网络,并且可以被该网络上的其他容器访问。
Compose创建的网络名称基于我们所在项目的目录名称
,比如我们项目在myapp目录下,那Compose会创建一个myapp_default
网络。
有些场景下,默认的网络配置不能满足我们的需求,我们可以通过networks
指令配置网络,通过default
对默认的网络进行配置:
1 |
|
我们还可以自定义网络:
1 |
|
我们这里定义了front和back两个网络,web应用同时在两个网络中均能访问redis和db,而redis和db实现了隔离。driver_opts
将选项列表指定为键值对以传递给此网络的驱动程序。
我们有时候不需要创建新的网络,只需要加入已有网络,可以使用external
选项,指定一个已经存在的网络名称:
1 |
|
加入网络时,我们可以指定容器的静态ip地址
,这样我们在进行数据库连接时,可以不用容器服务名称,而直接使用ip地址:
1 |
|
设置网络模式。使用和docker run的–network参数一样的值。
1 |
|
在容器中设置环境变量,等同于docker run -e VARIABLE=VALUE ...
1 |
|
从文件中获取环境变量,等同于docker run --env-file ...
。
1 |
|
环境变量文件中每一行必须符合格式,支持#
开头的注释行。
1 |
|
如果有变量名称与environment指令冲突,以environment指令为准。
volumes可以设置数据卷所挂载路径,它有两种方式,一种方式是设置宿主机路径(HOST:CONTAINER),通过[SOURCE:]TARGET[:MODE]
格式,最后的ro用于只读,rw用于读写(默认):
1 |
|
另一种方式直接设置数据卷的名称,需要在文件中配置数据卷:
1 |
|
端口暴露给宿主机,如果仅仅指定容器端口,宿主机将会随机选择端口。
1 |
|
暴露端口,和ports的区别是,expose不映射到宿主机,只被连接的服务访问。
1 |
|
自定义DNS服务器。可以是一个值,也可以是一个列表。
1 |
|
指定容器退出后的重启策略为始终重启。该命令对保持服务始终运行十分有效,在生产环境中推荐配置为always
或者 unless-stopped
。
1 |
|
下面推荐一些笔者常用的镜像。
如果我们自己想要一个私有的git开发仓库,或者公司小团队使用,gitbucket是一个不错的选择;相比于gitlab动辄就占用内存3G,gitbucket几百mb的大小已经是很小巧迷你了,再配上直男般的蓝黑色,让人简直。。。。不过好在一般我们都是敲的git命令,所以不用在意他的界面。
1 |
|
8080
端口是它的界面的地址,29418
端口是给git通过SSH去链接仓库的,建议开启。
如果你受够了某网盘几十KB的小水管速度,那么filebrowser
是你搭建一个轻量级的私有云盘不错的选择。另一款网盘工具nextcloud也十分的不错,功能丰富且强大,带有app功能,不过需要结合数据库使用,配置略微繁琐,喜欢折腾的小伙伴可以自己尝试。
filebrowser使用了go语言编写,可以通过浏览器对服务器上的文件进行管理。可以是修改文件,或者是添加删除文件,甚至可以分享文件,是一个很棒的文件管理器,使用非常简单方便,功能很强大。
使用docker安装也很方便,我们可以只映射/srv目录下的文件:
1 |
|
再通过nginx转发8080端口,这样我们就能在外网访问了;nginx配置文件中还需要修改上传文件的大小限制,这里我们改到2GB,大部分的文件都能上传了:
1 |
|
我们身边有很多的爬虫应用案例,比如百度、Google、必应等搜索引擎都有自己的爬虫,会定时来抓取你的网站;再比如过年回家需要抢火车票,我们经常能够看到很多的抢票软件等,也都是爬虫的应用;不过我们在网络上肆意使用爬虫的时候也要注意相关法律法规,毕竟俗话说得好:
爬虫写得好,牢饭吃得饱
Scrapy是由Python语言开发的一个快速、高层次的屏幕抓取和web抓取框架,用于抓取web站点并从页面中提取结构化的数据,只需要实现少量的代码,就能够快速的抓取。
Scrapy爬虫部署需要使用scrapyd和scrapydweb,scrapyd是由scrapy开发者开发的、通过简单的JSON API来管理多个项目的应用;通过docker我们可以很轻松的启动一个scrapyd服务器:
1 |
|
它的界面比较简单,我们需要通过繁琐的API接口来上传项目、启动或者停止项目:
因此Scrapy开发还提供了一个可视化管理爬虫的web应用,同时支持Scrapy日志分析和可视化。
1 |
|
我们在web界面上就可以清楚的看到每个服务器运行爬虫的状态和数量,以及定时启动爬虫。
Scrapy是一个非常好用的爬虫框架,如果本文的阅读量突破一万,后面笔者可以聊一下它的使用。
Chevereto是目前最好的图床之一了。功能也非常强大。其免费版和收费版的区别,在于收费版多了硬盘扩展,社交分享功能和技术支持,免费版的功能也够用了;Chevereto依赖的环境如下:
使用docker安装的话PHP的环境我们就可以省去安装步骤了,需要安装一个MySQL的环境,然后通过run命令设置MYSQL的环境变量:
1 |
|
Chevereto运行起来后我们初始化设置管理员账号密码,然后在设置中修改界面为中文:
PHP默认限制上传大小为2MB,我们需要修改容器中的文件解除此限制;首先使用cp命令
把容器中的.htaccess
文件拷贝出来:
1 |
|
然后编辑文件.htaccess,设置最大上传大小为大一点的数值,比如这里设为128MB,数值可以根据自己需要调整:
1 |
|
我们把文件拷贝回容器的原处即可:
1 |
|
最后一步我们在设置中修改上传限制,进入Chevereto,单击用户名弹出下拉菜单,选择【仪表盘】,然后点【设置】,弹出页面中选择【图片上传】,找到【最大上传文件大小】选项,修改不超过128的数值即可:
如果没有修改.htaccess文件,最后一个步骤的设置调整是不能超过2MB的。
这样我们的图床就搭建以及配置完毕了,其他个性化需要可以在设置中自行配置;我们去首页就能随意上传和查看图片了;通过nginx代理转发我们还可以暴露到外网,将你的美照分享给好友(前提是有公网IP或云服务器)。
Portainer是一个可视化的Docker操作界面,提供状态显示面板、应用模板快速部署、容器镜像网络数据卷的基本操作(包括上传下载镜像,创建容器等操作)、事件日志显示、容器控制台操作、Swarm集群和服务等集中管理和操作、登录用户管理和控制等功能。功能十分全面,基本能满足中小型单位对容器管理的全部需求。
通过一个run命令我们就可以启动portainer,/var/run/docker.sock
是绑定宿主机的docker文件,在容器内部直接与docker守护进程通信进行接口调用:
1 |
|
容器启动后,设置管理员账号密码,进入Portainer后台管理界面,点击Local环境就能够使用了:
容器和镜像的管理也很方便,在管理界面直接增删镜像或容器即可;创建容器也直接可视化了,我们打开【Container】=>【Add container】,然后设置容器运行所需要的参数,我们这里以mysql为例:
Restart policy
建议选择Always,相当于设置--restart=always
,保证了容器在服务器重启后总会自动重新启动。
Webssh是指通过浏览器以网页的形式通过SSH协议远程访问任何开启了SSH服务的设备;webssh工作的原理也很简单,大致如下:
1 |
|
在后台启动一个webssh的后端服务器(python程序或其他语言开发的),前端浏览器通过websocket和服务器进行通信,将一些命令发送到webssh服务器,webssh服务器再将接收命令发送给需要通信且开启了ssh功能的服务器。
使用Webssh的好处是:在存在堡垒机(即跳板机)的环境下,如果堡垒机本身有开启web服务的话,那可以在堡垒机上部署webssh,这时不用通过SSH或者RDP访问堡垒机,直接打开浏览器就能以web形式通过堡垒机来SSH远程访问网络设备,这在一些内网防火墙不允许SSH,但是允许HTTP和HTTPS的环境中很实用。而且免去了安装putty、secureCRT等SSH client软件的必要。
通过docker运行webssh服务器也很简单:
1 |
|
然后通过浏览器访问webssh服务器IP+8888端口号就可以进入它的界面了,它的界面也很简洁,甚至有着一丝丝的简陋,不过部署到堡垒机能用就行,还要啥自行车啊。通过填写堡垒机的hostname、端口等参数就能连接堡垒机ssh了:
为知笔记是一款老牌笔记应用了,支持markdown、网页笔记、网页剪藏和分享等多功能,最近推出了docker私有化部署的功能,同时支持5个用户,适合小团队使用。
我们新建一个wiznote目录,用于保存笔记的内容,然后run启动服务:
1 |
|
稍等几分钟就能看到服务启动了,在本地打开localhost:8080打开主界面,默认管理员账号:admin@wiz.cn和密码123456。
为知笔记支持多平台客户端和移动端客户端,我们在客户端界面点击【切换服务器】,选择【企业私有服务器】,输入服务器的ip地址及端口号就能登录私有服务器:
Jellyfin是一个自由的软件媒体系统,用于管理媒体和提供媒体服务,展示你自己的电影、电视剧、音乐等多媒体数据,并提供多平台访问播放服务。通过docker,我们可以很方便的启动它的服务;在本地创建media和config文件夹,media文件夹是媒体文件夹,我们可以根据需求继续创建media/movie、media/music等文件夹存放不同媒体资源:
1 |
|
绑定不同端口说明如下:
端口 | 说明 |
---|---|
8096 | WebUI 访问端口 |
7359/udp | (可选)允许本地网络的客户端发现 Jellyfin |
1900/udp | (可选)DLNA服务 |
容器创建后,我们打开localhost:8096就能打开安装向导页面,它的界面还是:
选择一系列的语言、国家(国家选择People's Republic of China
)以及配置账号密码后我们就进入它的首页了;我们选择【添加媒体库】,选择你media文件夹下的子文件夹,就能看到其下面的媒体文件了:
Aria2是一款开源下载工具,可帮助简化不同设备和服务器之间的下载过程。它支持磁力链接、BT种子、http等类型的文件下载,与迅雷及QQ旋风相比,Aria2有着优秀的性能及较低的资源占用,架构本身非常轻巧,通常只需要4兆字节(HTTP下载)到9兆字节(用于BitTorrent交互)之间。
另外,aria2由于它的开源特性,因此也用在很多离线下载的场景,比如很多路由器都支持aria2离线下载功能,我们在路由器的插件市场中安装aria2后,在路由器挂载u盘,上班的时候想要下载的电影、视频等链接丢给它,回到家就可以直接观看了;顺带提一下,chrome浏览器配合油猴插件直接愉快的离线下载百度网盘的文件。
我们通过docker来安装aria2十分方便,新建一个aria2-downloads
文件夹映射下载的目录,aria2-config
文件夹映射配置的目录。这里的p3terx/aria2-pro
镜像就是我们aria2下载的主程序,它是一个命令行的程序,因此搭配p3terx/ariang
镜像作为它的可视化管理界面。用到了多个镜像,我们就可以通过docker compose来进行构建:
1 |
|
项目启动后,我们打开AriaNg的管理界面,在设置中配置yarml文件中的RPC端口(默认6800)和RPC密钥。
配置完成后,点击【新建】,输入你的下载链接就可以愉快地等待了。
下载速度和你的实际带宽以及资源情况等都有关系,上图仅做展示。
]]>RN提供了一些核心的组件,我们可以直接import后使用,其中也包含一些iOS或者Android特有的组件,只能针对对应的平台使用,我们来看下具体用法。
首先是RN的五大基础组件,是我们在页面上最常用的组件,我们看下它们的的描述:
RN组件 | 对应Android视图 | 对应iOS视图 | 对应Html视图 |
---|---|---|---|
View | ViewGroup | UIView | 非滚动div |
Text | TextView | UITextView | span或p |
Image | ImageView | UIImageView | img |
ScrollView | ScrollView | UIScrollView | 滚动div |
TextInput | EditText | UITextField | input |
View组件
是最基础的组件,类似于div可以进行嵌套使用,在RN样式布局中我们介绍了它结合Flex样式进行页面布局;View在定位布局和div有一些区别,支持absolute绝对定位
,不支持fixed和sticky定位。
直接在View上绑定点击事件,是没有用的,View不支持点击事件,如果我们想要监听它的点击,需要在将它放到TouchableHighlight
等元素中:
1 |
|
在RN页面开发中,如果使用绝对定位布局,某个View有可能会遮住它下方的某个组件;比如我们在一个地图组件上覆盖了一个图片用来展示信息,又不想让其影响下方组件的点击、触摸事件,就可以用到pointerEvents
属性,它用于控制当前视图是否可以作为触控事件的目标,有以下几个值:
我们用样式构建三个重叠的View:
1 |
|
正常我们点击每个box都会触发对应的onPress事件;但是如果我们不想触发子元素box2和box3的事件,我们可以添加pointerEvents:none
:
1 |
|
我们看到元素pointerEvents设置为none后,其元素本身及子元素均已不能触发事件了。
Text组件
之前介绍过主要用来显示文字;我们经常需要对Text中超过多少行显示省略号,不能使用css中的text-overflow: ellipsis
,而是使用Text的numberOfLines
属性来进行文本行数的限制显示:
1 |
|
Text组件的样式和web端也有一定的区别,我们通常在html指定整个文档默认的字体、字号及颜色:
1 |
|
这样我们对某些div、span中的文字,如果没有指定颜色大小,浏览器就会一路向上查找到根节点html,然后继承它的样式;这样导致的问题就是任何节点都会有font-size属性,RN摒弃了样式继承,推荐使用包含相关样式的组件,然后重复使用这个组件:
1 |
|
我们在定义MyBodyText组件时,可以把它的内容放到Text中:
1 |
|
在Text组件上我们可以绑定点击事件onPress
1 |
|
在深入学习RN样式布局我们已经介绍了Image组件
通过source属性来加载本地图片或者网络,这里不再赘述了。
RN也支持对网络图片进行缓存
,访问过一次的图片,在一定时间内会缓存到手机中,当需要再次显示的时候,RN会直接从缓存读取;在Android平台,图片会缓存到本地;对于iOS平台,可以通过cache属性实现不同缓存效果:
1 |
|
cache属性可以取以下值:
RN还提供了一个统一的方式
来管理iOS和Android中的图片,我们首先给两个平台分别创建好两张不同的图片:
1 |
|
在我们的代码中,只需要引用logo.jpg图片,RN就会根据平台而选择不同的图片文件:
1 |
|
我们看到不同平台展示了对应logo图片;除此之外我们还可以使用@2x
,@3x
这样的文件名后缀,来为不同的屏幕精度提供图片。
我们经常会遇到文字嵌套背景图片的情况,除了使用Image组件进行绝对布局外,RN还提供了ImageBackground
组件,它的参数和Image组件完全相同,只需要把子组件嵌套即可:
1 |
|
TextInput组件
是一个允许用户在应用中通过键盘输入文本内容的组件;它接收一个value
属性作为输入的默认值,当文本框内容变化时回调onChange
函数:
1 |
|
和web端的input一样,TextInput同样也支持设置占位符的文字placeholder
,同时还能直接设置占位符的色值:
1 |
|
我们在不同的场景下经常能看到弹出不同键盘类型,比如输入密码、数字、邮箱等,我们通过keyboardType
属性来设置:
1 |
|
也可以设置为email-address
,输入邮箱,我们看下效果:
有一些特殊的值仅iOS下可用:
我们看下url的效果:
在电商应用中我们经常会遇到填写收货地址、说明信息等多行文本,我们可以设置multiline
为true,同时安卓上文本默认会垂直居中,我们可以添加textAlignVertical: 'top'
的样式:
1 |
|
在登录页面中,我们需要将输入后的密码进行遮挡,确保我们录入信息的安全,可以使用secureTextEntry
属性:
1 |
|
该属性在multiline={true}
时不可使用,我们看下密码遮挡的效果:
我们在iOS上经常会遇到输入框首字符大写的情况,通过autoCapitalize
属性可以自动将特定字符切换为大写:
1 |
|
它有下面几个枚举值:
我们看下不同枚举值的效果:
ScrollView组件
是用来当屏幕宽度或者高度不足以展示所有内容时进行滚动展示的组件,它的用法和View类似,但是必须有一个确定的高度,或者使用flex:1
来让它填充整个屏幕:
1 |
|
ScrollView所有的子元素就可以在垂直方向上滚动,我们看下效果:
通过horizontal
属性,我们可以设置子元素在水平方向上排成一行进行滚动:
1 |
|
Button组件
在很多UI库中也都有封装,像Element的el-button,AntDesign的a-button,在一些表单提交或者需要触发事件时使用。RN中Button有两个重要的props,title展示按钮的文字和onPress触发点击事件:
1 |
|
我们发现Button组件在Android和iOS上的表现形式有些区别:
Button的color属性
可以改变按钮的颜色,在Android下改变背景色,iOS改变文本颜色:
1 |
|
由于Button在不同平台的表现形式不一样,因此我们经常会使用View和Text封装自己的Button组件,或者使用社区组件,比如react-native-button
或者react-native-elements
的Button。
1 |
|
Switch组件
是一个跨平台通用的“开关”组件,在应用中很多时候会使用一个开关组件来控制某些功能是否开启;它提供的属性不多,value属性用来设置组件当前的值,onValueChange接收一个函数,当组件的值改变时调用此回调函数,回调函数的参数为组件新的值:
1 |
|
我们看下switch组件的效果:
switch组件还可以改变颜色,支持以下三种属性:
我们给三个不同属性设置不同颜色,不要问我为什么选红和蓝,问就是自古红蓝出CP:
1 |
|
我们看到thumbColor和ios_backgroundColor在iOS平台能够支持,但是trackColor表现的不是很明显:
在安卓平台就只能看到thumbColor的效果:
官方的Slider组件
已经废弃,推荐安装使用社区的Slider:
1 |
|
如果在iOS,还需要在ios目录下运行pod install
;Slider组件也是value属性设置进度,onValueChange值的回调函数:
1 |
|
如果我们需要固定的几个值,可以使用step
进行步进设置,step的值必须是在minimumValue
和maximumValue
范围之内的:
1 |
|
Slider还支持tapToSeek
属性,允许点击滑块轨迹来设置值,这个值默认为false,在安卓上没有影响:
1 |
|
我们看下Slider几个属性的效果:
FlatList组件
是一个高性能的列表组件;我们前面说到,ScrollView也是一个滚动展示的组件,那么两者有什么区别呢?我们通过ScrollView的用法也能看出来,它会简单粗暴地把所有子元素一次性全部渲染出来;如果列表数据量小进行展示没有问题,但是如果一次性展示好几屏的数据,那么创建和渲染都会造成性能的浪费。
而FlatList会惰性的渲染子元素,只在它们将要出现在屏幕中时开始渲染。除了高性能,FlatList还支持以下功能:
FlatList组件必须的两个属性是data
和renderItem
,data是列表的数据源,接收一个数组数据;renderItem则从数据源中逐个解析数据,然后回调一个渲染组件格式给FlatList进行渲染:
1 |
|
为了渲染的唯一性,我们还需要设置keyExtractor
属性,指定item的key作为列表每一项的唯一标识;然后我们看下它的下拉刷新功能,如果设置了onRefresh
事件,会在列表头部添加一个标准的RefreshControl控件,我们下拉时就会触发onRefresh回调:
1 |
|
上拉加载的处理相对比较简单了,当列表被滚动到距离内容最底部不足onEndReachedThreshold
属性的距离时,调用onEndReached
回调:
1 |
|
这样上拉加载后,如果数据请求完成,列表底部会突然一下多出很多数据,就很突然。
我们可以用ListFooterComponent
属性在列表底部渲染一个加载中的loading进行提示:
1 |
|
我们看下整个下拉刷新和上拉加载的效果:
通过ItemSeparatorComponent
属性,我们可以在行与行之间渲染分割线,分割线不会出现在第一行之前和最后一行之后。
1 |
|
我们看下分割线的效果:
FlatList提供了多个scroll函数,可以用来滚动到对应的位置,我们来看下scrollToIndex
的用法,这个函数用于滚动到对应的index的位置;首先我们需要通过getItemLayout
属性告诉Flatlist整个列表的一些信息:
1 |
|
getItemLayout属性是一个可选的优化属性,用于避免动态测量内容尺寸的开销,scrollToIndex函数调用时需要设置这个属性;这里的ITEM_HEIGHT表示整个item的行高,如果我们像上面一样使用了ItemSeparatorComponent属性,需要把分割线的高度
也算在ITEM_HEIGHT里面,否则滚动的距离会有偏差。
1 |
|
我们看下scrollToIndex的效果:
SectionList组件
和FlatList一样,都是列表组件,而且两者都是基于VirtualizedList
进行封装的,不同的是SectionList有一个分组(section)的功能,类似于通讯录的功能,它支持下面功能:
SectionList的数据sections也是需要分组的,数据在每个分类的data数组中;SectionList还多了一个renderSectionHeader
属性可以用来渲染section的头部:
1 |
|
在iOS上,每个section的头部会有吸顶的效果,Android上则没有该效果,我们看下SectionList效果:
下拉刷新和上拉加载的功能和FlatList组件相同,这里就不再赘述了。
VirtualizedList
组件是FlatList
和SectionList
的底层实现;FlatList和SectionList使用起来更加的方便,如果这两个组件满足不了我们的需求,我们可以考虑VirtualizedList。
VirtualizedList在屏幕窗口维护了一个固定高度的渲染窗口,在渲染窗口之外的元素用固定高度的空白元素进行渲染,同时让这个窗口监听内部的滚动事件,当元素离渲染窗口距离太远了,优先级较低,当元素距离可视区较近时,就自动获得了一个较高的渲染等级,通过这种方式逐步渲染;极大改善了渲染大批量数据时的内存消耗和使用性能。
它有下面几个必传的属性:
getItem
属性是VirtualizedList特有的属性,它接收列表数据和对应的index,我们可以对列表中相应的数据进行处理然后返回出一个元素给到renderItem进行渲染:
1 |
|
其他的属性和FlatList、SectionList基本相同,这里不再赘述了。
ActivityIndicator
组件用于显示一个圆形的loading
提示符号,它有下面几个属性:
#999
1 |
|
我们看下再iOS上的效果:
我们看到当animating为false时,隐藏ActivityIndicator的动效,整个指示器像是消失了一样;这是因为hidesWhenStopped默认为true,将指示器隐藏了,如果我们将该属性设为false,则会展示一个静止状态的指示器。我们再看下安卓下的指示器效果:
hidesWhenStopped
属性在安卓下没有起到作用。
StatusBar组件
用于控制应用顶部的状态栏,包含时间,运营商名称,网络情况,电池情况等;由于它可以在任何视图加载,当有多个StatusBar组件时,后加载的组件会覆盖前面的,因此使用时需要注意;
1 |
|
我们分别来看下它支持的属性。
animated
设置当状态栏的状态发生变化时,是否需要加入动画;动画支持backgroundColor、barStyle和hidden等属性的变化。hidden
属性用于设置状态栏是否隐藏:
1 |
|
我们看下改变hidden带上animated的效果:
如果animated为false则没有动画效果,切换的比较生硬了;backgroundColor
属性设置状态栏的背景颜色,仅支持安卓:
1 |
|
barStyle用于设置状态栏文字的颜色,其值是枚举类型enum(‘default’, ‘light-content’, ‘dark-content’),仅针对Android 6.0以上版本:
networkActivityIndicatorVisible属性是一个Boolean类型,在iOS平台上指定显示一个网络活动提示符:
translucent属性适用于Android平台,用来指定状态栏是否透明。
1 |
|
TouchableWithoutFeedback组件
是Touchable系列组件中最基本的一个组件,只响应用户的点击事件不会做任何UI上的改变
,这就是它的名字WithoutFeedback
,同时它也不能设置style样式;因此除非有特别的原因,否则一般很少用这个组件,它支持以下属性:
1 |
|
需要注意的是,Touchable系列组件都只支持一个子元素(有且仅有一个);如果我们想要放置多个元素,可以用一个View包裹它们。
TouchableHighlight
组件是Touchable系列组件中比较常用的,和它的名字Opacity一样,它是通过在按下去改变视图的不透明度来表示按钮被点击的,相比TouchableHighlight
少了一个额外的颜色变化,它支持设置style样式。
它最重要的一个属性就是activeOpacity
,表示按钮被触摸操作激活时以多少不透明度显示(0 到 1 之间),默认值为 0.2,数值越小透明效果效果越明显,我们可以设置一个大一点的数值:
1 |
|
我们看下TouchableOpacity点出触发的效果:
TouchableHighlight组件
也是比较常用的点击按钮,它在TouchableWithoutFeedback
的基础上添加了一些UI上的扩展,当按钮点击触发时,该视图的不透明度会降低,同时会看到相应的颜色;它支持以下属性:
1 |
|
我们看下TouchableHighlight的效果:
Modal组件
是一种简单的覆盖在屏幕最上层显示内容的组件,且用户无法对下层的UI进行操作,因此我们可以随意在Modal上进行UI操作,它有以下属性:
Modal组件通过visible
属性来控制是否出现;transparent
设置背景的透明度,如果为false,则整个背景都会变成不透明的白色覆盖;因此一般设置为true,再在根组件设置背景颜色为rbga(0,0,0,0.5)
增加黑色的蒙层。
1 |
|
我们看下三种动画的效果:
SafeAreaView
组件用于在一个安全
的可视区域内渲染内容;这个组件存在的目的就是针对iPhone X这样带有齐刘海的全面屏设备,为了避免页面内容渲染到刘海范围内。本组件目前仅支持iOS设备
以及iOS 11
或更高版本。
它的用法也非常简单,只要把我们原有的页面视图通过SafeAreaView
组件包裹起来,同时设置flex: 1
,一般可以在根组件上设置:
1 |
|
我们看下SafeAreaView组件不使用和使用的两种效果。
为了在样式上适配iPhone X的屏幕,我们可以在utils中封装一个函数,来单独判断iPhone X:
1 |
|
我们在样式中就可以直接使用ifIphoneX
函数:
1 |
|
TouchableNativeFeedback
组件是Touchable系列组件中最后一个组件,可以在Android 5.0以后触摸实现水波纹的效果,因此也只能在Android平台使用。
我们可以通过background
属性来自定义原生触摸操作反馈的背景,它接受一个有着type属性和一些基于type属性的额外数据的对象,这个对象推荐通过本组件的几个静态方法来创建:
上面的效果太绕口,我看下SelectableBackground的用法:
1 |
|
我们看到在View按钮点击时,有水波纹的效果:
SelectableBackgroundBorderless的效果则会超过整个按钮的边框限制:
1 |
|
我们上面创建的波纹都是默认的黑色,Ripple函数支持传入一个color参数来指定颜色,它的第二个参数borderless也能设置涟漪是否扩散到按钮之外,达到和SelectableBackgroundBorderless一样的效果:
1 |
|
1 |
|
效果如下:
]]>我们部署的容器中很多应用都是需要让外部通过网络端口来进行访问的,比如比如mysql的3306端口,mongodb的27017端口和redis的6379端口等等;不仅是外部访问,不同的容器之间可能还需要进行通信,比如我们的web应用容器需要来连接mysql或者mongodb容器,都涉及到了网络通信。
容器要想让外部访问应用,可以通过-P
或者-p
参数来指定需要对外暴露的端口:
1 |
|
使用-P
会在主机绑定一个随机端口
,映射到容器内部的端口;我们查看刚刚创建的容器,可以看到随机端口49154映射到了容器内部的80端口:
1 |
|
使用logs
命令我们可以看到nginx的访问日志:
1 |
|
docker port
可以快捷地让我们查看容器端口的绑定情况:
1 |
|
使用-p
参数可以指定一个端口进行映射:
1 |
|
也可以使用ip:hostPort:containerPort
格式指定映射一个特定的ip:
1 |
|
省略hostPort参数本地主机会自动分配一个端口,类似-P
参数的作用:
1 |
|
还可以使用udp
来指定映射到udp端口:
1 |
|
有时候我们想要映射容器的多个端口,可以使用多个-p参数:
1 |
|
或者映射某个范围内的端口列表:
1 |
|
我们想要将多个容器进行互联互通,为了避免不同容器之间互相干扰,可以给多个容器建立不同的局域网,让局域网络内的网络彼此联通。
要理解docker的网络模式,我们首先来看下docker有哪些网络;我们安装docker后,它会自动创建三个网络none、host和brdge,我们使用network ls
命令查看:
1 |
|
我们分别来看下这几个网络的用途;首先是none
,none顾名思义,就是什么都没有,该网络关闭了容器的网络功能,我们使用--network=none
指定使用none网络:
1 |
|
我们这里使用了busybox镜像,可能有的童鞋对它不了解,这是一个集成压缩了三百多个常用linux命令和工具的软件,它被称为Linux工具里的瑞士军刀
,我们这里主要用它的ip命令查看容器的网络详细情况。
我们看到这个容器除了lo本地环回网卡,没有其他的网卡信息;不仅不能接收信息,也不能对外发送信息,我们用ping命令测试网络情况:
1 |
|
这个网络相当于一个封闭的孤岛,那我们不禁会想,这样“自闭”的网络有什么用呢?
封闭意味着隔离,一些对安全性要求高并且不需要联网的应用可以使用none网络。比如某个容器的唯一用途是生成随机密码,就可以放到none网络中避免密码被窃取。
其次是bridge网络模式
,docker安装时会在宿主机上虚拟一个名为docker0
的网桥,如果不指定–network,创建的容器默认都会挂载到docker0
上,我们通过命令查看宿主机下所有的网桥:
1 |
|
这里的网桥我们可以把它理解为一个路由器,它把两个相似的网络连接起来,并对网络中的数据进行管理,同时也隔离外界对网桥内部的访问;同一个网桥下的容器之间可以相互通信;我们还是通过busybox查看容器的网络情况
1 |
|
我们看到这里容器多了一个eth0的网卡,它的ip是172.17.0.6
。
最后是host网络模式
,这种模式禁用了Docker的网络隔离,容器共享了宿主机的网络,我们还是通过busybox
来查看容器的网络情况:
1 |
|
我们发现这里容器的ip地址是192.168.0.100,和我宿主机的ip地址是一样的;host模式其实类似于Vmware的桥接模式
,容器没有独立的ip、端口,而是使用宿主机的ip、端口。
需要注意的是host模式下,不需要添加-p参数,因为它使用的就是主机的IP和端口,添加-p参数后,反而会出现以下警告:
1 |
|
host模式由于和宿主机共享网络,因此它的网络模型是最简单最低延迟的模式,容器进程直接与主机网络接口通信,与物理机性能一致。不过host模式也不利于网络自定义配置和管理,所有容器使用相同的ip,不利于主机资源的利用,一些对网络性能要求比较高的容器,可以使用该模式。
我们通过容器互联来测试两个容器在同一个网桥下面是如何进行连接互通的;首先我们自定义一个网桥:
1 |
|
如果对网桥不满意,可以通过rm
命令删除它:
1 |
|
我们新建两个容器,并且把它们连接到my-net
的网络中:
1 |
|
我们让两个容器之间互相ping,发现他们之间能够ping通:
1 |
|
我们在前一篇文章中简单提到了Dockerfile的两个命令FROM和RUN,其实它还提供了其他功能强大的命令,我们对它的命令深入讲解;首先我们知道Dockerfile是一个用来构建镜像的文本文件,文本内容包含了一条条构建镜像所需的指令和说明;在docker build命令中我们使用-f
参数来指向文件中任意位置的Dockerfile:
1 |
|
FROM指令
用来指定一个基础镜像,它决定了Dockerfile构建出的镜像为何物以及怎样的环境;大多数的Dockerfile,都是以FROM指令开始;它的语法如下:
1 |
|
Dockerfile必须以FROM指令开始,不过它支持在FROM之前由ARG指令定义一个变量:
1 |
|
我们在构建镜像时通常会有多个阶段的镜像需要来进行构建,比如vue项目构建镜像时,我们需要在编译阶段
打包出dist文件,还需要在生产运行阶段
使用dist文件作为静态资源;如果不使用多阶段构建,我们通常需要两个Dockerfile文件,构建出两个镜像,这样有一个镜像肯定是浪费的。
Docker从17.05开始,支持多阶段构建,就是我们在Dockerfile中可以使用多个FROM指令
,每个FROM指令都可以使用不同的基础镜像,并且每条指令都会开始新阶段的构建;在多阶段构建中,我们可以将资源从一个阶段复制到另一个阶段,在最终镜像中只保留我们所需要的内容。
1 |
|
第二个FROM指令开始一个新的构建阶段,COPY --from=0
代表从上一个阶段(即第一阶段)拷贝文件;默认情况下,构建阶段没有命名,可以使用从0开始的整数编号引用它;我们可以给FROM指令加上一个as <Name>
作为构建阶段的命名。
1 |
|
在后面的例子中,我们会来演示如何使用多阶段构建优化我们的构建过程。
由于基础镜像决定着构建出镜像产物的大小,因此选择一个合适的基础镜像显得十分重要了。如果我们去hub.docker.com查看node的标签,我们会发现除了版本号之外,后面还会带着一些看不懂的单词,什么alpine,什么slime了,这些版本号都代表着什么含义呢?我们简单的了解一下。
docker镜像之间的区别在于底层的操作系统
首先如果什么都不带,默认latest,那就是完整的镜像版本,如果你还是一个小白,对其他版本没有什么了解的话,那么选它是肯定不会出错的。
其次是slim版本
,slim表示最小安装包,仅包含需要运行指定容器的特定工具集。通过省去较少使用的工具,镜像会更小。如果我们服务器有空间限制且不需要完整版本,就可以使用此镜像。不过使用这个版本时,要进行彻底的测试。
然后是我们经常会看到的alpine版本
,alipine镜像基于alpine linux
项目,这是一个社区开发的面向安全应用的轻量级Linux发行版。它的优点就是基于的linux操作系统非常轻量,因此构建出来的镜像也非常的轻量;它的缺点也十分的明显,就是不包含一些有可能会用到的包,并且使用的glibc包等都是阉割版;因此如果我们使用这个版本,也需要进行彻底的测试。
我们ls看下这三个版本,也能发现它们的大小存在着差异:
1 |
|
其次是一些Debian的发行版,Debian是一个自由的,稳定得无与伦比操作系统;带有下面一些标签的镜像对应Debian发行版本号。
RUN指令
用于在镜像容器中执行命令,其有以下两种执行方式:
1 |
|
RUN指令常见的用法就是安装包用apt-get
,假设我们需要在镜像安装curl:
1 |
|
我们知道Dockerfile的指令是分层构建的,每一层都有缓存,假设我们下次添加了一个包wget:
1 |
|
如果我们下次再次构建时,apt-get update
指令也不会执行,使用之前缓存的镜像;而install由于update指令没有执行,可能安装过时的curl和wget版本。
因此我们通常会把update和install写在一条指令,确保我们的Dockerfiles每次安装的都是包的最新的版本;同时也可以减少镜像层数,减少包的体积:
1 |
|
WORKDIR指令
可以用来指定工作目录,以后各层的当前目录就被改为指定的工作目录;如果该目录不存在,WORKDIR会自动创建目录。
很多童鞋把Dockerfile
当成Shell脚本来写,因此可能会导致下面的错误:
1 |
|
这里echo的作用是将字符串hell重定向写入到world.txt中;如果我们把这个Dockerfile构建镜像运行后,会发现找不到/app/world.txt
;由于在Shell脚本中两次连续运行的命令是同一个进程执行环境,前一行命令运行影响后一个命令;而由于Dockerfile分层构建的原因,两个RUN命令执行的环境是两个完全不同的容器。
因此如果我们需要改变以后每层的工作目录的位置,可以使用WORKDIR
指令,建议在WORKDIR指令中使用绝对路径:
1 |
|
这样生成的world.txt就在/app目录下面了。
COPY指令
用于从构建上下文目录中复制文件到镜像内的目标路径中,类似linux的cp命令,它的语法格式如下:
1 |
|
复制的文件可以是一个文件、多个文件或者通配符匹配的文件:
1 |
|
但需要注意的是,COPY指令只能复制文件夹下的文件,而不能复制文件夹本身,和linux的cp命令有区别;比如下面复制src文件夹:
1 |
|
运行后我们发现src文件夹下面的文件都拷贝到/app目录下了,没有拷贝src文件夹本身,因此我们需要这样来写:
1 |
|
CMD指令
用于执行目标镜像中包含的软件,可以指定参数,它也有两种语法格式:
1 |
|
我们发现CMD和RUN都可以用来执行命令的,很相似,那他们两者有什么区别么?首先我们发现RUN是用来执行docker build构建镜像过程中要执行的命令,比如创建文件夹mkdir、安装程序apt-get等等。
而CMD指令在docker run时运行而非docker build,也就是启动容器的时候,它的首要目的在于为启动的容器指定默认要运行的程序,程序运行结束,容器也就结束。
而容器在run的时候只能创建一次,因此一个Dockerf中也只能有一个CMD指令;比如我们的容器运行node程序,最后需要启动程序:
1 |
|
ENTRYPOINT指令
的作用和CMD一样,也是在指定容器启动程序和参数;一个Dockerfile同样也只能有一个ENTRYPOINT指令;当指定了ENTRYPOINT后,CMD指令的含义发生了改变,不再是直接的运行其命令,而是将 CMD 的内容作为参数传给ENTRYPOINT指令,相当于:
1 |
|
那么这样的好处是啥呢?我们看一个使用的例子,我们在Docker中使用curl命令来获取公网的IP地址:
1 |
|
然后使用docker build -t myip .
来构建myip的镜像;当我们想要查询ip的时候,只需要执行如下命令:
1 |
|
这样我们就实现了把镜像当成命令使用,不过如果我们想要同时显示HTTP头信息,就需要加上-i
参数:
1 |
|
但是这个-i
参数加上后并不会传给CMD指令
,而是传给了docker run,但是docker run并没有-t参数,因此报错;如果我们想要加入-i,就需要重新完整的输入这个命令;而使用ENTRYPOINT指令就可以解决这个问题:
1 |
|
我们重新尝试加入-i
参数:
1 |
|
就可以发现http的头部信息也展示出来了。
VOLUME指令
用于暴露任何数据库存储文件,配置文件,或容器创建的文件和目录;其语法格式如下:
1 |
|
我们可以事先指定某些目录挂载为匿名卷,这样在运行时如果用户不指定挂载,其应用也可以正常运行,不会向容器存储层写入大量数据,比如:
1 |
|
这里的/data目录就会在容器运行时自动挂载为匿名卷,任何向/data中写入的信息都不会记录进容器存储层,从而保证了容器存储层的无状态化。
1 |
|
我们运行容器时可以本地目录覆盖挂载的匿名卷;需要注意的是,在Windows下挂载目录和Linux环境(以及Macos)挂载目录有一些区别,在Linux环境下由于是树状目录结构,我们挂载时直接找到目录即可,如果目录不存在,docker还会自动帮你创建:
1 |
|
windows环境下则需要对应盘符下的目录:
1 |
|
EXPOSE指令
是声明容器运行时提供服务的端口,这只是一个声明,在容器运行时并不会因为这个声明应用就会开启这个端口的服务;其语法如下:
1 |
|
在Dockerfile中写入这样的声明有两个好处,一个是帮助镜像使用者理解这个镜像服务的守护端口,以方便配置映射;另一个好处则是在运行时使用随机端口映射时,也就是 docker run -P 时,会自动随机映射 EXPOSE 的端口。
ENV指令
用于设置环境变量,其语法有两种,支持多种变量的设置:
1 |
|
这里的环境变量无论是后面的指令,如RUN
指令,还是运行时
的应用,都可以直接使用环境变量:
1 |
|
这里定义了环境变量NODE_VERSION
,后面的RUN指令中可以多次使用变量进行操作;因此如果我们后续想要升级node版本,只需要更新7.2.0
即可
ARG指令
和ENV一样,也是设置环境变量的,所不同的是,ARG设置的是构建环境的环境变量,在以后容器运行时是不会存在的。
当我们在本地开发完一个前端项目后,肯定要部署在服务器上让别人来进行访问页面的,一般都是让运维在服务器上配置nginx来将我们的项目打包后作为静态资源;在深入Nginx一文中,我们介绍了使用nginx如何来做静态服务器,这里我们自己配置nginx文件,结合docker来部署我们的项目。
首先在我们项目目录创建nginx的配置文件default.conf
:
1 |
|
该配置文件定义了我们打包后静态资源的目录为/usr/share/nginx/html
,因此我们需要将dist文件夹拷贝到该目录;同时使用了try_files
来匹配vue的history路由模式。
在项目目录再创建一个Dockerfile文件,写入以下内容:
1 |
|
我们在项目打包生成dist文件后就可以构建镜像了:
1 |
|
接下来基于该镜像启动我们的服务器:
1 |
|
这样我们的程序就起来了,访问http://localhost:8080
端口就可以看到我们部署的网站了。
我们还有一些node项目,比如expree、eggjs或者nuxt,也可以使用docker进行部署,不过我们需要把所有的项目文件都拷贝到镜像中去。
首先我们模拟一个简单的express的入口文件app.js
1 |
|
由于下面需要拷贝整个项目的文件,因此我们可以通过.dockerignore
文件来忽略某些文件:
1 |
|
然后编写我们的Dockerfile
:
1 |
|
我们看到上面的流程是先拷贝package*.json文件,安装依赖后再拷贝整个项目,那么为什么这么做呢?聪明的童鞋大概已经猜到了,大概率又双叒叕是跟docker的分层构建和缓存有关。
不错,如果我们把package*.json和代码程序一起拷贝,如果我们只更改了代码而没有新增依赖,但docker仍然会安装依赖;但是我们如果把它单独拿出来的话,就能够提高缓存的命中率。后面的构建镜像和启动容器也就不再赘述了。
上面我们在vue项目中手动打包
生成了dist文件,然后再通过docker进行部署;在FROM指令中我们也提到了多阶段构建,那么来看下如果使用多阶段构建如何来进行优化。
我们还是在项目中准备好nginx的配置文件default.conf
,但是这次我们不再手动生成dist文件,而是将构建的过程放到Dockerfile中:
1 |
|
我们看到在上面第一个compile
阶段,我们通过npm run build命令生成了dist文件;而第二个阶段中再把dist文件拷贝到nginx的文件夹中即可;最后构建的产物依然是最后FROM指令的nginx服务器。
多阶段构建用到的命令比较多,很多童鞋会想最后的镜像会不会很大;我们通过ls命令
查看构建后的镜像:
1 |
|
可以看到它的大小和单独用nginx构建差不多。
]]> Docker
是一个开源的引擎,可以轻松的为任何应用创建一个轻量级的、可移植的、自给自足的容器。
Docker的英文翻译是码头工人,码头工人一般搬运的都是集装箱(Container),集装箱最大的成功在于其产品的标准化以及由此建立的一整套运输体系。在一艘几十万吨的巨轮上,装满了各种各样满载的集装箱,彼此之间不会相互影响;因此其本身就有标准化
、集约化
的特性。
从Docker的logo我们也能看出,Docker的思想来自于集装箱;各个应用程序相当于不同的集装箱
,每个应用程序有着不同的应用环境,比如python开发的应用需要服务器部署一套python的开发环境,nodejs开发的应用需要服务器部署nodejs的环境,不同环境之间有可能还会彼此冲突,Docker可以帮助我们隔离不同的环境。
有些同学于是就想到了,这不是虚拟机干的活么。是的,虚拟机可以很好的帮我们隔离各个环境,我们可以在windows上运行macos、ubuntu等虚拟机,也可以在macos上安装windows的虚拟机;不过传统的虚拟机技术是虚拟一整套硬件后,在其上运行完整的操作系统,在该系统上再运行所需应用进程,这样导致一台电脑只能运行数量较少的虚拟机。
但是Docker使用的容器技术比虚拟机更加的轻便和快捷。容器内的应用进程直接运行于宿主的内核,容器内没有自己的内核,而且也没有进行硬件虚拟。因此容器要比传统虚拟机更为轻便,下图比较了两者的区别:
对比总结:
特性 | 容器 | 虚拟机 |
---|---|---|
启动 | 秒级 | 分钟级 |
硬盘使用 | 一般为 MB | 一般为 GB |
系统资源 | 0~5% | 5~15% |
性能 | 接近原生 | 弱于原生 |
系统支持量 | 单机支持上千个容器 | 一般几十个 |
docker有以下优势:
docker通常用于如下场景:
在Docker中有三个基本概念:
理解了Docker的基本概念,我们就理解了Docker的整个生命周期。
首先我们来弄懂镜像
的概念,Docker镜像是一个特殊的文件系统,除了提供容器运行时所需的程序、库、资源、配置等文件外,还包含了一些为运行时准备的一些配置参数(如匿名卷、环境变量、用户等)。镜像不包含任何动态数据,其内容在构建之后也不会被改变。
如果有装系统经验的童鞋,可以把Docker镜像理解为一个操作系统的镜像(ISO文件),它是一个固定的文件,从一个镜像中,我们可以装到很多电脑上,变成一个个的操作系统(相当于容器),每个系统都是相同的,不过可以选择定制化安装。
和系统镜像不同的是,Docker镜像并不是像ISO文件那样整体打包成一个文件的,而是设计成了分层存储
的架构,它并不是由一个文件组成,而是由多层文件联合组成。
构建镜像时,会一层层的构建,前面一层是后面一层的基础。每一层构建完就不会再发生改变,后一层上的任何改变只发生在自己这一层。
其次是容器
的概念,从编程的角度看,镜像和容器的关系更像是类和实例的关系;从一个镜像可以启动一个或者多个容器;镜像是静态的定义,容器是镜像运行时的实体。容器可以被创建、启动、停止、删除、暂停等。
前面讲过镜像使用的是分层存储,容器也是如此。每一个容器运行时,是以镜像为基础层,在其上创建一个当前容器的存储层,我们可以称这个为容器运行时读写而准备的存储层为容器存储层
。
容器存储层的生存周期和容器一样,容器消亡时,容器存储层也随之消亡。因此,任何保存于容器存储层的信息都会随容器删除而丢失。
因此容器不应该向其存储层内写入任何数据,容器存储层要保持无状态化。所有的文件写入操作,都应该使用数据卷(Volume)、或者绑定宿主目录,在这些位置的读写会跳过容器存储层。
最后是仓库(Repository)
的概念,我们构建一个镜像后,可以在本地运行,但是如果我们想要给网络上的其他用户使用,就要一个集中存储和分发镜像的服务器,仓库就是这样一个工具,有点类似Github。
镜像仓库Repository是同一类镜像的集合,包含了不同tag(标签)的Docker镜像,比如ubuntu
是仓库的名称,它里面有不同的tag,比如16.04
、18.04
,我们从镜像仓库中来获取镜像时可以通过<仓库名>:<标签>
的格式来指定具体版本的镜像,比如ubuntu18.04
;如果忽略标签,用latest
作为默认标签。
我们上面介绍过,镜像是Docker的三个基本组件之一;运行容器需要本地有相应的镜像,如果没有会从远程仓库下载;那么我们来看下如何操作镜像。
我们可以从Docker Hub
来搜索镜像
1 |
|
查找结果:
查找的列表中包含了以下几个字段:
我们要获取镜像,可以通过docker pull
命令,它的格式如下:
1 |
|
还是以ubuntu为例:
1 |
|
我们看到最后一行docker.io
显示这是从官方仓库拉取的。
从下载过程我们可以看出我们上面说的分层存储的概念,即镜像是由多层存储构成;下载也是一层层的去下载,而不是单独一个文件;因此如果下载中有某个层已经被其他镜像下载过,则会显示Already exists
。下载过程中给出了每一层的ID的前12位,下载结束后给出镜像完整的sha256
摘要。
Docker的镜像仓库分为官方仓库和非官方,官方的镜像就是从Docker Hub
拉取的;如果想要从第三方的镜像仓库获取,可以在仓库名称前加上仓库的服务地址:
1 |
|
通过下面的命令,我们可以列出本地已经下载的镜像:
1 |
|
运行命令出现以下列表:
列表包含了仓库名
、标签
、镜像ID
、创建时间
和所占用空间
;我们看到有两个mongo
的镜像,不过两个镜像有不同的标签。
ls命令
默认会列出所有的镜像,但是当本地镜像比较多的时候不方便查看,有时候我们希望列出部分的镜像,除了可以通过linux的grep命令,还可以在ls命令
后面跟上参数:
1 |
|
我们可以通过rm命令
删除本地镜像:
1 |
|
或者简写为rmi
命令:
1 |
|
这里的<镜像>
,可以是镜像短ID
、镜像长ID
、镜像名
或者镜像摘要
;docker image ls
列出来的已经是短ID了,我们还可以取前三个字符进行删除;比如我们想要删除上面的mongo:4.0
:
1 |
|
除了使用官方的镜像,我们可以构建自己的镜像;一般都在其他的镜像基础上进行构建,比如node、nginx等;构建镜像需要用到Dockerfile
,它是一个文本文件,文本内容包含了一条条构建镜像所需的指令和说明。
我们在一个空白目录新建一个Dockerfile
:
1 |
|
我们向Dockerfile
写入以下内容:
1 |
|
这里的Dockerfile很简单,就两个命令:FROM和RUN,我们后面会对Dockerfile的命令进行详细介绍;我们使用build
命令构建镜像,它的格式为:
1 |
|
因此,我们在Dockerfile所在的目录执行命令:
1 |
|
运行命令,我们看到镜像也是按照Dockerfile里面的步骤,分层进行构建的:
构建成功后,我们列出所有的镜像就能看到刚刚构建的mynginx了。在上面的命令中,我们发现最后有一个.
,它表示了当前目录,如果不写这个目录会报错提示;如果对应上面的格式,它其实就是上下文路径
,那这个上下文路径是做什么用的呢?要理解这个路径的作用,我们首先要来理解Docker的架构。
Docker是一个典型的C/S架构的应用,它可以分为Docker客户端
(平时敲的Docker命令)和Docker服务端
(Docker守护进程)。Docker客户端通过REST API和服务端进行交互,docker客户端每发送一条指令,底层都会转化成REST API调用的形式发送给服务端,服务端处理客户端发送的请求并给出响应。
因此表面上看我们好像在本机上执行各种Docker的功能,实际上都是都是在Docker服务端完成的,包括Docker镜像的构建、容器创建、容器运行等工作都是Docker服务端来完成的,Docker客户端只是承担发送指令的角色。
理解了Docker的架构就很容器理解Docker构建镜像的工作原理了,它的流程大致如下:
因此上下文路径本质上就是指定服务端上Dockerfile中指令工作的目录;比如我们在Dockerfile中经常需要拷贝代码到镜像中去,因此会这么写:
1 |
|
这里要复制的package.json文件,并不一定在docker build命令执行的目录下,也不一定是在Dockerfile文件同级目录下,而是docker build命令指定的上下文路径
目录下的package.json。
介绍了镜像,我们到了Docker第三个核心概念了:容器。容器是镜像的运行时的实例,我们可以从一个镜像上启动一个或多个容器。
对容器的管理包括创建、启动、停止、进入、导入导出和删除等,我们分别来看下每个操作的具体命令以及效果。
新建并启动一个容器用的命令是docker run
,它后面有时候会带上有很长很长的选项,不过其基本的语法如下:
1 |
|
它可以带上一些常见的选项:
我们创建一个hello world容器:
1 |
|
但是这样创建的容器只能看到一堆的打印说明,我们不能对容器进行任何操作,我们可以加上-it
选项(-i和-t的简写),来让Docker分配一个终端给这个容器:
1 |
|
我们可以在容器内部进行操作了,退出终端可以使用exit
命令或者ctrl+d
;我们退出容器后如果查看运行中的容器,发现并没有任何容器信息。
一般我们都是需要让容器在后台运行,因此我们加上-d
:
1 |
|
容器不再以命令行的方式呈现了,而是直接丢出一长串的数字字母组合,这是容器的唯一id;再用ps
命令查看运行状态的容器,看到我们的容器已经在后台默默运行了:
当使用run
命令创建容器时,Docker在后台进行了如下的操作:
我们可以使用stop命令来终止容器的运行;如果容器中的应用终结或者报错时,容器也会自动终止;我们可以使用ps
命令查看到的容器短id来终止对应的容器:
1 |
|
对于终止状态的容器,ps
命令已经不能看到它了,我们可以加上-a
选项(表示所有)来查看,它的STATUS已经变成了Exited
:
1 |
|
终止状态的容器我们可以使用docker start [容器id]
来让它重新进入启动状态,运行中的容器我们也可以使用docker restart [容器id]
让它重新启动。
有时候我们会需要进入容器进行一些操作,比如进入nginx容器进行平滑重启,我们可以使用docker attach
或者docker exec
进入,不过推荐使用exec
命令。
我们首先看下如果使用attach
命令:
1 |
|
当我们从终端exit后,整个容器会停止;而使用exec
命令不会导致容器停止。
如果只使用-i
参数,由于没有分配伪终端,界面没有我们熟悉的Linux命令提示符,但是执行命令仍然可以看到运行结果;当使用-i
和-t
参数时,才能看到我们常见的Linux命令提示符。
1 |
|
需要注意的是,我们进入的容器需要是运行状态,如果不是运行状态,则会报错:
1 |
|
我们经常需要对容器运行过程进行一些监测,查看它的运行过程记录的日志情况,以及是否报错等等;使用logs
命令获取容器的日志。
1 |
|
它还支持以下几个参数:
logs
命令会展示从容器启动以来的所有日志,如果容器运行时间久,会列出非常多的日志,我们可以加tail
参数仅展示最新的日志记录:
1 |
|
对于已经创建的容器,我们可以使用inspect
来查看容器的底层基础信息,包括容器的id、创建时间、运行状态、启动参数、目录挂载、网路配置等等;另外,该命令也能查看docker镜像的信息,它的格式如下:
1 |
|
inspect支持以下选项:
运行后,会通过JSON格式显示容器的基本信息:
但是这么大段的文字,我们想要获取对我们有用的信息十分的麻烦;除了用grep进行过滤外(万物皆可grep),我们还可以通过-f
参数:
1 |
|
如果一个容器我们不想再使用了,可以使用rm
命令来删除:
1 |
|
如果要删除一个运行中的容器,可以添加-f
参数:
1 |
|
我们上面介绍到容器是保持无状态化的,就是随用随删
,并不会保留数据记录;在使用docker的时候,经常会用到一些需要保留数据的容器,比如mysql、mongodb,往往需要对容器中的数据进行持久化;或者在多个容器之间进行数据共享,这就涉及到了容器的数据管理,主要有两种方式:
数据卷
是一个可供一个或多个容器使用的特殊目录,它绕过 UFS,可以提供很多有用的特性:
首先我们创建一个数据卷:
1 |
|
通过ls
可以列出我们本地所有的数据卷:
1 |
|
inspect
命令也可以查看我们数据卷的具体信息:
1 |
|
在启动容器时,使用--mount
将数据卷挂载在容器的目录里(可以有多个挂载点):
1 |
|
我们借助上面的inspect命令查看容器的挂载信息:
1 |
|
数据卷
是被设计用来持久化数据的,它的生命周期独立于容器;因此即使我们将容器删除了,数据卷的数据依然还是存在的,并且也不存在垃圾回收这样的机制来处理没有任何容器引用的数据卷
,我们可以在删除容器的时候使用docker rm -v
命令同时删除数据卷,或者手动来删除:
1 |
|
无主的数据卷可能会占据很多空间,要清理请使用以下命令(谨慎使用!):
1 |
|
我们发现上面数据卷
挂载的目录都是在docker的安装路径下,不利于我们进行维护,我们可以直接挂载自定义的目录。
1 |
|
挂载的本地目录的路径必须是
绝对路径
,不能是相对路径。
本地路径如果不存在,会自动生成。
默认挂载的主机目录的默认权限是读写
,可以通过增加readonly
指定为只读
。
1 |
|
加readonly后,如果我们在容器内的/usr/share/nginx/html
目录下修改文件或者新建文件就会报错。
需要注意的是,如果我们挂载本地目录,需要保证挂载的目录下面有程序运行所需要的文件,比如这里nginx容器需要在我们在本地目录/home/nginx下有index.html文件,如果没有的话会报403错误。
本章完结。
]]>前端模块化作为前端必备的一个技能,已经在前端开发中不可或缺;而模块化带来项目的规模不断变大,项目的依赖越来越多;随着项目的增多,如果每个模块都通过手动拷贝的方式无异于饮鸩止渴,我们可以把功能相似的模块或组件抽取到一个npm包中;然后上传到私有npm服务器,不断迭代npm包来更新管理所有项目的依赖。
首先我们来了解一下实现一个npm包需要包含哪些内容。
通常,我们把打包好的一些模块文件放在一个目录下,便于统一进行加载;是的,npm包也是需要进行打包的,虽然也能直接写npm包模块的代码(并不推荐),但我们经常会在项目中用到typescript、babel、eslint、代码压缩等等功能,因此我们也需要对npm包进行打包后再进行发布。
在深入对比Webpack、Parcel、Rollup打包工具中,我们总结了,rollup相比于webpack更适合打包一些第三方的类库,因此本文主要通过rollup来进行打包。
随着npm包越来越多,而且包名也只能是唯一的,如果一个名字被别人占了,那你就不能再使用这个名字;假设我想要开发一个utils包,但是张三已经发布了一个utils包,那我的包名就不能叫utils了;此时我们可以加一些连接符或者其他的字符进行区分,但是这样就会让包名不具备可读性。
在npm的包管理系统中,有一种scoped packages
机制,用于将一些npm包以@scope/package
的命名形式集中在一个命名空间下面,实现域级的包管理。
域级包不仅不用担心会和别人的包名重复,同时也能对功能类似的包进行统一的划分和管理;比如我们用vue脚手架搭建的项目,里面就有@vue/cli-plugin-babel
、@vue/cli-plugin-eslint
等等域级包。
我们在初始化项目时可以使用命令行来添加scope:
1 |
|
相同域级范围内的包会被安装在相同的文件路径下,比如node_modules/@username/
,可以包含任意数量的作用域包;安装域级包也需要指明其作用域范围:
1 |
|
在代码中引入时同样也需要作用域范围:
1 |
|
在npm包中的package.json
文件,我们经常会看到main
、jsnext:main
、module
、browser
等字段,那么这些字段都代表了什么意思呢?其实这跟npm包的工作环境
有关系,我们知道,npm包分为以下几种类型的包:
假如我们现在开发一个npm包,既要支持浏览器端,也要支持服务器端(比如axios、lodash等),需要在不同的环境下加载npm包的不同入口文件,只通过一个字段已经不能满足需求。
首先我们来看下main
字段,它是nodejs默认文件入口, 支持最广泛,主要使用在引用某个依赖包的时候需要此属性的支持;如果不使用main
字段的话,我们可能需要这样来引用依赖:
1 |
|
所以它的作用是来告诉打包工具,npm包的入口文件是哪个,打包时让打包工具引入哪个文件;这里的文件一般是commonjs(cjs)模块化的。
有一些打包工具,例如webpack或rollup,本身就能直接处理import导入的esm模块,那么我们可以将模块文件打包成esm模块,然后指定module
字段;由包的使用者来决定如何引用。
jsnext:main
和module
字段的意义是一样的,都可以指定esm模块的文件;但是jsnext:main是社区约定的字段,并非官方,而module则是官方约定字段,因此我们经常将两个字段同时使用。
在Webpack配置全解析中我们介绍到,mainFields
就是webpack用来解析模块的,默认会按照顺序解析browser、module、main字段。
有时候我们还想要写一个同时能够跑在浏览器端和服务器端的npm包(比如axios),但是两者在运行环境上还是有着细微的区别,比如浏览器请求数据用的是XMLHttpRequest,而服务器端则是http或者https;那么我们要怎样来区分不同的环境呢?
除了我们可以在代码中对环境参数进行判断(比如判断XMLHttpRequest是否为undefined),也可以使用browser
字段,在浏览器环境来替换main字段。browser的用法有以下两种,如果browser为单个的字符串,则替换main成为浏览器环境的入口文件,一般是umd模块的:
1 |
|
browser还可以是一个对象,来声明要替换或者忽略的文件;这种形式比较适合替换部分文件,不需要创建新的入口。key是要替换的module或者文件名,右侧是替换的新的文件,比如在axios的packages.json中就用到了这种替换:
1 |
|
打包工具在打包到浏览器环境时,会将引入来自./lib/adapters/http.js
的文件内容替换成./lib/adapters/xhr.js
的内容。
在有一些包中我们还会看到types
字段,指向types/index.d.ts
文件,这个字段是用来包含了这个npm包的变量和函数的类型信息;比如我们在使用lodash-es
包的时候,有一些函数的名称想不起来了,只记得大概的名字;比如输入fi就能自动在编译器中联想出fill或者findIndex等函数名称,这就为包的使用者提供了极大的便利,不需要去查看包的内容就能了解其导出的参数名称,为用户提供了更加好的IDE支持。
在npm包中,我们可以选择哪些文件发布到服务器中,比如只发布压缩后的代码,而过滤源代码;我们可以通过配置文件来进行指定,可以分为以下几种情况:
.npmignore
文件,以.npmignore
文件为准,在文件中的内容都会被忽略,不会上传;即使有.gitignore
文件,也不会生效。.npmignore
文件,以.gitignore
文件为准,一般是无关内容,例如.vscode等环境配置相关的。.npmignore
也不存在.gitignore
,所有文件都会上传。package.json
中存在files字段,可以理解为files为白名单。 ignore相当于黑名单,files字段就是白名单,那么当两者内容冲突时,以谁为准呢?答案是files
为准,它的优先级最高。
我们可以通过npm pack
命令进行本地模拟打包测试,在项目根目录下就会生成一个tgz的压缩包,这就是将要上传的文件内容。
在package.json文件中,所有的依赖包都会在dependencies和devDependencies字段中进行配置管理:
dependencies
字段指定了项目上线后运行所依赖的模块,可以理解为我们的项目在生产环境运行中要用到的东西;比如vue、jquery、axios等,项目上线后还是要继续使用的依赖。
devDependencies
字段指定了项目开发所需要的模块,开发环境会用到的东西;比如webpack、eslint等等,我们打包的时候会用到,但是项目上线运行时就不需要了,所以放到devDependencies中去就好了。
除了dependencies和devDependencies字段,我们在一些npm包中还会看到peerDependencies
字段,没有写过npm插件的童鞋可能会对这个字段比较陌生,它和上面两个依赖有什么区别呢?
假设我们的项目MyProject,有一个依赖PackageA,它的package.json中又指定了对PackageB的依赖,因此我们的项目结构是这样的:
1 |
|
那么我们在MyProject中是可以直接引用PackageA的依赖的,但如果我们想直接使用PackageB,那对不起,是不行的;即使PackageB已经被安装了,但是node只会在MyProject/node_modules
目录下查找PackageB。
为了解决这样问题,peerDependencies
字段就被引入了,通俗的解释就是:如果你安装了我,你最好也安装以下依赖。比如上面如果我们在PackageA的package.json中加入下面代码:
1 |
|
这样如果你安装了PackageA,那会自动安装PackageB,会形成如下的目录结构:
1 |
|
我们在MyProject项目中就能愉快的使用PackageA和PackageB两个依赖了。
比如,我们熟悉的element-plus组件库,它本身不可能单独运行,必须依赖于vue3环境才能运行;因此在它的package.json中我们看到它对宿主环境的要求:
1 |
|
这样我们看到它在组件中引入的vue的依赖,其实都是宿主环境提供的vue3依赖:
1 |
|
license
字段使我们可以定义适用于package.json
所描述代码的许可证。同样,在将项目发布到npm注册时,这非常重要,因为许可证可能会限制某些开发人员或组织对软件的使用。拥有清晰的许可证有助于明确定义该软件可以使用的术语。
借用知乎上Max Law的一张图来解释所有的许可证:
npm包的版本号也是有规范要求的,通用的就是遵循semver语义化版本规范,版本格式为:major.minor.patch,每个字母代表的含义如下:
先行版本号是加到修订号的后面,作为版本号的延伸;当要发行大版本或核心功能时,但不能保证这个版本完全正常,就要先发一个先行版本。
先行版本号的格式是在修订版本号后面加上一个连接号(-),再加上一连串以点(.)分割的标识符,标识符可以由英文、数字和连接号([0-9A-Za-z-])组成。例如:
1 |
|
常见的先行版本号有:
每个npm包的版本号都是唯一的,我们每次更新npm包后,都是需要更新版本号,否则会报错提醒:
当主版本号升级后,次版本号和修订号需要重置为0,次版本号进行升级后,修订版本需要重置为0。
但是如果每次都要手动来更新版本号,那可就太麻烦了;那么是否有命令行能来自动更新版本号呢?由于版本号的确定依赖于内容决定的主观性的动作,因此不能完全做到全自动化更新,谁知道你是改了大版本还是小版本,因此只能通过命令行实现半自动操作;命令的取值和语义化的版本是对应的,会在相应的版本上加1:
在package.json的一些依赖的版本号中,我们还会看到^
、~
或者>=
这样的标识符,或者不带标识符的,这都代表什么意思呢?
~
:固定主版本号和次版本号,修订号可以随意更改,例如~2.0.0
,可以使用 2.0.0、2.0.2 、2.0.9 的版本。^
:固定主版本号,次版本号和修订号可以随意更改,例如^2.0.0
,可以使用 2.0.1、2.2.2 、2.9.9 的版本。 通过上面对package.json
的介绍,相信各位小伙伴已经对npm包有了一定的了解,现在我们就进入代码实操阶段,开发并上传一个npm包。
相信不少童鞋在业务开发时都会遇到重复的功能,或者开发相同的工具函数,每次遇到时都要去其他项目中拷贝代码;如果一个项目的代码逻辑有优化的地方,需要同步到其他项目,则需要再次挨个项目的拷贝代码,这样不仅费时费力,而且还重复造轮子。
我们可以整合各个项目的需求,开发一个适合自己项目的工具类的npm包,包的结构如下:
1 |
|
首先看下package.json
的配置,rollup根据开发环境区分不同的配置:
1 |
|
然后配置rollup的base config
文件:
1 |
|
这里我们将打包成commonjs、esm和umd三种模块规范的包,然后是生产环境的配置,加入terser和filesize分别进行压缩和查看打包大小:
1 |
|
然后是开发环境的配置:
1 |
|
环境配置好后,我们就可以打包了
1 |
|
还有一类npm包比较特殊,是通过npm i -g [pkg]
进行全局安装的,比如常用的vue create
、static-server
、pm2
等命令,都是通过全局命令安装的;那么全局npm包如何开发呢?
我们来实现一个全局命令的计算器功能,新建一个项目然后运行:
1 |
|
在package.json中添加bin
属性,它是一个对象,键名是告诉node在全局定义一个全局的命令,值则是执行命令的脚本文件路径,可以同时定义多个命令,这里我们定义一个calc命令
:
1 |
|
命令定义好了,我们来实现calc.js中的内容:
1 |
|
需要注意的是,文件头部的#!/usr/bin/env node
是必须的,告诉node这是一个可执行的js文件,如果不写会报错;然后通过process.argv.slice(2)
来获取执行命令的参数,前两个参数分别是node的运行路径和可执行脚本的运行路径,第三个参数开始才是命令行的参数,因此我们在命令行运行来看结果:
1 |
|
如果我们的脚本比较复杂,想调试一下脚本,那么每次都需要发布到npm服务器,然后全局安装后才能测试,这样比较费时费力,那么有没有什么方法能够直接运行脚本呢?这里就要用到npm link命令
,它的作用是将调试的npm模块链接到对应的运行项目中去,我们也可以通过这个命令把模块链接到全局。
在我们的项目中运行命令:
1 |
|
可以看到全局npm目录下新增了calc文件,calc命令就指向了本地项目下的calc.js文件,然后我们就可以尽情的运行调试;调试完成后,我们又不需要将命令指向本地项目了,这个时候就需要下面的命令进行解绑操作
:
1 |
|
解绑后npm会把全局的calc文件删除,这时候我们就可以去发布npm包然后进行真正的全局安装了。
在Vue项目中,我们在很多项目中也会用到公共组件,可以将这些组件提取到组件库,我们可以仿照element-ui来实现一个我们自己的ui组件库;首先来构建我们的项目目录:
1 |
|
我们构建MyButton和MyInput两个组件,vue文件和scss不再赘述,我们来看下导出组件的index.js:
1 |
|
组件导出后在main.js
中统一组件注册:
1 |
|
然后配置rollup.config.js:
1 |
|
这里我们打包出commonjs和iife两个模块规范,一个可以配合打包工具使用,另一个可以直接在浏览器中script引入。我们通过rollup-plugin-vue
插件来解析vue文件,需要注意的是5.x版本解析vue2,最新的6.x版本解析vue3,默认安装6.x版本;如果我们使用的是vue2,则需要切换老版本的插件,还需要安装以下vue的编译器:
1 |
|
打包成功后我们就能看到lib
目录下的文件了,我们就能像element-ui一样,愉快的使用自己的ui组件了,在项目中引入我们的UI:
1 |
|
如果想要在本地进行调试,也可以使用link
命令创建链接,首先在my-ui目录下运行npm link
将组件挂载到全局,然后在vue项目中运行下面命令来引入全局的my-ui:
1 |
|
我们会看到下面的输出表示vue项目中my-ui模块已经链接到my-ui项目了:
1 |
|
我们的npm包完成后就可以准备发布了,首先我们需要准备一个账号,可以使用--registry
来指定npm服务器,或者直接使用nrm来管理:
1 |
|
然后进行登录,输入你注册的账号密码邮箱:
1 |
|
还可以用下面命令退出当前账号
1 |
|
如果不知道当前登录的账号可以用who命令查看身份:
1 |
|
登录成功就可以将我们的包推送到服务器上去了,执行下面命令,会看到一堆的npm notice:
1 |
|
如果某版本的包有问题,我们还可以将其撤回
1 |
|
Chrome插件,官方名称extensions(扩展程序);为了方便理解,以下都称为Chrome插件,或者简称插件,那么什么是Chrome插件呢?
扩展程序是自定义浏览体验的小型软件程序。它们让用户可以通过多种方式定制
Chrome
的功能和行为。
插件程序提供了以下几个功能:
我们可以通过点击更多工具 -> 扩展程序
来查看我们所有安装的插件,或者直接打开插件标签页。
那么学习开发插件有什么意义呢?我们为什么要来学习插件开发呢?个人总结下,有以下几方面的意义:
大多数Chrome用户从Chrome网上应用店获得插件程序。世界各地的开发人员会在Chrome网上应用店中发布他们的插件,经过Chrome的审查并向最终用户提供。
但是由于一些众所周知的原因,我们并不能访问网上应用店,但同时Chrome又要求插件必须从它的Chrome应用商店下载安装,这仿佛是一个绕不开的死循环,不过俗话说魔高一尺道高一尺一
,下面我们会讲解如何从本地加载插件,绕开网上应用店的限制。
插件是基于Web技术构建的,例如HTML、JavaScript和CSS。它们在单独的沙盒执行环境中运行并与Chrome浏览器进行交互。
插件允许我们通过使用API修改浏览器行为和访问Web内容来扩展和增强浏览器的功能。插件通过最终用户UI和开发人员API进行操作:
要创建插件程序,我们需要组合构成插件程序的一些资源清单,例如JS文件和HTML文件、图像等。对于开发和测试,可以使用扩展开发者模式将这些“解压”加载到Chrome中。如果我们对自己开发出来的插件程序感到满意,就可以通过网上商店将其打包并分享给其他的用户。
我们编写的插件想要发布到Chrome网上应用店
中,就必须遵守网上应用店政策,它规定了以下几点:
Chrome插件并没有很严格的项目结构要求,比如src、public、components等等,因此我们如果去看很多插件的源码,会发现每个插件的项目结构,甚至项目下的文件名称都大相径庭;但是在根目录下我们都会找到一个manifest.json
文件,这是插件的配置文件,说明了插件的各种信息;它的作用等同于小程序的app.json和前端项目的package.json。
我们在项目中创建一个最简单的manifest.json
配置文件:
1 |
|
我们经常会点击右上角插件图标时弹出一个小窗口的页面,焦点离开时就关闭了,一般做一些临时性的交互操作;在配置文件中新增browser_action
字段,配置popup弹框:
1 |
|
然后创建我们的弹框页面popup.html:
1 |
|
点击图标后,插件显示popup.html。
为了用户方便点击,我们还可以在manifest.json中设置一个键盘快捷键的命令,通过快捷键来弹出popup页面:
1 |
|
这样我们的插件就可以通过按键盘上的Ctrl+Shift+F
来弹出。
我们开发的插件需要在浏览器里面运行,打开插件标签页
,打开开发者模式
,点击加载已解压的扩展程序
,选择项目文件夹,就可将开发中的插件加载进来。
开发中更改了代码,点击插件右下角刷新按钮即可重新加载
如果我们的代码中有错误,加载插件后,会显示红色的错误按钮
:
点击错误按钮以查看错误的日志:
我们上面说过Chrome插件只能从网上应用店中下载安装,但是第三方平台也提供了下载的渠道,下载下来的文件后缀是.crx
的压缩包,现在的问题就是如何将crx文件进行安装了。
从Chrome 73版本开始,谷歌修改了插件策略,不可以随意安装crx文件:如果直接将crx文件拖拽安装可能会提示一下报错:
1 |
|
我们可以尝试以下几种方法,第一种方法:将crx后缀改为zip,解压后加载已解压的扩展程序
的方式,将插件用开发者模式进行加载。
第二种办法,通过Chrome插件伴侣
,将crx提取到桌面,然后还是用开发者模式进行加载。
使用插件伴侣提取插件后,插件内容默认会被放在你的电脑桌面上,可以把它剪切/复制到任意位置;加载插件选择的文件夹路径时,一定要包含manifest.json文件;加载后请勿删除提取的文件夹。
第三种方法就是用梯子了,直接去网上应用店下载,没有梯子的同学可以使用插件伴侣进行代理,插件伴侣的获取方式下文会给出。
我们的插件安装后,popup页面也运行了;但是我们也发现了,popup页面只能做临时性的交互操作,用完就关了,不能存储信息或者和其他标签页进行交互等等;这时就需要用到background(后台),它是一个常驻的页面,它的生命周期是插件中所有类型页面中最长的;它随着浏览器的打开而打开,随着浏览器的关闭而关闭,所以通常把需要一直运行的、启动就运行的、全局的代码放在background里面。
background也是需要在manifest.json
中进行配置,可以通过page
指定一张网页,或者通过scripts
直接指定一个js数组,Chrome会自动为js生成默认网页:
1 |
|
需要注意的是,page属性和scripts属性只需要配置一个即可,如果两个同时配置,则会报以下错误信息:
1 |
|
我们给background设置一个监听事件,当插件安装时打印日志:
1 |
|
点击查看视图
旁边的背景页
,看到我们设置的background:
我们在插件安装时在storage中设置一个值,这将允许多个插件组件访问该值并进行更新操作:
1 |
|
chrome.declarativeContent
用于精确地控制什么时候显示我们的页面按钮,或者需要在用户单击它之前更改它的外观以匹配当前标签页。
这里调用的chrome.storage和我们常用的localStorage和sessionStorage不是一个东西;由于调用到了storage和declarativeContent的API,因此我们需要在manifest中给插件注册使用的权限:
1 |
|
再次查看背景页的视图,我们就能看到打印的日志了;既然可以存储,那也能取出来,我们在popup中添加事件进行获取,首先我们新增一个触发的button:
1 |
|
我们再创建一个popup.js
的文件,用来从storage存储中拿到颜色值,并将此颜色作为按钮的背景色:
1 |
|
如果需要调试popup页面,可以在弹框中右击 => 检查,在DevTools中进行调试查看。
我们多次打开popup页面,发现页面每次点开按钮都会恢复最开始的默认状态。
现在,我们获取到了storage中的值,需要逻辑来进一步与用户交互;更新popup.js中的交互代码:
1 |
|
chrome.tabs
的API主要是和浏览器的标签页进行交互,通过query
找到当前的激活中的tab,然后使用executeScript
向标签页注入脚本内容。
manifest同样需要activeTab
的权限,来允许我们的插件使用tabs
的API。
1 |
|
重新加载插件,我们点击按钮,会发现当前页面的背景颜色已经变成storage中设置的色值了;但是某些用户可能希望使用不同的色值,我们给用户提供选择的机会。
现在我们的插件功能还比较单一,只能让用户选择唯一的颜色;我们可以在插件中加入选项页面,以便用户更好的自定义插件的功能。
在程序目录新增一个options.html文件:
1 |
|
然后添加选择页面的逻辑代码options.js
:
1 |
|
上面代码中预设了四个颜色选项,通过onclick事件监听,生成页面上的按钮;当用户单击按钮时,将更新storage中存储的颜色值。
options页面完成后,我们可以将其在manifest的options_page
进行注册:
1 |
|
重新加载我们的插件,点击详情,滚动到底部,点击扩展程序选项
来查看选项页面。
或者可以在浏览器右上角插件图标上右击 => 选项
。
通过上面一个简单的小插件,相信大家对插件的功能和组件都有了一个大致的了解,知道了每个组件在其中发挥的作用;但这还只是插件的一小部分功能,下面我们对插件每个部分的功能以及组件做一个更深入的了解。
background是插件的事件处理程序,它包含对插件很重要的浏览器事件的监听器。background处于休眠状态,直到触发事件,然后执行指示的逻辑;一个好的background仅在需要时加载,并在空闲时卸载。
background监听的一些浏览器事件包括:
加载完成后,只要触发某个事件,background就会保持运行状态;在上面manifest中,我们还指定了一个persistent
属性:
1 |
|
persistent
属性定义了插件常驻后台的方式;当其值为true时,表示插件将一直在后台运行,无论其是否正在工作;当其值为false时,表示插件在后台按需运行,这就是Chrome后来提出的Event Page
(非持久性后台)。Event Page
是基于事件驱动运行的,只有在事件发生的时候才可以访问;这样做的目的是为了能够有效减小插件对内存的消耗,如非必要,请将persistent设置为false。
persistent属性的默认值为true
一些基于DOM页面的计时器(例如window.setTimeout或window.setInterval),如果在非持久后台休眠时进行了触发,可能不会按照预定的时间运行:
1 |
|
Chrome提供了另外的API,alarms:
1 |
|
我们知道了browser_action字段用来配置popup的页面,在其他的一些文档中还给出了page_action
字段的配置,不过page_action并不是所有的页面都能够使用;不过随着Chrome的版本更新,这两者的功能也越来越相近;在Chrome 48版本之后,page_action也从原来的地址栏中移出来,和插件放在一起;笔者在配置page_action
的时候没有发现有什么比较大的区别,因此下面以browser_action为主。
在browserAction的配置中,我们可以提供多种尺寸的图标,Chrome会选择最接近的图标并将其缩放到适当的大小来填充;如果没有提供确切的大小,这种缩放会导致图标丢失细节或看起来模糊。
1 |
|
也可以通过调用browserAction.setPopup
动态设置弹出窗口。
1 |
|
要设置提示文案,使用default_title字段,或者调用browserAction.setTitle
函数。
1 |
|
Badge(徽章)就是在图标上显示的一些文本内容,用来详细显示插件的提示信息;由于Bage的空间有限,因此最多显示4个英文字符或者2个函数;badge无法通过配置文件来指定,必须通过代码实现,设置badge文字和颜色可以分别使用browserAction.setBadgeText()
和browserAction.setBadgeBackgroundColor()
:
1 |
|
content-scripts(内容脚本)是在网页上下文中运行的文件。通过使用标准的文档对象模型(DOM),它能够读取浏览器访问的网页的详细信息,对其进行更改,并将信息传递给其父级插件。内容脚本相对于background还是有一些访问API上的限制,它可以直接访问以下chrome的API:
内容脚本运行于一个独立、隔离的环境,它不会和主页面的脚本
或者其他插件的内容脚本
发生冲突,当然也不能调用其上下文和变量。假设我们在主页面中定义了变量和函数:
1 |
|
由于隔离的机制,在内容脚本中调用add函数会报错:Uncaught ReferenceError: add is not defined。
内容脚本分为以代码方式或声明方式注入。
对于需要在特定情况下运行的代码,我们需要使用代码注入的方式;在上面的popup页面中,我们就是将内容脚本以代码的方式进行注入到页面中:
1 |
|
或者可以注入整个文件。
1 |
|
在指定页面上自动运行的内容脚本,我们可使用声明式注入的方式;以声明方式注入的脚本需注册在manifest文件的content_scripts
属性下。它们可以包括JS文件或CSS文件。
1 |
|
声明式注入除了matches必须外,还可以包含以下字段,来自定义指定页面匹配:
Name | Type | Description |
---|---|---|
exclude_matches | 字符串数组 | 可选。排除此内容脚本将被注入的页面。 |
include_globs | 字符串数组 | 可选。 在 matches 后应用,以匹配与此 glob 匹配的URL。旨在模拟 @exclude 油猴关键字。 |
exclude_globs | 字符串数组 | 可选。 在 matches 后应用,以排除与此 glob 匹配的URL。旨在模拟 @exclude 油猴关键字。 |
声明匹配URL可以使用Glob属性,Glob属性遵循更灵活的语法。可接受的Glob字符串可能包含“通配符”星号和问号的URL。星号*
匹配任意长度的字符串,包括空字符串,而问号?
匹配任何单个字符。
1 |
|
将JS文件注入网页时,还需要控制文件注入的时机,由run_at
字段控制;首选的默认字段是document_idle
,但如果需要,也可以指定为 “document_start” 或“document_end”。
1 |
|
三个字段注入的时机区别如下:
Name | Type | Description |
---|---|---|
document_idle | string | 首选。 尽可能使用 “document_idle”。浏览器选择一个时间在 “document_end” 和window.onload 事件触发后立即注入脚本。 注入的确切时间取决于文档的复杂程度以及加载所需的时间,并且已针对页面加载速度进行了优化。在 “document_idle” 上运行的内容脚本不需要监听 window.onload 事件,因此可以确保它们在 DOM 完成之后运行。如果确实需要在window.onload 之后运行脚本,则扩展可以使用 document.readyState 属性检查 onload 是否已触发。 |
document_start | string | 在 css 文件之后,但在构造其他 DOM 或运行其他脚本前注入。 |
document_end | string | 在 DOM 创建完成后,但在加载子资源(例如 images 和 frames )之前,立即注入脚本。 |
尽管内容脚本的执行环境和托管它们的页面是相互隔离的,但是它们共享对页面DOM的访问;如果内容脚本想要和插件通信,可以通过onMessage
和sendMessage
1 |
|
更多消息通信的在后面我们会进行详细的总结。
contextMenus可以自定义浏览器的右键菜单(也有叫上下文菜单的),主要是通过chrome.contextMenus
API实现;在manifest中添加权限来开启菜单权限:
1 |
|
通过icons
字段配置contextMenus菜单旁边的图标:
我们可以在background中调用contextMenus.create
来创建菜单,这个操作应该在runtime.onInstalled
监听回调执行:
1 |
|
如果我们的插件创建多个右键菜单,则Chrome会自动将其折叠为一个父菜单。
contextMenus创建对象的属性可以在附录里面找到;我们看到在title属性中有一个%s
的标识符,当contexts为selection,使用%s
来表示选中的文字;我们通过这个功能可以实现一个选中文字调用百度搜索的小功能:
1 |
|
效果如下:
contextMenus还有一些API可以调用:
1 |
|
覆盖页面(override)是一种将Chrome默认的特定页面替换为插件程序中的HTML文件。除了HTML之外,覆盖页面通常还有CSS和JS代码;插件可以替换以下Chrome的页面。
PS:像我们熟知的Momentum插件,就是覆盖了新标签页面。
需要注意的是:单个插件只能覆盖某一个页面。例如,插件程序不能同时覆盖书签管理器和历史记录页面。
在manifest进行如下配置:
1 |
|
覆盖newtab效果如下:
如果我们覆盖多个特定页面,Chrome加载插件时会直接报错:
用户在操作时,会产生一些用户数据,插件需要在本地存储这些数据,在需要调用的时候再拿出来;Chrome推荐使用chrome.storage
的API,该API经过优化,提供和localStorage相同的存储功能;不推荐直接存在localStorage
中,两者主要有以下区别:
如果要使用storage的自动同步,我们可以使用storage.sync
:
1 |
|
当Chrome离线时,Chrome会将数据存储在本地。下次浏览器在线时,Chrome会同步数据。即使用户禁用同步,storage.sync仍将工作。
不需要同步的数据可以用storage.local
进行存储:
1 |
|
如果我们想要监听storage中的数据变化,可以用onChanged
添加监听事件;每当存储中的数据发生变化时,就会触发该事件:
1 |
|
用过Vue或者React的devtools的童鞋应该见过这样新增的扩展面板:
DevTools可以为Chrome的DevTools添加功能,它可以添加新的UI面板和侧边栏,与检查的页面交互,获取有关网络请求的信息等等;它可以访问以下特定的API:
DevTools扩展的结构与任何其他扩展一样:它可以有一个背景页面、内容脚本和其他项目。此外,每个DevTools扩展都有一个DevTools页面,可以访问DevTools的API。
配置devtools不需要权限,只要在manifest中配置一个devtools.html
:
1 |
|
devtools.html中只引用了devtools.js,如果写了其他内容也不会展示:
1 |
|
在项目中新建devtools.js
:
1 |
|
这里调用create创建扩展面板,createSidebarPane创建侧边栏,每个扩展面板和侧边栏都是一个单独的HTML页面,其中可以包含其他资源(JavaScript、CSS、图像等)。
DevPanel面板是一个顶级标签,和Element、Source、Network等是同一级,在一个devtools.js可以创建多个;在Panel.html
中我们先设置2个按钮:
1 |
|
panel.js中我们使用devtools.inspectedWindow
的API来和被检查窗口进行交互:
1 |
|
eval
函数为插件提供了在被检查页面的上下文中执行JS代码的能力,而getResources
获取页面上所有加载的资源;我们找到一个页面,然后右击检查打开调试工具,发现在最右侧多了一个DevPanel
的tab页,点击我们的调试按钮,那么日志在哪里能看到呢?
我们在调试工具上右击检查,再开一个调试工具,这个就是调试工具的调试工具。。。。
最终两个调试工具的效果如下:
回到devtools.js,我们使用createSidebarPane
创建了侧边栏面板,并且设置为sidebar.html
,最终呈现在Element
面板的最右侧:
有几种方法可以在侧边栏中显示内容:
通过JS表达式,我们可以很方便进行页面查询,比如,查询页面上所有的img元素:
1 |
|
另外,我们可以通过elements.onSelectionChanged
监听事件,在Element面板选中元素更改后,更新侧边栏面板的状态;例如,可以将我们关心的一些元素的样式进行实时展示在侧边面板,方面查看:
1 |
|
Chrome提供chrome.notifications
的API来推送桌面通知;同样也需要现在manifest中注册权限:
1 |
|
在background调用创建即可
1 |
|
效果如下:
根据chrome.notifications的API,笔者做了一个喝水小助手,和我一起成为一天八杯水的人吧!
chrome.notifications支持更多的属性详见附录。
通过webRequest的API可以对浏览器发出的任何HTTP请求进行拦截、组织或者修改;可以拦截的请求还包括脚本、样式的GET请求以及图片的链接;我们也需要在manifest中配置权限才能使用API:
1 |
|
权限中还需要声明拦截请求的URL,如果你想拦截所有的URL,可以使用*://*/*
(不过不推荐这么做,数据会非常多),如果我们想以阻塞方式使用Web请求API,则需要用到webRequestBlocking
权限。
比如我们可以对拦截的请求进行取消:
1 |
|
不同组件之间经常需要进行消息通信来进行数据的传递,我们来看下他们之间是如何进行通信的:
background和popup之间的通信比较简单,在popup中,我们可以通过extension.getBackgroundPage
直接获取到background对象,直接调用对象上的方法即可:
1 |
|
而background访问popup上则通过extension.getViews
来访问,不过前提是popup弹框已经展示,否则获取到的views是空数组:
1 |
|
在background和内容脚本通信,我们可以使用简单直接的runtime.sendMessage
或者tabs.sendMessage
发送消息,消息内容可以是JSON数据
从内容脚本发送消息如下:
1 |
|
而从后台发送消息到内容脚本时,由于有多个标签页,我们需要指定发送到某个标签页:
1 |
|
而不管是在后台,还是在内容脚本中,我们都使用runtime.onMessage
监听消息的接收事件,不同的是回调函数中的sender
,标识不同的发送方:
1 |
|
上面的runtime.sendMessage
和tabs.sendMessage
都属于短链接,所谓的短连接,就是类似于HTTP请求,如果接收方不在线,就会出现请求失败的情况;但有些情况下,需要持续对话,这时候就需要用到长链接,类似于websocket,可以在通信双方之间进行持久链接。
长链接使用runtime.connect
或tabs.connect
来打开长生命周期通道,通道可以有一个名称,以便区分不同类型的连接。
1 |
|
从background向内容脚本发送消息也类似,不同之处在于需要指定连接的tab页,将runtime.connect
改为tabs.connect
。
在接收端,我们需要设置onConnect
的事件监听器,当发送端调用connect
进行连接时触发该事件,以及通过连接发送和接收消息的port
对象:
1 |
|
介绍了这么多插件的开发,我们来介绍一下应用市场上的优秀插件,这些插件能够帮助我们在平时的开发中提高生产效率。
Adblock Plus是一款可以屏蔽广告以及任何你想屏蔽元素的软件;它不仅内置了一些过滤规则,可以自动屏蔽广告,还可以自行添加屏蔽内容。
选择拦截元素,淡黄色框住的内容就是拦截的内容
Axure RP Extension for Chrome是原型设计工具Axure RP的Chrome浏览器插件,chrome浏览器打开axure生成的HTML静态文件页面预览打开如下报错,这是因为chrome浏览器没有安装Axure插件导致的。
FE助手是由国人开发的一款前端工具集合的小插件,插件功能比较全面:包括字符串编解码、代码压缩、美化、JSON格式化、正则表达式、时间转换工具、二维码生成与解码、编码规范检测、页面性能检测、页面取色、Ajax接口调试。
Momentum插件是一款自动更换壁纸,自带时钟,任务日历和工作清单的chrome浏览器插件。官方的解释就是:替换你 Chrome 浏览器默认的“标签页”。里面的图片全部来自500PX里面的高清图,无广告,无弹窗,非常适合笔记本使用,让装逼再上新台阶。让我来感受下出自细节,触及心灵的美。
在Github上查看源代码的体验十分糟糕,尤其是从一个目录跳转到另一个目录的时候,非常麻烦。Octotree是一款chrome插件,用于将Github项目代码以树形格式展示,而且在展示的列表中,我们可以下载指定的文件,而不需要下载整个项目。
Chrome浏览器很好用,这是我们不得不承认的;但是人无完人,何况一个浏览器呢?一直以来,Chrome占用内存这样的“吃相”就很让人头疼。
OneTab是一款可以在用户打开过多Chrome标签页而“不知所措”的时候点击OneTab插件一键释放Chrome标签页内存的谷歌浏览器插件,OneTab插件并不是像关闭浏览器那样直接把所有的标签页都关闭掉,它会先把现有的标签页都缓存起来,然后使用一键关闭所有标签页的功能弹出只有一个恢复窗口的新标签页,在这个OneTab插件的标签页中用户可以选择恢复其中有用的Chrome标签页而放弃其他应该关闭的标签页。
在恢复标签页的时候,OneTab插件会以新标签页的方式去恢复,所以用户可以简单地点击几次鼠标都可以把有用的标签都找出来一起恢复,当用户打开的Chrome标签页过多的时候使用OneTab插件大约能够节省用户95%的系统内存,还可以让用户在标签页变小的情况下更加清晰地关注自己应该关注的Chrome标签页。
Tampermonkey(俗称油猴)是一款免费的浏览器扩展和最为流行的用户脚本管理器。虽然有些受支持的浏览器拥有原生的用户脚本支持,但 Tampermonkey将在您的用户脚本管理方面提供更多的便利。 它提供了诸如便捷脚本安装、自动更新检查、标签中的脚本运行状况速览、内置的编辑器等众多功能, 同时Tampermonkey还有可能正常运行原本并不兼容的脚本。
通过给管理器安装各类脚本,可以让大部分 HTML 为主的网页更方便易用,比如:全速下载网盘文件、去广告、悬停显示大图、Flash/HTML5 播放器转换、阅读模式等。有点像给Chrome的插件装上插件(这里又是一个套娃)。
多年来,Google 提供了很多工具:Lighthouse, Chrome DevTools, PageSpeed Insights, Search Console’s Speed Report 等来衡量和报告性能。而其中的衡量标准都很难学习和使用,Web Vitals计划的目的就是简化场景,降低学习成本,并帮助站点关注最重要的指标
Web Vitals是Google发起的,旨在提供各种质量信号的统一指南,其可获取三个关键指标(CLS、FID、LCP):
通过在浏览器安装Web Vitals插件,我们就可以在页面加载完成后很方便的查看这三个指标的情况。
我们开发过程中经常会遇到接口跨域的问题,通过Allow CORS: Access-Control-Allow-Origin这个插件,可以允许我们在接口的响应头轻松执行跨域请求,只需要激活插件并且执行。
安装插件后,默认是不会添加跨域响应头的,点击插件弹框的C字母按钮,按钮变成橙色插件激活。
Window Resizer是一款可以设置浏览器窗口大小的Chrome扩展,用户安装了window resizer插件后可以快速调节chrome的窗口大小,用户可以将窗口调节为320x480、480x800、1024x768等大小,也可以选择自定义浏览器窗口的尺寸。
以上所有插件和工具敬请关注公众号:
前端壹读
后,回复关键字Chrome插件
即可获取。
Name | Type | Description |
---|---|---|
type | string | 显示的通知类型,”basic”, “image”, “list”, or “progress” |
title | string | 通知的标题 |
message | string | 通知的主体内容 |
contextMessage | string | 通知的备选内容 |
buttons | array of object | 最多两个通知操作按钮的文本和图标。 |
iconUrl | string | 图标的URL |
imageUrl | string | “image”类型的通知的图片的URL |
eventTime | double | 通知的时间戳,单位ms |
items | array of object | 多项目通知的项目。Mac OS X上的用户只能看到第一项。 |
progress | integer | 当前的进度,有效范围0~100 |
isClickable | boolean | 通知窗口是否响应点击事件 |
Name | Type | Description |
---|---|---|
type | string [“normal”, “checkbox”, “radio”, “separator”] | 菜单项的类型。如果没有指定则默认为’normal’(普通) |
id | string | 唯一标志符,对于事件页面来说必须存在,不能与同一扩展程序中的其它标志符相同。 |
title | string | 显示在菜单项中的文字,除非类型是’separator’(分隔符)该参数是必选的。 当上下文为’selection’(选定内容)时,您可以在字符串中使用%s来显示选中的文本。 |
checked | boolean | 单选或复选菜单项的初始状态 |
contexts | enumerated string [“all”, “page”, “frame”, “selection”, “link”, “editable”, “image”, “video”, “audio”] | 列出该菜单项将会出现在哪些上下文中,包括”all”(全部)、”page”(页面)、”frame”(框架)、”selection”(选定内容)、”link”(链接)、”editable”(可编辑内容)、”image”(图片)、”video”(视频)、”audio”(音频),如果没有指定则默认为page(页面) |
onclick | function | 菜单项单击时的回调函数 |
parentId | integer or string | 父菜单项标识符 |
documentUrlPatterns | array of string | 将该菜单项限制在URL匹配给定表达式的文档中显示 |
targetUrlPatterns | array of string | 允许您基于img/audio/video标签的src属性以及 a 标签的href属性过滤 |
enabled | boolean | 该右键菜单项是否启用或禁用 |
权限名称 | 描述 |
---|---|
activeTab | 请求根据activeTab规范向扩展授予权限。 |
alarms | 授予您的扩展程序访问chrome.alarms API 的权限。 |
background | 使 Chrome 早启动晚关闭,从而延长扩展程序的使用寿命。 |
bookmarks | 授予您的扩展程序访问chrome.bookmarks API 的权限。 |
browsingData | 授予您的扩展程序访问chrome.browsingData API 的权限。 |
certificateProvider | 授予您的扩展程序访问chrome.certificateProvider API 的权限。 |
clipboardRead | 如果扩展使用document.execCommand(‘paste’). |
clipboardWrite | 表示扩展使用document.execCommand(‘copy’)或document.execCommand(‘cut’)。 |
contentSettings | 授予您的扩展程序访问chrome.contentSettings API 的权限。 |
contextMenus | 使您的扩展程序可以访问chrome.contextMenus API。 |
cookies | 允许您的扩展程序访问chrome.cookies API。 |
debugger | 授予您的扩展程序访问chrome.debugger API 的权限。 |
declarativeContent | 授予您的扩展程序访问chrome.declarativeContent API 的权限。 |
declarativeNetRequest | 使您的扩展程序可以访问chrome.declarativeNetRequest API。 |
declarativeNetRequestFeedback | 授予扩展访问chrome.declarativeNetRequest API 中的事件和方法的权限,这些事件和方法返回有关匹配的声明性规则的信息。 |
declarativeWebRequest | 授予您的扩展程序访问chrome.declarativeWebRequest API 的权限。 |
desktopCapture | 授予您的扩展程序访问chrome.desktopCapture API 的权限。 |
documentScan | 授予您的扩展程序访问chrome.documentScan API 的权限。 |
downloads | 授予您的扩展程序访问chrome.downloads API 的权限。 |
enterprise.deviceAttributes | 授予您的扩展程序访问chrome.enterprise.deviceAttributes API 的权限。 |
enterprise.hardwarePlatform | 使您的扩展程序可以访问chrome.enterprise.hardwarePlatform API。 |
enterprise.networkingAttributes | 授予您的扩展程序访问chrome.enterprise.networkingAttributes API 的权限。 |
enterprise.platformKeys | 授予您的扩展程序访问chrome.enterprise.platformKeys API 的权限。 |
experimental | 如果扩展程序使用任何chrome.experimental.* APIs则是必需的。 |
fileBrowserHandler | 授予您的扩展程序访问chrome.fileBrowserHandler API 的权限。 |
fileSystemProvider | 授予您的扩展程序访问chrome.fileSystemProvider API 的权限。 |
fontSettings | 授予您的扩展程序访问chrome.fontSettings API 的权限。 |
gcm | 使您的扩展程序可以访问chrome.gcm API。 |
geolocation | 允许扩展程序在不提示用户许可的情况下使用地理定位 API。 |
history | 授予您的扩展程序访问chrome.history API 的权限。 |
identity | 授予您的扩展程序访问chrome.identity API 的权限。 |
idle | 授予您的扩展程序访问chrome.idle API 的权限。 |
loginState | 授予您的扩展程序访问chrome.loginState API 的权限。 |
management | 授予您的扩展程序访问chrome.management API 的权限。 |
nativeMessaging | 使您的扩展程序可以访问本机消息传递 API。 |
notifications | 授予您的扩展程序访问chrome.notifications API 的权限。 |
pageCapture | 授予您的扩展程序访问chrome.pageCapture API 的权限。 |
platformKeys | 授予您的扩展程序访问chrome.platformKeys API 的权限。 |
power | 授予您的扩展程序访问chrome.power API 的权限。 |
printerProvider | 授予您的扩展程序访问chrome.printerProvider API 的权限。 |
printing | 授予您的扩展程序访问chrome.printing API 的权限。 |
printingMetrics | 授予您的扩展程序访问chrome.printingMetrics API 的权限。 |
privacy | 授予您的扩展程序访问chrome.privacy API 的权限。 |
processes | 授予您的扩展程序访问chrome.processes API 的权限。 |
proxy | 授予您的扩展程序访问chrome.proxy API 的权限。 |
scripting | 授予您的扩展程序访问chrome.scripting API 的权限。 |
search | 授予您的扩展程序访问chrome.search API 的权限。 |
sessions | 授予您的扩展程序访问chrome.sessions API 的权限。 |
signedInDevices | 授予您的扩展程序访问chrome.signedInDevices API 的权限。 |
storage | 授予您的扩展程序访问chrome.storage API 的权限。 |
system.cpu | 授予您的扩展程序访问chrome.system.cpu API 的权限。 |
system.display | 使您的扩展程序可以访问chrome.system.display API。 |
system.memory | 使您的扩展程序可以访问chrome.system.memory API。 |
system.storage | 授予您的扩展程序访问chrome.system.storage API 的权限。 |
tabCapture | 授予您的扩展程序访问chrome.tabCapture API 的权限。 |
tabGroups | 授予您的扩展程序访问chrome.tabGroups API 的权限。 |
tabs | 使您的扩展程序可以访问Tab多个API使用的对象的特权字段,包括chrome.tabs和chrome.windows。在许多情况下,您的扩展程序不需要声明’tabs’使用这些API的权限。 |
topSites | 授予您的扩展程序访问chrome.topSites API 的权限。 |
tts | 授予您的扩展程序访问chrome.tts API 的权限。 |
ttsEngine | 授予您的扩展程序访问chrome.ttsEngine API 的权限。 |
unlimitedStorage | 为存储客户端数据提供无限配额,例如数据库和本地存储文件。没有此权限,扩展程序仅限于 5 MB 的本地存储空间。 |
vpnProvider | 授予您的扩展程序访问chrome.vpnProvider API 的权限。 |
wallpaper | 使您的扩展程序可以访问chrome.wallpaper API。 |
webNavigation | 授予您的扩展程序访问chrome.webNavigation API 的权限。 |
webRequest | 授予您的扩展程序访问chrome.webRequest API 的权限。 |
webRequestBlocking | 如果扩展以阻塞方式使用chrome.webRequest API,则为必需。 |
一款好的APP离不开一个优雅的样式和合理的布局;在移动端,用户的注意力都集中在手掌大小的屏幕上,因此如果样式稍微有点瑕疵,很容易就影响美观,以及用户体验。而且移动端的组件都布局在相对较小的画布上,由于页面复杂,同时移动端设备尺寸众多,因此合理的页面布局就显得十分重要了。
在React Native中编写css样式和在网页中编写样式没有太大的不同,遵循了web上的css命名,不过按照JS的语法由中划线改为了小驼峰的形式,比如background-color
我们在RN中需要写成backgroundColor
。
所有的核心组件接收style
样式属性,它是一个普通的css对象:
1 |
|
在实际开发中样式会越来越庞大复杂,这样写的行内样式不利于复用和维护,我们使用StyleSheet.create
来创建样式表:
1 |
|
style属性也可以接收一个数组,接收样式列表:
1 |
|
我们知道在css中,一个div如果不设置宽度,默认会占用100%的宽度;在RN中,View和div的性质是一样的,我们可以简单验证一下:
1 |
|
通过上面的效果,我们发现View默认会百分百的占满整个父容器。
在RN中所有的文本内容需要用Text标签包裹,不能直接用View标签。
在上面的样式中,我们发现数值后面是没有单位的(默认单位是dp),表示的是与设备像素密度无关的逻辑像素点;dp是一种相对长度单位,1dp在不一样的屏幕或者不一样的ppi下展现出来的“物理长度”可能不一致,主要是由于ios端和安卓端尺寸单位的不同。
因此我们不能使用dp单位来实现自适应效果;回想一下我们在移动端,通常会使用lib-flexible
方案来实现屏幕尺寸的自适应,RN中也是类似的逻辑。
通常UI给出的默认640或者750的设计稿,我们可以通过Dimensions
,可以获取到整个设备屏幕的dp尺寸,然后再自定义一个转换函数,将我们的dp单位等比例放大到设备上:
1 |
|
调用时直接将样式中单位传入函数进行转换一下:
1 |
|
和原生的iOS以及Android的开发方式不同,RN的布局采用了Web端布局所常用的Flex布局。这个模型的特点在于能够在按照固定尺寸布局之后,灵活地分配屏幕上的剩余空间,利用这个模型可以轻松实现许多应用中所需要的布局设计。
掌握了Flex布局即可随心所欲地对屏幕上的组件元素进行布局,再结合RN所提供的获取屏幕信息、平台信息等API,就可以进阶实现响应式布局。
Flex弹性盒模型相信很多前端小伙伴都比较熟悉它,我们先来看下在web上和RN中的flex布局有哪些异同。
flexDirection:'column'
,在Web中默认为flex-direction:’row’alignItems:'stretch'
,在Web中默认align-items:’flex-start’我们来看下Flex的概念:主轴和侧轴:
1 |
|
我们看到在RN中flex-direction
中的四个属性和Web中表现是一致的,只不过默认是column
。
justifyContent属性定义了浏览器如何分配顺着父容器主轴的弹性(flex)元素之间及其周围的空间,默认为flex-start。
1 |
|
alignItems属性以与justify-content相同的方式在侧轴方向上将当前行上的弹性元素对齐,默认为stretch。
1 |
|
flexWrap属性定义了子元素在父视图内是否允许多行排列,默认为nowrap。
1 |
|
flex
属性定义了一个可伸缩元素的能力,默认为0。
1 |
|
RN中还提供了Web中另外两种常见的布局方式:绝对定位和相对定位,不过不支持固定定位(fixed)。
相对定位相对于原来的位置进行了移动,元素设置此属性仍然处理文档流中,不影响其他元素的布局。
1 |
|
相对定位的效果:
绝对定位的元素相对于父容器进行位置定位,当父容器没有设置相对定位或绝对定位时,元素会相对于根元素定位;绝对定位的元素会脱离文档流,影响到其他元素的定位:
1 |
|
绝对定位的效果:
同Web中的img标签一样,RN中也提供了Image组件用来显示各种图片资源,它可以展示三种图片资源:
我们来看下每种方式如何加载图片:
1 |
|
Image组件必须在样式中声明图片的宽和高;如果没有声明,则图片将不会被呈现在界面上。
在iOS平台,从iOS9开始引入了新特性App Transport Security (ATS)
,要求App内访问的网络必须使用HTTPS协议,因此只能加载https协议的图片和接口;我们可以在模拟器中进行设置,开启http服务:
RN默认支持jpg和png格式的图片,在iOS平台下,还支持GIF、WebP格式;在Android平台下,默认不支持GIF、WebP格式。可以通过修改Android工程设置让其支持这两种格式:
1 |
|
Image组件提供了一个静态函数getSize
,用来取得指定URI地址图片的宽和高(单位为像素)。在调用getSize函数取图片的宽、高时,RN事实上会下载这张图片,并且将该图片保存到缓存中;因此getSize函数也可以作为预加载图片资源的一个方法。
1 |
|
我们也可以使用 Image 组件的静态函数prefetch
来预下载某张网络图片。
1 |
|
当Image组件的实际宽、高与图片的实际宽、高不符时,要如何显示图片由样式定义中的resizeMode
取值来决定;resizeMode可取的五个值分别是:contain、cover、stretch、center和repeat,每种模式的效果如下。
cover模式(默认值),该模式要求图片能够填充整个Image组件定义的显示区域,可以对图片进行放大或者缩小,可以丢弃放大或缩小后的图片中的部分区域,只求在显示比例不失真的情况下填充整个显示区域。
1 |
|
contain模式要求显示整张图片,可以对它进行等比放大或者缩小,但不能丢弃改变后图片的某部分。这个模式下图片得到完整的呈现,比例不会变。但图片可能无法填充Image的所有区域,会在侧边或者上下留下空白,由Image组件的底色填充。
1 |
|
stretch模式要求图片填充整个Image定义的显示区域,因此会对图片进行任意的缩放,不考虑保持图片原来的宽、高比。这种模式显示出来的图片有可能会出现明显的失真。
1 |
|
center模式要求图片图片位于显示区域的中心。这种模式下图片可能也无法填充Image的所有区域,会在侧边或者上下留下空白,由Image组件的底色填充。
1 |
|
repeat模式的图片处理思路是用一张或者多张图片来填充整个Image定义的显示区域。
1 |
|
不过该模式在iOS和安卓下表现形式不一样,在iOS下会向X轴和Y轴方向重复填充:
安卓下repeat则只会在X轴方向重复填充两个图片:
]]> React Native(简称RN)是Facebook于2015年4月开源的跨平台移动应用开发框架,用于创建原生移动Android
和iOS
平台应用程序。它基于Facebook的JavaScript库React,为移动平台创建用户界面。
官方slogan:一次学习,随处编写
开发人员可以使用React Native编写Android和iOS应用程序,这些程序的操作和外观都与原生应用程序相似。使用React Native编写的代码也可以跨平台共享,这就允许IOS和Android同时进行高效开发;React Native有以下优势:
但是它同时也有不少缺点:
那么React Native是如何做到跨平台的呢?我们先看下React和React Native的区别,很多童鞋对他们俩的关系都傻傻分不清:
首先我们需要知道的是:React只是一个纯JS库,它不涉及任何移动平台的功能,React的代码经过编译后需要JS Engine来进行解析执行;它封装了一套Virtual Dom
的概念,实现驱动编程模式,为复杂的Web UI实现了一种无状态管理的机制;但是底层的一些事情,比如驱动原生控件展示、读写磁盘、网络等,它就无能为力了。
但是React Native就不一样了,JSX的源码通过React Native编译后,通过对应平台的Bridge
实现了与原生框架的通信;Bridge的作用就是给RN内嵌的JS提供原生接口的扩展供JS调用;所有的原生功能,例如原生控件绘制、图片资源、本地存储、网络访问、地图定位等功能,都是通过Bridge封装成JS接口注入JS Engine供JS调用。
我们通过一张图来具体的看下:
绿色的部分就是我们应用开发的部分,主要就是我们写的JSX代码;蓝色部分代表公用的跨平台的代码和工具引擎;黄色的部分就是我们的Bridge,和每个原生平台相关的代码;红色部分是每个系统平台的东西。
需要的软件
React Native要求JDK的版本为1.8,我们从官网下载适合自己系统的jdk,然后配置系统的环境变量,网上有很多教程,这里就不再赘述。
然后配置Android环境,从官网下载Android Studio,下载下来是一个exe程序:
在安装界面中我们可以勾选Android Virtual Device
:
Android Studio默认会安装最新版本的Android SDK,我们可以查看它的配置路径,具体路径是Appearance & Behavior → System Settings → Android SDK
;默认SDK的路径是如下格式:
1 |
|
RN需要环境变量来知道我们的SDK安装在什么路径下,从而来编译;打开控制面板 -> 系统和安全 -> 系统 -> 高级系统设置 -> 高级 -> 环境变量 -> 新建
,创建一个ANDROID_HOME
的环境变量,指向我们上面找到的SDK所在的目录。
SDK下有一些工具,我们需要在全局使用,打开控制面板 -> 系统和安全 -> 系统 -> 高级系统设置 -> 高级 -> 环境变量
,选中Path变量,然后点击编辑,将以下目录添加进去:
1 |
|
Android Debug Bridge(安卓调试桥),是一个命令行窗口,用于通过电脑端与模拟器或者是安卓设备之间的交互;adb是一个标准的CS结构的工具,主要包含如下三部分:
adb的下载和安装主要有两种方式,第一种直接下载adb的工具压缩包解压,然后将它的目录配置到环境变量Path
中;第二种就是利用上面已经安装的Android Studio
,它本身带有adb工具,在SDK目录下的platform-tools
中;如果按照上一节配置好Android Studio
的环境变量,一般是直接可以调用adb命令的。
在命令行输入adb version
查看版本号,如果有输出,就已经配置成功了。
1 |
|
配置好环境后,我们还需要打开android手机的USB调试
功能;首先要进入手机的开发者模式,在手机设置 -> 关于手机
连续点击版本号七次就打开了开发者模式;然后进入设置 -> 开发者选项
,打开USB调试
以及允许ADB的一些权限,这样手机就配置好了。下面我们来看下adb的一些常用命令。
启动/杀死adb,我们在上面提到的Server端进程,由于adb并不稳定,有时候莫名的问题掉线时,可以先kill-server, 然后start-server来确保Server进程启动. 往往可以解决问题.
列举当前连接的调试设备:
1 |
|
其中前面的一串字符串代表了手机的serialNumber,后面的device
代表手机的连接状态,有以下几种状态:
我们可以通过手机的serialNumber看出设备的类型,第一个字母和数字的代表usb连接的设备,emulator表示是一个Android模拟器,而最后一个IP端口的表示是通过无线网络的方式连接的。
如果我们同时连接了多个设备,需要操作其中一台设备,可以通过-s [serialNumber]
方式来执行:
1 |
|
比如上面的指令,我们指定9d03b4c4
这个设备来获取屏幕分辨率。
Android系统是基于Linux内核的,所以Linux里的很多命令在Android里也有相同或类似的实现,在adb shell里可以调用;进入设备的shell界面,此时可以使用很多指令:
1 |
|
此时如果想要退出可以输入exit
,我们也可以通过adb shell [指令]
的方式执行,不需要进入设备的shell界面。
1 |
|
安装和卸载apk
1 |
|
在调试设备和开发PC之间拷贝文件,从PC拷贝文件到设备中用push
;用设备拷贝文件到PC中用pull
。
1 |
|
通过无线网络连接,我们就可以无需USB实际连接设备,也可以避免USB连接不稳定等问题出现。
首先确保ADB的版本不宜过低(至少30.0.0),然后将Android设备和调试PC机连接到同一个局域网;首次连接我们还是需要将手机通过USB连接PC,然后开启调试的端口,端口号建议选用不常用未被占用的端口:
1 |
|
端口开启后,我们就可以移除USB的连接,然后查看手机WiFi的IP地址,然后通过adb去进行连接:
1 |
|
连接后我们可以通过devices
来查看设备是否连接成功:
1 |
|
IOS下的环境配置相较于安卓则要容易些,不需要配置那么多繁琐的环境变量,只要把Xcode的IDE和Xcode的命令行工具,可以从APP开发者官网上下载;然后我们还要装一些其他的软件:
1 |
|
Watchman是 facebook 的一个开源项目,它开源用来监视文件并且记录文件的改动情况,当文件变更它可以触发一些操作,例如执行一些命令等等。
然后我们可以使用yarn来代替npm管理我们的依赖包,方便加速下载。
1 |
|
启动Xcode,在Xcode -> Preferences -> Locations
菜单中检查是否配置某个版本的Command Line Tools
:
安装IOS模拟器只需要在Xcode -> Preferences -> Components
下,就可以看到不同版本的模拟器,下载启动即可。启动模拟器的时候可能会遇到下面的报错:
1 |
|
原因是一些依赖没有安装,切换到ios的目录下进行安装:
1 |
|
我们先安装React Native脚手架,然后通过脚手架来创建一个demo项目:
1 |
|
接下来,我们就需要一台安卓设备了,既可以是真机,也可以是安卓模拟器;安卓官方提供了Android Virtual Device
(简称 AVD)的模拟器,在Android Studio
中,打开AVD Manager
来创建和管理模拟器,不过官方的性能较差,我们可以安装一些第三方的。
这里推荐夜神模拟器,安装后通过adb连接,我们可以在项目里运行以下命令进行编译和安装:
1 |
|
需要注意的是,第一次编译需要下载依赖,会比较慢;如果配置都没有问题,我们就可以在设备上看到我们第一个RN应用了:
在Mac中则运行npm run ios
命令,启动ios的模拟器:
我们来看下package.json
中有哪些启动命令:
1 |
|
很多童鞋看到这几个命令很疑惑,npm run start
和npm run android/ios
不都是启动么?有什么区别?
npm run android/ios
在启动开发服务器的同时,会检测有没有设备已经连接;如果有的话,会将开发包安装到开发设备中(安卓or苹果手机),而npm run start
只是启动开发服务器,并不安装应用程序,如果你的开发设备中已经有开发包,只需要启动服务器即可。
RN的开发,不仅涉及React中js的调试,还会涉及到样式、原生组件功能的调试,因此,掌握调试方式对我们的后续的开发有很大的帮助。
Developer Menu(简称DevMenu)是React Native给开发者定制的一个开发者菜单,来帮助开发者调试React Native应用;如果是在iOS模拟器中运行,可以通过按下Command⌘ + D 快捷键
来打开:如果是在Android官网模拟器
中,则是Command⌘ + M
,在第三方模拟器中一般在侧边栏中有一个菜单键;在Android真机中可以通过摇晃设备来打开。
开发者菜单在生产环境的应用下是不能唤起的。
我们看到菜单中有下面几个选项:
当我们修改RN的代码后,肯定想第一时间就能看到修改后的效果,在原生App的开发中,修改后经常需要重新编译、运行等才可以看到效果;但是RN中我们可以直接通过开发者菜单上的Reload
按钮自动完成代码的打包编译和运行,方便我们修改后直接查看效果。
但是RN觉得这样还是要去点击Reload按钮,还不够,有没有什么更加“偷懒”的方式能够自动帮我们触发Reload呢?这就是快速刷新(Fast Refresh)功能,RN在监测到JS代码更改后,自动编译运行,立即就能看更改后的效果;如果觉得太频繁了,可以Disable禁用该功能。
当我们只启动开发服务器,adb也已经连接设备,但是在开发设备中出现No apps connected. Sending "reload" to all React Native apps failed
的错误提示时,我们可以在开发者菜单中选择Change Bundle Location
,然后输入``
Show Element Inspect
选项可以调出元素检查的悬浮框,展示当前选中元素的位置、样式、层级关系、盒子模型信息等等,有点类似Chrome的DevTools,让我们可以很方便的调试元素的样式。
Debug允许我们在Chrome中调试应用,其调试方式和我们在Web中调试一模一样;点击该选项,会自动启动Chrome浏览器,并且打开一个http://localhost:8081/debugger-ui/
的新标签页,在这个标签页里,我们打开开发者工具,就能看到JS输出的日志信息了。
在Sources Tab
页中还可以显示当前调试项目的所有js文件。并在上面进行断点调试。
熟悉项目的结构,是项目上手的第一步,也是最重要的一步,有助于我们更好的理解项目,我们来看下项目中每个文件是做什么的:
1 |
|
index.js这个文件是IOS和Android在相应设备上打包运行的入口文件,在0.49
之前版本的React Native项目,入口则是index.ios.js
和index.android.js
两个单独的入口文件。
当使用一些第三方库时,有一些通过script标签引入的全局变量,TypeScript会出现识别不到而报错的情况,我们需要对其进行声明,这些声明就需要写到声明文件中。比如我们在项目中使用jQuery,在全局使用变量$
或jQuery
:
1 |
|
我们就需要将jQuery的声明语句放到单独的文件中,这就是声明文件:
1 |
|
一般ts会解析项目src文件夹下的所有.ts
文件,因此也会解析.d.ts
文件,这样所有的ts文件就会得到jQuery的类型定义了。
当然,jQuery的声明文件,社区已经写好了,不需要我们自己来定义;我们可以使用@types来管理声明文件:
1 |
|
通过配置tsconfig.json
,将声明文件引入:
1 |
|
声明文件的语法主要有下面几种:
declare let
和 declare const
声明是最简单的,用来声明一个全局变量类型;let定义的全局变量允许修改,而const定义的则不允许修改
1 |
|
一般来说,声明的全局变量都是禁止修改的常量,所以大部分的情况都应该使用declare const
进行声明。同时需要注意的是,声明语句中只能定义类型,而不能定义具体的实现代码。
declare function
用来定义全局函数的类型,jQuery是一个函数,因此我们也可以通过函数的方式来进行定义:
1 |
|
在函数声明中也能够支持函数重载:
1 |
|
declare class
用来声明一个全局类:
1 |
|
declare enum
用来声明全局枚举类型:
1 |
|
declare namespace
用来声明含有子属性的全局对象(模块)。刚开始ts使用module
关键字来表示内部的模块,但随着ES6也使用了module
关键字,ts为了兼容ES6,从1.5版本开始将module
改名为namespace
;比如jQuery是一个全局变量对象,它上面挂载了很多的方法可以调用,我们就通过namespace
来进行声明:
1 |
|
在jQuery内部,我们还可以使用const、class、enum等语句进行声明:
1 |
|
同时,如果需要声明的对象层级较深,我们还可以使用namespace
进行嵌套声明:
1 |
|
泛型(Generics)是指在定义函数、接口或类的时候,不预先指定具体的类型,而在使用的时候再指定类型的一种特性;简单来说,就是一种创建可复用代码组件的工具,这种组件不止能被一种类型使用,而是能够被多种类型进行复用。
我们来实现一个重复元素功能的函数,将给定的元素重复给定的次数,最后返回一个数组:
1 |
|
我们接收了string类型,并且返回string类型的数组;但是这显得太死板了,因为我们只能接收string类型,如果我们想传入number或者object都会报错。那如果改成any
呢?
1 |
|
使用any会导致这个函数可以接收任意类型的参数,这样就导致这个函数缺乏了有效的信息提示,不能告诉函数的调用者传入类型和返回数组中的类型应该是相同的;假设我们传入一个数字,我们只能知道任何类型的值都有可能被返回。
这样我们就需要通过泛型来定义这个函数:
1 |
|
我们在函数名后面添加了类型变量<T>
,T
用来指代任意输入的类型,在后面的输入参数的类型和输出函数的类型中都可以使用。
需要注意的是,这里的字母T
只是代表了一个变量,在数学中和x、y的性质是一样的;我们还可以用其他的参数,比如用S、U、T、Y等其他字母来替代。
定义泛型函数后,我们可以用两种方式来调用,第一种,传入所有的参数,包含类型参数:
1 |
|
第二种方式,利用类型推论,让编译器自动确定类型变量的类型:
1 |
|
在定义函数时,我们有可能会用到多个泛型变量,,用逗号分隔这多个变量:
1 |
|
在swap函数中,我们通过2个变量来交换输入的数组中的元素
我们不仅用泛型定义函数,还可以用泛型定义一个类,和函数类似,也是通过<T>
跟在类名后面:
1 |
|
在函数内部,如果需要使用泛型变量上的属性,由于不知道它的类型(等同于Unknow类型),因此不能随意调用属性和方法:
1 |
|
泛型变量T不一定有属性length,因此会报错;我们可以对泛型变量进行约束,只允许传入包含length属性的变量:
1 |
|
另外,多个泛型参数之间也可以互相约束:
1 |
|
我们将source上所有的属性拷贝到target上,通过T extends U
,保证了source上所有的属性在target上都有。
在ts基础篇中,我们通过接口来定义了函数表达式:
1 |
|
同时可以使用含有泛型的接口来约束函数:
1 |
|
说了这么多ts的知识,我们来把他结合到项目中进行使用和配置。
tsconfig.json是ts编译器的配置文件,ts编译器可根据它的信息来对代码进行编译;运行tsc,它会在当前目录或者父级目录寻找配置文件。在配置文件中可以通过compilerOptions
来定制我们的编译选项:
1 |
|
也可以通过files
显式指定需要编译的文件:
1 |
|
还可以使用include
和exclude
选项来指定需要包含的文件和排除的文件:
1 |
|
include
和exclude
支持的glob通配符有:
*
匹配0或多个字符(不包括目录分隔符)?
匹配一个任意字符(不包括目录分隔符)**/
递归匹配任意子目录有些童鞋可能会有疑惑了,ts在编译阶段就能排查出代码错误,为什么还需要用到eslint来检查呢?因为ts重点关注的是类型的检查,而不是代码和风格的检查,有一些代码的问题,比如==与===的检查、禁用var等功能,还是需要eslint来配合;首先在项目中安装eslint的依赖:
1 |
|
这三个依赖的作用分别是:
安装依赖后我们就可以在.eslintrc.js中配置插件:
1 |
|
在一文彻底读懂ESLint中还介绍了Eslint配合了Prettier,在ts项目,我们也可以搭配Prettier来格式化代码,首先也是进行安装:
1 |
|
然后还是在.eslintrc.js配置Prettier:
1 |
|
在vue中使用ts,推荐使用基于类的注解装饰器
进行开发,vue官方推荐vue-class-component
插件,但是我们在实际开发中都会用到vue-class-component
这个插件,也是vue社区推荐的;它是基于vue-class-component
开发而成,但是性能上有一些改进;他具备以下几个装饰器和功能:
我们来看下每个装饰器的用法:
@Component装饰器接口一个对象做参数,可以在对象中声明components
,filters
,directives
等装饰器的选项,也可以声明computed,watch等。
1 |
|
除了上面介绍的属性,还可以注册钩子函数:
1 |
|
@Prop装饰器同vue中props功能相同,接收一个参数,这个参数可以有三种写法:
1 |
|
需要注意的是:属性的ts类型后面需要加上undefined类型;或者在属性名后面加上!,表示非null 和 非undefined
的断言,否则编译器会给出错误提示。
@PropSync装饰器与@prop用法类似,二者的区别在于:
@PropSync本质上就是通过vue的sync方式传参:
1 |
|
@Watch装饰器同vue中的watch功能相同,监听依赖的变量值变化而做一系列操作,它接收两个参数:
1 |
|
@Emit同vue中的$emit,它接收一个可选参数,该参数是$emit的第一个参数,充当事件名;如果没有提供这个参数,$Emit会将回调函数名的camelCase转为kebab-case,并将其作为事件名。
1 |
|
最后相当于以下代码:
1 |
|
@Emit会将回调函数的返回值作为第二个参数返回给父级函数,如果没有返回值,则会默认使用括号里的参数:
1 |
|
等同于以下代码:
1 |
|
@Model装饰器允许我们在一个组件上自定义v-model,它接收两个参数:
1 |
|
我们将父组件接收的value值作为变量val,将接收的input函数改名change,在输入框改变时触发了change函数(也就是input函数)。
@Ref同vue中的$ref,接收一个可选字符串,用来指向元素或子组件的引用信息;如果没有这个参数,则使用装饰器后面的属性名:
1 |
|
Reveal.js是一个运行在浏览器上的幻灯片展示框架,我们可以在任何浏览器上展示我们想要的幻灯片,并且可以自己定义行为、动画等。
reveal.js是一个开源的HTML展示框架。它是一种工具,可让任何拥有Web浏览器的人免费创建功能齐全且精美的演示文稿。
和传统的幻灯片相比,Reveal.js有以下优势:
首先我们需要将reveal.js克隆到本地:
1 |
|
这样就将整个仓库作为一个静态资源服务器启动了,开发服务器的默认端口8000
;可以使用port参数切换到不同的端口:
1 |
|
我们可以复制一份仓库中的index.html,新建自己的html,就可以通过地址访问到我们自己的幻灯片了;或者将仓库中的dist
和plugin
两个文件夹拷贝到自己仓库的静态资源中,通过外链方式引入。
第二种方式是通过npm的方式进行安装,不过需要注意的是,reveal.js只是针对浏览器环境:
1 |
|
安装后我们就可以将reveal.js作为模块引入了:
1 |
|
但是样式并不会随模块一起引入,我们还需要在页面中包含样式:
1 |
|
安装完成后,我们就可以来编辑幻灯片内容了,他的内容就是一个HTML文件,来看一下它的标准结构:
1 |
|
我们看到页面的层级为`.reveal > .slides > section``,位于最里面的section元素就是我们需要呈现出来的幻灯片,可以是连续多张。
如果有某张幻灯片我们需要对其内容进行更详细的阐述,那么可以通过垂直幻灯片的方式,将多个section元素包含在一页幻灯片内容中:
1 |
|
我们切换幻灯片时,默认都是从右到左进行幻灯片的切换过渡,默认是滑动的效果;我们可以在每页幻灯片上添加data-transition
属性来制动过渡的效果:
1 |
|
下面是所有过渡效果的列表:
过渡名称 | 影响 |
---|---|
none | 立即切换背景 |
fade | 淡入淡出 |
slide | 在背景之间滑动(默认) |
convex | 以凸角滑动 |
concave | 以凹角滑动 |
zoom | 将传入的幻灯片向上缩放,使其从屏幕中心向内增长 |
如果我们想统一改变默认的过渡效果,在全局初始化的时候配置backgroundTransition
属性:
1 |
|
在上面代码中我们发现,引入了两个不同的css,第一个reveal.css
是依赖的基础的样式文件,而引入的第二个theme/black.css
就是在基础样式上的主题样式,可以根据我们自己的喜好,选择不同主题;Revealjs也内置了以下主题:
主题名称 | 主题效果 |
---|---|
black | 黑色背景、白色文本、蓝色链接(默认) |
white | 白色背景,黑色文字,蓝色链接 |
league | 灰色背景,白色文本,蓝色链接 |
beige | 米色背景、深色文本、棕色链接 |
sky | 蓝色背景,细的深色文本,蓝色链接 |
night | 黑色背景,粗白文本,橙色链接 |
serif | 卡布奇诺背景,灰色文本,棕色链接 |
simple | 白色背景,黑色文字,蓝色链接 |
solarized | 奶油色背景、深绿色文本、蓝色链接 |
blood | 深色背景,厚实的白色文本,红色链接 |
moon | 深蓝色背景,粗灰色文本,蓝色链接 |
需要更改主题,我们只需要从theme中引入对应主题的css即可。
Revealjs也支持使用Markdown来编写幻灯片内容,对于简单的内容,会更加方便简洁;首先我们需要引入Markdown的插件:
1 |
|
然后将Markdown编写的内容放到section > textarea
中,同时需要在标签上加上特殊的属性进行标识:
1 |
|
不过要注意的是,Markdown语法对空格缩进和换行符(避免连续中断)的检测很严格。
一般情况下,我们的背景颜色是跟随主题的,我们可以通过在每页上设置data-background
属性,来手动的设置某页幻灯片的背景颜色。
1 |
|
除了设置背景纯色,我们还可以设置为背景图片
1 |
|
背景图片支持以下属性:
参数 | 默认 | 描述 |
---|---|---|
data-background-image | 要显示的图像的 URL | |
data-background-size | cover | 背景图像大小 |
data-background-position | center | 背景图像位置 |
data-background-repeat | no-repeat | 背景图像重复 |
data-background-opacity | 1 | 0-1 范围内背景图像的不透明度。0 是透明的,1 是完全不透明的。 |
有些幻灯片中我们想实现一些简单的动画效果,比如渐变、滑动等;Reveal.js会自动为幻灯片中的元素设置动画效果;我们需要做的就是在相邻的两页section
元素上添加data-auto-animate
属性,这样Reveal.js会为所有匹配的元素设置动画。
1 |
|
我们看一个简单的例子,在页面上的h1
标签,我们在不同幻灯片中设置了不同的样式,Reveal.js会自动为这个元素设置动画效果。
对于幻灯片上新增或者删除的元素,Reveal.js在排列内容时也会自动为其添加动画效果:
1 |
|
我们可以用ul > li
的布局来生成多条排列整齐的论述列表,每页幻灯片内容都有新增的列表内容;Reveal.js会自动为元素的增加和删减添加动画。
那么Reveal.js是如何来自动匹配相同的元素的呢?对于文本元素,如果节点的类型和文本内容相同,我们就认为它是相同的,对于图片、视频和iframe等多媒体元素,Reveal.js通过比较他们的src属性。
回到我们最开始的那个动画例子,如果我们将文本内容进行改变,那么动画效果就会失效;在自动匹配元素不可行的情况下,我们可以在元素上添加data-id
属性来强制进行动画效果,这样Reveal.js就会优先考虑元素的data-id
属性:
1 |
|
我们看到第一个动画到第二个动画之间,由于自动匹配失效,因此没有动画;第二个动画到第三个动画之间由于我们强制加上了data-id
属性,因此动画效果依然是生效的。
有时候我们会存在多个动画,相邻的动画有可能会互相干扰,可以通过data-auto-animate-id
和data-auto-animate-restart
对动画进行分组。
我们可以对相邻分组的幻灯片上加上data-auto-animate-id
属性,属性的值可以是任意的,相同组保证值相同即可;这样,相邻的幻灯片会识别相同的id进行动画效果。
1 |
|
如果同一组的幻灯片比较多,我们需要加上很多的id,上面的方式显得比较繁琐;因此Reveal.js提供另一种控制动画的方式:data-auto-animate-restart
属性;这个属性会阻止上一张幻灯片和本组幻灯片之间的动画效果(即使他们有相同的id)。
1 |
|
Reveal.js在呈现代码方面也比普通的PPT更有优势,可以显示语法突出显示的代码,这个功能需要引入highlight.js
插件;我们写的代码需要包含在pre > code
标签中:
1 |
|
然后引入highlight.js
插件,插件中默认包含了monokai.css的样式:
1 |
|
我们可以在highlight.js的demo中找到更多的高亮主题。
通过在code标签添加data-line-numbers
属性,可以启用代码的行号;如果要突出显示特定的行号,可以提供逗号分隔的行号列表,在下面的例子中,第3行和第8-10行就被高亮显示:
1 |
|
我们还可以将高亮的代码进行分步骤显示,用|
分隔每一个步骤;例如下面的3-5|8-10|13-15
将产生3个步骤:
1 |
|
如果我们想要幻灯片中展示数学公式,可以借助于MathJax插件轻松的实现;它是一个开源的基于 Ajax 的数学公式显示的解决方案,结合多种先进的Web技术,支持主流的浏览器;首先我们需要引入本地仓库的math.js插件和远程加载MathJax插件:
1 |
|
然后我们就可以尽情使用想要的数学符号和公式了:
1 |
|
我们的演示文稿最后可能需要存档或者发给领导,如果直接存档项目则需要打包整个项目,别人可能也不知道怎么用;Reveal.js提供了特殊的打印样式,可以让我们将文稿导出为PDF。
http://localhost:8080/index.html?print-pdf
。然后就能看到我们保存到本地的pdf文件了。
]]>从平凡到不凡,英文原句:
From Zero To Hero
,让我们学习TS从Zero
开始,到达Hero
。
那么,什么是TypeScript呢?
TypeScript是JavaScript类型的超集,它可以编译成纯JavaScript。
这里的超集是数学上的概念,与它相对的概念就是子集
;所谓的超集子集,他们是成对出现的,有超集必然有子集;TypeScript是JavaScript类型的超集,也就是说JavaScript具有的特性和功能,TypeScript全都有,并在这个基础上有一些JavaScript不具备的特性和功能,形成了自己的优势。
用图形来表示就是这样的:
那么问题来了,TypeScript的优势体现在哪里呢?我们都知道JavaScript是弱类型语言,它没有Java一样对变量类型严苛的约束,这样带来的灵活性一方面能够让自身降低准入门槛,蓬勃发展,一直稳居GitHub热门编程语言宝座;另一方面,灵活性也使得它的代码质量参差不齐,维护成本高以及容易产生运行时错误。
从TypeScript名字中的类型
就能看出,其核心特性就是它的类型系统,来弥补JavaScript灵活性带来的弊端;因此TypeScript相较于JavaScript有以下优势:
好了,夸了这么多彩虹屁,我们还是来到安装环节:
1 |
|
全局安装后,我们就可以在任何地方通过tsc
命令编译我们的TypeScript文件了,比如我们常见的hello.ts:
1 |
|
然后执行编译命令,就变成我们常见的js了:
1 |
|
在上面ts文件中,使用:
来指定变量的类型。
我们先从TypeScript的一些基本概念开始介绍,让大家对它有一个基本的了解(以下简称ts)。
布尔值是基础的数据类型,在ts中,使用boolean
定义布尔值类型:
1 |
|
我们需要区分一下boolean
和Boolean
,前者是用来定义类型,后者是一个构造函数,用来创建对象:
1 |
|
这里new Boolean()
返回的是一个Boolean对象,本质上是对象,而Boolean()
直接调用也可以返回一个boolean类型的值;这里要注意两者的区别,下面的几种基本数据类型也都有这样的区别,不在赘述。
1 |
|
ts用number
表示数值类型,除了十进制和十六进制的数值,还支持二进制和八进制,后面两者会被编译成二进制的数字。
1 |
|
我们使用string表示文本字符串类型,可以使用单引号(’)、双引号(”)或者es6中的模板字符串(`)。
在ts中,可以用void
来表示没有任何返回值的函数:
1 |
|
声明一个void类型的变量没什么用,我们只能给它赋值undefined或者null:
1 |
|
never类型
表示那些永远不会存在值的类型;例如, never 类型是那些总是会抛出异常或根本就不会有返回值的函数表达式或箭头函数表达式的返回值类型。
此外,变量也可能是never类型,当它们被永不为真的类型保护所约束时。为了让大家更好的理解never类型,我们来举一些实际的例子。
1 |
|
那么never类型有什么用呢?尤大大在知乎上举了一个例子,利用never的特性来实现详细的检查:
1 |
|
假设我们定义了一个联合类型,在函数中进行类型判断;如果逻辑正确,那么最后的else是永远到达不了的;但是如果有一天你的同事修改了Foo的类型:
1 |
|
同时忘记修改handleValue
函数中的控制流程,那么这时,else中的check变量就会被收窄为boolean类型,就会产生编译错误。
通过这样的方式,我们可以穷尽Foo所有可能的类型,避免新增了联合类型却没有对应的实现。
当我们在写应用的时候可能会需要描述一个我们还不知道其类型的变量;这些值可以来自动态的内容,例如从用户获得,或者我们想在API中接收所有可能类型的值。在这些情况下,我们想要让编译器以及未来的用户知道这个变量可以是任意类型。这个时候我们会使用 unknown
类型。
1 |
|
如果你有一个 unknwon
类型的变量,你可以通过进行 typeof
比较或者更高级的类型检查来将其的类型范围缩小:
1 |
|
在ts中,可以使用null和undefined来定义这两个数据类型:
1 |
|
这两个数据类型只能分别唯一定义各自的数据,因此本身用处不是很大;不过undefined和null是所有类型的子类型,因此它们可以赋值给其他类型的变量,包括void:
1 |
|
在编程的时候,我们可能还没有确定一个变量的数据类型,这个值可能来自动态的内容,比如接口数据,或者第三方的库,我们不希望对它进行类型检查,因此可以用任意值any
来标记允许变量赋值为任意数据类型。
1 |
|
在任意值上可以访问任何属性
和调用任何函数
:
1 |
|
如果变量在声明时未指定其类型,会被识别为任意值类型:
1 |
|
我们看到上面的Never和Void有点类似,都可以用于描述函数没有返回值,但是两者有本质的区别:
什么也不返回
,但实际上它是会返回的。在这些情况下,我们通常忽略返回值。在ts中这些函数的返回类型被推断为 void
。永不返回
,它也不返回 undefined。该函数没有正常完成,这意味着它可能会抛出异常
或根本无法退出执行。1 |
|
在ts中,当我们不确定一个类型是什么类型的,可以选择给其声明为any或者unkown。但实际上,ts推荐使用unknown,因为unknown是类型安全的。
如果是any类型,你可以对它任意的取值和赋值,完全放弃了类型检查;但unknow类型就不一样了,必须进行类型收窄才能进行取值。
1 |
|
这反映了两者的一个本质区别:
在ts中,如果一个变量没有明确指定类型,那么会按照类型推论的规则来推断出一个类型。
1 |
|
这里变量myNumber被推断为数字,因此再次改变其类型就报错了。
联合类型表示取值可以为多种类型中的一种;
1 |
|
我们用竖线分隔每个类型,允许myData的类型可以是string
或者number
,但不允许是其他类型。
偶尔我们会遇到函数传参时,一个参数允许传入不同类型,也可以用到联合类型:
1 |
|
当ts不确定联合类型的变量到底是哪一种类型的时候,只能访问此联合类型的所有类型里共有的属性或方法:
1 |
|
联合类型的变量在赋值时,会根据类型推论推断出一个类型:
1 |
|
在ts中我们可以用多种方式来定义数组。
最简单的表示数组是使用类型+[]
的形式:
1 |
|
数组的项中不能出现其他的类型,包括调用数组添加的方法:
1 |
|
针对一些复杂结构的数组,我们可以通过any来表示数组中允许出现任意的类型
1 |
|
我们可以使用数组泛型来表示数组:
1 |
|
更多关于泛型的下面会涉及。
我们也可以用接口来表示一个数组:
1 |
|
这样定义数组比较繁琐,不常用,但是我们会用这种形式来表示类数组:
1 |
|
我们直接用数组的类型来表达arguments会报错,因为它本身不是一个数组,没有数组的push、pop等函数,我们可以通过接口的方式:
1 |
|
这里的ArgumentInterface
实际上在ts内部已经定义好了,我们可以直接拿来用:
1 |
|
枚举在项目中随处可见,比如一系列月份、日期的选择,或者和后台约定的列表范围内的选择等;通过ts,我们可以更清晰更方便的来定义枚举值。
1 |
|
枚举通过enum
来定义,枚举成员会被从0开始递增赋值,同时也会进行枚举值到枚举名的反向映射
:
1 |
|
事实上,上面枚举代码会被编译为以下js代码:
1 |
|
如果我们对枚举值有其他需求,可以进行手动赋值:
1 |
|
未手动赋值的枚举项会接着上一个枚举项的值递增;如果手动赋值的枚举项和后面递增的重复了,ts也不会报错:
1 |
|
可以看到,当递增到3时,Wed与前面的Sun重复了,枚举项的值还是能继续取到;但是枚举值对应的枚举项会被覆盖,上面代码会被如下编译:
1 |
|
手动赋值的枚举值也可以为小数或者负数,后续为赋值的项递增步长仍为1:
1 |
|
虽然最后编译成对象,但是枚举项的值是不可修改的:
1 |
|
枚举值还可以设为字符串:
1 |
|
由于未设置的枚举值是递增关系,因此我们我们不能将中间的枚举值设为字符串,这样它后面的枚举值就不知道从哪里开始了:
1 |
|
字符串的枚举值是不做双向映射的:
1 |
|
上面代码会被如下编译:
1 |
|
枚举项可以分为常数项
和计算项
,常数项有以下三种情况:
常量枚举表达式
初始化其他的情况都是计算项:
1 |
|
常数项会在编译时计算出结果,然后以常量的形式出现在代码中,上述代码会被如下编译:
1 |
|
常量枚举是通过const enum
定义的枚举类型:
1 |
|
常量枚举在编译阶段会被移除;当我们不需要一个对象,而只需要对象的值,就可以使用常量枚举,这样就能避免在编译时生成多余的代码和间接引用:
1 |
|
由于常量枚举在编译时会被移除,因此常量枚举不能包含计算项:
1 |
|
外部枚举是使用declare enum
定义的枚举类型:
1 |
|
外部枚举与声明语句一样,常出现在声明文件中。
在ts中,我们使用接口来定义对象的类型;有时这也被叫做鸭式辨型法
:
像鸭子一样走路、游泳和嘎嘎叫的就是鸭子
也就是说,哪怕是一条狗,如果它也能像鸭子那样走路、游泳和叫,那么我们也认为它是一只鸭子。
很多童鞋可能就难以理解,就算事实上真的有一条狗这么去走路这么去叫,它本质上也是狗,怎么会变成鸭子呢?这不是典型的指鹿为马么?
但是如果我们把这两类动物放到程序中来,
1 |
|
这里构造了duck和dog这两个动物实例,我们需要驱动鸭子往前走起来(即调用他们的函数),但是程序并不需要确切的区分谁是谁,只要能够保证它有duckWalk函数就可以了,这就是所谓的鸭式辨型法。
在面向对象的语言中,接口就是来保证对象有我们需要的函数,它是对类的行为进行的抽象。
1 |
|
我们定义了一个接口Duck,规定了字面量dog的类型是Duck,这样就约束了dog的属性必须和接口定义的保持一致,如果多一些或者少一些属性都是不允许的,都会报错。
1 |
|
但是有一些属性是可有可无的,我们不希望完全匹配,那么就可以用可选属性
:
1 |
|
可选属性在属性后面加一个问号,表示该属性是非必须的,一个接口中可以同时存在多个可选属性;但是这时其他属性还是不允许添加的。
我们希望在一个接口中添加任意的属性,可以通过任意属性的方式:
1 |
|
不过需要注意的是,如果我们定义了任意属性,那么我们在上面定义的确定属性和可选属性必须是它的类型的子集:
1 |
|
这里age和gender分别是number和boolean类型,不是string类型的子集,因此就会报错;在一个接口中只能定义一个任意类型,如果接口中有多个类型的属性,可以使用联合类型:
1 |
|
有时候我们希望有一些属性只能被读取,不能进行修改,可以将其定义为只读属性readonly
,比如唯一标识的id信息;只读属性只能在初始化时被赋值:
1 |
|
有些童鞋可能想到了,那我初始化对象时不赋值,后面不就可以再修改只读属性的值了吗?
1 |
|
然而很遗憾,你就会收获另一个错误。
只读属性的性质和确定属性一样,是不能缺少的;有些童鞋可能又想到了,那我把可选属性也加上不就行了,变成了只读可选属性:
1 |
|
这样也是不行的,因此我们发现了:
只读的约束存在于第一次给对象赋值的时候,而不是第一次给只读属性赋值的时候
在js中有两种声明函数的方式:函数声明和函数表达式,我们先来看下函数声明的类型定义,只需要把函数的输入和输出都考虑到即可:
1 |
|
如果调用时的参数多于或者少于要求的参数,都会报错:
1 |
|
我们也可以对函数表达式进行相同的类型定义:
1 |
|
不过这样只对等号右侧的匿名函数
进行了类型定义,而等号左侧的变量sum
则是通过类型推论得到的,我们也可以手动给它添加类型:
1 |
|
注意这里的=>
和es6中的=>
是不一样的,ts中的=>
用来表示函数的定义,左边是输入类型,右边是输出类型。
我们也可以通过接口来定义函数表达式
1 |
|
上面我们说到,对于函数参数个数,是必须按照定义传参的,那么如何定义可选参数呢?和接口定义对象的可选属性类似,我们也用到了?
来表示:
1 |
|
需要注意的是,可选参数必须在必须参数后面
:
1 |
|
和es6的函数一样,在ts中我们也可以给函数的参数添加默认值:
1 |
|
ts会将添加了默认值的参数自动识别为可选参数,此时就不受可选参数必须在必须参数后面
限制:
1 |
|
在es6中,我们可以通过...rest
的方式获取函数中剩余的参数:
1 |
|
rest参数本质上是一个any[]
类型的数组,它只能作为函数的最后一个参数。
函数重载是为同一个函数提供多种函数类型的定义来进行函数重载;我们可以定义多个函数重载的类型:
1 |
|
需要注意的是,必须要把精确的定义放在前面,最后函数实现时,需要使用联合类型或者任意类型,把所有可能的输入类型全部包含进去,以具体实现;即最下面的方法需要兼容上面的方法。
上面的例子中,虽然我们定义了3个重载的add函数,前两次都是函数的定义,最后一次是函数的实现,因此本质上我们只有定义了两次:
1 |
|
最后一个函数调用时我们并没有定义两个都是字符串的参数,因此报错。
]]>Verdaccio是什么?
Verdaccio
是一个 Node.js创建的轻量的私有npm代理注册源(proxy registry)
通过Verdaccio搭建私有npm服务器有着以下优势:
Verdaccio是sinopia开源框架的一个fork,由于sinopia作者两年前就已经停止更新,坑比较多,因此Verdaccio是目前最好的选择;首先进行全局安装:
1 |
|
安装后直接输入命令即可运行:
config.yaml
是配置文件的路径,我们可以进行权限等配置;4873是Verdaccio启动的默认端口,支持在配置文件中进行修改;我们再浏览器中打开localhost:4873就能看到他的管理界面了:
通过命令行启动的话,如果终端停止了,那我们的服务器也就停止了,因此一般我们通过pm2启动守护进程。
1 |
|
这样verdaccio就在后台运行了。
除此之外,我们还可以通过docker来进行安装
1 |
|
Verdaccio安装好后,我们可以更改npm源为本地地址:
1 |
|
或者针对某个依赖安装时选用自己的源地址:
1 |
|
但是如果我们想再次切换到淘宝或者其他的镜像地址,就不那么方便了;我们可以通过nrm
这个工具来管理我们的源地址,可以查看和切换地址;首先还是进行安装:
1 |
|
安装后我们可以通过nrm add [name] [address]
这个命令来新增一个源地址:
1 |
|
使用nrm ls
可以查看我们使用的所有源地址,带*
是正在使用的地址;通过nrm use [name]
来切换地址:
通过配置文件,我们可以对Verdaccio进行更多的自定义设置,默认的配置文件如下:
1 |
|
Verdaccio默认只能本机访问,我们可以在配置文件最后加入监听的端口,让局域网访问:
1 |
|
所有的账号密码都会保存在htpasswd
这个文件中,我们可以通过在线htpasswd生成器生成加密的密码文件,或者通过命令行的形式来添加用户
1 |
|
上游链接uplinks
表示如果在本地找不到依赖包,去上游哪个地址获取包;我们可以配置多个地址,每个地址都有一个唯一的key值:
1 |
|
官方的地址会比较慢,一般建议配置淘宝的地址:配置好上游链接后我们就可以将包的代理指向链接,支持多个链接地址,请求失败时会向后面的链接进行尝试。
从verdaccio@4.0.0
开始支持配置自定义令牌签名,要启用JWT签名,我们需要将jwt添加到api部分:
1 |
|
在packages
字段中,我们可以对每个域下面的包进行管理,在verdaccio中有三种身份:所有人、匿名用户、认证(登陆)用户(即”$all”, “$anonymous”, “$authenticated”),默认情况下所有人都有访问的权限(access),认证的用户才有包的发布权限(publish)和撤回权限(unpublish)。
除此之外,我们可以对包进行更进一步的划分,比如公司的包和个人的包,通过@scope
的方式进行访问控制:
1 |
|
默认情况下,任何用户都可以通过npm adduser
来向我们的服务器注册用户;如果我们的npm服务器部署在外网的话是不利的,我们还可以通过max_users为-1来禁止注册用户,如果在内网的话,可以设置某个具体的值来限制用户的数量:
1 |
|
搭建好了npm私服,我们就可以上传npm包了,我们创建一个utils项目,放一些自己的工具等,首先通过npm init
初始化package.json,然后我们可以对包的信息进行一些修改:
1 |
|
我们就可以在入口文件index.js
中写代码了,比如这里我们定义了判断环境的几个变量:
1 |
|
然后我们登录上面注册的用户,输入用户名、密码以及邮箱等:
1 |
|
也可以通过npm logout
退出登录,以及npm who am i
查看当前登录的用户名;登录成功后就可以将我们的包进行发布了:
1 |
|
在首页我们登录后就能成功看到刚刚发布的第一个包了,也能给其他同事使用:
我们注意到整个包的入口是main
字段指定的index.js,这是一个es module模块规范的包,但是在commonjs规范的项目中就不能使用了,因此我们可以通过打包工具对其进行打包优化。
我们在深入对比Webpack、Parcel、Rollup打包工具的不同介绍过Rollup比较适合用来打包一个类库,因此这里就选用Rollup进行打包,我们在项目下新建rollup.config.js
:
1 |
|
通过rollup将入口文件index.js
打包成commonjs、es module和iife三种规范,然后就在package.json
中分别定义三种规范的文件路径:
1 |
|
如果还想对我们的包有详细的使用说明等,可以在项目中新建一个README.md文档;build打包后,再次publish发包就能更新线上的依赖包了。
]]>那么Nginx到底是什么,首先我们来看一下百度百科对Nginx的定义:
Nginx是一个高性能的HTTP和反向代理web服务器,同时也提供了IMAP/POP3/SMTP服务。
这里有三个词很关键,我们来拆解一下,分别是是高性能、反向代理和web服务器;首先这个web服务器自不用多说,像我们熟知的Apache、IIS、Tomcat等都是web服务器;然后是高性能,一个服务器的性能自然是网站开发者最为关心的,那么服务器的性能如何来进行衡量呢?一般可以通过CPU和内存的使用量来进行衡量。经过笔者简单的并发测试,在20000个并发链接时,CPU和内存占用也非常低,CPU仅占5%,内存占用也才2MB不到。
我们可以通过一个web压力测试工具Apache Bench
,对Nginx进行简单的压力测试;通过在命令行ab -n 20000 -c 10000 [url]
,我们对Nginx的首页发起请求总数为20000,并发数为10000的请求测试,测试结果如下:
我们看到总的请求时间(Time taken for tests)是25秒,平均每个请求耗时(Time per request)1.25毫秒,在这么高的并发量下面,服务器响应性能还是挺不错的。
然后是反向代理,与之对应的就是正向代理,这两者的区别也是面试中经常被问到的。我们先来看一下什么是正向代理,一个正向代理最典型的例子就是我们常用的“梯子”。
我们直接访问Google,是访问不到的,但是如果我们使用了代理服务器,那么通过访问代理服务器就可以浏览Google,这里的代理服务器就属于正向代理
;通过正向代理我们可以访问原来无法访问的资源。
那么什么是反向代理呢?反向代理最典型的例子就是我们的Nginx服务器了;比如我们在访问某个网站时,由代理服务器去目标服务器获取数据后返回给客户端,这样就能够隐藏真实服务器的IP地址,只对外开放代理服务器,以防止外网对内网服务器的恶性攻击。
理解了上面两个典型的案例,相信大家对正向反向代理也了解了,我们总结一下:
Nginx安装程序分为Linux版和Windows版,Windows版本的Nginx下载解压后就可以直接运行了,而Linux版本的需要make、configure等命令编译安装,好处是可以方便灵活的编译不同的模块到Nginx;网上也有很多的安装教程,这里就不再赘述了,可以从官网下载适合自己的版本,下载好后我们来看一下他的目录结构:
1 |
|
我们经常用到的就是conf目录和html目录;而在根目录可以运行常用的一些命令对Nginx进行操作控制:
1 |
|
我们看前四个命令会发现,这四个命令可以分为两种,重启和停止Nginx,不过一种是强制的方式,另一种是优雅的方式;强制的方式就是让Nginx立即停止当前处理的所有请求,丢弃链接,停止工作;而优雅的方式是允许Nginx将当前正在处理的请求处理完成,但是不再接收新的请求,所有处理完成后再停止工作。
我们再来看一下主要配置文件nginx.conf的基本结构:
1 |
|
配置文件中主要可以分为以下几个块:
很多时候,我们不会将所有的配置全都写在一个主配置文件,因为这样会显得冗长,也不知道每个模块是做什么用的;而是会根据项目来拆分多个配置文件,每个配置文件彼此独立,互不干扰,然后在主配置文件中引入;我们在conf目录下新建一个projects目录,然后可以新建多个.conf配置文件:
1 |
|
然后在主配置nginx.conf中将projects目录下的所有配置文件引入:
1 |
|
这样我们可以直接在projects目录下新增.conf后缀的配置文件,而不用修改主配置文件;但是我们修改完还不能确定是否会有错误,可以通过命令对配置文件进行检测:
1 |
|
通过检测发现没有任何报错,就可以优雅的重启服务器了:
1 |
|
作为一个web服务器,最重要的就是能够对静态资源提供访问服务,我们的Nginx服务器可以用来托管一些静态的资源,比如js、css、图片等,访问某一特定的静态资源路径时会转发到本地目录文件上;那么我们就来看Nginx是如何一步一步的通过域名配置、URI配置以及目录配置来命中请求的。
在上面的配置中,我们主要是将server_name
设置为localhost
,但是这样仅能让局域网内的主机访问到;我们想要让广域网上的其他主机访问,可以将server_name
匹配域名,它的参数值可以是以下几种:
*.my.com或者www.my.*
~
作为正则表达式字符串的开始标记,如~^www\d+\.my\.com$
在上面正则表达式中,^
表示以www开头,紧跟一个或多个数字(\d+),然后跟上域名my.com,最后以$
结尾;因此上面的表达式可以匹配的域名比如www1.my.com,但是www.my.com就不行。
正则表达式还支持字符串捕获功能,即将正则表达式匹配成功的名称中的一部分字符串截取出来,放在变量中供后面使用;比如将server_name进行如下设置:
1 |
|
这样,通过二级域名home.my.com到达Nginx时,被server_name
正则表达式捕获,将其中的home
字符串存入$1
变量中,我们在/usr/share/nginx/html/home
目录下的静态资源就能通过home.my.com域名来访问了;我们服务器的目录就可以是这样的:
1 |
|
这样就只需要一个server块来完成多个站点的配置。
nginx允许一个虚拟主机有多个域名,因此我们可以给server_name同时配置多个域名,多个之间以空格分隔:
1 |
|
由于server_name支持以上三种配置方式,如果出现多个server块同时匹配了相同的域名,那么这个请求交给哪个server呢?因此优先级顺序如下:
如果我们想让局域网内的设备访问nginx,可以将server_name
设置ip地址的方式:
1 |
|
如果还不能访问,可以查看下是否是防火墙的原因,在防火墙允许通过的应用中将Nginx勾选(没有找到Nginx可以点击允许其他应用
进行新增):
有时候我们还会见到将server_name设置为_
(下划线),意味着server_name为空,即匹配全部的主机;我们可以配置host,将a.com、b.com和c.com都指向本机,然后配置nginx:
1 |
|
这样我们不仅可以通过域名a.com、b.com、c.com来访问,也能通过ip的方式。
location用于匹配不同的URI请求,它的语法如下:
1 |
|
这里的uri
就是待匹配的请求字符串,可以是不含正则的字符串,比如/home
,称为标准URI
;也可以是包含正则的字符串,比如\.html$
(表示以.html结尾),称为正则URI
。而方括号中的四种匹配符都是可选的,用来改变请求字符串与URI的匹配方式,我们来看下四种匹配符的解释:
匹配符 | 解释 |
---|---|
不填 | location后没有参数,直接跟着标准URI,表示前缀匹配,代表跟请求中的URI从头开始匹配 |
= | 用于标准URI前,要求请求字符串与其精准匹配,成功则立即处理,nginx停止搜索其他匹配 |
^~ | 用于标准URI前,要求一旦匹配就会立即处理,不再去匹配其他正则URI,一般用来匹配目录 |
~ | 用于正则URI前,表示URI包含正则表达式,区分大小写 |
~* | 用于正则URI前,表示URI包含正则表达式,不区分大小写 |
@ | 定义一个命名的location,@定义的location名字一般用在内部定向 |
我们来看下每种匹配规则能匹配的url,首先不填代表的话表示前缀匹配,如果我们有多个相似的前缀匹配:
1 |
|
对于请求/pre/fix/home
,根据最大匹配原则,匹配第一个location。
然后是=
,要求路径完全匹配:
1 |
|
其次是^~
最佳匹配,它的优先级高于正则表达式:
1 |
|
接着是~
正则表达式匹配,它区分大小写匹配(注意:windows版本nginx不区分):
1 |
|
~*
同样也是正则匹配,只不过它不区分大小写,这里就不再演示。
如果我们的URI匹配到了多个location,其并不完全按照在配置文件中出现的顺序来进行匹配,URI会按照如下规则进行匹配:
~
和~*
的指令,如果找到相应的匹配,则nginx停止搜索其他匹配;当没有正则表达式或者没有正则表达式被匹配的情况下,那么匹配程度最高的逐字匹配指令会被使用。 在location匹配URI后,就需要在服务器指定的目录中寻找请求资源,而root
和alias
就是用来指定目录的两种指令,两者主要的区别在于如何解析location后面的路径;我们首先来看下root的用法,假如我们需要将/data/
下面的所有路径转发到html/roottest
下面:
1 |
|
当location接收到/data/index.html
的请求时,会在html/roottest/data/
目录下找到index.html文件并进行相应,root会将root路径和location路径进行拼接。
而alias指令则改变location接收到的请求路径,假如我们需要将/data1/
下面的所有路径转发到html/aliastest
下面:
1 |
|
当location接收到/data1/index.html
的请求时,会在html/aliastes/
目录下查找index.html文件并响应。
需要注意的是:alias指令后面的路径
必须以/结束
,否则会找不到文件,而root则可有可无。
针对一些静态资源,我们可能会设置一些用户访问权限,比如和js一起打包产出的.map
文件,会对源码进行映射;但是我们想让它只能针对公司的ip进行开放,对外网的ip禁止访问,这时就需要用到allow
和deny
命令了。
假如局域网还有两个设备,我们只能让这两个设备的ip通过访问:
1 |
|
deny和allow指令是由ngx_http_access_module模块提供,Windows版本的Nginx并不包含该模块。
还可以对前端的.map文件进行访问权限控制,打包后的map文件一般会放在服务器上,但是如果能对所有人开放,别人就能查看到对应源码;因此我们可以控制只有公司的ip才有访问权限:
前端在配置路由时经常会用到history路由模式,因此后台就需要映射对应的路由到index.html;但是如果我们给每个路由都配置一个location就会比较繁琐,因此可以通过try_files
指令来进行尝试解析;try_files
的语法规则如下:
1 |
|
假设我们打包出来的单页面位于/html/my/index.html
,我们想要将/login、/regisrer等路由指向index.html,我们可以配置try_files:
1 |
|
对于多页面的应用,假设我们的页面都放在/html/pages/
目录下,我们想要访问/login
时响应/html/pages/login.html
页面,可以通过$uri
:
1 |
|
这里我们设置root目录为html/pages,当我们访问/login
路由时,这里的$uri就是/login,try_files会去尝试在根目录下找/login.html
;如果找不到就尝试/login/index.html
,最后找不到则会默认返回index.html。
我们都知道在服务端开启gzip压缩能够使得js、css、html等文件在传输时大幅提高访问速度,优化网站性能;gzip压缩后的文件大小可以变为原来的30%甚至更小;而对于图片、视频、音频等其他多媒体文件,因为压缩效果不好,所以不会开启压缩。
gzip压缩本质上是服务器端压缩,传输到浏览器后解压解析,我们来看下gzip的原理示意:
可以看到在请求和相应头上分别加了accept-encoding和content-encoding来进行传输;我们可以通过一个js的请求数据来查看:
既然gzip有这么多的好处,我们来看下nginx如何进行配置,gzip的配置可以在http块或者server块中:
1 |
|
对于一些简单的页面,我们想要通过密码来限制其他用户的访问,但是又不想接入复杂的账号体系,Nginx提供了简单的账号密码控制;首先我们通过Linux的工具创建一个密码本存放账号密码:
1 |
|
passwd/passwd
文件就是生成的密码文件,运行后会要求连续两次输入密码,成功后为admin用户添加了密码;然后我们就修改nginx的配置文件,对站点开启密码验证:
1 |
|
重启nginx,再次访问站点就会出现需要身份验证的弹框了。
上面我们介绍了正向代理和反向代理的区别,反向代理功能是nginx的三大主要功能之一(静态web服务器、反向代理、负载均衡)。反向代理不需要额外的模块,默认自带proxy_pass和fastcgi_pass指令,通过在location块中配置即可实现:
1 |
|
在配置proxy_pass时,我们需要注意url后面的/`;当我们通过下面几种情况访问
/proxy/home.html``时:
1 |
|
第一种情况url后面带上/,则会被代理到http://192.168.1.102:8080/home.html
。
1 |
|
第二种情况url后不带/,则会被代理到http://192.168.1.102:8080/proxy/home.html
1 |
|
第三种情况代理/doc/,则会被代理到http://192.168.1.102:8080/doc/home.html
1 |
|
第四种情况代理/doc,则会被代理到http://192.168.1.102:8080/dochome.html
在配置反向代理时,我们还可以修改代理请求的请求参数:
1 |
|
经过反向代理后,由于客户端和web服务器之间增加了一个代理层,因此web服务器无法拿到客户端请求的host和真实ip,我们通过proxy_set_header指令修改代理请求的头部;$host和$remote_addr是用户真实的host和ip,这里作为变量传入Host和X-Real-IP字段,因此我们在客户端服务器想要获取真实ip就可以通过request.getAttribute(“X-real-ip”)的方式。
随着互联网的发展,用户规模的增加,服务器的压力也越来越大,如果只使用一台服务器有时候不能承受流量的压力,这时我们就需要将部分流量分散到多台服务器上,使得每台服务器都均衡的承担压力。
nginx负载均衡目前支持六种策略:轮询策略、加权轮询策略、ip_hash策略、url_hash策略、fair策略和sticky策略;六种策略可以分为两大类,内置策略(轮询、加权轮询、ip_hash)和扩展策略(url_hash、fair、sticky);默认情况下内置策略自动编译在Nginx中,而扩展策略需要额外安装。
既然是负载,那么我们需要启用多台服务器;这里为了方便演示,我们在一台电脑上运行node脚本来模拟3台服务器;同时为了方便看到每台服务器有多少流量,每访问一次就计数一次:
1 |
|
然后我们修改端口号,这样我们就有8080、8081、8082三个服务器了。
轮询策略,顾名思义,就是按照请求顺序,逐一分配到不同的服务器节点;如果某台服务器出现问题,会自动剔除。
1 |
|
我们还是通过测试工具Apache Bench
来并发100个请求到Nginx:
1 |
|
最后统计每台服务器的结果,每台服务器的请求还是很平均的:
1 |
|
加权轮询在基本轮询策略上考虑各服务器节点接受请求的权重,指定服务器节点被轮询的权重,主要用于服务器节点性能不均的情况。
通过在server节点后配置weight来设置权重,weight的大小和访问比率成正比(weight的默认值为1);我们给三台服务器设置访问比是1:3:2
。
1 |
|
压力测试后统计服务器的请求结果,和我们配置的比率还是几乎相同的:
1 |
|
注:由于weight是内置,所以可以直接和其他策略配合使用。
ip_hash策略是将前端访问的ip进行hash操作后,然后根据hash的结果将请求分配到不同的节点上,这样使得每个ip都会固定访问服务节点;这样做的好处是用户的session只在一个后端服务器节点上,不必考虑一个session存在多台服务器节点出现session共享问题。
1 |
|
压力测试后统计服务器的请求结果,我们发现所有的请求都到固定一台服务器上了:
1 |
|
url_hash策略是将url地址进行hash操作,根据hash结果请求定向到同一服务器节点上;url_hash的优点是能够提高后端缓存服务器的效率。
1 |
|
压力测试后统计服务器的请求结果:
1 |
|
如果我们切换不同的url,/home、/list等,都会分配到不同的服务器节点。
fair策略请求转发到负载最小的后端服务器节点上。Nginx通过服务器节点对响应时间来判断负载情况,响应时间最短的节点负载就相对较轻,Nginx就会将前端请求转发到此服务器节点上。
注:fair策略默认不被编译进nginx内核,需要额外安装
1 |
|
压力测试后统计服务器的请求结果:
1 |
|
sticky策略是基于cookie的一种负载均衡解决方案,通过分发和识别cookie,使来自同一个客户端的请求落在同一台服务器上,默认cookie标识名为route。
sticky策略看起来和ip_hash策略类似,但是又有一定区别。假设在一个局域网内有3台电脑,他们有3个内网IP,但是他们发起请求时,却只有一个外网IP,如果使用ip_hash方式,则Nginx会将请求分配到同一服务器;如果使用sticky策略,则会把请求分配到不同服务器上,这是ip_hash无法做到的。
注:sticky策略默认不被编译进nginx内核,需要额外安装
1 |
|
sticky默认的cookie的名称是route
,我们可以通过name修改,还有一些其他的cookie参数可以进行修改:
我们通过浏览器来访问,在cookie中可以看到sticky下发的cookie
注:由于cookie最初由服务器端下发,如果客户端禁用cookie,则cookie不会生效。
upstream还有一些参数我们可以配合负载均衡:
参数 | 描述 |
---|---|
fail_timeout | 与max_fails结合使用 |
max_fails | 设置在fail_timeout参数设置的时间内最大失败次数,如果在这个时间内,所有针对该服务器的请求都失败了,那么认为该服务器会被认为是停机了 |
fail_time | 服务器会被认为停机的时间长度,默认为10s。 |
backup | 标记该服务器为备用服务器。当主服务器停止时,请求会被发送到它这里。 |
down | 标记服务器永久停机了。 |
keepalive | 连接数(keepalive的值)指定了每个工作进程中保留的持续连接到nginx负载均衡器缓存的最大值。如果超过这个设置值的闲置进程想链接到nginx负载均衡器组,最先连接的将被关闭。 |
1 |
|
Vite对其自身的定义为:
下一代前端开发与构建工具
在深入对比Webpack、Parcel、Rollup打包工具的不同一章中,我们分别详细的对比了Rollup、Parcel和Webpack之间的异同,也分析了每个打包工具使用的场景;那么Vite作为次时代的打包工具,我们先来看下它的优点:
我们看到Vite主打的特点就是极速服务启动,也就是一个字:快!俗话说得好,天下武功,无坚不摧,唯快不破;我们先来搭建第一个项目看下,通过--template
来指定预设的模板:
1 |
|
Vite还支持以下模板预设:
接着我们运行npm run dev
或者yarn dev
来启动服务器,可以看到服务器很快就启动了,不到400毫秒:
Vue预设模板的项目结构大概如下:
可以发现Vue预设模板的目录结构和vue-cli很相似,不同的是index.html文件的位置和配置文件,vite是vite.config.js
,而不是vue.config.js
。
最后就是我们的package.json
,来看下vite需要哪些依赖:
1 |
|
可以看到Vite的依赖非常简单,默认支持Vue3.0,@vitejs/plugin-vue
和@vue/compiler-sfc
都是Vue3.0的编译插件;如果想要支持Vue2.x,需要安装vite-plugin-vue2
插件。
我们知道vue-cli的页面模板是在/public/目录下,那么来看下vite根目录下index.html
有什么不同的地方:
1 |
|
我们发现这里多引用了一个main.js,并且还有一个type="module"
属性,那么这个属性有什么用呢?我们都知道我们代码中引用的ES模块必须要通过打包工具比如webpack等进行处理后才能在浏览器中进行使用;但是一些主流浏览器(Chrome、Edge、Safari、Firefox等)都在尝试原生支持ES模块,这是一个主要的新特性,也就是说我们在浏览器里就能直接使用ES的模块,不过让我们来看下浏览器对这个新特性的支持情况:
我们可以写个demo在浏览器上进行测试:
1 |
|
我们先定义好es模块,然后需要通过script的方式进行引用:
1 |
|
原生es和普通js脚本有写不同:
我们都知道,js的加载解析默认是会阻塞浏览器的,因此script标签一般都放在页面底部;但是我们可以给script加上defer来让js并发加载执行:
我们发现原生ES模块和加上defer属性的效果是一样的;vite利用了浏览器对原生ES模块的支持,跳过打包(no bundle)过程,将ES模块解析编译后直接提供给浏览器;只在必要请求时进行代码转换,这样自然就节省了费时费力的打包时间。
例如我们在请求首页home.vue模块时,只有在浏览器请求home.vue才将vue文件的template等解析编译,解析成浏览器可以执行的js返回。
我们看下官方给出的传统打包工具的打包过程,从入口一直解析庞大的模块,然后打包成bundle,最后才能启动服务器:
但是Vite可以直接启动服务器,加载入口的文件:
灰色部分的是暂时没有用到的模块,初始化不会参与构建,随着项目的路由越来越复杂,构建速度也不会变慢。
当我们首次运行项目时可能会发现下面的提示小字:
我们上面说过Vite是不依赖构建的,那这里为什么还需要预构建呢?这里官方给出了两个原因:
兼容CommonJS和UMD不用多说,就是为了模块引用规范的统一,对模块化规范不了解的童鞋可以看这篇深入学习CommonJS和ES6模块化规范;提升性能官方给了一个现成的案例就是lodash-es
依赖,当我们import { chunk } from "lodash-es"
,由于内部有600+个模块,相互导入,因此浏览器会去同时加载600+个http请求,虽然每个请求只有1~2kb,但是大量的请求也会造成网络堵塞;我们可以将lodash-es剔除预构建来看下效果:
1 |
|
我们在vite.config.js
中修改optimizeDeps.exclude
,然后就能在浏览器中看到效果:
我们看到虽然请求数据不多,但是架不住600+大量的请求,正所谓乱拳打死老师傅;如果通过预构建将这些模块都统一到一起,那么速度快了不是一点点;那么什么样的模块会进行预构建呢?
默认情况下,Vite会将package.json中生产依赖dependencies
的部分启用依赖预编译,即会先对该依赖进行编译,然后将编译后的文件缓存在内存中(node_modules/.vite文件下),在启动DevServer
时直接请求该缓存内容
很多情况下我们需要对打包的变量根据环境进行区分,比如请求的域名等,和vue-cli一样,vite也可以区分打包环境,不过它的变量比较特殊;我们知道它并不是通过webpack的DefinePlugin方式来定义全局变量,因此不能通过process.env
来获取;而是通过一个特殊的import.meta.env
对象来暴露,这个对象有一些公共的内在变量:
--mode
来设置Vite支持dotenv,可以从项目根目录的文件加载额外的环境变量:
1 |
|
加载的变量也会通过import.meta.env暴露给客户端代码,不过为了防止变量泄露,只有VITE_
为前缀的变量才会暴露。
这里引入一个模式的概念,默认情况下serve命令运行开发模式(development),而build命令会运行生产模式(production),但是我们可以通过env文件定义自己需要的模式;可以通过--mode
选项覆盖命令使用的默认模式。
比如我们项目在测试和正式环境之外可能还会设置一个预发环境,将一些线上的数据拷贝过来以便模拟真实场景,我们就可以定义一个staging模式:
1 |
|
我们可以将用到的环境配置放入.env.staging
文件:
1 |
|
这样staging模式就会打包和生产环境类似的代码,但是环境变量却是staging模式的,我们就成功新增了一种模式。
当我们运行vite时,默认会解析项目目录下的vite.config.js
地配置文件,基础的配置文件导出一个对象
1 |
|
但是我们一般会引入defineConfig
帮手函数,这样在没有jsdoc的配合下,也能获取类型提示:
1 |
|
如果我们的配置文件还需要根据环境或者模式的的不同来传递不同的插件或配置等,可以通过导出一个函数的方式:
1 |
|
对css预处理,Vite提供了开箱支持,我们只要安装对应的预处理依赖,无需配置,即可进行使用:
1 |
|
这样我们就可以通过<style lang="sass">
自动开启预处理了;针对postcss的功能,我们可以直接配置postcss.config.js
,它会自动应用所有的css,不过也需要安装对应的插件。
和webpack类似,resolve
字段用来表示如何来解析模块,首先我们看下常用的别名设置alias
:
1 |
|
这样我们就能用@
替换相对路径了,可以通过@
来引入组件:
1 |
|
还有mainFields
,主字段解析,标志vite默认从模块的package.json中哪个字段引入模块,默认配置是:
1 |
|
Vite提供了server
选项来配置开发服务器,默认情况只允许localhost访问,我们可以指定server.host
来让局域网主机也能访问:
1 |
|
如果我们想让服务器启动时自动在浏览器中打开应用程序,可以配置server.open
,配置类型boolean|string
,配置为字符串时,会被用作 URL 的路径名:
1 |
|
和webpack一样,我们可以通过server.proxy
来为开发服务器配置代理,如果key值以^
开头,则会被解析为正则表达式:
1 |
|
我们在使用element时,经常会需要按需引入组件,在vue-cli中使用的是babel的一个插件babel-plugin-component;vite有自己的按需引入插件vite-plugin-style-import
,首先我们安装一下:
1 |
|
然后在vite.config.js
中进行配置:
1 |
|
接下来如果我们只希望引入部分组件,就可以在main.js中加入:
1 |
|
尤大大在B站直播时分享了Vue3.0的几个亮点:
在性能方面,对比Vue2.x,性能提升了1.3~2倍左右;打包后的体积也更小了,如果单单写一个HelloWorld进行打包,只有13.5kb;加上所有运行时特性,也不过22.5kb。
那么作为终端用户的我们,在开发时,和Vue2.x有什么不同呢?Talk is cheap
,我们还是来看代码。
Vue3最重要的变化之一就是引入了Tree-Shaking,Tree-Shaking带来的bundle体积更小是显而易见的。在2.x版本中,很多函数都挂载在全局Vue对象上,比如$nextTick、$set等函数,因此虽然我们可能用不到,但打包时只要引入了vue这些全局函数仍然会打包进bundle中。
而在Vue3中,所有的API都通过ES6模块化的方式引入,这样就能让webpack或rollup等打包工具在打包时对没有用到API进行剔除,最小化bundle体积;我们在main.js中就能发现这样的变化:
1 |
|
创建app实例方式从原来的new Vue()
变为通过createApp函数进行创建;不过一些核心的功能比如virtualDOM更新算法和响应式系统无论如何都是会被打包的;这样带来的变化就是以前在全局配置的组件(Vue.component)、指令(Vue.directive)、混入(Vue.mixin)和插件(Vue.use)等变为直接挂载在实例上的方法;我们通过创建的实例来调用,带来的好处就是一个应用可以有多个Vue实例,不同实例之间的配置也不会相互影响:
1 |
|
因此Vue2.x的以下全局API也需要改为ES6模块化引入:
reactive
除此之外,vuex和vue-router也都使用了Tree-Shaking进行了改进,不过api的语法改动不大:
1 |
|
更多关于Tree-Shaking的使用可以在Webpack配置全解析中查看。
我们都知道,在Vue2.x中有8个生命周期函数:
在vue3中,新增了一个setup
生命周期函数,setup执行的时机是在beforeCreate
生命函数之前执行,因此在这个函数中是不能通过this
来获取实例的;同时为了命名的统一,将beforeDestroy
改名为beforeUnmount
,destroyed
改名为unmounted
,因此vue3有以下生命周期函数:
同时,vue3新增了生命周期钩子,我们可以通过在生命周期函数前加on
来访问组件的生命周期,我们可以使用以下生命周期钩子:
那么这些钩子函数如何来进行调用呢?我们在setup中挂载生命周期钩子,当执行到对应的生命周期时,就调用对应的钩子函数:
1 |
|
说完生命周期,下面就是我们期待的Vue3新增加的那些功能。
我们在深入学习Object.defineProperty和Proxy讲解过Proxy优点以及Vue3为什么改用Proxy实现响应式,同时Vue3也将一些响应式的API进行抽离,以便代码更好的复用。
我们可以使用reactive
来为JS对象创建响应式状态:
1 |
|
reactive相当于Vue2.x中的Vue.observable
。
reactive函数只接收object和array等复杂数据类型。
对于一些基本数据类型,比如字符串和数值等,我们想要让它变成响应式,我们当然也可以通过reactive函数创建对象的方式,但是Vue3提供了另一个函数ref
:
1 |
|
ref返回的响应式对象是只包含一个名为value参数的RefImpl对象,在js中获取和修改都是通过它的value属性;但是在模板中被渲染时,自动展开内部的值,因此不需要在模板中追加.value
。
1 |
|
reactive主要负责复杂数据结构,而ref主要处理基本数据结构;但是很多童鞋就会误解ref只能处理基本数据,ref本身也是能处理对象和数组的:
1 |
|
当我们处理一些大型响应式对象的property时,我们很希望使用ES6的解构来获取我们想要的值:
1 |
|
但是很遗憾,这样会消除它的响应式;对于这种情况,我们可以将响应式对象转换为一组ref,这些ref将保留与源对象的响应式关联:
1 |
|
对于一些只读数据,我们希望防止它发生任何改变,可以通过readonly
来创建一个只读的对象:
1 |
|
有时我们需要的值依赖于其他值的状态,在vue2.x中我们使用computed函数
来进行计算属性,在vue3中将computed功能进行了抽离,它接受一个getter函数,并为getter返回的值创建了一个不可变的响应式ref对象:
1 |
|
或者我们也可以使用get和set函数创建一个可读写的ref对象:
1 |
|
和computed相对应的就是watch,computed是多对一的关系,而watch则是一对多的关系;vue3也提供了两个函数来侦听数据源的变化:watch和watchEffect。
我们先来看下watch,它的用法和组件的watch选项用法完全相同,它需要监听某个数据源,然后执行具体的回调函数,我们首先看下它监听单个数据源的用法:
1 |
|
我们也可以把多个值放在一个数组中进行侦听,最后的值也以数组形式返回:
1 |
|
如果我们来侦听一个深度嵌套的对象属性变化时,需要设置deep:true
:
1 |
|
最后的打印结果可以发现都是改变后的值,这是因为侦听一个响应式对象始终返回该对象的引用,因此我们需要对值进行深拷贝:
1 |
|
一般侦听都会在组件销毁时自动停止,但是有时候我们想在组件销毁前手动的方式进行停止,可以调用watch返回的stop函数进行停止:
1 |
|
还有一个函数watchEffect也可以用来进行侦听,但是都已经有watch了,这个watchEffect和watch有什么区别呢?他们的用法主要有以下几点不同:
1 |
|
watchEffect会在页面加载时自动执行一次,追踪响应式依赖;在加载后定时器每隔1s执行时,watchEffect都会监听到数据的变化自动执行,每次执行都是获取到变化后的值。
Composition API(组合API)也是Vue3中最重要的一个功能了,之前的2.x版本采用的是Options API
(选项API),即官方定义好了写法:data、computed、methods,需要在哪里写就在哪里写,这样带来的问题就是随着功能增加,代码也越来复杂,我们看代码需要上下反复横跳:
上图中,一种颜色代表一个功能,我们可以看到
Options API
的功能代码比较分散;Composition API
则可以将同一个功能的逻辑,组织在一个函数内部,利于维护。
我们首先来看下之前Options API的写法:
1 |
|
Options API
就是将同一类型的东西放在同一个选项中,当我们的数据比较少的时候,这样的组织方式是比较清晰的;但是随着数据增多,我们维护的功能点会涉及到多个data和methods,但是我们无法感知哪些data和methods是需要涉及到的,经常需要来回切换查找,甚至是需要理解其他功能的逻辑,这也导致了组件难以理解和阅读。
而Composition API
做的就是把同一功能的代码放到一起维护,这样我们需要维护一个功能点的时候,不用去关心其他的逻辑,只关注当前的功能;Composition API
通过setup
选项来组织代码:
1 |
|
我们看到这里它接收了两个参数props和context,props就是父组件传入的一些数据,context是一个上下文对象,是从2.x暴露出来的一些属性:
注:props的数据也需要通过toRefs解构,否则响应式数据会失效。
我们通过一个Button按钮来看下setup具体的用法:
1 |
|
很多童鞋可能就有疑惑了,这跟我在data和methods中写没什么区别么,不就是把他们放到一起么?我们可以将setup
中的功能进行提取分割成一个一个独立函数,每个函数还可以在不同的组件中进行逻辑复用:
1 |
|
所谓的Fragment,就是片段;在vue2.x中,要求每个模板必须有一个根节点,所以我们代码要这样写:
1 |
|
或者在Vue2.x中还可以引入vue-fragments
库,用一个虚拟的fragment代替div;在React中,解决方法是通过的一个React.Fragment
标签创建一个虚拟元素;在Vue3中我们可以直接不需要根节点:
1 |
|
这样就少了很多没有意义的div元素。
Teleport翻译过来就是传送、远距离传送的意思;顾名思义,它可以将插槽中的元素或者组件传送到页面的其他位置:
在React中可以通过createPortal
函数来创建需要传送的节点;本来尤大大想起名叫Portal
,但是H5原生的Portal标签
也在计划中,虽然有一些安全问题,但是为了避免重名,因此改成Teleport
。
Teleport一个常见的使用场景,就是在一些嵌套比较深的组件来转移模态框的位置。虽然在逻辑上模态框是属于该组件的,但是在样式和DOM结构上,嵌套层级后较深后不利于进行维护(z-index等问题);因此我们需要将其进行剥离出来:
1 |
|
这里的Teleport中的modal div就被传送到了body的底部;虽然在不同的地方进行渲染,但是Teleport中的元素和组件还是属于父组件的逻辑子组件,还是可以和父组件进行数据通信。Teleport接收两个参数to
和disabled
:
Suspense是Vue3推出的一个内置组件,它允许我们的程序在等待异步组件时渲染一些后备的内容,可以让我们创建一个平滑的用户体验;Vue中加载异步组件其实在Vue2.x中已经有了,我们用的vue-router中加载的路由组件其实也是一个异步组件:
1 |
|
在Vue3中重新定义,异步组件需要通过defineAsyncComponent
来进行显示的定义:
1 |
|
同时对异步组件的可以进行更精细的管理:
1 |
|
这样我们对异步组件加载情况就能掌控,在加载失败也能重新加载或者展示异常的状态:
我们回到Suspense,上面说到它主要是在组件加载时渲染一些后备的内容,它提供了两个slot插槽,一个default
默认,一个fallback
加载中的状态:
1 |
|
非兼容的功能主要是一些和Vue2.x版本改动较大的语法,已经在Vue3上可能存在兼容问题了。
在Vue2.x中,我们可以定义data为object
或者function
,但是我们知道在组件中如果data是object的话会出现数据互相影响,因为object是引用数据类型;
在Vue3中,data只接受function
类型,通过function
返回对象;同时Mixin
的合并行为也发生了改变,当mixin和基类中data合并时,会执行浅拷贝合并:
1 |
|
我们看到最后合并的结果,vue2.x会进行深拷贝,对data中的数据向下深入合并拷贝;而vue3只进行浅层拷贝,对data中数据发现已存在就不合并拷贝。
在vue2.x中,我们还可以通过过滤器filter
来处理一些文本内容的展示:
1 |
|
最常见的就是处理一些订单的文案展示等;然而在vue3中,过滤器filter已经删除,不再支持了,官方建议使用方法调用或者计算属性computed
来进行代替。
在Vue2.x中,v-model
相当于绑定value
属性和input
事件,它本质也是一个语法糖:
1 |
|
在某些情况下,我们需要对多个值进行双向绑定,其他的值就需要显示的使用回调函数来改变了:
1 |
|
在vue2.3.0+版本引入了.sync
修饰符,其本质也是语法糖,是在组件上绑定@update:propName
回调,语法更简洁:
1 |
|
Vue3中将v-model
和.sync
进行了功能的整合,抛弃了.sync,表示:多个双向绑定value值直接用多个v-model传就好了;同时也将v-model默认传的prop名称由value改成了modelValue:
1 |
|
如果我们想通过v-model传递多个值,可以将一个argument
传递给v-model:
1 |
|
在Vue2.x中,我们都知道v-for每次循环都需要给每个子节点一个唯一的key,还不能绑定在template标签上,
1 |
|
而在Vue3中,key值应该被放置在template标签上,这样我们就不用为每个子节点设一遍:
1 |
|
在vue2.x中,如果一个元素同时定义了v-bind="object"
和一个相同的单独的属性,那么这个单独的属性会覆盖object
中的绑定:
1 |
|
然而在vue3中,如果一个元素同时定义了v-bind="object"
和一个相同的单独的属性,那么声明绑定的顺序决定了最后的结果(后者覆盖前者):
1 |
|
vue2.x中,在v-for上使用ref
属性,通过this.$refs
会得到一个数组:
1 |
|
但是这样可能不是我们想要的结果;因此vue3不再自动创建数组,而是将ref的处理方式变为了函数,该函数默认传入该节点:
1 |
|
在vue2.x中,在一个元素上同时使用v-for和v-if,v-for
有更高的优先级,因此在vue2.x中做性能优化,有一个重要的点就是v-for和v-if不能放在同一个元素上。
而在vue3中,v-if
比v-for
有更高的优先级。因此下面的代码,在vue2.x中能正常运行,但是在vue3中v-if生效时并没有item
变量,因此会报错:
1 |
|
以上就是Vue3.0作为终端用的我们可能会涉及到的一些新特性和新功能,其实Vue3.0还有很多的改动,这里由于篇幅原因就不一一展开了,大家可以自行查阅官方文档,期待Vue3能带给我们更便利更友好的开发体验。
]]>ESLint是一个插件化的代码检测工具,正如它官网描述的slogan:
可组装的JavaScript和JSX检查工具
ESLint不仅可以检测JS,还支持JSX和Vue,它的高可扩展性让它能够支持更多的项目。
提到ESLint,我们就不得不提及他的前辈们JSLint和JSHint,以及它们的区别;首先就是JSLint,它是由Douglas Crockford
开发的;JSLint的灵感来源于C语言的检查工具Lint
,Lint最初被发明用来扫描C语言源文件以便找到其中的错误,后来随着语言的成熟以及编译器能够更好的找到问题,Lint工具也逐渐不再被需要了。
JavaScript最开始被发明只是用来在网页上做一些简单的工作(点击事件、表单提交等),随着JS语言的发展完善以及项目复杂程度的增加,急需一个用来检查JS语法或者其他问题的校验工具,因此JSLint
就诞生了;它是由Douglas Crockford
在2010年开源的第一款针对JS的语法检测工具,它和Lint做着相同的事,扫描JS的源文件来找到错误;它内部也是通过fs.readFile
来读取文件然后逐行来进行检查。
我们可以在全局安装jslint,然后jslint source.js
对我们的代码进行检查;JSLint刚开始确实帮助很多JS开发者节省了不少排查错误的时间,但是JSLint的问题也很明显:所有的配置项都内置不可配置,因此你要用JSLint只能遵循Douglas Crockford
老爷子自己定义的代码风格和规范;再加上他本身推崇爱用不用
的传统,不像开发者开放配置或者修改他觉得对的规则,因此很多人也无法忍受他的规则。
由于JSLint让很多人无法忍受,所以Anton Kovalyov
基于JSLint开发了JSHint,它的初衷就是为了能让开发者自定义规则lint rules
,因此提供了丰富的配置项,给开发者极大的自由;同时它也提供了一套相当完善的编辑器插件,我们常用的VIM、Sublime、Atom、Vs Code等都有插件支持,方便开发。
JSHint一开始就保持了开源软件的风格,并且由社区来驱动,因此一推出就很快发展起来,我们熟知的一些项目或者公司也使用了JSHint,比如:Facebook、Google、Jquery、Disqus等。
JSHint相比于JSLint,最大的特点就是可配置,我们可以在项目中放入一个.jshintrc
的配置文件,JSLint就会加载配置文件用于代码分析,配置文件的部分内容如下:
1 |
|
由于JSHint是基于JSLint开发的,因此JSLint的一些问题也继承下来了,比如不易扩展以及不容易直接根据报错定位到具体的配置规则等;在2013年,Zakas
大佬发现JSHint无法满足自己定制化规则的需要,因此设想开发一个基于AST的Linter,可以动态执行额外的规则,同时可以很方面的扩展规则,于是在13年6月份开源推出了全新的ESLint。
ESLint号称下一代的JS Linter工具,它的灵感来源于PHP Linter,将源码解析成AST,然后检测AST是否符合规则;ESLint最开始使用esprima
解析器将源码解析成AST,然后就可以使用任意规则来检测AST是否符合预期,这也是ESLint高可扩展的原因。
刚开始ESlint的推出并没有撼动JSHint的霸主地位,由于ESlint需要将源码转为AST,而JSHint直接检测源文件字符串,因此执行速度比JSHint慢很多;真正让ESLint实现弯道超车的是ES6的出现。
2015年,ES6规范发布后,由于大部分浏览器支持程度不高,因此需要Babel将代码转换编译成ES5或者更低版本;同时由于ES6变化很大,短期内JSHint无法完全支持,这时ESLint的高扩展性的优点显现出来了,不仅可以扩展规则,连默认的解析器也能替换;Babel团队就为ESLint开发了babel-eslint
替换默认的解析器esprima
,让ESLint率先支持ES6。
ESLint被设计成完全可配置的,我们可以用多种方式配置它的规则,或者配置要检测文件的范围。
如果想在现有的项目中引入eslint,我们可以在项目中进行初始化:
1 |
|
在经过一系列问答后,会在项目根目录创建一个我们熟悉的.eslintrc.js
配置文件;安装后就可以通过命令行对项目中的文件需要检测了:
1 |
|
一般我们会把eslint命令行配置到packages.json
中:
1 |
|
这里有一个--fix
后缀,是ESLint提供自动修复基础错误的功能,我们运行lint:fix
后发现有一些报错信息消失了,代码也改变了;不过它只能修复一些基础的不影响代码逻辑的错误,比如代码末尾加上分号、表达式的空格等等。
ESLint默认只会检测.js
后缀的文件,如果我们想对更多类型的文件进行检测,比如.vue、.jsx,可以使用--ext
选项,参数用逗号分隔:
1 |
|
对于一些公共的js,或者测试脚本,不需要进行检测,我们可以通过在项目根目录创建一个.eslintignore
告诉ESLint去忽略特定的目录或者文件:
1 |
|
除了.eslintignore
中指定的文件或目录,ESLint总是忽略/node_modules/*
和/bower_components/*
中的文件;因此对于一些目前解决不了的规则报错,但是我们需要打包上线,在不影响运行的情况下,我们就可以利用.eslintignore
文件将其暂时忽略。
ESLint一共有两种配置方式,第一种方式是直接把lint规则嵌入源代码中;
1 |
|
eqeqeq
代表eslint校验规则,error代表校验报错级别,后面会详细说明;这个eslint校验规则只会对该文件生效:
我们还可以使用其他注释,更精确地管理eslint对某个文件或某一行代码的校验:
1 |
|
第二种方式是直接把lint规则放到我们的配置文件中,上面init初始化生成的.eslintrc.js
就是一个配置文件,官方还提供了其他几种配置文件名称(优先级从上到下):
1 |
|
一般情况下我们使用.eslintrc.js
就可以了。
我们详细看下.eslintrc.js
文件内部有哪些配置选项:
1 |
|
首先是我们的globals
,ESLint会检测未声明的变量,并发出报错,比如node环境中的process,浏览器环境下的全局变量console,以及我们通过cdn引入的jQuery定义的$等;我们可以在globals
中进行变量声明:
1 |
|
但是node或者浏览器中的全局变量很多,如果我们一个个进行声明显得繁琐,因此就需要用到我们的env
,这是对环境定义的一组全局变量的预设:
1 |
|
更多的环境参数可以看ESLint声明环境。
然后就是我们的解析器parse
和parserOptions
;我们上面说到ESLint可以更换解析器,"parse": "babel-eslint"
就是用来指定要使用的解析器,它有以下几个选择:
那么这几个解析器怎么选择呢?如果你想使用一些先进的语法(ES6789),就使用babel-eslint(需要npm安装);如果你想使用typescript,就使用@typescript-eslint/parser。
选好了解析器,我们可以通过parserOptions
给解析器传入一些其他的配置参数:
1 |
|
ESLint可以配置大量的规则,我们可以在配置文件的rules
属性自定义需要的规则:
1 |
|
对于检验规则,有3个报错等级:
有些规则没有属性,只需控制开启还是关闭;有些规则可以传入属性,我们通过数组的方式传入参数:
1 |
|
对于刚接触ESLint的同学,看到这么多的规则肯定很懵逼,难道要一条一条来记么?肯定不是的;项目的ESLint配置文件并不是一次性完成的,而是在项目开发中慢慢完善起来的,因为并不是所有的规则都是我们项目所需要的。因此我们可以先进行编码,在编码的过程中使用npm run lint
校验代码规范,如果报错,可以通过报错信息去详细查看是那一条规范报错:
比如这里的报错no-unused-vars
我们可以看到它来自第六行,再去文档查找,发现是我们在js中有一个定义了却未使用的变量;在团队协商后可以进一步来确定项目是否需要这条规范。
如果每条规则都需要团队协商配置还是比较繁琐的,在项目开始配置时,我们可以先使用一些业内已经成熟的、大家普遍遵循的编码规范(最佳实践);我们可以通过extends
字段传入一些规范,它接收String/Array:
1 |
|
extends可以使用以下几种类型的扩展:
eslint:recommended
(推荐规范)和eslint:all
(所有规范)。eslint-config-
,比如上面的可以直接写成standard
需要注意的是:多个扩展中有相同的规则,以后面引入的扩展中规则为准。
eslint:recommended
推荐使用的规则在规则列表的右侧用绿色√
标记。
插件类型的扩展一般先通过npm安装插件,以上面的vue为例,我们先来安装:
1 |
|
安装后一个插件中会有很多同类型扩展可供选择,比如vue就有以下几种扩展:
针对扩展中的规则,我们也能够通过rules来对它进行覆写:
1 |
|
除了上面的eslint-config-standard
,还有以下几个比较知名的编码规范:
不过需要注意的是,很多规范不仅需要安装扩展本身,还需要配合插件,比如eslint-config-standard
,我们还需要安装下面几个插件才能有效:
1 |
|
在Webpack中,插件是用来扩展功能,让其能够处理更多的文件类型以及功能,ESLint中的插件也是同样的作用;虽然ESLint提供了几百种规则可供选择,但是随着JS框架和语法的发展,这么多规则还是显得不够,因为官方的规则只能检查标准的JS语法;如果我们写的是vue或者react的jsx,那么ESLint就不能检测了。
这时就需要安装ESLint插件,用来定制一些特色的规则进行检测;eslint插件以eslint-plugin-
开头,使用时可以省略;比如我们上面检测.vue
文件就用到eslint-plugin-vue
插件;需要注意的是,我们在配置eslint-plugin-vue
这个插件时,如果仅配置"plugins": ["vue"]
,vue文件中template内容还是会解析失败。
这是因为不管是默认的espree还是babel-eslint解析器都无法解析.vue中template的内容;eslint-plugin-vue
插件依赖vue-eslint-parser
解析器,而vue-eslint-parser
解析器只会解析template内容,不会检测script标签
中的JS内容,因此我们还需要指定一下解析器:
1 |
|
上面parserOptions.parser
不少同学肯定看的有点迷糊,这是由于外层的解析器只能有一个,我们已经用了vue-eslint-parser
就不能再写其他的;因此vue-eslint-parser
的做法是在解析器选项中再传入一个解析器选项用来处理script
中的JS内容。
如果想让ESLint检测vue文件,确保将
.vue
后缀加入--ext
选项中。
而react配置则较为简单了,引入插件,选择对应的扩展规则即可:
1 |
|
虽然ESLint会对我们的代码格式进行一些检测(比如分号、单双引号等),但是并不能完全统一代码风格,我们还需要一个工具Prettier;Prettier是什么?Prettier是一个支持很多语言的代码格式化工具,官网用了一个“贬义”的单词来形容它opinionated
,翻译过来就是固执己见的。
Prettier还有以下四个特点:
那么为什么Prettier要用opinionated这个词呢?每个团队成员可能会用不同的编辑器或是不同的插件,每个插件也会有自己的格式化规范,这样就导致了我们在开发时代码风格极大的不统一,甚至造成不必要的冲突;Prettier就给我们定义好了风格,按照它的风格来(是不是很像JSLint);但是又没有完全封闭,开放了一些必要的设置,这也是最后一点few options
的含义;因此我们只需要将代码的美化交给Prettier来做就好了。
首先还是安装,我们将所需的插件进行安装,这里用到prettier的三个包:
1 |
|
首先就是这个eslint-plugin-prettier插件,它会调用prettier对你的代码风格进行检查,其原理是先使用prettier对你的代码进行格式化,然后与格式化之前的代码进行对比,如果过出现了不一致,这个地方就会被prettier进行标记。
被标记后Prettier并不会有任何提示,我们还需要对标记后的代码进行报错处理,在rules
中进行添加配置:
1 |
|
如果不希望Prettier影响项目打包,我们也可以将prettier的报错由error改为warn
借助ESLint的自动修复--fix
,我们可以修复这种简单的样式问题;那如果我们想自定义一些样式怎么办呢?没关系,虽然Prettier是一个固执己见的工具,但是人家也是开放了一些配置可供我们进行自定义的,我们可以在项目中新建一个.prettierrc.json
文件:
1 |
|
这里简单贴一些常用的,我们可以在官网选项配置找到更多的配置规则。
这样配置后虽然能修复代码了,但是如果遇到另一个也固执己见的扩展,比如我们引入eslint-config-standard
这个扩展,它也有自己的代码风格;如果通过Prettier格式化,standard不干了;如果通过standard自动修复,那么Prettier又要报错了,两边都是大爷这可咋整呢?
机智的Prettier已经帮我们考虑到这个问题了,利用extends中最后一个覆盖前面扩展的特性,我们将eslint-config-prettier
配置在extends最后,就能够关闭一些与Prettier的规则:
1 |
|
另外eslint-plugin-prettier插件也附带有plugin:prettier/recommended
扩展配置,可以同时启用插件和eslint-config-prettier扩展,因此我们可以只需要配置recommended就可以了:
1 |
|
Vue中为了支持Prettier,也将eslint-plugin-prettier和eslint-config-prettier整合到一起,放到了node_modules/@vue/eslint-config-prettier
目录中(加了一层作用域),因此我们在Vue脚手架生成的项目经常能看到@vue/prettier
这个扩展,打开它的目录发现其本质是一样的:
1 |
|
一般在我们的印象里,单元测试都是测试工程师的工作,前端负责代码就行了;百度搜索Vue单元测试,联想词出来的都是“单元测试有必要吗?”
“单元测试是做什么的?”
虽然我们平时项目中一般都会有测试工程师来对我们的页面进行测试“兜底”,但是根据我的观察,一般测试工程师并不会覆盖所有的业务逻辑,而且有一些深层次的代码逻辑测试工程师在不了解代码的情况下也根本无法进行触发。因此在这种情况下,我们并不能够完全的依赖测试工程师对我们项目测试,前端项目的单元测试就显得非常的有必要。
而且单元测试也能够帮助我们节省很大一部分自我测试的成本,假如我们有一个订单展示的组件,根据订单状态的不同以及其他的一些业务逻辑来进行对应文案的展示;我们想在页面上查看文案展示是否正确,这时就需要繁琐的填写下单信息后才能查看;如果第二天又又加入了一些新的逻辑判断(你前一天下的单早就过期啦),这时你有三个选择,第一种选择就是再次繁琐地填写订单并支付完(又给老板提供资金支持了),第二种选择就是死皮赖脸的求着后端同事给你更改订单状态(后端同事给你一个白眼自己体会),第三种选择就是代理接口或者使用mock数据(你需要编译整个项目运行进行测试)。
这时,单元测试就提供了第四种成本更低的测试方式,写一个测试用例,来对我们的组件进行测试,判断文案是否按照我们预想的方式进行展示;这种方式既不需要依赖后端的协助,也不需要对项目进行任何改动,可谓是省时又省力。
说到单元测试,我们首先来介绍一下流行的测试框架,主要是mocha和jest。先简单介绍下mocha,翻译成中文就是摩卡
(人家是一种咖啡!不是抹茶啊),名字的由来估猜是因为开发人员喜欢喝摩卡咖啡,就像Java名字也是从咖啡由来一样,mocha的logo也是一杯摩卡咖啡:
和jest相比,两者主要的不同就是jest内置了集成度比较高的断言库expect.js
,而mocha需要搭配额外的断言库,一般会选择比较流行的chai
作为断言库,这里一直提到断言库,那么什么是断言库呢?我们首先来看下mocha是怎么来测试代码的,首先我们写了一个addNum函数
,但是不确定是否返回我们想要的结果,因此需要对这个函数进行测试:
1 |
|
然后就可以写我们的测试文件了,所有的测试文件都放在test目录下,一般会将测试文件和所要测试的源码文件同名,方便进行对应,运行mocha时会自动对test目录下所有js文件进行测试:
1 |
|
上面这段代码就是测试脚本的语法,一个测试脚本会包括一个或多个describe
块,每个describe
又包括一个或多个it
块;这里describe
称为测试套件
(test suite),表示一组相关的测试,它包含了两个参数,第一个参数是这个测试套件
的名称,第二个参数是实际执行的函数。
而it
称为测试用例
,表示一个单独的测试,是测试的最小单位,它也包含两个参数,第一个参数是测试用例
的名称,第二个参数是实际执行的函数。
it
块中就是我们需要测试的代码,如果运行结果不是我们所预期的就抛出异常;上面的测试用例写好后,我们就可以运行测试了,
运行结果通过了,是我们想要的结果,说明我们的函数是正确的;但是每次都通过抛出异常来判断,多少有点繁琐了,断言库就出现了;断言的目的就是将测试代码运行后和我们的预期做比较,如果和预期一致,就表明代码没有问题;如果和预期不一致,就是代码有问题了;每一个测试用例最后都会有一个断言进行判断,如果没有断言,测试就没有意义了。
上面也说了mocha一般搭配chai断言库,而chai有好几种断言风格,比较常见的有should和expect两种风格,我们分别看下这两种断言:
1 |
|
这里should是后置的,在断言变量之后,而expect是前置的,作为断言的开始,两种风格纯粹看个人喜好;我们发现这里expect是从chai中获取的一个函数,而should则是直接调用,这是因为should实际上是给所有的对象都扩充了一个 getter
属性should
,因此我们才能够在变量上使用.should
方式来进行断言。
和chai的多种断言风格不同,jest内置了断言库expect,它的语法又有些不同:
1 |
|
jest中的expect直接通过toBe
的语法,在形式上相较于mocha更为简洁;这两个框架在使用上极其相似,比如在异步代码上都支持done
回调和async/await
关键字,在断言语法和其他用法有些差别;两者也有相同的钩子机制,连名字都相同beforeEach和afterEach;在vue cli脚手架创建项目时,也可以在两个框架中进行选择其一,我们这里主要以jest进行测试。
Jest是Facebook出品的一个测试框架,相较于其他测试框架,最大的特点就是内置了常用的测试工具,比如自带断言、测试覆盖率工具,实现了开箱即用,这也和它官方的slogan相符。
Jest 是一个令人愉快的 JavaScript 测试框架,专注于
简洁明快
。
Jest几乎是零配置的,它会自动识别一些常用的测试文件,比如*.spec.js
和 *.test.js
后缀的测试脚本,所有的测试脚本都放在tests
或__tests__
目录下;我们可以在全局安装jest或者局部安装,然后在packages.json中指定测试脚本:
1 |
|
当我们运行npm run test
时会自动运行测试目录下所有测试文件,完成测试;我们在jest官网可能还会看到通过test函数写的测试用例:
1 |
|
和it函数相同,test函数也代表一个测试用例,mocha只支持it
,而jest支持it
和test
,这里为了和jest官网保持统一,下面代码统一使用test
函数。
我们经常需要对测试代码返回的值进行匹配测试,上面代码中的toBe
是最简单的一个匹配器,用来测试两个数值是否相同。
1 |
|
toBe函数内部使用了Object.is
来进行精确匹配,它的特性类似于===
;对于普通类型的数值可以进行比较,但是对于对象数组等复杂类型,就需要用到toEqual
来比较了:
1 |
|
我们有时候还需要对undefined、null等类型或者对条件语句中的表达式的真假进行精确匹配,Jest也有五个函数帮助我们:
1 |
|
toBeTruthy和toBeFalsy用来判断在if语句中的表达式是否成立,等价于`if(n)和
if(!n)``的判断。
对于数值类型的数据,我们有时候也可以通过大于或小于来进行判断:
1 |
|
浮点类型的数据虽然我们也可以用toBe和toEqual来进行比较,但是如果遇到有些特殊的浮点数据计算,比如0.1+0.2就会出现问题,我们可以通过toBeCloseTo
来判断:
1 |
|
对于数组、set或者字符串等可迭代类型的数据,可以通过toContain
来判断内部是否有某一项:
1 |
|
我们项目中经常也会涉及到异步代码,比如setTimeout、接口请求等都会涉及到异步,那么这些异步代码怎么来进行测试呢?假设我们有一个异步获取数据的函数fetchData
:
1 |
|
在2秒后通过回调函数返回了一个字符串,我们可以在测试用例的函数中使用一个done
的参数,Jest会等done回调后再完成测试:
1 |
|
我们将一个回调函数传入fetchData,在回调函数中对返回的数据进行断言,在断言结束后需要调用done;如果最后没有调用done,那么Jest不知道什么时候结束,就会报错;在我们日常代码中,都会通过promise来获取数据,将我们的fetchData
进行一下改写:
1 |
|
Jest支持在测试用例中直接返回一个promise,我们可以在then中进行断言:
1 |
|
除了直接将fetchData返回,我们也可以在断言中使用.resolves/.rejects
匹配符,Jest也会等待promise结束:
1 |
|
除此之外,Jest还支持async/await
,不过我们需要在test的匿名函数加上async修饰符
表示:
1 |
|
全局挂载和卸载有点类似Vue-Router的全局守卫,在每个导航触发前和触发后做一些操作;在Jest中也有,比如我们需要在每个测试用例前初始化一些数据,或者在每个测试用例之后清除数据,就可以使用beforeEach
和afterEach
:
1 |
|
这样,每个测试用例进行测试前都会调用init,每次结束后都会调用clear;我们有可能会在某些test
中更改cityList的数据,但是在beforeEach
进行初始化的操作后,每个测试用例获取的cityList数据就保证都是相同的;和上面一节异步代码一样,在beforeEach
和afterEach
我们也可以使用异步代码来进行初始化:
1 |
|
和beforeEach
和afterEach
相对应的就是beforeAll
和afterAll
,区别就是beforeAll
和afterAll
只会执行一次;beforeEach
和afterEach
默认会应用到每个test,但是我们可能希望只针对某些test,我们可以通过describe
将这些test放到一起,这样就只应用到describe
块中的test:
1 |
|
在项目中,一个模块的函数内常常会去调用另外一个模块的函数。在单元测试中,我们可能并不需要关心内部调用的函数的执行过程和结果,只想知道被调用模块的函数是否被正确调用,甚至会指定该函数的返回值,因此模拟函数十分有必要。
如果我们正在测试一个函数forEach,它的参数包括了一个回调函数,作用在数组上的每个元素:
1 |
|
为了测试这个forEach,我们需要构建一个模拟函数,来检查模拟函数是否按照预期被调用了:
1 |
|
我们发现在mockCallback有一个特殊的.mock
属性,它保存了模拟函数被调用的信息;我们打印出来看下:
它有四个属性:
在上面属性中有一个instances
属性,表示了函数的this指向,我们还可以通过bind
函数来更改我们模拟函数的this:
1 |
|
通过bind更改函数的this之后,我们可以用instances
来进行检测;模拟函数可以在运行时将返回值进行注入:
1 |
|
我们第一次执行myMock,由于没有注入任何返回值,然后通过mockReturnValueOnce
和mockReturnValue
进行返回值注入,Once只会注入一次;模拟函数在连续性函数传递返回值时使用注入非常的有用:
1 |
|
我们还可以对模拟函数的调用情况进行断言:
1 |
|
除了能对函数进行模拟,Jest还支持拦截axios返回数据,假如我们有一个获取用户的接口:
1 |
|
现在我们想要测试fetchUserData
函数获取数据但是并不实际请求接口,我们可以使用jest.mock
来模拟axios模块:
1 |
|
一旦我们对模块进行了模拟,我们可以用get函数提供一个mockResolvedValue方法,以返回我们需要测试的数据;通过模拟后,实际上axios并没有去真正发送请求去获取/user.json
的数据。
Vue Test Utils是Vue.js官方的单元测试实用工具库,能够对我们编写的Vue组件进行测试。
在Vue中我们通过import
引入组件,然后在components
进行注册后就能使用;在单元测试中,我们使用mount
来进行挂载组件;假如我们写了一个计数器组件counter.js
,用来展示count,并且有一个按钮操作count:
1 |
|
组件进行挂载后得到一个wrapper(包裹器),wrapper会暴露很多封装、遍历和查询其内部的Vue组件实例的便捷的方法。
1 |
|
我们可以通过wrapper.vm
来访问组件的Vue实例,进而获取实例上的methods和data等;通过wrapper,我们可以对组件的渲染情况做断言:
1 |
|
上面几个函数我们根据名字也能猜出它们的作用:
find返回的是查找的第一个DOM节点,但有些情况我们希望能操作一组DOM,我们可以用findAll
函数:
1 |
|
有些组件需要通过外部传入的props、插槽slots、provide/inject等其他的插件或者属性,我们在mount挂载时可以传入一个对象,设置这些额外属性:
1 |
|
stubs
主要用来处理在全局注册的自定义组件,比如我们常用的组件库Element等,直接使用el-button
、el-input
组件,或者vue-router注册在全局的router-view
组件等;当我们在单元测试中引入时就会提示我们对应的组件找不到,这时我们就可以通过这个stubs
来避免报错。
我们在对某个组件进行单元测试时,希望只针对单一组件进行测试,避免子组件带来的副作用;比如我们在父组件ParentComponent
中判断是否有某个div时,恰好子组件ChildComponent
也渲染了该div,那么就会对我们的测试带来一定的干扰;我们可以使用shallowMount
挂载函数,相遇比mount,shallowMount不会渲染子组件:
1 |
|
这样就保证了我们需要测试的组件在渲染时不会渲染其子组件,避免子组件的干扰。
我们经常需要对子组件中的元素或者子组件的数据进行一些操作和修改,比如页面的点击、修改data数据,进行操作后再来断言数据是否正确;我们以一个简单的Form组件为例:
1 |
|
我们可以向Form表单组件传入一个title,作为表单的名称,其内部也有input、radio和checkbox等一系列元素,我们就来看下怎么对这些元素进行修改;首先我们来修改props的值,在组件初始化的时候我们传入了propsData
,在后续的代码中我们可以通过setProps
对props值进行修改:
1 |
|
我们满怀期待进行测试,但是发现最后一条断言报错了;这是因为Vue异步更新数据,我们改变prop和data后,获取dom发现数据并不会立即更新;在页面上我们一般都会通过$nextTick
进行解决,在单元测试时,我们也可以使用nextTick配合获取DOM:
1 |
|
和Jest中测试异步代码一样,我们也可以使用done回调
或者async/await
来进行异步测试;除了设置props,setData
可以用来改变wrapper中的data:
1 |
|
对于input、textarea或者select这种输入性的组件元素,我们有两种方式来改变他们的值:
1 |
|
可以看出,通过input.element.value
或者setValue
的两种方式改变值后,由于v-model绑定关系,因此vm中的data数据也进行了改变;我们还可以通过input.element.value
来获取input元素的值。
对于radio、checkbox选择性的组件元素,我们可以通过setChecked(Boolean)
函数来触发值的更改,更改同时也会更新元素上v-model绑定的值:
1 |
|
对于按钮等元素,我们希望在上面触发点击操作,可以使用trigger
进行触发:
1 |
|
对于一些组件,可能会通过$emit
触发一些返回数据,比如我们改写上面Form表单中的submit按钮,点击后返回一些数据:
1 |
|
除了触发组件中元素的点击事件进行$emi,我们还可以通过wrapper.vm
触发,因为vm本身相当于组件的this
:
1 |
|
最后,所有$emit触发返回的数据都存储在wrapper.emitted()
,它返回了一个对象;结构如下:
1 |
|
emitted()
返回对象中的属性是一个数组,数组的length代表了这个方法被触发了多少次;我们可以对对象上的属性进行断言,来判断组件的emit是否被触发:
1 |
|
我们也可以把emitted()
函数进行改写,并不是一次性获取整个emitted对象
:
1 |
|
有一些组件触发emit事件可能是由其子组件触发的,我们可以通过子组件的vm进行emit:
1 |
|
在有些组件中,我们有可能会用到Vue-Router
的相关组件或者Api方法,比如我们有一个Header组件:
1 |
|
直接在测试脚本中引入会报错,提示找不到router-link
和router-view
两个组件和$route
属性;这里不推荐使用Vue.use(VueRouter)
,因为会污染全局的Vue;我们有两种方法解决,第一种使用createLocalVue
创建一个Vue的类,我们可以在这个类中进行添加组件、混入和安装插件而不会污染全局的Vue类:
1 |
|
我们来看下这里做了哪些操作,通过createLocalVue
创建了一个localVue,相当于import Vue
;然后localVue.use
告诉Vue来使用VueRouter,和Vue.use
有着相同的作用;最后实例化创建router
对象传入shallowMount进行挂载。
第二种方式是注入伪造数据,这里主要用的就是mocks
和stubs
,mocks
用来伪造$route和$router等全局对象,是一种将属性添加到Vue.prototype
上的方式;而stubs
用来覆写全局或局部注册的组件:
1 |
|
相比于第一种方式,第二种方式可操作性更强,可以直接伪造$route路由的数据;一般第一种方式不会单独使用,经常会搭配第二种伪造数据的方式。
我们通常会在组件中会用到vuex,我们可以通过伪造store
数据来模拟测试,假如我们有一个的count组件,它的数据存放在vuex中:
1 |
|
在vuex中我们通过mutations
对number进行修改:
1 |
|
那我们现在如何来伪造store
数据呢?这里和Vue-Router
的原理是一样的,通过createLocalVue
创建一个隔离的Vue类:
1 |
|
我们看一下这里做了什么操作,前面和VueRouter一样创建一个隔离类localVue
;然后通过new Vuex.Store
创建了一个store并填入假数据state和mutations;这里我们并不关心mutations中函数做了哪些操作,我们只要知道元素点击触发了哪个mutations函数,通过伪造的函数我们去断言mutations是否被调用。
另一种测试store
数据的方式是创建一个运行中的store,不再通过页面触发Vuex中的函数,这样的好处就是不需要伪造Vuex函数;假设我们有一个store/list.js
1 |
|
1 |
|
我们直接创建了一个store,通过store来进行commit和getters的操作。
前端框架迭代不断,但是前端单元测试确显有人关注;一个健壮的前端项目应该有单元测试的模块,保证了我们的项目代码质量和功能的稳定;但是也并不是所有的项目都需要有单元测试的,毕竟编写测试用例也需要成本;因此如果你的项目符合下面的几个条件,就可以考虑引入单元测试:
用户在页面上进行窗口大小的调整、滚动页面或者在输入框搜索联想词等一系列操作时,都会频繁的触发事件处理函数;如果这时候又需要在事件处理函数里去异步获取数据或者进行DOM的操作等耗性能的操作时,容易导致页面卡顿等影响用户的体验;这时就可以通过防抖(debounce)和节流(throttle)函数来限制事件处理函数的调用频率,提升用户的体验。
最上面正常执行每一条竖线代表了每一次事件处理函数的调用,中间是经过防抖函数处理后实际的调用情况,最下面是经过节流函数处理后的调用情况;发现比最上面密集调用的情况要少了很多。
防抖,最开始是用在相机上,我们在拍照时(包括用手机拍),经常会发现由于手的抖动,拍摄出来的画面发生重影或者模糊的情况;而现在的相机或手机基本都会加入防抖技术,除非我们抖动特别的厉害,防抖技术的加入可以让我们拍摄更多清晰的照片。
而在我们的JS中,防抖是指触发事件后n秒
后才执行函数,如果在 n 秒内又触发了事件,则会重新计算函数执行时间;这段话比较绕口,我们以scroll函数为例:
1 |
|
我们在页面滚动时会不断触发scrollHandler
函数,但是我们不希望每次都触发,因此我们可以通过包装防抖函数来进行限制,当延迟时间超过n秒才真正执行scrollHandler函数。
而防抖函数实现的方式也很简单,在每次触发事件时,都设置一个定时器,延迟执行,并且取消之前的定时器。
1 |
|
debounce
主要的目的就是延迟执行传入的fn
函数,我们发现它返回了一个函数,是典型的闭包结构;页面滚动将每次触发scrollHandler变成每次都会触发debounce中返回的闭包函数;由于闭包的存在,因此timeout
定时器变量会一直存在,触发闭包函数时都会清除上次设置的定时器。
这里调用fn时,很多fn函数都是滚动或者点击的回调函数,会提供Event对象进行处理,因此我们需要将原来的参数传入以及this进行绑定;因此分别赋值了变量context
和args
,这里arguments
是类数组对象,那么什么是类数组对象呢,我们在函数中console.log出来看一下:
我们发现它的属性名是按照从0开始的index,第一个参数的属性是’0’,第二个属性名是’1’,并且它还有个length属性;但是它和数组不同的是它__proto__
直接指向了Object,而数组的__proto__
指向了Array;因此Array原型上的一些map、find等方法arguments也是没有的。
我们回到防抖函数,上面的防抖函数是非立即执行的,也就是触发事件后不会马上执行,但是我们某些场景下需要立即执行;立即执行后当n秒内触发事件才能再次执行。
因此我们来看下立即执行版本的防抖函数时如何来实现的:
1 |
|
在上面代码中我们还是通过闭包返回了一个匿名函数,但是在里面增加了一个变量callNow的判断,判断上一次的定时器是否已经被清除,如果没有定时器则立即执行fn函数。
在开发过程中我们需要根据不同的场景来切换不同版本的防抖函数,因此将两个防抖函数结合起来,根据参数来进行判断:
1 |
|
到这里我们的防抖函数已经接近完美了,但是最后如果我们希望能够取消这里的debounce函数,比如我们传入wait是10秒,immediate为true,刚开始是立即执行fn函数的,但是我们需要等待10秒才能重新去触发fn函数,中间做的所有操作都是无效的;我们希望能有一个按钮,点击后能够取消上一次的防抖,然后我们就能够再次触发了。
这里改动也很简单,我们需要对返回闭包函数进行处理,但是由于是匿名函数,我们给他具名,同时赋值一个cancel
函数用来清除闭包外的定时器timeout即可:
1 |
|
那么如何来调用这个cancel函数呢?我们还是以scroll函数为例,通过给页面上的btn取消按钮增加点击事件进行触发:
1 |
|
到这里我们的防抖函数就很完美了。
节流函数是指当持续触发事件时,保证一定时间段内只调用一次事件处理函数;也就是会稀释处理函数的执行频率。我们通过时间轴来清晰的看下它的执行过程:
我们可以看出,节流函数不管在一个周期内触发了多少次scroll函数,也不管触发的时间间隔,最后只会执行周期内的最后一次(或者第一次);节流函数应用场景一般在窗口resize时进行布局的调整或者移动端监听touchmove事件时移动DOM元素等;节流函数同样也有时间戳和定时器两个版本,我们先来看定时器版的节流函数实现方式:
1 |
|
和防抖函数每次清除timeout不同,这里对timeout进行非空判断,只有它为空的时候才能设置定时器,这样保证了在一段时间内同时只有一个定时器,在时间到之后会释放定时器并且执行fn函数,重新设置定时器。
我们再来看下时间戳版本的节流函数:
1 |
|
时间戳版本的函数是在闭包函数的外部存储了一个previous
变量,是上次执行的一个时间戳;每次触发内部闭包函数时与上次的时间戳进行对比判断,如果间隔时间大于我们设置的等待时间则执行fn函数,同时更新时间戳;同时由于我们初始化previous
是0,而now
当前的时间戳减去0肯定是会大于wait时间的,因此时间戳版本的节流函数fn一开始就会被触发。
通过上面我们很容易就能发现,两个版本的节流函数最大的不同就是fn函数执行的时间点,定时器版本由于setTimeout延时的特性,在时间段结束的时候触发fn函数,而时间戳版本是在时间段开始的时候触发。
同样的,我们可以将两种节流函数结合到一个函数,我们可以加上cancel取消方法:
1 |
|
我们先来看一下什么是观察者模式的定义:
观察者模式定义了对象间的一种
一对多
的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知,并自动更新。观察者模式属于行为型模式。
这里又多了一个术语,行为型模式,它是对在不同的对象之间划分责任
和算法
的抽象化,行为型模式不仅仅关注类和对象的结构,而且重点关注它们之间的相互作用;行为型模式一共有以下11种,今天我们要说的观察者模式就是其中的一种:
我们回到观察者模式的定义,它定义一种一对多
的关系;这里的一
我们称为目标对象(Subject),它有增加/删除/通知等方法,而多
则称为观察者对象(Observer),它可以接收目标对象(Subject)的状态改变并进行处理;目标对象可以添加一系列的观察者对象,当目标对象的状态发生改变时,就会通知所有的观察者对象。
下面我们通过代码来更具体的看一下目标对象和观察者对象是如何进行联系的:
1 |
|
我们在这里定义了目标对象和观察者对象两个类,在目标对象中维护了一个观察者的数组,新增时将观察者向数组中push;然后通过notify通知所有的观察者;而观察者只有一个update函数,用来接收观察者更新后的一个回调;在有些版本的代码中会将观察者直接定义为一个函数,而非一个类,但是其本质都是一样的,都是调用观察者的更新接口进行通知。
这种模式的应用在日常中也很常见,比如我们给div绑定click监听事件,其本质就是观察者模式的一种应用:
1 |
|
这里的btn可以看作是我们的目标对象(被观察对象),当它被点击时,也就是它的状态发生了变化,那么它就会通知内部添加的观察者对象,也就是我们通过addEventListener
函数添加的两个匿名函数。
我们发现,观察者模式好处是能够降低耦合,目标对象和观察者对象逻辑互不干扰,两者都专注于自身的功能,只提供和调用了更新接口;而缺点也很明显,在目标对象中维护的所有观察者都能接收到通知,无法进行过滤筛选。
我们去搜索24种基本的设计模式,会发现其中并没有发布订阅模式;刚开始发布订阅模式只是观察者模式的一个别称,但是经过时间的沉淀,他改进了观察者模式的缺点,渐渐地开始独立于观察者模式;我们也来看一下它的一个定义:
发布订阅模式是基于一个事件(主题)通道,希望接收通知的对象
Subscriber
通过自定义事件订阅主题,被激活事件的对象Publisher
通过发布主题事件的方式通知各个订阅该主题的Subscriber
对象。
我们看到定义里面也涉及到了两种对象:接收通知的对象(Subscriber)和被激活事件的对象(Publisher);被激活事件对象(Publisher)我们可以类比为观察者模式中的目标对象,来发布事件通知,而接收通知对象(Subscriber)可以类比为观察者对象,订阅各种通知。
发布订阅模式和观察者模式的不同在于,增加了第三方即事件中心
;目标对象状态的改变并直接通知观察者,而是通过第三方的事件中心来派发通知。
为了加深理解,我们以生活中的情形为例;比如我们订阅报纸杂志等,一般不会直接跑到报社去订阅,而是通过一个平台,比如街边的报亭或者邮局也可以订阅;而报纸杂志也会有多种,比如晨报晚报日报等等;我们订阅报纸后报社出版后会通过平台来给我们投递,通过邮局邮寄或者自取等等,那么这里就涉及到了报社、订阅者和第三方平台三个对象,我们通过代码来模拟三者的动作:
1 |
|
这里的报社我们可以理解为发布者(Publisher)的角色,订报纸的读者理解为订阅者(Subscriber),第三方平台就是事件中心;报社在平台上注册某一类型的报纸,然后读者就可以在平台订阅这种报纸;三个类准备好了,我们来看下他们彼此如何进行联系:
1 |
|
由于平台是沟通的桥梁,因此我们先定义了一个调度中心channel,然后分别定义了两个报社pub1、pub2,以及三个读者sub1、sub2和sub3;两家报社在平台注册了晨报1、晚报1和晨报2三种类型的报纸,三个读者各自订阅各家的报纸,也能取消订阅。
我们可以发现在发布者中并没有直接维护订阅者列表,而是注册了一个事件主题,这里的报纸类型相当于一个事件主题;订阅者订阅主题,发布者推送某个主题时,订阅该主题的所有读者都会被通知到;这样就避免了观察者模式无法进行过滤筛选的缺陷。
我们通过一张图来形象的描述两种模式的区别。
我们在深入学习Object.defineProperty和Proxy中介绍过,Vue2.0响应式是通过Object.defineProperty()
来处理的,将每个组件data中的数据进行get/set劫持(也就是Reactive化),那么劫持后是如何来通知页面进行更新操作呢?这里就用到了发布订阅模式,我们首先来看下官网是如何介绍的:
每个组件实例都对应一个 watcher 实例,它会在组件渲染的过程中把“接触”过的数据 property 记录为依赖。之后当依赖项的 setter 触发时,会通知 watcher,从而使它关联的组件重新渲染。
相信看过源码的同学对Watcher和Dep的代码看的是云里雾里,不了解这两个类的作用;我们剔除不相关的代码,对主要代码逐段分析。
1 |
|
我们在初始化data时或者用$set给data新增属性都会给每个属性循环遍历调用defineReactive进行数据劫持;我们看到在每个属性中构造了一个dep对象,并且在属性触发getter和setter时都会调用,它其实是依赖收集和触发更新的一个第三方,相当于发布订阅模式中事件中心的一个角色;而且由于getter/setter函数内对它闭包引用,因此我们在this.num
和this.num=1
都是调用它下面的函数,因此我们来看下它的实现原理:
1 |
|
Dep的全程是Dependency,翻译过来也是依赖、依赖关系的意思,从意思上能看出来是用来做依赖收集的;我们看到Dep下面有一个subs数组,它是一组Watcher的列表,存放的就是我们收集的依赖列表;然后通过addSub和removeSub新增和删除某个依赖,当数据更新时通过notify通知列表中所有的依赖对象;可以发现这些函数和我们的事件中心的代码很相似,不过它不是基于事件主题,而是直接通过一个列表。
Dep源码看完了,下面就来看我们收集的依赖Watcher,也就是订阅者,都做了哪些事情:
1 |
|
我们看到Watcher和我们的订阅者代码也很相似,在update中对视图进行更新操作;由于data数据可以传入不同的子组件,而在data中数据更新时,每个子组件中的页面都需要重新更新,因此每一个Vue组件都会在mount阶段都会创建一个Watcher,然后保存在_watcher上:
1 |
|
因此Dep和Watcher两者关系如下图:
我们回到Dep的源码中,发现有一个静态属性Dep.target是Watcher,进行依赖收集的时候也是通过Dep.target,那么它是做什么用的呢?让我们继续回到Watcher的构造器:
1 |
|
在Dep代码中同时维护了一个targetStack,也就是我们常说的堆栈,它遵从着先进后出的原则,我们只能通过pushTarget(压栈)和popTarget(出栈)来对它进行操作,那么它是什么时候需要进行压栈和出栈的操作呢?
在Watcher的源码中我们发现的原因,由于Water实例是在组件mounted时被构建的,在构建时需要把实例暂存到Dep.target上以便Dep进行依赖收集;如果Dep.target上有其他组件的watcher实例,需要先把其他的watcher实例暂存到targetStack
中,然后调用expOrFn函数
渲染组件;这里的expOrFn渲染组件时会将data中定义的数据取值,取值的过程就会自动调用Reactive化后的getter函数,因此就把Dep.target上的watcher实例收集到了每个数据的Dep中,收集完成后再把上一个watcher出栈。
总结,经过两者关系的分析,我们发现Vue是一个典型的发布订阅模式,data中的数据就是我们需要观察的目标对象,Dep相当于事件中心,而Watcher则是订阅者。
]]>老规矩,还是先来看一下官网的slogan:
Element,一套为开发者、设计师和产品经理准备的基于
Vue 2.0
的桌面端组件库
可以看出,Element的使用范围涵盖了大部分的研发人员;产品经理可以用来参考逻辑交互,设计师可以借鉴图标和组件设计,开发者可以使用它来布局页面。
我们从github将整个项目clone
下来后,来看一下有哪些目录文件:
1 |
|
因此packages和src目录是我们需要关注的两个重要的目录;大致了解了目录结构后,下一个需要关注的就是package.json
文件,这个文件包括了一些项目描述、项目依赖以及脚本命令等;有时候我们第一眼找不到项目的入口文件,就可以从这里来找。
首先是"main":"lib/element-ui.common.js"
,main字段定义了npm包的入口文件,我们在项目中require("element-ui")
,其实就是引用了element-ui.common.js文件;然后我们来看一下有哪些脚本命令,这里引用了重要的几个命令:
1 |
|
可以看出来前面的几个build
命令是用来构建一些工具、样式的,主要是dist
命令,通过webpack进行打包,还进行了三次打包,我们分别来看下这三次打包分别是打包什么文件的;首先我们来看下前两个配置文件webpack.conf.js和webpack.common.js,这里只截取配置文件的部分代码:
1 |
|
发现两个文件的入口都是src/index.js
,不同的是webpack.conf.js打包的是umd规范
,而且通过minimizer
进行了压缩;而webpack.common.js打包的是commonjs规范
,并且没有进行压缩;通过两种规范来打包的主要原因也是因为Element安装方式的不同,umd规范主要针对CDN引入的方式,在页面上引入js和css:
1 |
|
而webpack.common.js打包出来的element-ui.common.js
则是针对npm的引入方式:
1 |
|
那么最后一个build/webpack.component.js
也不难猜到了,是为了在npm引入时,只引入需要的部分组件,而不对整体进行打包:
1 |
|
这里components.json
就是每个组件所在的入口文件。
我们在上面一节通过查看webpack的配置文件找到了入口文件/src/index.js
,那么我们就来看一下Element在入口是如何来注册这么多组件的。
1 |
|
这里Element暴露出去一个install函数
,这是因为Element本身就是一个插件,我们在调用Vue.use(ElementUI)
注册时,本质上就是调用这个install函数;那么Vue.use
是如何注册插件的呢?
在Element的install函数中,我们发现从传入的options
参数中取出size和zIndex,存到Vue.prototype.$ELEMENT
全局配置中,这样在组件中我们就可以获取size和zIndex,根据size进行不同组件尺寸的展示。
在Element文档全局配置中,也指出了可以在引入Element时覆写全局配置:
1 |
|
在上面install函数中,我们发现Element注册插件有三种方式,第一种是像Button和Input,在数组循环遍历,通过Vue.component
中注册成全局组件,就可以在页面直接引用;第二种是InfiniteScroll和Loading,在全局注册指令,通过v-infinite-scroll
和v-loading
等指令式来调用;第三种是MessageBox、Notification和Message,在全局Vue.prototype添加了方法,可以通过函数进行调用。
首先我们从几个简单的布局容器组件开始,我们简单看一下demo回顾一下这几个组件的使用方法:
1 |
|
我们先来看下el-header的源码,实现逻辑也很简单,通过slot插槽将元素进行渲染;(el-footer和el-aside也是同样的,这里不再展示了):
1 |
|
这里传参的props和文档中给出也是一致的,el-header、el-footer和el-aside三个组件都是类似,都传width或者height等一些宽高的字符串数值。
我们重点来看下el-container的代码,接收一个参数direction,可选值horizontal/vertical,不过它的默认值比较特殊,文档中是这么说的:
子元素中有
el-header
或el-footer
时为vertical
,否则为horizontal
了解了它的传参逻辑,我们来看下源码是如何来实现的:
1 |
|
代码中比较难理解的是isVertical中的逻辑判断,我们一段一段来看;this.$slots
是用来获取组件中所有的插槽组件,和this.$refs
有点像,都是对象,用来存放多个插槽对象;而this.$slots.default
是获取默认的那个插槽,它是一个数组,存放是插槽中的节点;然后some函数中的判断就很好理解了,用来判断数组中的vnode节点的是否有el-header或者el-footer两个标签,有的话就返回true,就会渲染is-vertical类名。
我们再来看两个布局组件Row和Col,用于创建栅格布局,我们还是简单的看一下这两个组件的用法:
1 |
|
看用法这两个组件也是在页面通过插槽的方式渲染页面,不过当我们来看源码会发现它的插槽和上面组件的插槽用法还不一样:
1 |
|
我们发现el-row插件没有模板template渲染,而是通过render渲染函数来渲染页面的;但是这里为什么需要用到渲染函数呢?这里和el-row的参数有关,在传参列表中我们可以看到参数中有一个tag自定义元素标签,也就是定义最外层的标签类型;如果通过模板渲染的话肯定需要多个if判断,比较繁琐,但是通过渲染函数,就直接渲染标签了;渲染函数有关使用方法可以查看官网文档。
Col组件和Row类似,都是通过render函数进行渲染,不过Col组件获取父级组件Row的参数方式值的我们来学习一下,这里贴上部分代码:
1 |
|
由于Row组件传入的gutter表示栅格间隔,因此Rol组件也需设置一定的padding,但是怎么能从父组件获取参数呢?Col通过一个while循环
,不断向上获取父组件并且判断组件名称。
看完布局组件,我们来看一下表单组件,表单最顶层的是Form和Form-Item组件,我们可以通过向Form传入参数rules
来校验表单中的Input输入框或者其他组件的值,首先来看下Form的源码,由于篇幅问题这里只贴出部分源码:
1 |
|
我们看到Form的页面结构非常简单,只有一个form标签,而且props也只用到了labelPosition
和inline
两个,其他的属性会在Form-Item中用到;Form中还用到了一个provide函数
,在Vue中组件通信方式中,我们介绍过provide/inject
,主要是用来跨多层组件通信的,在后面组件的介绍中,我们会取出来用到。
重点我们看下常用的表单校验函数validate
是如何来实现的;在created中,我们看到在注册了两个事件:addField
和removeField
,这是用来在所有的子组件Form-Item初始化时调用,进行一个收集存储,存到fields数组
中,那么这里为什么不用$children呢?因为页面结构的不确定,Form下一级子组件不一定就是Form-Item,如果进行循环的话比较费时费力,而且对子组件管理操作也比较频繁,因此通过事件的方式;收集所有的Form-Item后,我们就可以对每个表单元素遍历并且校验。
接着就是Form-Item,来看下它的源码:
1 |
|
我们看到这里还是用了provide/inject
来处理跨组件的数据通信,将Form引入,用到了Form的几个props值来渲染类名,同时将本身inject向下传递。在sizeClass
中我们看到获取size也是向上渐进获取的一个过程,首先是Form-Item本身的size,然后是Form的size,最后才是我们挂载在全局$ELEMENT
的size,我们去查看其他组件例如Input、Button、Radio,都是通过这种方式来渲染size。
在Form-Item生命周期函数中我们也看到了,通过触发了Form的addField和removeField来进行表单的收集,不过通过一个dispatch
函数,这个函数既不是vue官网中的,在methods
中也没有进行定义,那么它是如何来触发的呢?我们仔细看代码,会发现一个mixins:[emitter]
数组,原来Form-Item是通过mixins将一些公共的函数提取出来,那么我们来看一下emitter里面是做了哪些操作:
1 |
|
我们看到dispatch
是用来向父组件派发事件,也是通过while向上遍历循环,而broadcast
是向子组件广播事件的。
Button是我们常用的组件,我们来看下它的源码:
1 |
|
我们看到Button逻辑相较于上面的组件很简单,通过computed计算了buttonSize和buttonDisabled进行类名渲染,同时依赖了注入的elForm和elFormItem中的数据;在点击时触发了click事件;我们还可以通过Button-Group将多个按钮进行嵌套,来看下它的源码:
1 |
|
它的代码更简单,只用了一个slot嵌套了所有的Button。
指令式组件通过Vue.directive(name, opt)
来注册,name就是我们要注册的指令名称,而opt是一个对象,包含了5个钩子函数,我们可以根据需要只写其中的几个函数:
1 |
|
每个钩子函数都有三个回调参数,el
表示了指令所绑定的元素,可以用来直接DOM操作;而binding
就是我们的绑定信息了,它是一个对象,包含以下属性:
v-
前缀。 InfiniteScroll无限滚动组件的用法也很简单,在我们想要滚动加载的列表上加上v-infinite-scroll
,赋值自定义加载的函数,在列表滚动到底部时就会自动触发函数,我们来看一个官方的Demo:
1 |
|
在ul滚动到最底部时,触发load函数,加载更多的数据;因此这个指令的实现原理也很简单,就是监听容器滚动,滚动到底部时进行函数调用,我们来看下具体是怎么来实现的:
1 |
|
在inserted
函数中逻辑也很简单,首先cb就是我们自定义的回调函数,用来触发;通过getScrollContainer
判断我们的el是否是滚动容器,然后将handleScroll
处理滚动逻辑的函数用节流函数throttle进行封装成onScroll,绑定到容器的滚动事件上去。
我们在文档中还看到有四个参数,都是以infinite-scroll-
开头的,用来控制触发加载函数的时间;在源码中我们看到它是通过getScrollOptions
函数来进行获取,定义了一个对象用来存储这四个参数的名称、类型和默认值,用Object.keys变成数组后再通过reduce
函数处理变成map对象返回。
函数组件调用后会将组件插入到body或者其他节点中去,就不能够通过Vue.component
注册到全局组件;而是通过Vue.extend
创建一个子类构造器,参数是包含组件选项的对象,构造器实例化后通过$mount
挂载到页面元素上去。
1 |
|
或者实例化后通过$mount
获取到DOM结构,然后挂载到body上:
1 |
|
Message组件在入口文件中就挂载到全局变量$message上,然后通过this.$message()
来进行调用,还能通过this.$message.error()
的方式来调用,因此我们猜测Message肯定是一个函数,在这个函数上面还挂载了success、error等函数来复用Message函数本身;我们来看下Message源码(部分):
1 |
|
我们发现在Message的构造函数中首先对options进行一个处理,因为可以传入字符串或者对象两种调用方式,因此首先把字符串的options
统一成对象形式;然后通过Vue.extend创建的构造函数实例化一个instance,将所有的参数options传到instance中进行渲染,最后把instance.$el插入到页面上去渲染;这里为了对所有的实例进行管理,比如根据所有实例个数,渲染最后一个实例的高度还有关闭实例对象等操作,因此维护了一个数组instances来进行管理,还给每个实例一个自增id方便进行查找。
然后我们来看下Vue.extend传入的参数main.vue的源码(部分):
1 |
|
我们看到这里data参数和文档中给出的参数是一样的,在上面构造函数中正是通过options传入进来进行覆盖;这里关闭组件是通过watch监听closed是否为true,然后再给visible
赋值false;那么页面上明明渲染的是visible,为什么这里不直接给visible赋值呢?个人猜测是为了在关闭的同时触发回调函数onClose
;在组件动画结束后也调用了parentNode.removeChild
将组件从body中移除,
我们从webpack配置文件入手,找到了入口文件进行组件的注册配置和导出,对众多的组件进行了分类,归为三大类;由于文章篇幅有限,这里只展示了每一类组件中部分组件的源码,像很多常用的组件,比如Input、Radio和Checkbox等很多组件都是大同小异,大家可以继续深入学习其源码;在看源码的同时建议可以查看官方文档中的参数以及参数说明,这样能够更好地理解源码中的思想逻辑,不至于看的一头雾水。
]]> Parcel官网的定义就是极速零配置Web应用打包工具
,它利用多核处理提供了极快的速度,并且不需要任何配置。
它有以下优点:
import()
语法, Parcel 将你的输出文件束(bundles)分拆,因此你只需要在初次加载时加载你所需要的代码。我们从以下几个方面分别来看下Parcel如何进行配置。
Parcel可以使用任何类型的文件作为入口,但是最好还是使用Html或者JS文件。我们新建一个项目,创建html和js文件:
1 |
|
1 |
|
文件创建好后我们就可以启动Parcel了,它默认内置了一个用于开发环境的服务器,如果将入口设置为js文件,我们可以在浏览器里查看到js文件内容,但这样看不到任何页面效果,因此我们将入口设置为html文件:
1 |
|
现在可以在浏览器打开http://localhost:1234/
,也可以添加-p <prot number>
覆盖默认的1234端口号;这个开发服务器也支持模块热更新,我们改变js或者html内容,浏览器就会自动更新。同时,Parcel也支持多页面入口,假设我们的项目结构如下:
1 |
|
我们可以繁琐的手动指定需要加载入口的文件名称,比如:
1 |
|
也可以通过glob文件匹配模式来匹配所有的html文件:
1 |
|
这样,我们就可以通过http://localhost:1234/home/index.html
来访问到页面了。
我们发现在入口文件这方面,Parcel相较于Webpack要灵活不少,在Webpack中我们需要通过entry
字段来指定入口,而且入口只能是JS文件,要以html为入口还需要使用第三方插件如html-webpack-plugin;而Parcel在入口文件则宽松很多,可以用html文件作为入口,Parcel会自动加载html文件中引用的脚本或者样式进行打包;我们甚至还可以将一张图片或者vue文件直接作为入口文件。
通常打包工具只知道如何处理JS文件,在处理其他资源时,比如less/sass、图片、vue文件等,都需要通过通过转换器进行转换,比如webpack的loader就是充当了转换器的角色;而Parcel支持许多开箱即用的转换器,比如在项目根目录配置.babelrc
和.postcssrc
,Parcel就会自动在所有的JS和css上进行转换。
再比如我们一般在项目中使用sass或者less,然后在js中import来引入样式,如果在webpack中也是需要通过配置loader才能解析;而我们在Parcel可以不通过任何配置直接import进来,Parcel会自动给我们安装sass的依赖包:
虽然项目代码的增多,打包出来的文件也会随之增大;通过代码分割我们可以将首屏不加载的页面或者模块拆分到独立的包中,然后进行按需加载。
在Webpack中实现代码分割主要是通过配置splitChunks和模块内的内联函数动态引入来实现代码分割,而Parcel支持零配置代码分割,并且开箱即用。我们可以将代码拆分成单独的包,这些包可以按需加载;主要是通过动态import()
函数来实现,这个函数与普通的import
语句类似,但返回一个Promise对象:
1 |
|
这样,Parcel在打包时会将about.js
单独进行打包,然后点击body时才按需加载。
模块热更新(HMR)我们在Webpack中也介绍过,是通过在运行时自动更新浏览器中的模块而无需刷新整个页面,从而改善了开发体验;其实现的原理主要是在服务器和浏览器之间维护了一个websocket连接,服务器自动推送每次代码改变。
在webpack中主要是通过官方的webpack-dev-server
服务器来实现的,而在Parcel中HMR服务器是内置的,开箱即用,在启动开发环境服务器时自动启用,无需配置。
Parcel不支持Tree Shaking
我们体验完整个Parcel,发现确实一行配置代码都没有写,和官网的slogan极速零配置
确实符合;在Webpack最难配置的模块转换方面,Parcel做到了自动识别处理导入的模块,这是让我们眼前一亮的地方;而且内部启用了多进程工作,在打包相同体量的项目时,Parcel会比Webpack快很多;不过Webpack的很多功能Parcel也并不具备,比如常用的Tree Shaking、提取公共代码等,因此我们在使用它时需要考虑到这些功能是否是我们项目所必须的。
我们有时候在开发一些自己项目中用的JS类库时,比如弹框组件、校验组件、工具组件等,如果使用Webpack,在打包时会产生很多冗余代码,导致一个简单的类库打包出来体积也比较庞大;而Rollup就是专门针对类库进行打包,它的优点是小巧而专注,因此现在很多我们熟知的库都都使用它进行打包,比如:Vue、React和three.js等。
Rollup 是一个
JavaScript
模块打包器,可以将小块代码编译成大块复杂的代码,例如 library 或应用程序。
Rollup既然是打包类库文件,那么它的入口也就只能是JS文件了(通过第三方插件可以支持Html,这里不作展开),因此我们新建一个main.js
作为入口文件,打包出来的文件我们命名为bundle.js
,我们可以简单的通过命令行进行打包:
1 |
|
但是和Webpack一样,推荐使用配置文件进行打包:
1 |
|
我们来看下rollup.config.js
配置文件需要包含哪些选项:
1 |
|
我们发现这里input
、output.file
和output.format
都是必传的,因此,一个基础配置文件如下:
1 |
|
这里的format
字段大家看了可能不太理解,尤其是里面的cjs
代表什么意思;由于JS有多种模块化方式,Rollup可以针对不同的模块规范打包出不同的文件,它有以下五种选项;
script
标签 和Webpack一样,Rollup也支持配置多个文件入口,我们新建foo.js
和bar.js
两个入口文件:
1 |
|
这样打包出来的两个文件就放入dist中。
插件拓展了Rollup处理其他类型文件的能力,它的功能有点类似于Webpack的loader
和plugin
的组合;不过配置比webpack中要简单很多,不用逐个声明哪个文件用哪个插件处理,只需要在plugins
中声明,在引入对应文件类型时就会自动加载;我们来看几个常用的插件。
首先是rollup-plugin-json
,让Rollup可以从JSON文件中读取数据:
1 |
|
Rollup默认只能加载ES6模块的js,但是我们项目中通常也会用到CommonJS的模块,这样Rollup解析就会出现问题,比如:
1 |
|
由于ES6模块导入默认会去找default
,因此这里打包会报错;这时就需要用到rollup-plugin-commonjs
插件来进行转换:
1 |
|
而且目前npm中大多数依赖包都是以CommonJS模块形式出现,都需用通过这个插件来进行模块化解析;除此之外,我们引用node_modules
中的第三方模块,还需要用到rollup-plugin-node-resolve
进行解析,这两个插件通常组合使用:
1 |
|
除此之外还有很多有用的插件,这里就不一一赘述使用方法,列出来有需要的童鞋可以根据情况进行使用:
Rollup本身不支持启动开发服务器,我们可以通过rollup-plugin-serve
第三方插件来启动一个静态资源服务器:
1 |
|
不过由于其本质就是一个静态资源的服务器,因此不支持模块热更新
由于Rollup本身支持ES6模块化规范,因此不需要额外配置即可进行Tree Shaking
Rollup代码分割和Parcel一样,也是通过按需导入的方式;但是我们输出的格式format不能使用iife,因为iife自执行函数会把所有模块放到一个文件中,可以通过amd
或者cjs
等其他规范。
1 |
|
这样我们通过import()
动态导入的代码就会单独分割到独立的js中,在调用时按需引入;不过对于这种amd模块的文件,不能直接在浏览器中引用,必须通过实现AMD标准的库加载,比如Require.js
。
通过对Rollup的使用介绍,我们发现它有以下优点:
但是他也有以下不可忽视的缺点:
经过以上对三个打包工具多方面的使用对比,相信大家都有了一个初步的印象;我们来简单总结一下三个打包工具的使用环境,如果我们需要构建一个简单的小型应用并让它快速运行起来,可以使用Parcel;如果需要构建一个类库只需要导入很少第三方库,可以使用Rollup;如果需要构建一个复杂的应用,需要集成很多第三方库,并且需要代码分拆、HMR等功能,推荐使用Webpack。
]]>CommonJS规范是一种同步加载模块的方式,也就是说,只有当模块加载完成后,才能执行后面的操作。由于Nodejs主要用于服务器端编程,而模块文件一般都已经存在于本地硬盘,加载起来比较快,因此同步加载模块的CommonJS规范就比较适用。
CommonJS规范规定,每一个JS文件就是一个模块,有自己的作用域;在一个模块中定义的变量、函数等都是私有变量,对其他文件不可见。
1 |
|
在上面的number.js中,变量num和函数add就是当前文件私有的,其他文件不能访问。同时CommonJS规定了,每个模块内部都有一个module
变量,代表当前模块;这个变量是一个对象,它的exports
属性(即module.exports
)提供对外导出模块的接口。
1 |
|
这样我们定义的私有变量就能提供对外访问;加载某一个模块,就是加载这个模块的module.exports
属性。
上面说到,module
变量代表当前模块,我们来打印看一下它里面有哪些信息:
1 |
|
我们发现它有以下属性:
如果我们通过命令行调用某个模块,比如node temp.js
,那么这个模块就是顶级模块,它的module.parent
就是null;如果是在其他模块中被调用,比如require('temp.js')
,那么它的module.parent
就是调用它的模块。
但是在最新的Nodejs 14.6版本中module.parent
被弃用了,官方推荐使用require.main
或者module.children
代替,我们来看一下弃用的原因:
module.parent
值为通过required引用的这个模块的值。如果为当前运行进程的入口,值为null。如果这个模块被非commonJS格式引入,如REPL,或者import导入,值为undefined
为了导出模块方便,我们还可以通过exports
变量,它指向module.exports
,因此这就相当于在每个模块隐性的添加了这样一行代码:
1 |
|
在对外输出模块时,可以向exports对象添加属性。
1 |
|
需要注意的是,不能直接将exports
变量指向一个值,因为这样等于切断了exports
和module.exports
之间的联系
1 |
|
虽然我们通过exports
导出了字符串,但是由于切断了exports = module.exports
之间的联系,而module.exports
实际上还是指向了空对象,最终导出的结果也是空对象。
require的基本功能是读取并执行JS文件,并返回模块导出的module.exports
对象:
1 |
|
如果模块导出的是一个函数,就不能定义在exports
对象上:
1 |
|
require除了能够作为函数调用加载模块以外,它本身作为一个对象还有以下属性:
Module
对象,表示当进程启动时加载的入口脚本。 当我们在一个项目中多次require
同一个模块时,CommonJS并不会多次执行该模块文件;而是在第一次加载时,将模块缓存;以后再加载该模块时,就直接从缓存中读取该模块:
1 |
|
我们多次require加载number模块,但是内部只有一次打印输出;第二次加载时还改变了内部变量的值,第三次加载时内部变量的值还是上一次的赋值,这就证明了后面的require
读取的是缓存。
在上面require
中,我们介绍了它下面所有的属性,发现有一个cache属性,就是用来缓存模块的,我们先打印看一下:
1 |
|
cache按照路径的形式将模块进行缓存,我们可以通过delete require.cache[modulePath]
将缓存的模块删除;我们把上面的代码改写一下:
1 |
|
很明显的发现,number模块运行了两遍,第二次加载模块我们又把模块的缓存给清除了,因此第三次读取的num值也是最新的;我们也可以通过Object.keys
循环来删除所有模块的缓存:
1 |
|
CommonJS的加载机制是,模块输出的是一个值的复制拷贝;对于基本数据类型的输出,属于复制,对于复杂数据类型,属于浅拷贝,我们来看一个例子:
1 |
|
由于CommonJS是值的复制,一旦模块输出了值,模块内部的变化就影响不到这个值;因此main.js中的number
变量本身和number.js
没有任何指向关系了,虽然我们调用模块内部的add
函数来改变值,但也影响不到这个值了;反而我们在输出后可以对这个值进行任意的编辑。
针对require
这个特性,我们也可以理解为它将模块放到自执行函数中执行:
1 |
|
而对于复杂数据类型,由于CommonJS进行了浅拷贝,因此如果两个脚本同时引用了同一个模块,对该模块的修改会影响另一个模块:
1 |
|
上面代码中我们通过a.js、b.js两个脚本同时引用一个模块进行修改和读取;需要注意的是由于缓存,因此b.js加载时其实已经是从缓存中读取的模块。
我们上面说过require
加载时,会执行模块中的代码,然后将模块的module.exports
属性作为返回值进行返回;我们发现这个加载过程发生在代码的运行阶段,而在模块被执行前,没有办法确定模块的依赖关系,这种加载加载方式称为运行时加载
;由于CommonJS运行时加载模块,我们甚至能够通过判断语句,动态的选择去加载某个模块:
1 |
|
但也正是由于这种动态加载,导致没有办法在编译时做静态优化。
由于缓存机制的存在,CommonJS的模块之间可以进行循环加载,而不用担心引起死循环:
1 |
|
在上面代码中,逻辑看似很复杂,a.js加载了b.js,而b.js加载了a.js;但是我们逐一来进行分析,就会发现其实很简单。
因此最后打印的结果:
1 |
|
尤其需要注意的是第一个b模块中的console,由于此时a模块虽然已经加载在缓存中,但是并没有执行完成,a模块只导出了第一个{a:1}
。
我们发现循环加载,属于加载时执行;一旦某个模块被循环加载,就只输出已经执行的部分,还未执行的部分不会输出。
与CommonJS规范动态加载不同,ES6模块化的设计思想是尽量的静态化,使得在编译时就能够确定模块之间的依赖关系。我们在Webpack配置全解析(优化篇)就聊到,利用ES6模块静态化加载方案,就可以实现Tree Shaking
来优化代码。
和CommonJS相同,ES6规范也定义了一个JS文件就是一个独立模块,模块内部的变量都是私有化的,其他模块无法访问;不过ES6通过export
关键词来导出变量、函数或者类:
1 |
|
或者我们也可以直接导出一个对象,这两种方式是等价的:
1 |
|
在导出对象时,我们还可以使用as
关键词重命名导出的变量:
1 |
|
通过as
重名了,我们将变量进行了多次的导出。需要注意的是,export
规定,导出的是对外的接口,必须与模块内部的变量建立一一对应的关系。下面两种是错误的写法:
1 |
|
使用export
导出模块对外接口后,其他模块文件可以通过import
命令加载这个接口:
1 |
|
上面代码从number.js模块中加载了变量,import命令接受一对大括号,里面指定了从模块导入变量名,导入的变量名必须与被导入模块对外接口的变量名称相同。
和export命令一样,我们可以使用as
关键字,将导入的变量名进行重命名:
1 |
|
除了加载模块中指定变量接口,我们还可以使用整体加载,通过(*)指定一个对象,所有的输出值都加载在这个对象上:
1 |
|
import命令具有提升效果,会提升到整个模块的头部,首先执行:
1 |
|
上面代码不会报错,因为import会优先执行;和CommonJS规范的require不同的是,import是静态执行,因此import不能位于块级作用域内,也不能使用表达式和变量,这些都是只有在运行时才能得到结果的语法结构:
1 |
|
在上面代码中import导入export对外接口时,都需要知道对外接口的准确名称,才能拿到对应的值,这样比较麻烦,有时我们只有一个接口需要导出;为此ES6规范提供了export default
来默认导出:
1 |
|
由于export default
是默认导出,因此,这个命令在一个模块中只能使用一次,而export
导出接口是可以多次导出的:
1 |
|
export default
其实是语法糖,本质上是将后面的值赋值给default
变量,所以可以将一个值写在export default
之后;但是正是由于它是输出了一个default
变量,因此它后面不能再跟变量声明语句:
1 |
|
既然export default
本质上是导出了一个default变量的语法糖,因此我们也可以通过export
来进行改写:
1 |
|
上面两个代码是等效的;而我们在import导入时,也是把default
变量重命名为我们想要的名字,因此下面两个导入代码也是等效的:
1 |
|
在一个模块中,export
可以有多个,export default
只能有一个,但是他们两者可以同时存在:
1 |
|
在CommonJS中我们说了,模块的输出是值的复制拷贝;而ES6输出的则是对外接口,我们将上面CommonJS中的代码进行改写来理解两者的区别:
1 |
|
我们发现和CommonJS中运行出来结果完全不一样,调用模块中的函数影响了模块中的变量值;正是由于ES6模块只是输出了一个对外的接口,我们可以把这个接口理解为一个引用
,实际的值还是在模块中;而且这个引用
还是一个只读引用
,不论是基本数据类型还是复杂数据类型:
1 |
|
import也会对导入的模块进行缓存,重复import导入同一个模块,只会执行一次,这里就不进行代码演示。
ES6模块之间也存在着循环引用,我们还是将CommonJS中的代码来进行改造看一下:
1 |
|
刚开始我们肯定会想当然的以为b.js
中打印的是1和undefined,因为a.js
只加载了第一个export;但是打印结果后,b.js
中两个都是undefined,这是因为import有提升效果。
通过上面我们对CommonJS规范和ES6规范的比较,我们总结一下两者的区别:
this
指向当前模块,ES6中this
指向undefined 首先来看一下MDN对Object.defineProperty()
的一个定义:
Object.defineProperty()
方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。
它的语法是传入三个参数:
Object.defineProperty(obj, prop, descriptor)
三个参数的作用分别是:
我们先来看下这个函数的简单用法;既然它能够在对象上定义新的属性,那我们通过它来给对象添加新的属性:
1 |
|
这里描述符中的value
值即是需要在对象上定义或者修改的属性值(如果对象上本身有该属性,则会进行修改操作);除了字符串,还可以是JS的其他数据类型(数值,函数等)。
属性描述符是个对象,那么就有很多操作的地方了,它除了value这个属性,还有以下:
属性名 | 作用 | 默认值 |
---|---|---|
configurable | 只有该属性的configurable 为true,该属性的描述符才能够被改变,同时该属性也能从对应的对象上被删除。 | false |
enumerable | 只有该属性的enumerable 为true,该属性才会出现在对象的枚举属性中。 | false |
writable | 只有该属性的enumerable 为true,才能被赋值运算符改变。 | false |
value | 该属性对应的值 | undefined |
get | 属性的getter函数,当访问该属性时,会调用此函数。 | undefined |
set | 当属性值被修改时,会调用此函数。该方法接受一个参数,会传入赋值时的 this 对象。 | undefined |
我们一一来看每个属性的用法;首先configurable
用来描述属性是否可配置(改变和删除),主要有两个作用:
在非严格模式下,属性配置configurable:false
后进行删除操作会发现属性仍然存在。
1 |
|
而在严格模式下会抛出错误:
1 |
|
configurable:false
配置后也不能重新修改:
1 |
|
enumerable
用来描述属性是否能出现在for in
或者Object.keys()
的遍历中:
1 |
|
很明显,enumerable为true的gender就会被遍历到,而birth则不会。
writable用来描述属性的值是否可以被重写,值为false时属性只能读取:
1 |
|
在非严格模式下给name属性再次赋值会静默失败,不会抛出错误;而在严格模式下会抛出异常:
1 |
|
当需要设置或者获取对象的属性时,可以通过getter/setter
方法:
1 |
|
当获取name时和赋值name时,都会分别调用一次get和set函数;看到这里,很多同学可能会有疑问,为什么这里要用一个initName
,而不是在get和set函数中直接return user.name
和user.name = val
呢?
如果我们直接在get函数中return user.name
的话,这里的user.name
同时也会调用一次get函数,这样的话会陷入一个死循环;set函数也是同样的道理,因此我们通过一个第三方的变量initName
来防止死循环。
但是如果我们需要代理更多的属性,不可能给每一个属性定义一个第三方的变量,可以通过闭包来解决
注:get和set函数不是必须成对出现,可以只出现一个;两个函数如果不设置,则默认值为undefined。
在上面表格中可以看到,上述的三种描述符configurable
、enumerable
和writable
的默认值都是false,因此我们一旦使用Object.defineProperty给对象添加属性,如果不设置属性的特性,那么这些值都是false:
1 |
|
而我们通过点运算符
给属性赋值时,则默认给三种描述符都赋值true:
1 |
|
属性描述符主要有两种形式:数据描述符和存取描述符;数据描述符特有的两个属性:value
和writable
;存取描述符特有的两个属性:get
和set
;两种形式的属性描述符不能混合使用,否则会报错,下面是一个错误的示范:
1 |
|
我们简单想一下就能理解为什么两种描述不能混合使用;value用来定义属性的值,而get和set同样也是定义和修改属性的值,两种描述符在功能上有明显的相似性。
虽然数据描述符和存取描述符不能混着用,但是他们均能分别和configrable
、enumerable
一起搭配使用,下面表格表示了两种描述符可以同时拥有的健值:
configurable | enumerable | value | writable | get | set | |
---|---|---|---|---|---|---|
数据描述符 | Yes | Yes | Yes | Yes | No | No |
存取描述符 | Yes | Yes | No | No | Yes | Yes |
通过上面的代码我们可以发现,虽然Object.defineProperty
能够劫持对象的属性,但是需要对对象的每一个属性进行遍历劫持;如果对象上有新增的属性,则需要对新增的属性再次进行劫持;如果属性是对象,还需要深度遍历。这也是为什么Vue给对象新增属性需要通过$set
的原因,其原理也是通过Object.defineProperty
对新增的属性再次进行劫持。
Object.defineProperty
除了能够劫持对象的属性,还可以劫持数组;虽然数组没有属性,但是我们可以把数组的索引看成是属性:
1 |
|
虽然我们监听到了数组中元素的变化,但是和监听对象属性面临着同样的问题,就是新增的元素并不会触发监听事件:
1 |
|
为此,Vue的解决方案是劫持Array.property
原型链上的7个函数,我们通过下面的函数简单进行劫持:
1 |
|
我们在一文读懂JS中类、原型和继承中讲过:
实例对象能够获取原型对象上的属性和方法
我们在数组上进行操作的push、shift等函数都是调用的原型对象上的函数,因此我们将改写后的原型对象重新给绑定到实例对象上的__proto__
,这样就能进行劫持。
除此之外,直接修改数组的length
属性也会导致Object.defineProperty
的监听失败:
1 |
|
通过给length修改为10,数组中有10个undefied,虽然我们给每个元素都劫持了,但是没有触发get/set函数。
我们总结一下Object.defineProperty
在劫持对象和数组时的缺陷:
相较于Object.defineProperty劫持某个属性,Proxy则更彻底,不在局限某个属性,而是直接对整个对象进行代理,我们看一下ES6文档对Proxy的描述:
Proxy
可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。
首先还是来看一下Proxy的语法:
1 |
|
Proxy本身是一个构造函数,通过new Proxy
生成拦截的实例对象,让外界进行访问;构造函数中的target
就是我们需要代理的目标对象,可以是对象或者数组;handler
和Object.defineProperty
中的descriptor描述符有些类似,也是一个对象,用来定制代理规则。
1 |
|
可以看到Proxy直接代理了target
整个对象,并且返回了一个新的对象,通过监听代理对象上属性的变化来获取目标对象属性的变化;而且我们发现Proxy不仅能够监听到属性的增加,还能监听属性的删除,比Object.defineProperty
的功能更为强大。
除了对象,我们来看一下Proxy面对数组时的表现如何:
1 |
|
不管是数组下标或者数组长度的变化,还是通过函数调用,Proxy都能很好的监听到变化;而且除了我们常用的get、set,Proxy更是支持13种拦截操作。
可以看到Proxy相较于Object.defineProperty在语法和功能上都有着明显的优势;而且Object.defineProperty存在的缺陷,Proxy也都很好地解决了。
]]>Babel官网对Babel的定义就是:
Babel 是一个 JavaScript 编译器。
用通俗的话解释就是它主要用于将高版本的JavaScript代码转为向后兼容的JS代码,从而能让我们的代码运行在更低版本的浏览器或者其他的环境中。
比如我们在代码中使用了ES6箭头函数:
1 |
|
但我们如果用IE11浏览器(鬼知道用户会用什么浏览器来看)运行的话会出现报错;但是经过Babel编译之后的代码就可以运行在IE11以及更低版本的浏览器中了:
1 |
|
Babel就是做了这样的编译转换工作,来让我们不用考虑浏览器的兼容性问题,只要专心于代码的编写工作。
Babel的前身是从6to5
这个库发展而来,6to5
的作者是Facebook的澳大利亚工程师Sebastian McKenzie
在2014年发布的;从它的名字我们也能看出来,主要的功能就是将ES6转成ES5,我们如今也还能在npm官网看到这个包,不过作者提示已经迁移到Babel了:
在2015年1月份,6to5
和Esnext
的团队决定一起开发6to5
,并且改名为Babel
,解析引擎改名为Babylon
babel5及之前是一个包含CLI工具+编译器+转换器的集合工具包;babel6之后进行了拆分,集合包被分成多个包:
babel6默认情况下不携带任何转换器,需要自行安装所需的插件和转换器,通过babel-xxx
来安装对应的工具包。
而Babel7用了npm的private scope,把所有的包都挂载@babel
下,通过@babel/xxx
来安装,不用在node_modules下看到一堆的babel-xxx包。
本文主要以Babel7作为开发工具。
@babel/core我们在很多地方都看到,它是Babel进行转码的核心依赖包,我们常用的babel-cli和babel-node都依赖于它,我们通过例子来看一下它是如何来进行解析(相关代码在demo0):
1 |
|
可以发现原来的es6箭头函数在结果中几乎原封不动的返回出来了;Babel的运行方式总共可以分为三个阶段:解析(parsing)、转换(transforming)和生成(generating);负责解析阶段的插件是@babel/parser
,其作用就是将源码解析成AST;而负责生成阶段的插件是@babel/generator
,其作用就是将转好好的AST重新生成代码。
而@babel/core本身不具备转换处理的功能,它把转换的功能拆分到一个个插件(plugins)中;因此当我们不添加任何插件的时候,输入输出代码是相同的。
在@babel/core转换时还有几个副产物:code、mast和map,我们可以通过options
配置,根据需要对这几个副产物进行选择性的输出。除了transform
这个转换方法,还有transformSync
、transformAsync
和transformFileSync
等同步异步API,可以在babel官网找到。
@babel/cli
是Babel自带了一个内置的CLI命令行工具,我们就可以通过命令行来编译文件;它有两种调用方式,可以通过全局安装或者本地安装调用,选用一种即可,推荐在项目本地安装。
1 |
|
@babel/cli
还可以使用以下命令参数:
命令参数 | 缩写 | 作用 | 示例 |
---|---|---|---|
–out-file | -o | 输出文件名称 | babel a.js -o b.js |
–watch | -w | 实时监控输出 | babel a.js -o b.js -w |
–source-maps | -s | 输出map文件 | babel a.js -o b.js -s |
–source-maps inline | -s inline | 行内source map | babel a.js -o b.js -s inline |
–out-dir | -d | 编译文件夹 | babel src -d dist |
–ignore | 无 | 忽略某些文件 | babel src -d dist –ignore src/**/*.spec.js |
–copy-files | 无 | 复制不需要编译的文件 | babel src -d dist –copy-files |
–plugins | 无 | 指明使用插件 | babel a.js -o b.js –plugins @babel/plugin-transform-arrow-functions |
–presets | 无 | 指明使用预设 | babel src -d dist –presets @babel/preset-env |
@babel/cli
命令行的具体用法在demo1
我们虽然可以在命令行中配置各种插件(plugins)或者预设(presets,也就是一组插件),但是这样并不利于后期的查看或者维护,而且大多时候babel都是结合webpack或者gulp等打包工具开发,不会直接通过命令行的方式;因此Babel推荐通过配置文件的方式来进行管理。
Babel的配置文件主要有.babelrc
、.babelrc.js
、babel.config.js
和package.json
,他们的配置选项都是相同的,作用也是一样,主要区别在于格式语法的不同,因此我们在项目中只需要选择其中一种即可。
对于.babelrc
,它的配置主要是JSON格式的,像这样:
1 |
|
而.babelrc.js
和babel.config.js
同样都是JS语法,通过module.exports输出配置:
1 |
|
我们还可以根据环境来进行动态的配置。而在package.json
中,需要增加babel
的属性:
1 |
|
我们可以在配置文件中加入一些插件或者预设,来扩展@babel/core的转换功能;只需要将对应的插件或预设名字加入数组即可;比如我们常用的ES6箭头函数,就是通过@babel/plugin-transform-arrow-functions
这个插件来转换:
1 |
|
但有时候我们需要对插件和预设设置参数,就不能直接使用字符串的形式了;而应再包裹一层数组,数组的第一项是名称,第二项是设置的参数对象:
1 |
|
这样我们的箭头函数就能正常转换了,相关代码在demo2
Babel的插件大致可以分为语法插件
和转换插件
:
babel-plugin-syntax
开头。babel-plugin-transform
(正式)或 babel-plugin-proposal
(提案)开头。转换插件将启用相应的语法插件,因此不必同时指定这两种插件。
语法插件虽名为插件,但其本身并不具有功能性。语法插件所对应的语法功能其实都已在@babel/parser
里实现,插件的作用只是将对应语法的解析功能打开。所以本文提及的 Babel 插件将专指转换插件。
Babel官网提供了近一百个插件,但是如果我们的代码中一个一个的配置插件就需要对每一个插件有所了解,这样必然会耗费大量的时间精力;为此,Babel提供了预设(presets)的概念,意思就是预先设置好的一系列插件包;这就相当于肯德基中的套餐,将众多产品进行搭配组合,适合不同的人群需要;总有一款适合我们的套餐。
比如@babel/preset-es2015
就是用来将部分ES6语法转换成ES5语法,@babel/preset-stage-x
可以将处于某一阶段的js语法编译为正式版本的js代码,而@babel/preset-stage-x
也已经被Babel废弃了,有兴趣的童鞋可以看这篇官方的文章
我们实际会用到的预设有以下:
根据名字我们可以大致猜出每个预设的使用场景,我们重点了解一下@babel/preset-env
,它的作用是根据环境来转换代码。
插件和预设都是通过数组的形式在配置文件中配置,如果插件和预设都要处理同一个代码片段,那么会根据以下执行规则来判定:
我们来看一下官网对它的描述:
@babel/preset-env是一个智能预设,可让您使用最新的JavaScript,而无需微观管理目标环境所需的语法转换(以及可选的浏览器polyfill)。这都使您的生活更轻松,JavaScript包更小!
我们在项目中不会关心Babel用了哪些插件,支持哪些ES6语法;我们更多关心的是支持哪些浏览器版本这个层面,比如我们在项目中使用了箭头函数、Class、Const和模板字符串:
1 |
|
但是假如我们的项目需要支持IE10,因此我们需要修改.babelrc
:
1 |
|
或者对它进行缩写:
1 |
|
通过Babel编译后输出:
1 |
|
可以发现虽然我们没有配置任何转换插件,但是上面写的的箭头函数、Class、Const和模板字符串语法都已经被转换了;默认情况下,@babel/env等于@babel/preset-es2015、@babel/preset-es2016和@babel/preset-es2017三个套餐的叠加。
那如果我们只需要支持最新的Chrome了,可以继续修改.babelrc
:
1 |
|
targets中的含义是最新的两个Chrome版本,Babel再次编译输出:
1 |
|
而最新版本的Chrome已经支持箭头函数、Class、Const和模板字符串,所以在编译时不会在进行转换,相关代码在demo3。
上面的target
字段不少同学肯定看着很眼熟,这个工具能够根据项目中指定的目标浏览器自动来进行配置,这里我们就不展开深入讨论了;它也可以单独在项目中配置一个.browserslistrc
文件:
1 |
|
这样和targets字段的使用效果是一样的;正常情况下,推荐使用browserslist的配置而很少单独配置@babel/preset-env的targets;@babel/preset-env有一些常用的配置项让我们来看一下:
虽然targets不推荐使用,但是我们还是来了解一下它的用法,它是用来描述我们在项目中想要支持的目标浏览器环境,它可以是Browserslist格式的查询:
1 |
|
或者可以是一个对象,用来描述支持的最低版本的浏览器:
1 |
|
其他的浏览器版本还可以是:opera
、edge
、firefox
、safari
、ios
、android
、node
、electron
等
这个属性主要是给其他插件传递参数(比如@babel/plugin-transform-arrow-functions),默认是false,设为true后,我们的箭头函数会有以下改变:
.bind(this)
包裹一下,以便在函数内部继续使用this,而不是重命名this。这个属性也主要是给其他插件传递参数(比如@babel/plugin-transform-classes),默认是false,类的方法直接定义在构造函数上;而设置为true后,类的方法被定义到了原型上面,这样在类的继承时可能会引起问题。
转换时总是会启用插件的数组,格式是Array<string|RegExp>
,它可以是一下两种值:
es.map
,es.set
等 比如我们在last 2 Chrome versions
目标浏览器环境下,不会转换箭头函数和Class,但是我们可以将转换箭头函数的插件配置到include中,这样不管我们的目标浏览器怎么更换,箭头函数语法总是会转换:
1 |
|
useBuiltIns
这个属性决定是否引入polyfill,可以配置三个值:false(不引入)、usage(按需引入)和entry(项目入口处引入);corejs
表示引入哪个版本的core-js,可以选择2(默认)或者3,只有当useBuiltIns不为false时才会生效。
虽然@babel/preset-env可以转换大多高版本的JS语法,但是一些ES6原型链上的函数(比如数组实例上的的filter、fill、find等函数)以及新增的内置对象(比如Promise、Proxy等对象),是低版本浏览器本身内核就不支持,因此@babel/preset-env面对他们时也无能为力。
比如我们常用的filter函数,在IE浏览器上就会出现兼容性问题,因此我们通过polyfill(垫片)的方式来解决,下面是filter函数简单的兼容代码:
1 |
|
但是ES有那么多函数和内置对象,我们不可能一个一个都手写来解决,这就到了@babel/polyfill
用武之处了;首先我们需要在项目中安装它:
1 |
|
安装完成后在需要转换的文件入口加入引用代码:
1 |
|
或者我们也可以在Webpack入口处进行引入:
1 |
|
然后通过webpack来打包,这样就能看到在我们的代码中加入了很多的兼容代码,相关代码在demo4:
发现我们数组的fill、filter和findIndex等方法都打包进去了,但是看到这么多密密麻麻的兼容代码,眼尖的童鞋肯定会发现以下两个问题:
因此从Babel7.4
开始@babel/polyfill就不推荐使用了,而是直接引入core-js
与regenerator-runtime
两个包;而@babel/polyfill本身也是这两个包的集合;在上面webpack打包出来的dist文件我们也可以看到,引用的也是这两个包。那core-js
到底是什么呢?
目前我们使用的默认都是core-js@2
,但它已经封锁了分支,在此之后的特性都只会添加到core-js@3
,因此也是推荐使用最新的core-js@3
。
在上面@babel/preset-env配置中有useBuiltIns和corejs两个属性,是用来控制所需的core-js版本;我们以Object.assign、filter和Promise为例,相关代码在demo5:
1 |
|
然后修改配置文件,如果我们将useBuiltIns
配置为非false而没有指定corejs的版本,Babel会提示我们需要配置corejs的版本:
秉承着用新不用旧的原则,毅然选择core-js@3
:
1 |
|
可以看到我们的打包的文件自动引入了core-js中的模块:
1 |
|
而且我们发现它只引入了部分模块;这就比较厉害了,它不仅会考虑到代码中用到的新特性,还会参考目标浏览器的环境来进行按需引入;而useBuiltIns设置为entry的情况则会将core-js中的模块在入口处全部引入,这里就不再演示。
我们在上面通过@babel/preset-env转换Class类时发现输出文件的头部多了_classCallCheck、_defineProperties和_createClass三个函数声明,这就是注入的函数,称为辅助函数
;@babel/preset-env在转换时注入了函数声明,以便语法转换后使用。
但是我们开发项目时,文件少则几十个,多个上百个,如果每个文件都注入了函数声明,再通过打包工具打包后输出文件又会非常庞大,影响性能。
因此,Babel提供的解决思路是把这些辅助函数都放到一个npm包里面,在每次需要使用的时候就从这个包里把函数require
出来;这样即使有几千个文件,也都是对函数进行引用,而不是复制代码;最后通过webpack等工具打包时,只会将npm包中引用到的函数打包一次,这样就复用了代码,减少打包文件的大小。
@babel/runtime就是这些辅助函数的集合包,我们查看@babel/runtime下面的helpers,可以发现导出了很多函数,以及我们上面提及到的_classCallCheck函数:
首先当然是需要安装@babel/runtime这个包,除此之外还需要安装@babel/plugin-transform-runtime,这个插件的作用是移除辅助函数,将其替换为@babel/runtime/helpers中函数的引用。
1 |
|
然后修改我们的配置文件,相关代码在demo6:
1 |
|
再次打包发现我们的辅助函数已经变成下面的引用方式了:
1 |
|
上面我们说到@babel/polyfill会建立一个完整的ES2015环境,因此造成了全局变量的污染;虽然使用core-js不会引入全部模块,但是也会污染部分全局变量。
而@babel/plugin-transform-runtime除了能够转换上面的辅助函数,还能对代码中的新特性API进行一个转换,还是以我们的filter函数和Promise对象为例,相关代码在demo7:
1 |
|
然后修改我们的配置文件.babelrc:
1 |
|
再次查看打包出来的文件发现filter和Promise已经转换成了引用的方式:
1 |
|
我们发现打包出来的模块是从@babel/runtime-corejs3
这个包里面引用的;经过查看,发现它下面包含了三个文件夹:core-js、helpers和regenerator,因此我们可以发现:
@babel/runtime-corejs2 ≈ @babel/runtime+core-js+regenerator ≈ @babel/runtime+@babel/polyfill
经过下面这么多例子,总结一下@babel/polyfill和@babel/runtime的区别:前者改造目标浏览器,让你的浏览器拥有本来不支持的特性;后者改造你的代码,让你的代码能在所有目标浏览器上运行,但不改造浏览器。
一个显而易见的区别就是打开IE11浏览器,如果引入了@babel/polyfill,在控制台我们可以执行Object.assign({}, {});而如果引入了@babel/runtime,会提示你报错,因为Object上没有assign函数。
]]>我们在在Webpack配置基础篇介绍过,loader是链式传递的,对文件资源从上一个loader传递到下一个,而loader的处理也遵循着从下到上的顺序,我们简单了解一下loader的开发原则:
Webpack
制定的设计规则和结构,输入与输出均为字符串,各个Loader
完全独立,即插即用; 因此我们就来尝试写一个less-loader
和style-loader
,将less文件
处理后通过style标签的方式渲染到页面上去。
loader默认导出一个函数,接受匹配到的文件资源字符串和SourceMap,我们可以修改文件内容字符串后再返回给下一个loader进行处理,因此最简单的一个loader如下:
1 |
|
导出的
loader函数
不能使用箭头函数,很多loader内部的属性和方法都需要通过this
进行调用,比如this.cacheable()
来进行缓存、this.sourceMap
判断是否需要生成sourceMap等。
我们在项目中创建一个loader文件夹,用来存放我们自己写的loader,然后新建我们自己的style-loader:
1 |
|
这里的source
就可以看做是处理后的css文件字符串,我们把它通过style标签的形式插入到head中;同时我们也发现最后返回的是一个JS代码的字符串,webpack最后会将返回的字符串打包进模块中。
上面的style-loader
都是同步操作,我们在处理source时,有时候会进行异步操作,一种方法是通过async/await,阻塞操作执行;另一种方法可以通过loader本身提供的回调函数callback
。
1 |
|
callback的详细传参方法如下:
1 |
|
有些时候,除了将原内容转换返回之外,还需要返回原内容对应的Source Map,比如我们转换less和scss代码,以及babel-loader转换ES6代码,为了方便调试,需要将Source Map也一起随着内容返回。
1 |
|
这样我们在下一个loader就能接收到less-loader返回的sourceMap了,但是需要注意的是:
Source Map生成很耗时,通常在开发环境下才会生成Source Map,其它环境下不用生成。Webpack为loader提供了
this.sourceMap
这个属性来告诉loader当前构建环境用户是否需要生成Source Map。
loader文件准备好了之后,我们需要将它们加载到webpack配置中去;在基础篇中,我们加载第三方的loader只需要安装后在loader属性中写loader名称即可,现在加载本地loader需要把loader的路径配置上。
1 |
|
我们可以在loader中配置本地loader的相对路径或者绝对路径,但是这样写起来比较繁琐,我们可以利用webpack提供的resolveLoader
属性,来告诉webpack应该去哪里解析本地loader。
1 |
|
这样webpack会先去loader文件夹下找loader,没有找到才去node_modules;因此我们写的loader尽量不要和第三方loader重名,否则会导致第三方loader被覆盖加载。
我们在配置loader时,经常会给loader传递参数进行配置,一般是通过options属性来传递的,也有像url-loader
通过字符串来传参:
1 |
|
webpack也提供了query属性
来获取传参;但是query属性
很不稳定,如果像上面的通过字符串来传参,query就返回字符串格式,通过options方式就会返回对象格式,这样不利于我们处理。因此我们借助一个官方的包loader-utils
帮助处理,它还提供了很多有用的工具。
1 |
|
常用的就是getOptions
将处理后的参数返回出来,它内部的实现逻辑也非常的简单,也是根据query属性
进行处理,如果是字符串的话调用parseQuery
方法进行解析,源码如下:
1 |
|
获取到参数后,我们还需要对获取到的options
参数进行完整性校验,避免有些参数漏传,如果一个个判断校验比较繁琐,这就用到另一个官方包schema-utils
:
1 |
|
validate
函数并没有返回值,打印返回值发现是`undefined,因为如果参数不通过的话直接会抛出
ValidationError异常,直接进程中断;这里引入了一个
schema.json,就是我们对
options``中参数进行校验的一个json格式的对应表:
1 |
|
properties
中的健名就是我们需要检验的options
中的字段名称,additionalProperties
代表了是否允许options
中还有其他额外的属性。
写完我们自己简单的less-loader
,让我们来看一下官方的less-loader
源码到底是怎么样的,这里贴上部分源码:
1 |
|
可以看到官方的less-loader和我们写的简单的loader本质上都是调用less.render
函数,对文件资源字符串进行处理,然后将处理好后的字符串和sourceMap通过callback返回。
在loader中,我们有时候也会使用到外部的资源文件,我们需要在loader对这些资源文件进行声明;这些声明信息主要用于使得缓存loader失效,以及在观察模式(watch mode)下重新编译。
我们尝试写一个banner-loader
,在每个js文件资源后面加上我们自定义的注释内容;如果传了filename
,就从文件中获取预设好的banner内容,首先我们预设两个banner的txt:
1 |
|
然后在我们的banner-loader中根据参数来进行判断:
1 |
|
这里使用了this.addDependency
的API将当前处理的文件添加到文件依赖中(并不是项目的package.json)。如果在观察模式下,依赖的text文件发生了变化,那么打包生成的文件内容也随之变化。
如果不添加
this.addDependency
的话项目并不会报错,只是在观察模式下,如果依赖的文件发生了变化生成的bundle文件并不能及时更新。
在有些情况下,loader处理需要大量的计算非常耗性能(比如babel-loader),如果每次构建都重新执行相同的转换操作每次构建都会非常慢。
因此webpack默认会将loader的处理结果标记为可缓存,也就是说在需要被处理的文件或者其依赖的文件没有发生变化时,它的输出结果必然是相同的;如果不想让webpack缓存该loader,可以禁用缓存:
1 |
|
手写loader所有代码均在webpackdemo19
在 Webpack 运行的生命周期中会广播出许多事件,Plugin 可以监听这些事件,在合适的时机通过Webpack提供的API改变输出结果。和手写loader一样,我们先来写一个简单的plugin:
1 |
|
plugin的本质是类;我们在定义plugin时,其实是在定义一个类;定义好plugin后就可以在webpack配置中使用这个插件:
1 |
|
这样我们的插件就在webpack中生效了;这时有些童鞋可能会想起来,我们在使用HtmlWebpackPlugin
或者CleanWebpackPlugin
等一些官方插件时,可以通过实例化插件传入参数;那么这里我们是否也能通过这种方式给我们的插件传参呢?
1 |
|
我们在构建插件时就能通过options
获取配置信息,对插件做一些初始化的工作。在构造函数中我们发现多了一个apply
函数,它会在webpack运行时被调用,并且注入compiler
对象;其工作流程如下:
我们可以通过apply
函数中注入的compiler
对象进行注册事件:
1 |
|
compiler不仅有同步的钩子,通过tap函数来注册,还有异步的钩子,通过tapAsync
和tapPromise
来注册:
1 |
|
这里又有一个compilation
对象,它和上面提到的compiler
对象都是Plugin和webpack之间的桥梁:
compiler
对象包含了 Webpack 环境所有的的配置信息。这个对象在启动 webpack 时被一次性建立,并配置好所有可操作的设置,包括 options,loader 和 plugin。当在 webpack 环境中应用一个插件时,插件将收到此 compiler 对象的引用。可以使用它来访问 webpack 的主环境。compilation
对象包含了当前的模块资源、编译生成资源、变化的文件等。当运行webpack 开发环境中间件时,每当检测到一个文件变化,就会创建一个新的 compilation,从而生成一组新的编译资源。compilation 对象也提供了很多关键时机的回调,以供插件做自定义处理时选择使用。compiler和compilation的区别在于:
了解了compiler和compilation的区别,我们就来尝试一个简单的示例插件,在打包目录生成一个filelist.md
文件,文件的内容是将所有构建生成文件展示在一个列表中:
1 |
|
我们这里用到了assets
对象,它是所有构建文件的一个输出对象,打印出来大概长这样:
1 |
|
我们手动加入一个filelist.md
文件的输出;打包后我们在dist文件夹中会发现多了这个文件:
1 |
|
这个插件就完成了我们的预期任务了。
参考
]]>我们在网易云听歌时,可以打开多个标签页进行播放;但是我们发现在一个标签页播放的同时,其他标签如果正在播放,都会自动的停止。
想想这样也是合理的,因为毕竟如果多个标签页都同时播放声音就会干扰,同一时间只能存在一个音乐播放;因此我们也来尝试实现这样一个需求,在不同浏览器中进行数据通信:
我们首先来准备一些数据,和网易云一样,准备一个专辑列表,每个专辑列表中有不同的歌曲,可以通过URL参数传递id来获取不同的专辑页面:
1 |
|
1 |
|
要想在所有的标签页中实现通信,我们必须将数据存放到一个公共的存储空间,所有的标签页都能获取并且还能进行修改;我们知道,cookie在用户所有浏览器标签页中都是共享的,因此,我们可以尝试把选中的数据存放到cookie中去:
1 |
|
由于更新cookie并不能触发任何事件,因此我们需要通过定时器setInterval
来主动监听cookie中的值是否改变;代码看起来没有问题,让我们看一下运行的效果:
存在下面两个问题:
因此我们需要给每一个页面区分一个页面id;这个页面id可以从后台接口中获取,这里为了简单展示,我们使用时间戳作为页面id:
1 |
|
我们虽然能通过给每个页面分配id来解决问题2,但是由于定时器的弊端,cookie+setInterval
的方案会存在延时的情况。
localStorage也是浏览器多个页面共用的存储空间;而且localStorage在一个页面中添加、修改或者删除时,都会在非当前页面中被动触发一个storage
事件,我们通过在其他页面中监听storage
事件,即可拿到storage
更新前后的值:
1 |
|
相较于cookie的主动监听,localStorage的被动触发不仅在代码显得更加友好,而且还极大的避免了定时器带来的性能损耗。
我们在《从一道面试题来理解JS事件循环》提到,webworker只能用来做一些消耗CPU的逻辑运算等;webworker也分为Worker和SharedWorker,普通的worker可以直接使用new Worker()
创建,只在当前页面中使用;而SharedWorker通过名字我们也能看出,是可以在多个标签页面中数据是共享的;
SharedWorker和Worker不同之处在于它第二个参数可以做直接指定name
,或者使用对象参数,因此下面三种构造方式是相同的:
1 |
|
构造了SharedWorker实例对象后,我们需要通过其port
属性进行通信,主要的API如下:
1 |
|
由于构造的多个SharedWorker实例形成了一个共享的连接,因此在连接成功时,我们给每个实例分配一个唯一id:
1 |
|
我们在ShareWorker内部监听connect
事件,并且处理内部的port
事件:
1 |
|
当写shared.js,我们经常会遇到问题,那么怎么来调试sharedworker呢?直接console.log
并不会在标签页面中有输出;我们打开新的标签页chrome://inspect
,选择Shared workers
然后再选择对应脚本,就能愉快的调试了。
websocket作为全双工通信,自然可以实现多个标签页之间的通信;WebSocket是HTML5新增的协议,它的目的是在浏览器和服务器之间建立一个不受限的双向通信的通道。
这里我们使用express的一个框架express-ws
来模拟websocket服务器;由于服务器会储存很多标签页的连接对象信息,因此我们需要给每个用户进行唯一标识进行区分;我们从服务器获取user_id
并保存。
1 |
|
通过user_id
我们就可以向websocket服务器连接并发起请求了。
1 |
|
在标签页每次和websocket建立连接后,将连接对象存放到数组中。
1 |
|
本文所有代码都在git仓库
]]>本文将从缩小文件搜索范围、减少打包文件、缓存和多进程四个方面来了解Webpack的优化配置。
Webpack会从Entry入口出发,解析文件中的导入模块语句,再递归解析;每次遇到导入语法时会做两件事情:
require('vue')
就去引入/node_modules/vue/dist/vue.runtime.common.js
文件当项目只有几个文件时,解析文件流程只有几百毫秒,然而随着项目规模的增大,解析文件会越来越耗时,因此我们通过webpack的配置来缩小我们搜索模块的范围
在上一篇中,我们介绍了使用include/exclude
将node_modules中的文件进行包括/排除。
1 |
|
include
表示哪些目录中的文件需要进行babel-loader,exclude
表示哪些目录中的文件不要进行babel-loader。这是因为在引入第三方模块的时候,很多模块已经是打包后的,不需要再被处理,比如vue、jQuery等;如果不设置include/exclude
就会被loader处理,增加打包时间。
如果一些第三方模块没有使用AMD/CommonJs规范,可以使用noParse
来标记这个模块,这样Webpack在导入模块时,就不进行解析和转换,提升Webpack的构建速度;noParse可以接受一个正则表达式或者一个函数:
1 |
|
对于jQuery、lodash、chartjs等一些库,庞大且没有采用模块化标准,因此我们可以选择不解析他们。
注:被不解析的模块文件中不应该包含
require
、import
等模块语句
经过多次打包尝试,打包性能大概能提升10%~20%;本实例完整代码demo,
modules用于告诉webpack去哪些目录下查找引用的模块,默认值是["node_modules"]
,意思是在./node_modules
查找模块,找不到再去../node_modules
,以此类推。
我们代码中也会有大量的模块被其他模块依赖和引入,由于这些模块位置分布不固定,路径有时候会很长,比如import '../../src/components/button'
、import '../../src/utils'
;这时我们可以利用modules进行优化
1 |
|
这样我们可以简单的通过import 'components/button'
、import 'utils'
进行导入,webpack会会优先从src
目录下进行查找
alias通过创建import或者require的别名,把原来导入模块的路径映射成一个新的导入路径;它和resolve.modules
不同的的是,它的作用是用别名代替前面的路径,不是省略;这样的好处就是webpack直接会去对应别名的目录查找模块,减少了搜索时间。
1 |
|
这样我们就能通过import Buttom from '@/Button'
来引入组件了;我们不光可以给自己写的模块设置别名,还可以给第三方模块设置别名:
1 |
|
我们在import Vue from 'vue'
时,webpack就会帮我们去vue依赖包的dist文件下面引入对应的文件,减少了搜索package.json的时间。
mainFields用来告诉webpack使用第三方模块中的哪个字段来导入模块;第三方模块中都会有一个package.json
文件用来描述这个模块的一些属性,比如模块名(name)、版本号(version)、作者(auth)等等;其中最重要的就是有多个特殊的字段用来告诉webpack导入文件的位置,有多个字段的原因是因为有些模块可以同时用于多个环境,而每个环境可以使用不同的文件。
mainFields的默认值和当前webpack配置的target
属性有关:
webworker
或web
(默认),mainFields默认值为["browser", "module", "main"]
["module", "main"]
这就是说当我们require('vue')
的时候,webpack先去vue下面搜索browser字段,没有找到再去搜索module字段,最后搜索main字段。
为了减少搜索的步骤,在明确第三方模块入口文件描述字段时,我们可以将这个字段设置尽量少;一般第三方模块都采用main
字段,因此我们可以这样配置:
1 |
|
extensions字段用来在导入模块时,自动带入后缀尝试去匹配对应的文件,它的默认值是:
1 |
|
也就是说我们在require('./utils')
时,Webpack先匹配utils.js
,匹配不到再去匹配utils.json
,如果还找不到就报错。
因此extensions
数组越长,或者正确后缀的文件越靠后,匹配的次数越多也就越耗时,因此我们可以从以下几点来优化:
以上实例完整代码demo。
在我们项目中不可避免会引入第三方模块,webpack打包时也会将第三方模块作为依赖打包进bundle中,这样就会增加打包文件尺寸和增加耗时,如果能合理得处理这些模块就能提升不少webpack的性能。
我们的项目通常有多个页面或者多个页面模块(单页面),多个页面之间通常都有公用的函数或者第三方模块,在每个页面中都打包这些模块会造成以下问题:
在Webpack4之前,都是通过CommonsChunkPlugin插件来提取公共代码,然而存在着以下问题
Webpack4引入了SplitChunksPlugin
插件进行公共模块的抽取;由于webpack4开箱即用的特性,它不用单独安装,通过optimization.splitChunks
进行配置即可,官方给的默认配置参数如下:
1 |
|
我们在home、list、detail三个页面分别引入了vue.js、axios.js和公用的工具函数模块utils.js;我们首先将使用到的第三方模块提取到一个单独的文件,这个文件包含了项目的基础运行环境,一般称为vendors.js
;在抽离第三方模块后我们将每个页面都依赖的公共代码提取出来,放到common.js
中。
1 |
|
有时候项目依赖模块比较多,vendors.js
文件会特别大,我们还可以对它进一步拆分,按照模块划分:
1 |
|
DLL即动态链接库(Dynamic-Link Library)的缩写,熟悉Windows系统的童鞋在电脑中也经常能看到后缀是dll的文件,偶尔电脑弹框警告也是因为电脑中缺失了某些dll文件;DLL最初用于节约应用程序所需的磁盘和内存空间,当多个程序使用同一个函数库时,DLL可以减少在磁盘和内存中加载代码的重复量,有助于代码的复用。
在Webpack中也引入了DLL的思想,把我们用到的模块抽离出来,打包到单独的动态链接库中去,一个动态链接库中可以有多个模块;当我们在多个页面中用到某一个模块时,不再重复打包,而是直接去引入动态链接库中的模块。
Webpack中集成了对动态链接库的支持,主要用到的两个插件:
我们首先使用DllPlugin来创建动态链接库文件,在项目下新建webpack.dll.js
文件:
1 |
|
这里entry
设置了多个入口,每个入口也有多个模块文件;然后在package.json
添加打包命令
1 |
|
执行npm run build:dll
后,我们在/public/vendor
目录下得到了我们打包后的动态链接库的文件:
1 |
|
生成出来的打包文件正好是以两个入口名来命名的,以vue为例,看一下vue.dll.js
的内容:
1 |
|
可以看出,动态链接库中包含了引入模块的所有代码,这些代码存在一个对象中,通过模块路径作为键名来进行引用;并且通过vue_dll_lib暴露到全局;vue.manifest.json则是用来描述动态链接库文件中包含了哪些模块:
1 |
|
manifest.json描述了对应js文件包含哪些模块,以及对应模块的键名(id),这样我们在模板页面中就可以将动态链接库作为外链引入,当Webpack解析到对应模块时就通过全局变量来获取模块:
1 |
|
最后我们在打包时,通过DllReferencePlugin
将动态链接库引入到主配置中:
1 |
|
注:动态链接库打包到
/public/vendor
目录下,还需要通过CopyWebpackPlugin
插件将它拷贝到生成后的目录中,否则会出现引用失败的报错;打包动态链接库文件只需要执行一次,除非以后模块升级或者引入新的模块。
引入动态链接库可以将项目中一些不经常更新的模块放到外部文件中,我们再次打包页面逻辑代码时会发现构建速度有了比较大的提升,大概30%~40%,相关代码在demo10。
我们在项目打包时,有一些第三方的库会从CDN引入(比如jQuery等),如果在bundle中再次打包项目就过于臃肿,我们就可以通过配置externals
将这些库在打包的时候排除在外。
1 |
|
这样就表示当我们遇到require('jquery')
时,从全局变量去引用jQuery
,其他几个包也同理;这样打包时就把jquery、react、vue和react-dom从bundle中剔除了,本实例完整代码demo。
Tree Shaking最早由rollup实现,后来webpack2页实现了这项功能;Tree Shaking的字面意思是摇树,一棵树上有一些树叶虽然还挂着,但是它可能已经死掉了,通过摇树方式把这些死掉的树叶去除。
我们项目中也是同样的,我们并没有用到文件的所有模块,但是webpack仍会将整个文件打包进来,文件中一直用不到的代码就是“死代码”;这种情况就用用到Tree Shaking
帮我们剔除这些用不到的代码模块。
比如我们定义了一个utils.js
文件导出了很多工具模块,然后在index.js
中只引用了某些模块:
1 |
|
我们希望在代码中只打包isArray
函数到bundle中;需要注意的是,为了让Tree Shaking生效,我们需要使用ES6模块化的语法,因为ES6模块语法是静态化加载模块,它有以下特点:
如果是require
,在运行时确定模块,那么将无法去分析模块是否可用,只有在编译时分析,才不会影响运行时的状态。
使用ES6模块后还有一个问题,因为我们的代码一般都采用babel进行编译,而babel的preset默认会将任何模块类型编译成Commonjs,因此我们还需要修改.babelrc
配置文件:
1 |
|
配置好babel后我们需要让webpack先将“死代码”标识出来:
1 |
|
运行打包命令后,当我们打开输出的bundle文件时,我们发现虽然一些“死代码”还存在里面,但是加上了一个unused harmony export
的标识
1 |
|
虽然webpack给我们指出了哪些函数用不上,但是还需要我们通过插件来剔除;由于uglifyjs-webpack-plugin
不支持ES6语法,这里我们使用terser-webpack-plugin
的插件来代替它:
1 |
|
这样我们发现打包出来的文件就没有多余的代码了。
注: Tree Shaking在生产环境(production)是默认开启的
对于我们常用的一些第三方模块,我们也可以实现Tree Shaking;以lodash
为例,它整个包有非常多的函数,但并不是所有的函数都是我们所用到的,因此我们也需要对它没有用到的代码进行剔除。
1 |
|
打包出来发现包的大小还是能达到70+kb,如果只引用了chunk不应该有这么大;我们打开/node_modules/lodash/index.js
发现他还是使用了require的模式导入导出模块,因此导致Tree Shaking失败;我们先安装使用ES6模块版本的lodash:npm i -S lodash-es
,然后修改引入包:
1 |
|
这样我们生成的bundle包就小很多;本实例完整代码demo。
我们知道webpack会对不同的文件调用不同的loader进行解析处理,解析的过程也是最耗性能的过程;我们每次改代码也只是修改项目中的少数文件,项目中的大部分文件改动的次数不是那么频繁;那么如果我们将解析文件的结果缓存下来,下次发现同样的文件只需要读取缓存就能极大的提升解析的性能。
cache-loader可以将一些对性能消耗比较大的loader生产的结果缓存在磁盘中,等下次再次打包时如果是相同的代码就可以直接读取缓存,减少性能消耗。
注:保存和读取缓存也会产生额外的性能开销,因此cache-loader适合用于对性能消耗较大的loader,否则反而会增加性能消耗
cache-loader的使用也非常简单,安装后在所需要缓存的loader前面添加即可(因为loader加载的顺序是反向的),比如我们需要给babel-loader
添加缓存:
1 |
|
然而我们发现第一次打包的速度并没有发生明显变化,甚至可能还比原来打包的更慢了;同时还多了/node_modules/.cache/cache-loader/
这个目录,看名字就是一个缓存文件;我们继续打包,下面图表记录了我几次打包的耗时:
我们发现第一次打包时间都差不多,但是第二次开始缓存文件就开始发挥了重要的作用了,直接减少了75%的耗时。
除了使用cache-loader,babel-loader也提供缓存功能,通过cacheDirectory
进行配置:
1 |
|
在/node_modules/.cache/babel-loader
也多了缓存文件。经过两个使用结果的对比,cache-loader的性能提升更加出色一些;本实例完整代码demo。
HardSourceWebpackPlugin也可以为模块提供缓存功能,同意也是将文件缓存在磁盘中
首先通过npm i -D hard-source-webpack-plugin
来安装插件,并且在配置中添加插件:
1 |
|
一般HardSourceWebpackPlugin默认缓存是在/node_modules/.cache/hard-source/[hash]
目录下,我们可以设置它的缓存目录和何时创建新的缓存哈希值。
1 |
|
通过尝试多次打包,发现能节省大概90%的时间;本实例完整代码demo。
我们在事件循环中讲到过,js是一门单线程的语言,在同一事件线上只有一个线程在处理任务;因此在webpack解析到JS、CSS、图片或者字体文件时,它需要一个个的去解析编译,不能同时处理多个任务;我们可以通过插件来将任务分给多个子进程去并发执行,子进程处理完成后再将结果发送给主进程。
happypack会自动帮我们分解任务和管理进程,通过名字我们也能看出来,这是一款能够带来快乐的插件。
我们通过npm i -D happypack
后就能在webpack中进行配置了:
1 |
|
我们将rules/loader
的处理全部交给了happypack进行处理,并且通过id来调用具体的实例,然后在实例中配置具体的loader进行处理;在happypack的实例中除了id和loaders我们还可以配置进程数量:
1 |
|
注:threads和threadPool字段只需要配置一个即可。
我们通过happypack.ThreadPool
创建了一个包含5个子进程的共享进程池,每个happypack实例可以通过共享进程池来处理文件;相对于给每个happypack实例分配进程,这样可以防止占用过多无用的进程;我们打包看一下所耗时间:
我们发现有了happypack耗时居然还增加了20%~30%,说好的多进程带来快乐呢。
由于我们的项目不够庞大,而加载多进程也需要耗费时间和性能,因此我们才会出现使用了happypack反而增加耗时的情况;所以一般happypack适用于比较大的项目中;本实例完整代码demo。
把thread-loader放置在其他loader之前,在它之后的loader就会在一个单独的进程池中运行,但是在进程池中运行的loader有以下限制:
因此,也就是说像MiniCssExtractPlugin.loader
等一些提取css的loader是不能使用thread-loader的;跟happypack一样,它也只适合用于文件较多的大项目:
1 |
|
本实例完整代码demo。
]]>首先来看一下axios有哪些特性:
axios的大致处理流程如下:
axios除了以上的特性,还支持了多种请求方式,方便我们通过不同的方式来请求。
第一种方式axios(option)
。
1 |
|
第二种方式axios(url[, config])
。
1 |
|
第三种方式axios.request(url?,option)
,第三种方式同第一种方式本质上一样。
1 |
|
第四种方式axios.request(url,option)
,第四种方式同第三种方式本质上一样。
1 |
|
第五种方式axios[method](url,option)
,这种请求方式主要针对get、delete、head、options方法。
1 |
|
第六种方式axios[method](url,data,option)
,这种请求方式主要针对post、put、patch方法。
1 |
|
这六种请求方式也是我们常见的方式;我们发现前两种请求方式axios作为一个函数直接来请求,而后面四种方式axios则是一个对象;因此我们猜测,axios首先肯定是一个函数,这个函数上又挂载了request、get、post等函数方便我们具体调用某个方法。
看代码前先来看一下项目的整体结构
1 |
|
可以发现,我们需要用到的代码大多在/lib
目录下。
看源码之前我们首先来学习一下axios用到的几个易于混淆的工具函数,以及它们具体是用来实现什么功能的。
bind函数用来给某一函数指定调用时的上下文,其源码如下:
1 |
|
它的实现效果同Function.prototype.bind
1 |
|
forEach用来遍历对象或者数组;我们知道对象需要for in
遍历,而数组用for
循环遍历,forEach将两者遍历的方式整合到一起,其源码如下:
1 |
|
可以看出forEach对字符串或者数字等基本数据类型做了兼容,对数组和对象做了不同的遍历处理;其中如果遍历的是对象,回调函数每次返回对象的值、键以及对象本身。
1 |
|
extend将一个对象b上面所有的属性和方法扩展到另一个对象a上,并且指定方法调用的上下文,其源码如下:
1 |
|
这里forEach就用来遍历对象了;a是目标对象,b是源对象,thisArg是执行上下文,我们可以通过代码尝试一下:
1 |
|
最后运行可以发现source对象上的属性方法都赋值到target对象上,执行上下文是context对象了。
merge函数用来将多个对象深度合并为一个新的对象,其源码如下:
1 |
|
isPlainObject判断一个对象是否是一个JS原生对象,即使用Object构造函数创建的对象;如果是对象的话就进行深度的合并,我们写一个demo测试一下:
1 |
|
介绍完了工具函数我们就真正的进入axios的核心源码部分;首先在index.js
中,我们看到通过module.exports = require('./lib/axios');
导出了axios,因此我们找到/lib/axios
文件:
1 |
|
这段代码看上去比较绕,不过我们发现核心部分就是通过createInstance
创建了一个axios实例对象,创建的同时传入了defaultConfig
对象(根据名字我们也能猜出来这是默认配置),然后将实例对象导出;因此createInstance
创建的就是我们使用的那个axios函数。
由于createInstance
创建是通过Axios构造函数创建的,因此我们把createInstance
放一放,先看一下Axios构造函数做了哪些操作:
1 |
|
Axios构造函数仅仅做了两个事,一个是将默认配置保存到defaults,另一个则是构造了interceptors
拦截器对象;Axios函数在原型对象上还挂载了request、get、post等函数,但是get、post等函数最终都是通过request
函数来发起请求的。而且request
函数最终返回了一个Promise对象, 因此我们才能通过then函数接收到请求结果。对原型链不了解的童鞋可以看这篇文章一文读懂JS中类、原型和继承。
了解了Axios构造函数的本质,让我们再回到createInstance
函数:
1 |
|
我们发现Axios构造出了实例对象context,然而createInstance并不是直接返回了context对象;这是因为上面我们也说了axios是一个函数,然而context是对象,返回对象的话是并不能直接调用的,那怎么办呢?
我们在Axios源码中发现,真正调用的是Axios原型链上的request
方法;因此导出的axios需要关联到request
方法,这里巧妙的通过bind函数进行关联,生成关联后的instance
函数,同时指定它的调用上下文就是Axios的实例对象,因此instance
调用时也能获取到实例对象上的defaults和interceptors属性;但是仅仅关联request
还不够,再通过extend
函数将Axios原型对象上的所有get、post等函数扩展到instance
函数上,因此这也是我们才能够使用多种方式调用的原因所在。
同时,如果我们需要创建多个axios实例,但是某几个axios实例的配置(用了同样的域名等)是一样的,我们不希望每次都要写重复的配置,axios还提供了另一种创建实例模板的方式:
1 |
|
通过create
函数创建了一个有默认配置的实例,这样我们只需要愉快的调用axios的API方法即可;需要请求其他域名只需要再次create即可,这也是工厂模式的一种体现,它的源码实现也很简单,也是通过createInstance
创建一个合并配置后的实例:
1 |
|
实例对象创建好之后,我们就需要把配置进行合并,方便后面发送请求。在看源码前,我们发现上面代码中主要有两种config,一种是defaultConfig
,即默认配置,在构造实例的时候传入;另一种是userConfig
,也就是我们调用实例进行请求时传入的配置;在上面的代码中,也出现了很多次mergeConfig
这个函数,根据命名我们也能看出来,这是用来合并两种配置的。
首先让我们看一下axios给我们默认配置了哪些属性:
1 |
|
可以发现,axios定义了默认的适配器(用于发送请求)、转换器(转换请求和响应数据)和请求头等一些数据;getDefaultAdapter
函数用来获取默认的适配器,这样在浏览器端和node环境都可以发送请求;可以看到axios给每个请求方法都定义了一个默认的请求头和一个公共的请求头common
,在后面发送请求时会根据传入的请求方法类型使用相应的请求头。
下面我们就来看一下mergeConfig是如何将两种config合并的;首先mergeConfig将所有config中用到的字段进行了划分,分成了三类:
userConfig
中有,则以userConfig
为准;没有取defaultConfig
的值1 |
|
对于第一种属性,url、method和data不能从默认配置中取值,因此如果userConfig中有就直接取userConfig中的值。
1 |
|
对于第二种属性,如果userConfig中有,就将其与defaultConfig进行合并。
1 |
|
对于第三种普通属性,如果userConfig中有就取userConfig,没有就取defaultConfig中的值。
1 |
|
对于除这三种属性之外的其他属性,当做第二种属性处理,通过mergeDeepProperties
函数进行整合。
1 |
|
axios的源码还是有很多的地方值得我们来深入学习的,比如工具函数和它如何构造实例等;我们根据axios的多种请求方式,找到了它在构造实例时巧妙的绑定方式来实现多种请求的调用,构造实例后就需要将用户传入的配置和默认的配置进行整合起来;在下一篇文章中我们会了解axios是如何使用整合后的配置来通过适配器发起请求的。
]]> linear-gradient()
称为线性过渡函数,用于创建一个线性渐变的图像。为了创建一个线性渐变,我们需要最少传入三个元素:渐变方向、起始点颜色和终止点颜色(可以有多个终止点);因此,它的语法也是传入多个节点的颜色即可:
background: linear-gradient(direction, color-stop1, color-stop2, …);
direction
表示渐变的一个方向,color-stop
表示渐变方向上不同的节点;我们尝试构建一个彩虹色渐变的div:
1 |
|
我们定义了渐变方向上红橙黄绿青蓝紫七个节点,节点会在div上自动排列分布,节点中间的渐变部分则由浏览器自动计算绘制,因此一个彩虹div就出来了:
在渐变容器中,穿过容器中心点和颜色停止点连接在一起的线称为渐变线.
这里的渐变线从A到B,穿过容器的中心点C;渐变线AB与容器的垂直线形成的夹角α,称为渐变角度。
在上面语法中的direction
就是用来控制渐变线的方向,它可以接收两种类型的值:
to top
、to bottom
、to left
、to right
、to top right
、to top left
、to bottom right
和to bottom left
45deg
、1turn
等(turn表示圈,1turn=360deg) 如果省略角度值的设置,默认direction就是to bottom
,它转换成角度单位就是180deg或者0.5turn。
在上图中,没有设置direction,white至red渐变色是从上至下,它和使用to bottom
方向关键词得到的效果是一致的。
另外,使用to top
和0deg
的效果也是一样的:
除了left、right、top和bottom,另外四个使用了顶角关键词,比如:to top right
,表示从容器的一个顶角到它的对角,因此他们的角度依赖于容器的尺寸大小。
可以看出来,如果容器是一个长方形,to top right
角度就不为45deg
。
在上面demo中多个渐变节点都是浏览器自己均匀分布后计算出来的,我们可以在颜色后面显示的定义颜色在渐变色的位置;每个位置可以用百分比表示,也可以用CSS长度单位(px)来表示。
我们给每个颜色定义了自己的位置,以百分比为单位,这些单位都是以渐变线的开始位置进行计算的。
如果我们将两个渐变节点的位置重合,那么会发生什么呢?
我们惊奇的发现之前的渐变色没了,而是分割成了不同的色块;同时我们发现两边的色块呈现梯形;在面试完50个人后我写下这篇总结中,我们通过border属性来画了三角形和梯形,因此我们可以通过linear-gradient
很方便的就能画出来,这里不再演示。
linear-gradient还有一些丰富的效果
这样的进度条效果我们在浏览网页的时候经常会看到,很多都是通过图片或者animate实现的,我们可以通过linear-gradient
实现简单的静态效果。
1 |
|
通过渐变节点前后相连接,这样一个卡通风格的进度条就完成了。
缺角按钮偶尔也会看到,它的实现其实很简单,将拼接部分的颜色设置为透明即可:
1 |
|
开关效果也是很多jQuery组件经常用的
1 |
|
我们将从下面六个角度来具体剖析一下GET和POST的真正区别。
一般认为POST在传输数据时更加的安全,因为GET传输时将在URL中显示参数,而POST的数据则放在了请求体(body)
,所以更安全。
这种想法的由来多半是因为表单form数据提交时默认GET传输时会把表单中的字符拼接到URL后作为参数传送,POST传输表单数据会放在body中;当GET方式提交后会把提交的数据显示在URL上。
1 |
|
两种方式提交后URL上的差别让人们觉得POST相对于GET更安全。
然而安全性的说法存在着两个问题:
请求体(body)
中的数据请求体(body)
中传输数据既然HTTP规范没有规定,那么为什么就不能传呢?我试着用axios和jQuery来在GET请求体中加入数据,无一例外的失败了;既然框架靠不住那就直接用原生的吧:
1 |
|
我们通过express来接收一下传输的数据:
1 |
|
正当我兴奋的准备接收数据时,现实狠狠的给了我一拳:
1 |
|
浏览器并没有往请求体中存放数据,直接把send中传输的数据给丢弃了,那么GET的BODY中真的就不能放数据吗?
答案自然是可以的,浏览器由于规范问题不会在GET的请求体中添加数据,那我们可以尝试一下通过Fiddler的Composer来添加:
这样我们就能在express中看到GET请求中的
1 |
|
既然GET也能在Body中添加数据,那么POST请求数据的安全性并不是面试官期望听到的回答。
常见的GET和POST区别还有GET传输的数据比较少,POST传输数据多;在HTTP规范中并没有对URL的长度和传输的数据大小进行限制,但是在实际开发时,由于浏览器和服务器均对URL的长度进行了限制,因此表现出了GET传输数据少的缺点。在笔者的测试过程中,Chrome的URL长度限制比较好,基本能达到1mb的限制;而在IE11中4kb~5kb的时候浏览器就会自动将URL后的参数丢弃了。
而对于POST请求,由于数据放在请求体中,虽然理论上不会受到限制,但是实际开发中各个服务器也会对POST的数据大小进行一定的限制;比如nginx默认上传图片的大小是2mb,图片超过2mb就会提示413 Request Entity Too Large
。
因此不管GET还是POST,数据传输大小都会有限制,只是POST的传输大小相对于GET来说比较大;数据量大小也并不是面试官期望听到的回答。
GET请求多是用来获取数据的,比如一件商品的信息、商品列表等,在一定时间内,商品的信息不会那么快的变化,因此它通常用于缓存。
而POST请求多是用于将表单数据提交到服务端,比如修改姓名、修改密码等操作,需要浏览器必须发送请求到达服务器才能进行操作,因此POST请求不能被缓存。
从上图可以看出,我们同时将GET和POST请求在浏览器中多提交几次,会发现不管多少次提交,POST请求都是200;而GET请求第一次200,后面的请求会304进行缓存下来。
GET请求由于请求数据一般都是在URL上,所以GET一般都是URL编码;而POST请求由于请求参数的多样性,因此有多重编码方式;一般POST请求有application/x-www-form-urlencoded
、multipart/form-data
和application/json
三种传输方式。
multipart/form-data
一般在上传文件的时候通过Html5的FormData新特性来构造上传表单对象会用到这种编码方式,通过FormData上传的请求类似这样:
application/x-www-form-urlencoded
是Form表单的默认提交方式,这种方式的好处就是浏览器都支持;但是缺点是在请求发送前需要对数据进行序列化处理,以键值对形式,例如:key1=value1&key2=value2的形式发送到服务器;如果使用jQuery的ajax方法,它内部已经对JSON格式的数据进行了序列化处理了;而使用axios默认使用application/json
进行编码,或者自己通过原生Ajax请求就需要自己进行序列化。
随着JSON规范越来越流行,并且浏览器支持程度比较好,application/json
编码也越来越被更多的开发者使用;通过Content-Type告诉服务器主体内容是JSON格式的字符串,服务器端就会进行解析;这样的好处是前端人员不需要关心数据结构的复杂度,只要是标准的JSON格式就能提交成功。
初次看到幂等
这个词很多童鞋都很陌生,我们首先来看一下幂等的定义:
在编程中,一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同。幂等函数,或幂等方法,是指可以使用相同参数重复执行,并能获得相同结果的函数。
HTTP的幂等是指无论调用多少次,无论调用一次还是一千次,都具有同样的副作用
。GET请求用于获取资源,不管调用多少次接口,都不会影响返回的资源;因此我们说GET请求是幂等的
。
1 |
|
比如我们在获取列表数据或者获取详情数据,只是查询数据,不会影响到数据本身,也不会对资源产生副作用
;因此我们认为它是幂等的。
这里有的小伙伴可能会提出疑问了,如果通过GET请求返回当前时间,那岂不是返回的结果每时每刻都不一样吗?怎么会幂等呢?
注意,我们这里强调的是一次和N次对资源产生的副作用是相同的
,而非单纯的比较结果相同。比如我们GET /news/1
用来获取某个新闻,不管我们调用N次返回的都是新闻,没有产生副作用;但是新闻偶尔会更新,返回的结果可能不尽相同。
而POST请求很明显是非幂等的,因为请求多次,都会产生不同的资源。比如在支付中,我们调用POST请求,虽然我们每次就支付一块钱,但是每次必然会产生不同的订单(如果是同一个订单相信你肯定要投诉的)
get会产生一个TCP数据包,POST会产生两个TCP数据包。
get会发送http header和data给服务端,服务端返回一个200,请求成功。
post会先发送http header给服务端,告诉服务端等一下会有数据过来,服务端返回100,告诉客户端我已经准备接收数据,post在发送一个data给服务端,服务端返回200,请求成功。
]]>本文所有的demo代码均在WebpackDemo
来看一下官网对webpack的定义:
本质上,webpack 是一个现代 JavaScript 应用程序的静态模块打包器(module bundler)。当 webpack 处理应用程序时,它会递归地构建一个依赖关系图(dependency graph),其中包含应用程序需要的每个模块,然后将所有这些模块打包成一个或多个bundle。
首先webpack是一个静态模块打包器,所谓的静态模块,包括脚本、样式表和图片等等;webpack打包时首先遍历所有的静态资源,根据资源的引用,构建出一个依赖关系图,然后再将模块划分,打包出一个或多个bundle。再次白piao一下官网的图,生动的描述了这个过程:
提到webpack,就不得不提webpack的四个核心概念
我们首先在全局安装webpack:
1 |
|
webpack可以不使用配置文件,直接通过命令行构建,用法如下:
1 |
|
这里的entry和output就对应了上述概念中的入口和输入,我们来新建一个入口文件:
1 |
|
有了入口文件我们还需要通过命令行定义一下输入路径dist/bundle.js:
1 |
|
这样webpack就会在dist目录生成打包后的文件。
我们也可以在项目目录新建一个html引入打包后的bundle.js文件查看效果。
命令行的打包方式仅限于简单的项目,如果我们的项目较为复杂,有多个入口,我们不可能每次打包都把入口记下来;因此一般项目中都使用配置文件来进行打包;配置文件的命令方式如下:
1 |
|
配置文件默认的名称就是webpack.config.js
,一个项目中经常会有多套配置文件,我们可以针对不同环境配置不同的文件,通过--config
来进行切换:
1 |
|
config配置文件通过module.exports
导出一个配置对象:
1 |
|
除了导出为对象,还可以导出为一个函数,函数中会带入命令行中传入的环境变量等参数,这样可以更方便的对环境变量进行配置;比如我们在打包线上正式环境和线上开发环境可以通过env
进行区分:
1 |
|
另外还可以导出为一个Promise,用于异步加载配置,比如可以动态加载入口文件:
1 |
|
正如在上面提到的,入口是整个依赖关系的起点入口;我们常用的单入口配置是一个页面的入口:
1 |
|
它是下面的简写:
1 |
|
但是我们一个页面可能不止一个模块,因此需要将多个依赖文件一起注入,这时就需要用到数组了,代码在demo2中:
1 |
|
有时候我们一个项目可能有不止一个页面,需要将多个页面分开打包,entry支持传入对象的形式,代码在demo3中:
1 |
|
这样webpack就会构建三个不同的依赖关系。
output
选项用来控制webpack如何输入编译后的文件模块;虽然可以有多个entry,但是只能配置一个output
:
1 |
|
这里我们配置了一个单入口,输出也就是bundle.js;但是如果存在多入口的模式就行不通了,webpack会提示Conflict: Multiple chunks emit assets to the same filename
,即多个文件资源有相同的文件名称;webpack提供了占位符
来确保每一个输出的文件都有唯一的名称:
1 |
|
这样webpack打包出来的文件就会按照入口文件的名称来进行分别打包生成三个不同的bundle文件;还有以下不同的占位符字符串:
占位符 | 描述 |
---|---|
[hash] | 模块标识符(module identifier)的 hash |
[chunkhash] | chunk 内容的 hash |
[name] | 模块名称 |
[id] | 模块标识符 |
[query] | 模块的 query,例如,文件名 ? 后面的字符串 |
在这里引入Module、Chunk和Bundle的概念,上面代码中也经常会看到有这两个名词的出现,那么他们三者到底有什么区别呢?首先我们发现module是经常出现在我们的代码中,比如module.exports;而Chunk经常和entry一起出现,Bundle总是和output一起出现。
我们通过下面一张图更深入的理解这三个概念:
总结:
module,chunk 和 bundle 其实就是同一份逻辑代码在不同转换场景下的取了三个名字:我们直接写出来的是module,webpack处理时是chunk,最后生成浏览器可以直接运行的bundle。
理解了chunk的概念,相信上面表中chunkhash和hash的区别也很容易理解了;
在webpack2和webpack3中我们需要手动加入插件来进行代码的压缩、环境变量的定义,还需要注意环境的判断,十分的繁琐;在webpack4中直接提供了模式这一配置,开箱即可用;如果忽略配置,webpack还会发出警告。
1 |
|
开发模式是告诉webpack,我现在是开发状态,也就是打包出来的内容要对开发友好,便于代码调试以及实现浏览器实时更新。
1 |
|
生产模式不用对开发友好,只需要关注打包的性能和生成更小体积的bundle。看到这里用到了很多Plugin,不用慌,下面我们会一一解释他们的作用。
相信很多童鞋都曾有过疑问,为什么这边DefinePlugin定义环境变量的时候要用JSON.stringify("production")
,直接用"production"
不是更简单吗?
我们首先来看下JSON.stringify("production")
生成了什么;运行结果是""production""
,注意这里,并不是你眼睛花了或者屏幕上有小黑点,结果确实比"production"
多嵌套了一层引号。
我们可以简单的把DefinePlugin这个插件理解为将代码里的所有process.env.NODE_ENV
替换为字符串中的内容
。假如我们在代码中有如下判断环境的代码:
1 |
|
这样生成出来的代码就会编译成这样:
1 |
|
但是我们代码中可能并没有定义production
变量,因此会导致代码直接报错,所以我们需要通过JSON.stringify来包裹一层:
1 |
|
这样编译出来的代码就没有问题了。
在上面的代码中我们发现都是手动来生成index.html,然后引入打包后的bundle文件,但是这样太过繁琐,而且如果生成的bundle文件引入了hash值,每次生成的文件名称不一样,因此我们需要一个自动生成html的插件;首先我们需要安装这个插件:
1 |
|
在demo3中,我们生成了三个不同的bundle.js,我们希望在三个不同的页面能分别引入这三个文件,如下修改config文件:
1 |
|
我们以index.html作为模板文件,生成home、list、detail三个不同的页面,并且通过chunks分别引入不同的bundle;如果这里不写chunks,每个页面就会引入所有生成出来的bundle。
html-webpack-plugin还支持以下字段:
1 |
|
上面设置title后需要在模板文件中设置模板字符串:
1 |
|
loader用于对模块module的源码进行转换,默认webpack只能识别commonjs代码,但是我们在代码中会引入比如vue、ts、less等文件,webpack就处理不过来了;loader拓展了webpack处理多种文件类型的能力,将这些文件转换成浏览器能够渲染的js、css。
module.rules
允许我们配置多个loader,能够很清晰的看出当前文件类型应用了哪些loader,loader的代码均在demo4中。
1 |
|
我们可以看到rules属性值是一个数组,每个数组对象表示了不同的匹配规则;test属性时一个正则表达式,匹配不同的文件后缀;use表示匹配了这个文件后调用什么loader来处理,当有多个loader的时候,use就需要用到数组。
多个loader支持链式传递,能够对资源进行流水线处理,上一个loader处理的返回值传递给下一个loader;loader处理有一个优先级,从右到左,从下到上;在上面demo中对css的处理就遵从了这个优先级,css-loader先处理,处理好了再给style-loader;因此我们写loader的时候也要注意前后顺序。
css-loader和style-loader从名称看起来功能很相似,然而两者的功能有着很大的区别,但是他们经常会成对使用;安装方法:
1 |
|
css-loader用来解释@import和url();style-loader用来将css-loader生成的样式表通过<style>标签
,插入到页面中去。
1 |
|
然后在入口文件中将index.css引入,就能看到打包的效果,页面中插入了三个style标签,代码在demo4:
这两个loader看名字大家也能猜到了,就是用来处理sass和less样式的。安装方法:
1 |
|
在config中进行配置,代码在demo4:
1 |
|
都0202年了,小伙伴肯定不想一个一个的手动添加-moz、-ms、-webkit等浏览器私有前缀;postcss提供了很多对样式的扩展功能;啥都不说,先安装起来:
1 |
|
老规矩,还是在config中进行配置:
1 |
|
正当我们兴冲冲的打包看效果时,发现样式还是老样子,并没有什么改变。
这是因为postcss主要功能只有两个:第一就是把css解析成JS可以操作的抽象语法树AST,第二就是调用插件来处理AST并得到结果;所以postcss一般都是通过插件来处理css,并不会直接处理,所以我们需要先安装一些插件:
1 |
|
在项目根目录新建一个.browserslistrc
文件。
1 |
|
我们将postcss的配置单独提取到项目根目录下的postcss.config.js
:
1 |
|
有了autoprefixer
插件,我们打包后的css就自动加上了前缀。
兼容低版本浏览器的痛相信很多童鞋都经历过,写完代码发现自己的js代码不能运行在IE10或者IE11上,然后尝试着引入各种polyfill;babel的出现给我们提供了便利,将高版本的ES6甚至ES7转为ES5;我们首先安装babel所需要的依赖:
1 |
|
然后在config添加loader对js进行处理:
1 |
|
同样的,我们把babel的配置提取到根目录,新建一个.babelrc
文件:
1 |
|
我们可以在index.js中尝试写一些es6的语法,看到代码会被转译成es5,代码在demo4中。由于babel-loader的转译速度很慢,在后面我们加入了时间插件后可以看到每个loader的耗时,babel-loader是最耗时间;因此我们要尽可能少的使用babel来转译文件,我们对config进行改进,
1 |
|
正则上使用$
来进行精确匹配,通过exclude将node_modules中的文件进行排除,include将只匹配src中的文件;可以看出来include的范围比exclude更缩小更精确,因此也是推荐使用include。
file-loader和url-loader都是用来处理图片、字体图标等文件;url-loader工作时分两种情况:当文件大小小于limit参数,url-loader将文件转为base-64编码,用于减少http请求;当文件大小大于limit参数时,调用file-loader进行处理;因此我们优先使用url-loader,首先还是进行安装,安装url-loader之前还需要把file-loader先安装:
1 |
|
接下来还是修改config:
1 |
|
我们在css中给body添加一个小于10k的居中背景图片:
1 |
|
打包后查看body的样式可以发现图片已经被替换成base64格式的url了,代码在demo4。
如果我们在页面上引用一个图片,会发现打包后的html还是引用了src目录下的图片,这样明显是错误的,因此我们还需要一个插件对html引用的图片进行处理:
1 |
|
老样子还是在config中对html进行配置:
1 |
|
然鹅,打开页面发现却是这样的:
这是因为在url-loader中把每个图片作为一个模块来处理了,我们还需要去url-loader中修改:
1 |
|
这样我们在页面上的图片引用也被修改了,代码在demo4中。
注
html-withimg-loader会导致html-webpack-plugin插件注入title的模板字符串<%= htmlWebpackPlugin.options.title %>
失效,原封不动的展示在页面上;因此,如果我们想保留两者的功能需要在配置config中把html-withimg-loader删除并且通过下面的方式来引用图片:
1 |
|
最后说一下一个比较特殊的vue-loader,看名字就知道是用来处理vue文件的。
1 |
|
我们首先来创建一个vue文件,具体代码在demo5中:
1 |
|
然后在webpack的入口文件中引用它:
1 |
|
不过vue-loader和其他loader不太一样,除了将它和.vue
文件绑定之外,还需要引入它的一个插件:
1 |
|
这样我们就能愉快的在代码中写vue了。
在上面的demo中我们都是通过命令行打包生成dist文件,然后直接打开html或者通过static-server
来查看页面的;但是开发中我们写完代码每次都来打包会严重影响开发的效率,我们期望的是写完代码后立即就能够看到页面的效果;webpack-dev-server就很好的提供了一个简单的web服务器,能够实时重新加载。
首先在我们的项目中安装依赖:
1 |
|
webpack-dev-server的用法和wepack一样,只不过他会额外启动一个express的服务器。我们在项目中新建一个webpack.dev.config.js
配置文件,单独对开发环境进行一个配置,相关代码在demo6中:
1 |
|
通过命令行webpack-dev-server
来启动服务器,启动后我们发现根目录并没有生成任何文件,因为webpack打包到了内存中,不生成文件的原因在于访问内存中的代码比访问文件中的代码更快。
我们在public/index.html的页面上有时候会引用一些本地的静态文件,直接打开页面的会发现这些静态文件的引用失效了,我们可以修改server的工作目录,同时指定多个静态资源的目录:
1 |
|
热更新(Hot Module Replacemen简称HMR)是在对代码进行修改并保存之后,webpack对代码重新打包,并且将新的模块发送到浏览器端,浏览器通过新的模块替换老的模块,这样就能在不刷新浏览器的前提下实现页面的更新。
可以看出浏览器和webpack-dev-server之间通过一个websock进行连接,初始化的时候client端保存了一个打包后的hash值;每次更新时server监听文件改动,生成一个最新的hash值再次通过websocket推送给client端,client端对比两次hash值后向服务器发起请求返回更新后的模块文件进行替换。
我们点击源码旁的行数看一下编译后的源码是什么样的:
发现跟我们的源码差距还是挺大的,本来是一个简单add函数,通过webpack的模块封装,已经很难理解原来代码的含义了,因此,我们需要将编译后的代码映射回源码;devtool中不同的配置有不同的效果和速度,综合性能和品质后,我们一般在开发环境使用cheap-module-eval-source-map
,在生产环境使用source-map
。
1 |
|
其他各模式的对比:
在上面我们也介绍了DefinePlugin、HtmlWebpackPlugin等很多插件,我们发现这些插件都能够不同程度的影响着webpack的构建过程,下面还有一些常用的插件,plugins相关代码在demo7中。
clean-webpack-plugin用于在打包前清理上一次项目生成的bundle文件,它会根据output.path自动清理文件夹;这个插件在生产环境用的频率非常高,因为生产环境经常会通过hash生成很多bundle文件,如果不进行清理的话每次都会生成新的,导致文件夹非常庞大;这个插件安装使用非常方便:
1 |
|
安装后我们在config中配置一下就可以了:
1 |
|
我们之前的样式都是通过style-loader插入到页面中去,但是生产环境需要单独抽离样式文件,mini-css-extract-plugin就可以帮我从js中剥离样式:
1 |
|
我们在开发环境使用style-loader,生产环境使用mini-css-extract-plugin:
1 |
|
引入loader后,我们还需要配置plugin,提取的css同样支持output.filename
中的占位符字符串。
我们可以发现虽然配置了production
模式,打包出来的js压缩了,但是打包出来的css确没有压缩;在生产环境我们需要对css进行一下压缩:
1 |
|
然后也是引入插件:
1 |
|
和demo6中一样,我们在public/index.html中引入了静态资源,但是打包的时候webpack并不会帮我们拷贝到dist目录,因此copy-webpack-plugin就可以很好地帮我做拷贝的工作了
1 |
|
在config中配置我们需要拷贝的源路径和目标路径:
1 |
|
ProvidePlugin可以很快的帮我们加载想要引入的模块,而不用require。一般我们加载jQuery需要先把它import:
1 |
|
但是我们在config中配置ProvidePlugin插件后能够不用import,直接使用$
:
1 |
|
但是如果在项目中引入了太多模块并且没有require会让人摸不着头脑,因此建议加载一些常见的比如jQuery、vue、lodash等。
介绍了这么多loader和plugin,我们来回顾一下他们两者的区别:
loader:由于webpack只能识别js,loader相当于翻译官的角色,帮助webpack对其他类型的资源进行转译的预处理工作。
plugins:plugins扩展了webpack的功能,在webpack运行时会广播很多事件,plugin可以监听这些事件,然后通过webpack提供的API来改变输出结果。
最后,介绍了这么多,本文是webpack基础篇,还有很多生产环境的优化还没有写到;因此各位看官敬请期待优化篇。
参考:
]]> 所谓Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。Promise
提供统一的API
,各种异步操作都可以用同样的方法进行处理。
Promise出现之前都是通过回调函数来实现,回调函数本身没有问题,但是嵌套层级过深,很容易掉进回调地狱
。
1 |
|
如果每次读取文件后还要进行逻辑的判断或者异常的处理,那么整个回调函数就会非常复杂且难以维护。Promise的出现正是为了解决这个痛点,我们可以把上面的回调嵌套用Promise改写一下:
1 |
|
promise最早是在commonjs社区提出来的,当时提出了很多规范。比较接受的是promise/A规范。但是promise/A规范比较简单,后来人们在这个基础上,提出了promise/A+规范,也就是实际上的业内推行的规范;es6也是采用的这种规范,但是es6在此规范上还加入了Promise.all、Promise.race、Promise.catch、Promise.resolve、Promise.reject等方法。
我们可以通过脚本来测试我们写的Promise是否符合promise/A+的规范。将我们实现的Promise加入以下代码:
1 |
|
然后通过module.exports导出,安装测试的脚本:
1 |
|
在实现Promise的目录执行以下命令:
1 |
|
接下来,脚本会对照着promise/A+的规范,对我们的脚本来一条一条地进行测试。
我们先回顾一下,我们平时都是怎么使用Promise的:
1 |
|
首先看出来,Promise是通过构造函数实例化一个对象,然后通过实例对象上的then方法,来处理异步返回的结果。同时,promise/A+规范规定了:
promise 是一个拥有 then 方法的对象或函数,其行为符合本规范;
一个 Promise 的当前状态必须为以下三种状态中的一种:等待态(Pending)、执行态(Fulfilled)和拒绝态(Rejected)。
1 |
|
当我们实例化Promise时,构造函数会马上调用传入的执行函数executor,我们可以试一下:
1 |
|
因此在Promise中构造函数立马执行,同时将resolve函数和reject函数作为参数传入:
1 |
|
但是executor也会可能存在异常,因此通过try/catch来捕获一下异常情况:
1 |
|
promise/A+规范中规定,当Promise对象已经由等待态(Pending)改变为执行态(Fulfilled)或者拒绝态(Rejected)后,就不能再次更改状态,且终值也不可改变。
因此我们在回调函数resolve和reject中判断,只能是pending状态的时候才能更改状态:
1 |
|
我们更改状态的同时,将回调函数中成功的结果或者失败的原因都保存在对应的属性中,方便以后来获取。
当Promise的状态改变之后,不管成功还是失败,都会触发then回调函数。因此,then的实现也很简单,就是根据状态的不同,来调用不同处理终值的函数。
1 |
|
在规范中也说了,onFulfilled和onRejected是可选的,因此我们对两个值进行一下类型的判断:
onFulfilled 和 onRejected 都是可选参数。如果 onFulfilled 不是函数,其必须被忽略。如果 onRejected 不是函数,其必须被忽略
代码写到这里,貌似该有的实现方式都有了,我们来写个demo测试一下:
1 |
|
然鹅,很遗憾,运行起来我们发现只打印了构造函数中的执行
,下面的then函数根本都没有执行。我们整理一下代码的运行流畅:
当then里面函数运行时,resolve由于是异步执行的,还没有来得及修改state,此时还是PENDING状态;因此我们需要对异步的情况做一下处理。
那么如何让我们的Promise来支持异步呢?我们可以参考发布订阅模式,在执行then方法的时候,如果当前还是PENDING状态,就把回调函数寄存到一个数组中,当状态发生改变时,去数组中取出回调函数;因此我们先在Promise中定义一下变量:
1 |
|
这样,当then执行时,如果还是PENDING状态,我们不是马上去执行回调函数,而是将其存储起来:
1 |
|
存储起来后,当resolve或者reject异步执行的时候就可以来调用了:
1 |
|
有童鞋可能会提出疑问了,为什么这边onFulfilled和onRejected要存在数组中,直接用一个变量接收不是也可以么?下面看一个例子:
1 |
|
我们分别调用了两次then,如果是一个变量的话,最后肯定只会运行后一个then,把之前的覆盖了,如果是数组的话,两个then都能正常运行。
至此,我们运行demo,就能如愿以偿的看到运行结果了;一个四十行左右的简单Promise垫片就此完成了。这里贴一下完整的代码:
1 |
|
相信上面的Promise垫片应该很容易理解,下面链式调用才是Promise的难点和核心点;我们对照promise/A+规范,一步一步地来实现,我们先来看一下规范是如何来定义的:
then 方法必须返回一个 promise 对象
promise2 = promise1.then(onFulfilled, onRejected);
也就是说,每个then方法都要返回一个新的Promise对象,这样我们的then方法才能不断的链式调用;因此上面的简单垫片中then方法就不适用了,因为它什么都没有返回,我们对其进行简单的改写,不论then进行什么操作,都返回一个新的Promise对象:
1 |
|
我们继续看then的执行过程:
[[Resolve]](promise2, x)
首先第一点,我们知道onFulfilled和onRejected执行之后都会有一个返回值x,对返回值x处理就需要用到Promise解决过程,这个我们下面再说;第二点需要对onFulfilled和onRejected进行异常处理,没什么好说的;第三和第四点,说的其实是一个问题,如果onFulfilled和onRejected两个参数没有传,则继续往下传(值的传递特性);举个例子:
1 |
|
这里不管onFulfilled和onRejected传什么值,只要不是函数,就继续向下传入,直到有函数进行接收;因此我们对then方法进行如下完善:
1 |
|
我们发现函数中有一个resolvePromise,就是上面说的Promise解决过程,它是对新的promise2和上一个执行结果 x 的处理,由于具有复用性,我们把它抽成一个单独的函数,这也是上面规范中定义的第一点。
由于then的回调是异步执行的,因此我们需要把onFulfilled和onRejected执行放到异步中去执行,同时做一下错误的处理:
1 |
|
Promise 解决过程是一个抽象的操作,其需输入一个 promise 和一个值,我们表示为
[[Resolve]](promise, x)
,如果 x 有 then 方法且看上去像一个 Promise ,解决程序即尝试使 promise 接受 x 的状态;否则其用 x 的值来执行 promise 。
这段话比较抽象,通俗一点的来说就是promise的解决过程需要传入一个新的promise和一个值x,如果传入的x是一个thenable的对象(具有then方法),就接受x的状态:
1 |
|
定义好函数后,来看具体的操作说明:
首先第一点,如果x和promise相等,这是一种什么情况呢,就是相当于把自己返回出去了:
1 |
|
这样会陷入一个死循环中,因此我们首先要把这种情况给排除掉:
1 |
|
接下来就是对不同情况的判断了,首先我们把 x 为对象或者函数的情况给判断出来:
1 |
|
如果 x 为对象或函数,就把 x.then 赋值给 then好理解,但是第二点取then有可能会报错是为什么呢?这是因为需要考虑到所有出错的情况(防小人不防君子),如果有人实现Promise对象的时候使用Object.defineProperty()恶意抛错,导致程序崩溃,就像这样:
1 |
|
因此,我们取then的时候也需要try/catch:
1 |
|
取出then后,回到3.3,判断如果是一个函数,就将 x 作为函数的作用域 this 调用,同时传入两个回调函数作为参数。
1 |
|
这样,我们的链式调用就能顺利的调用起来了;但是还有一种特殊的情况,如果resolve的y值还是一个Promise对象,这时就应该继续执行,比如下面的例子:
1 |
|
这时候第二个then打印出来的是一个promise对象;我们应该继续递归调用resolvePromise(参考规范3.3.1),因此,最终resolvePromise的完整代码如下:
1 |
|
到这里,我们的Promise也能够完整的实现链式调用了;然后把代码用promises-aplus-tests测试一下,完美的通过了872项测试。
完整Promise代码如下:
1 |
|
译者序:一年前曾译过 Promise/A+ 规范,适时完全不懂 Promise 的思想,纯粹将翻译的过程当作学习,旧文译下来诘屈聱牙,读起来十分不顺畅。谁知这样一篇拙译,一年之间竟然点击数千,成为谷歌搜索的头条。今日在理解之后重译此规范,以飨读者。
一个开放、健全且通用的 JavaScript Promise 标准。由开发者制定,供开发者参考。
译文术语
fulfill
来表示解决,但在后世的 promise 实现多以 resolve
来指代之。Promise 表示一个异步操作的最终结果,与之进行交互的方式主要是 then
方法,该方法注册了两个回调函数,用于接收 promise 的终值或本 promise 不能执行的原因。
本规范详细列出了 then
方法的执行过程,所有遵循 Promises/A+ 规范实现的 promise 均可以本标准作为参照基础来实施 then
方法。因而本规范是十分稳定的。尽管 Promise/A+ 组织有时可能会修订本规范,但主要是为了处理一些特殊的边界情况,且这些改动都是微小且向下兼容的。如果我们要进行大规模不兼容的更新,我们一定会在事先进行谨慎地考虑、详尽的探讨和严格的测试。
从历史上说,本规范实际上是把之前 Promise/A 规范 中的建议明确成为了行为标准:我们一方面扩展了原有规范约定俗成的行为,一方面删减了原规范的一些特例情况和有问题的部分。
最后,核心的 Promises/A+ 规范不设计如何创建、解决和拒绝 promise,而是专注于提供一个通用的 then
方法。上述对于 promises 的操作方法将来在其他规范中可能会提及。
promise 是一个拥有 then
方法的对象或函数,其行为符合本规范;
是一个定义了 then
方法的对象或函数,文中译作“拥有 then
方法”;
指任何 JavaScript 的合法值(包括 undefined
, thenable 和 promise);
是使用 throw
语句抛出的一个值。
表示一个 promise 的拒绝原因。
一个 Promise 的当前状态必须为以下三种状态中的一种:等待态(Pending)、执行态(Fulfilled)和拒绝态(Rejected)。
处于等待态时,promise 需满足以下条件:
处于执行态时,promise 需满足以下条件:
处于拒绝态时,promise 需满足以下条件:
这里的不可变指的是恒等(即可用 ===
判断相等),而不是意味着更深层次的不可变(译者注: 盖指当 value 或 reason 不是基本值时,只要求其引用地址相等,但属性值可被更改)。
一个 promise 必须提供一个 then
方法以访问其当前值、终值和据因。
promise 的 then
方法接受两个参数:
1 |
|
onFulfilled
和 onRejected
都是可选参数。
onFulfilled
不是函数,其必须被忽略onRejected
不是函数,其必须被忽略onFulfilled
特性如果 onFulfilled
是函数:
promise
执行结束后其必须被调用,其第一个参数为 promise
的终值promise
执行结束前其不可被调用onRejected
特性如果 onRejected
是函数:
promise
被拒绝执行后其必须被调用,其第一个参数为 promise
的据因promise
被拒绝执行前其不可被调用onFulfilled
和 onRejected
只有在执行环境堆栈仅包含平台代码时才可被调用[注1][1]
onFulfilled
和 onRejected
必须被作为函数调用(即没有 this
值)[注2][2]
then
方法可以被同一个 promise
调用多次
promise
成功执行时,所有 onFulfilled
需按照其注册顺序依次回调promise
被拒绝执行时,所有的 onRejected
需按照其注册顺序依次回调then
方法必须返回一个 promise
对象 [注3][3]
1 |
|
onFulfilled
或者 onRejected
返回一个值 x
,则运行下面的 Promise 解决过程:[[Resolve]](promise2, x)
onFulfilled
或者 onRejected
抛出一个异常 e
,则 promise2
必须拒绝执行,并返回拒因 e
onFulfilled
不是函数且 promise1
成功执行, promise2
必须成功执行并返回相同的值onRejected
不是函数且 promise1
拒绝执行, promise2
必须拒绝执行并返回相同的据因译者注: 理解上面的“返回”部分非常重要,即:不论 promise1
被 reject 还是被 resolve 时 promise2
都会被 resolve,只有出现异常时才会被 rejected。
Promise 解决过程 是一个抽象的操作,其需输入一个 promise
和一个值,我们表示为 [[Resolve]](promise, x)
,如果 x
有 then
方法且看上去像一个 Promise ,解决程序即尝试使 promise
接受 x
的状态;否则其用 x
的值来执行 promise
。
这种 thenable 的特性使得 Promise 的实现更具有通用性:只要其暴露出一个遵循 Promise/A+ 协议的 then
方法即可;这同时也使遵循 Promise/A+ 规范的实现可以与那些不太规范但可用的实现能良好共存。
运行 [[Resolve]](promise, x)
需遵循以下步骤:
x
与 promise
相等如果 promise
和 x
指向同一对象,以 TypeError
为据因拒绝执行 promise
x
为 Promise如果 x
为 Promise ,则使 promise
接受 x
的状态 [注4][4]:
x
处于等待态, promise
需保持为等待态直至 x
被执行或拒绝x
处于执行态,用相同的值执行 promise
x
处于拒绝态,用相同的据因拒绝 promise
x
为对象或函数如果 x
为对象或者函数:
x.then
赋值给 then
[注5][5]x.then
的值时抛出错误 e
,则以 e
为据因拒绝 promise
then
是函数,将 x
作为函数的作用域 this
调用之。传递两个回调函数作为参数,第一个参数叫做 resolvePromise
,第二个参数叫做 rejectPromise
:resolvePromise
以值 y
为参数被调用,则运行 [[Resolve]](promise, y)
rejectPromise
以据因 r
为参数被调用,则以据因 r
拒绝 promise
resolvePromise
和 rejectPromise
均被调用,或者被同一参数调用了多次,则优先采用首次调用并忽略剩下的调用then
方法抛出了异常 e
:resolvePromise
或 rejectPromise
已经被调用,则忽略之e
为据因拒绝 promise
then
不是函数,以 x
为参数执行 promise
x
不为对象或者函数,以 x
为参数执行 promise
如果一个 promise 被一个循环的 thenable 链中的对象解决,而 [[Resolve]](promise, thenable)
的递归性质又使得其被再次调用,根据上述的算法将会陷入无限递归之中。算法虽不强制要求,但也鼓励施者检测这样的递归是否存在,若检测到存在则以一个可识别的 TypeError
为据因来拒绝 promise
[注6][6]。
注1 这里的平台代码指的是引擎、环境以及 promise 的实施代码。实践中要确保 onFulfilled
和 onRejected
方法异步执行,且应该在 then
方法被调用的那一轮事件循环之后的新执行栈中执行。这个事件队列可以采用“宏任务(macro-task)”机制或者“微任务(micro-task)”机制来实现。由于 promise 的实施代码本身就是平台代码(译者注: 即都是 JavaScript),故代码自身在处理在处理程序时可能已经包含一个任务调度队列或[『跳板』]。
译者注: 这里提及了 macrotask 和 microtask 两个概念,这表示异步任务的两种分类。在挂起任务时,JS 引擎会将所有任务按照类别分到这两个队列中,首先在 macrotask 的队列(这个队列也被叫做 task queue)中取出第一个任务,执行完毕后取出 microtask 队列中的所有任务顺序执行;之后再取 macrotask 任务,周而复始,直至两个队列的任务都取完。
两个类别的具体分类如下:
setTimeout
, setInterval
, setImmediate
, I/O, UI renderingprocess.nextTick
, Promises
(这里指浏览器实现的原生 Promise), Object.observe
, MutationObserver
注2 也就是说在 严格模式(strict) 中,函数 this
的值为 undefined
;在非严格模式中其为全局对象。
注3 代码实现在满足所有要求的情况下可以允许 promise2 === promise1
。每个实现都要文档说明其是否允许以及在何种条件下允许 promise2 === promise1
。
注4 总体来说,如果 x
符合当前实现,我们才认为它是真正的 promise 。这一规则允许那些特例实现接受符合已知要求的 Promises 状态。
注5 这步我们先是存储了一个指向 x.then
的引用,然后测试并调用该引用,以避免多次访问 x.then
属性。这种预防措施确保了该属性的一致性,因为其值可能在检索调用时被改变。
注6 实现不应该对 thenable 链的深度设限,并假定超出本限制的递归就是无限循环。只有真正的循环递归才应能导致 TypeError
异常;如果一条无限长的链上 thenable 均不相同,那么递归下去永远是正确的行为。
记得之前有一个需求,就是根据文字的行数来显示展开更多的一个按钮,因此我们在Vue中给数据赋值之后需要获取文字高度。
1 |
|
这时不管怎么获取,文字的Div高度都是0;但是直接获取却是有值:
同样的情况也发生在给子组件传参上;我们给子组件传参数后,在子组件中调用函数查看参数。
1 |
|
虽然页面上展示了子组件的name,但是打印出来却是空值:
我们发现上述两个问题的发生,不管子组件还是父组件,都是在给data
中赋值后立马去查看数据导致的。由于“查看数据”这个动作是同步操作的,而且都是在赋值之后;因此我们猜测一下,给数据赋值操作是一个异步操作,并没有马上执行,Vue官网对数据操作是这么描述的:
可能你还没有注意到,Vue 在更新 DOM 时是异步执行的。只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。然后,在下一个的事件循环“tick”中,Vue 刷新队列并执行实际 (已去重的) 工作。Vue 在内部对异步队列尝试使用原生的 Promise.then、MutationObserver 和 setImmediate,如果执行环境不支持,则会采用 setTimeout(fn, 0) 代替。
也就是说我们在设置this.msg = 'some thing'
的时候,Vue并没有马上去更新DOM数据,而是将这个操作放进一个队列中;如果我们重复执行的话,队列还会进行去重操作;等待同一事件循环中的所有数据变化完成之后,会将队列中的事件拿出来处理。
这样做主要是为了提升性能,因为如果在主线程中更新DOM,循环100次就要更新100次DOM;但是如果等事件循环完成之后更新DOM,只需要更新1次。还不了解事件循环的童鞋,可以看我的另一篇文章从一道面试题来理解JS事件循环
为了在数据更新操作之后操作DOM,我们可以在数据变化之后立即使用Vue.nextTick(callback)
;这样回调函数会在DOM更新完成后被调用,就可以拿到最新的DOM元素了。
1 |
|
了解了nextTick的用法和原理之后,我们就来看一下Vue是怎么来实现这波“操作”的。
Vue把nextTick的源码单独抽到一个文件中,/src/core/util/next-tick.js
,删掉注释也就大概六七十行的样子,让我们逐段来分析。
1 |
|
我们首先找到nextTick
这个函数定义的地方,看看它具体做了什么操作;看到它在外层定义了三个变量,有一个变量看名字就很熟悉:callbacks,就是我们上面说的队列;在nextTick的外层定义变量就形成了一个闭包,所以我们每次调用$nextTick的过程其实就是在向callbacks新增回调函数的过程。
callbacks新增回调函数后又执行了timerFunc函数,pending
用来标识同一个时间只能执行一次。那么这个timerFunc函数是做什么用的呢,我们继续来看代码:
1 |
|
这里出现了好几个isNative
函数,这是用来判断所传参数是否在当前环境原生就支持;例如某些浏览器不支持Promise,虽然我们使用了垫片(polify),但是isNative(Promise)还是会返回false。
可以看出这边代码其实是做了四个判断,对当前环境进行不断的降级处理,尝试使用原生的Promise.then
、MutationObserver
和setImmediate
,上述三个都不支持最后使用setTimeout;降级处理的目的都是将flushCallbacks
函数放入微任务(判断1和判断2)或者宏任务(判断3和判断4),等待下一次事件循环时来执行。MutationObserver
是Html5的一个新特性,用来监听目标DOM结构是否改变,也就是代码中新建的textNode;如果改变了就执行MutationObserver构造函数中的回调函数,不过是它是在微任务中执行的。
那么最终我们顺藤摸瓜找到了最终的大boss:flushCallbacks;nextTick不顾一切的要把它放入微任务或者宏任务中去执行,它究竟是何方神圣呢?让我们来一睹它的真容:
1 |
|
本来以为有多复杂的flushCallbacks,居然不过短短的8行。它所做的事情也非常的简单,把callbacks数组复制一份,然后把callbacks置为空,最后把复制出来的数组中的每个函数依次执行一遍;所以它的作用仅仅是用来执行callbacks中的回调函数。
到这里,整体nextTick的代码都分析完毕了,总结一下它的流程就是:
再回到我们开头说的setTimeout,可以看出来nextTick是对setTimeout进行了多种兼容性的处理,宽泛的也可以理解为将回调函数放入setTimeout中执行;不过nextTick优先放入微任务执行,而setTimeout是宏任务,因此nextTick一般情况下总是先于setTimeout执行,我们可以在浏览器中尝试一下:
1 |
|
最后验证猜想,当前宏任务执行完成后,优先执行两个微任务,最后再执行宏任务。
]]>首先让我们看一下,在其他语言中是怎么来定义类的。在JAVA中类可以看出是创建对象的模板,我们可以这样定义类:
1 |
|
但是ES6之前都没有class,那么JS怎么定义类呢?在JS中函数是一等公民,我们可以通过构造函数(即JAVA中的类)来创建对象。所谓构造函数,就是提供了一个生成对象的模板并描述对象的基本结构的函数。一个构造函数,可以生成多个对象,每个对象都有相同的结构。总的来说,构造函数就是对象的模板,对象就是构造函数的实例。
1 |
|
我们通过new来构建实例化对象,类函数中的this
总是指向实例化的对象,每一个实例对象都有一个不可枚举的属性constructor
属性来指向构造函数,即Person。
我们把person1
打印出来看一下到底有什么:
实例对象中可以看到我们在类中定义的name属性和sayName方法都有了,但是constructor
属性并没有,但是却能取到值。
所有的实例对象都会单独创建自己的属性和方法,不同实例对象之间无法共享通用的属性。
1 |
|
但是有的属性或者方法是共有的,我们希望每个实例对象创建的时候就能有,比如说每个人天生就会哭(cry),不用一出生的时候还要“手把手教”。
为了解决实例对象之间共享属性的问题,JS提供了prototype属性。
1 |
|
prototype是从一个函数指向一个对象
,即函数才有prototype
属性。它的作用是让该构造函数创建的所有实例对象们都能找到公用的属性和方法。任何函数在创建实例对象的时候,其实会关联该函数的prototype对象。因此我们继续把原型图补充完整:
需要注意的是,我们可以修改原型对象的引用,但是仍需要把constructor
属性指向回构造函数;上面的cry
函数绑定我们可以这样改写:
1 |
|
有了原型对象,我们知道了,实例对象的属性和方法,有可能是定义在自身,也有可能是定义在他的原型对象上。通过上面的cry
函数我们可以看出,实例对象能够直接获取原型对象上的属性和方法,那么它是怎么获取的呢?在上面打印的person1
中我们发现有一个特别的属性__proto__
展开看一下:
因此__proto__
指向了实例对象的原型对象;当你访问一个对象上没有的属性时,对象就会去__proto__
上面找,如果还是找不到,就会继续找原型对象的__proto__
,直到原型对象为null;因此__proto__
构成了一条原型链。
同时我们也解答了上面实例对象上没有constructor
属性的问题,constructor
属性真正存在于原型对象上,所以实例对象才能获取到,我们继续完善原型图(虚线表示该属性或方法并不是真正存在):
同时,原型对象也是一个对象,既然是对象,那么肯定也有它自己的原型对象,那么它的原型对象是谁呢?我们知道,JS中所有的对象都是Object
的实例,并继承Object.prototype
的属性和方法;字面量var a = {}
实际上也是new Object()
的语法糖,因此:
1 |
|
我们继续完善原型图:
我们说过constructor
用来指向构造函数;同时,constructor
真正存在于原型对象上,因此,我们可以得到下面的等式关系:
1 |
|
在学数据类型判断的时候学过,constructor
可以用来进行数据类型的判断:
1 |
|
这种方式看起来能判断所有类型,但是一旦我们更改了原型对象,这种方式就不可靠了。
1 |
|
在JS中,函数本身也可以看成是对象,对这种又是函数,又是对象,有一个特殊的称呼:函数对象
;我们调用函数的fn.call
和fn.apply
其实调用的是继承自其原型对象上的Function.prototype.call
和Function.prototype.apply
,因此函数都是Function函数的实例对象;既然是实例对象,所以Person
函数也拥有__proto__
和constructor
属性,我们来看一下函数的属性:
1 |
|
可以看出来,Person构造函数和JS普通的对象没有任何区别,有自己的constructor
属性,指向Function函数
,说明Person函数是Function函数
的实例对象;而且constructor
属性不在Person本身,而在其原型对象Function.prototype
上,因此我们再次完善一下原型图:
到这里,我们发现最终原型图指向了四个基本的东西:Object
、Object.prototype
、Function
和Function.prototype
,他们之间的关系是整个原型关系里面最难理解的,为了避免干扰,我们给他们四个单独开个图:
我们知道Object
函数和Person
函数一样,都是函数对象,因此都是Function
函数的实例对象。
1 |
|
因此我们完善Object和Function的关系:
既然Object
是构造函数,我们又想起Function
也能通过new Function()
来构造匿名函数,同时自己又是自己的constructor
。
1 |
|
同时我们猜测Function.prototype
和Person.prototype
一样是个对象,因此它的原型对象肯定就是Object.prototype
。
1 |
|
我们继续完善原型图:
这样,整个原型链最有意思的一幕出现了;Object
是构造函数,继承了Function.prototype
;Function
函数也是对象,继承了Object.prototype
,那么到底是先有了Object
,还是先有Function
?这似乎是一个无解的悖论。
我们发现导致鸡和蛋问题的根本原因在于Function.__proto__
指向了Function.prototype
,让Function
继承了Object.prototype
上的方法,因此我们需要对Function.prototype
来进一步的了解:
1 |
|
我们发现Function.prototype
是个特殊的函数对象,但是没有prototype属性;针对上面的代码,我们梳理了以下几点:
Function.prototype
像普通函数一样可以调用,但总是返回undefined
Function.prototype
继承于Object.prototype
,并且没有prototype
这个属性 因此Function.prototype
是个标准的内置对象,它继承于Object.prototype
,而我们知道Object.prototype===null
,说明原型链到Object.prototype
就终止了。
结论:先有
Object.prototype
(原型链顶端),Function.prototype
继承Object.prototype
而产生,最后,Function
和Object
和其它构造函数继承Function.prototype
而产生。
所谓的静态方法,是指不需要声明类的实例就可以使用的方法。在JAVA中我们可以直接在类中加一个static定义静态方法
1 |
|
在ES5中,我们直接将它作为类函数的属性即可:
1 |
|
静态方法和实例方法最主要的区别就是实例方法可以访问到实例对象,可以对实例进行操作,而静态方法一般用于跟实例无关的操作。静态方法最常见的是在jQuery的一些工具函数中,比如$.ajax()、$.trim(),可以看出来这两个函数也是直接定义在jQuery对象(即$对象)上的,因为其不需要获取DOM元素$(‘div’)。
除了constructor
,我们还有instanceof
来进行数据类型的判断;instanceof
主要用来判断一个实例是否属于某种类型,让我们先看一下instanceof的简单用法:
1 |
|
instanceof
第一个变量是一个对象A,第二个变量是一个函数B,沿着A的原型链__proto__
一直向上找,如果能找到一个__proto__
等于B的prototype,则返回true;如果找到终点还没找到则返回false。
1 |
|
通过上面的的原型链,我们知道了new本质上就是调用构造函数生成一个对象,这个对象能够访问构造函数的的原型对象,因为我们来尝试模拟一下new的实现。
1 |
|
我们首先构建了一个空对象;然后将空对象作为this,调用构造函数绑定参数;最后将该对象的__proto__指向构造函数的原型对象。
可以看到生成出来的对象该有的属性都有了,原型链也绑定成功了,但是存在的问题就是不能进行传参,因此我们进行一下改进:
1 |
|
可以看到返回的对象已经和原生new生成出来的几乎一模一样了。但是我们对构造函数进行一些修改:
1 |
|
我们在构造函数中返回了多种类型,经过测试发现:如果构造函数返回引用类型,new生成的就是返回的对象;如果返回基本数据类型,new生成新的对象。因此我们终极版的new函数如下:
1 |
|
所谓的继承,就是把子类继承父类所有的属性和方法;同时我们也知道父类上的属性和方法不仅在自身构造函数,原型链上也会有属性和方法,因此我们也需要继承过来。
既然继承是继承父类的属性和方法,那么我们上面的myNew
函数也相当于是一种继承;让我们再看看看还有哪些继承的方式。
1 |
|
我们把父类的实例挂载到子类的原型上,那么所有的子类就能访问到父类的属性和方法了,但是由于所有子类共享原型对象,所以会存在以下问题:
1 |
|
1 |
|
每次创建子类实例的时候调用父类的构造函数,避免了引用类型的属性被所有实例共享,也可以向父类传参数;但是没有继承父类原型上的属性和方法。
1 |
|
融合了原型链继承和构造函数继承的优点,是JS中常用的继承方式。
ES6新增了class
关键词,用来定义一个类,和JAVA中的有种似曾相识的感觉;但是本质上其实是ES5构造函数的语法糖,大多部分功能ES5都能实现:
1 |
|
ES6的继承可以通过extends
关键词实现,比ES5的修改原型链实现继承要更清晰和方便:
1 |
|
可以很清晰的看出来子类继承了父类本身以及原型上的属性和方法。同时,在ES5中所有的继承我们发现都不支持静态函数的继承,但是在ES6中支持。
参考
]]> 父组件通过prop
的方式向子组件传递数据,而通过$emit
子组件可以向父组件通信。
1 |
|
我们可以通过prop
向子组件传递数据;用一个形象的比喻来说,父子组件之间的数据传递相当于自上而下的下水管子,管子中的水就像数据,水只能从上往下流,不能逆流。这也正是Vue的设计理念之单向数据流。而prop
正是管道与管道之间的一个衔接口,这样水(数据)才能往下流。
1 |
|
在子组件中我们通过props对象定义了接收父组件值的类型和默认值,然后通过$emit()
触发父组件中的自定义事件。prop/$emit
传递数据的方式在日常开发中用的非常多,一般涉及到组件开发都是基于通过这种方式;通过父组件中注册子组件,并在子组件标签上绑定对自定义事件的监听。他的优点是传值取值方便简洁明了,但是这种方式的缺点是:
有些情况下,我们希望在子组件能够“直接修改”父组件的prop值,但是双向绑定会带来维护上的问题;vue提供了一种解决方案,通过语法糖.sync修饰符。
.sync修饰符在 vue1.x
的时候曾作为双向绑定功能存在,即子组件可以修改父组件中的值。但是它违反了单向数据流的设计理念,所以在 vue2.0
的时候被干掉了。但是在 vue2.3.0+
以上版本又重新引入了。但是这次它只是作为一个编译时的语法糖存在。它会被扩展为一个自动更新父组件属性的v-on
监听器。说白了就是让我们手动进行更新父组件中的值了,从而使数据改动来源更加的明显。
1 |
|
我们在Child组件传值时给每个值添加一个.sync修饰,在编译时会被扩展为如下代码:
1 |
|
因此子组件中只需要显示的触发update的更新事件:
1 |
|
这种“双向绑定”的操作是不是看着似曾相识?是的,v-model本质上也是一种语法糖,只不过它触发的不是update方法而是input方法;而且v-model没有.sync来的更加灵活,v-model只能绑定一个值。
总结:.sync修饰符优化了父子组件通信的传值方式,不需要在父组件再写多余的函数来修改赋值。
当需要用到从A到C的跨级通信时,我们会发现prop传值非常麻烦,会有很多冗余繁琐的转发操作;如果C中的状态改变还需要传递给A,使用事件还需要一级一级的向上传递,代码可读性就更差了。
因此vue2.4+
版本提供了新的方案:$attrs和$listeners
,我们先来看一下官网对$attrs的描述:
包含了父作用域中不作为 prop 被识别 (且获取) 的特性绑定 (class 和 style 除外)。当一个组件没有声明任何 prop 时,这里会包含所有父作用域的绑定 (class 和 style 除外),并且可以通过 v-bind=”$attrs” 传入内部组件——在创建高级别的组件时非常有用。
这一大段话第一次读非常的绕口,而且晦涩难懂,不过没关系,我们直接上代码:
1 |
|
我们首先定义了两个msg,一个给子组件展示,另一个给孙组件展示,首先将这两个数据传递到子组件中,同时将两个改变msg的函数传入。
1 |
|
在子组件中我们通过props获取子组件所需要的参数,即childMsg;剩余的参数就被归到了$attrs
对象中,我们可以在页面中展示出来,然后把它继续往孙组件中传;同时把所有的监听函数归到$listeners,也继续往下传。
1 |
|
在孙组件中我们继续取出所需要的数据进行展示或者操作,运行结果如下:
当我们在组件上赋予一个非prop声明时,比如child组件上的notuse和grandchildmsg属性我们没有用到,编译之后的代码会把这个属性当成原始属性对待,添加到html原生标签上,所以我们查看代码是这样的:
这样会很难看,我们可以在组件上加上inheritAttrs
属性将它去掉:
1 |
|
总结:$attrs和$listeners很好的解决了跨一级组件传值的问题。
虽然$attrs和$listeners可以很方便的从父组件传值到孙组件,但是如果跨了三四级,并且想要的数据已经被上级组件取出来,这时$attrs就不能解决了。
provide/inject是vue2.2+
版本新增的属性,简单来说就是父组件中通过provide来提供变量, 然后再子组件中通过inject来注入变量。这里inject注入的变量不像$attrs
,只能向下一层;inject不论子组件嵌套有多深,都能获取到。
1 |
|
我们在父组件通过provide注入了两个变量,并且在两秒之后修改变量的值,然后就在子组件和孙组件取出来。
1 |
|
可以看到子组件和孙组件都能取出值,并且渲染出来。需要注意的是,一旦子组件注入了某个数据,在data中就不能再声明这个数据了。
同时,过了两秒后我们发现childmsg和grandmsg的值并没有按照预期的改变,也就是说子组件并没有响应修改后的值,官网的介绍是这么说的:
提示:
provide
和inject
绑定并不是可响应的。这是刻意为之的。然而,如果你传入了一个可监听的对象,那么其对象的属性还是可响应的。
vue并没有把provide和inject设计成响应式的,这是vue故意的,但是如果传入了一个可监听的对象,那么就可以响应了:
1 |
|
那么为什么上面的props和$attrs都是响应式的,连破坏“单向数据流”的.sync
修饰符都是响应式的,但到了provide/inject就不是响应式的了呢?在网上找了半天的资料也没有找到确切的答案,本文就此结束。
就这么结束了吗?当然没有!在一(zi)个(ji)哥(xue)们(xi)的帮(yuan)助(ma)下,我总算找到了答案。首先我们试想一下,如果有多个子组件同时依赖于一个父组件提供的数据,那么一旦父组件修改了该值,那么所有组件都会受到影响,这是我们不希望看到的;这一方面增加了耦合度,另一方面使得数据变化不可控制。接着看一下vue是怎么来实现provide/inject的。
1 |
|
可以看到初始化provide的时候将父组件的provide挂载到_provided
,但它不是一个响应式的对象;然后子组件通过$parent
向上查找所有父组件的_provided
获取第一个有目标属性的值,然后遍历绑定到子组件上;因为只是初始化的时候绑定的,而且_provided
也不是响应式的,所以造成了provide/inject的这种特性。
那么provide/inject这么危险,又不是响应式的,它能拿来做什么呢?打开element-ui
的源码搜索provide,我们可以看到非常多的组件使用了provide/inject,我们就拿form、form-item和button举个例子。
form和form-item都可以传入一个属性size来控制子组件的尺寸,但是子组件的位置是不固定的,可能会嵌套了好几层el-row或者el-col,如果一层一层的通过props传size下去会很繁琐,这是provide/inject就派上用处了。
1 |
|
我们通过父组件将elFormItem本身注入到子组件中,子组件通过inject获取父组件本身然后动态地计算buttonSize。
总结:provide/inject能够解决多层组件嵌套传值的问题,但是是非响应的,即provide与inject之间没有绑定,注入的值是在子组件初始化过程中决定的。
EventBus
我刚开始直接翻译理解为事件车
,但比较官方的翻译是事件总线
。它的实质就是创建一个vue实例,通过一个空的vue实例作为桥梁实现vue组件间的通信。它是实现非父子组件通信的一种解决方案,所有的组件都可以上下平行地通知其他组件,但也就是太方便所以若使用不慎,就会造成难以维护的“灾难”。
1 |
|
首先创造一个空的vue对象并将其导出,他是一个不具备DOM
的组件,它具有的仅仅只是它实例方法而已,因此它非常的轻便。
1 |
|
将其挂载到全局,变成全局的事件总线,这样在组件中就能很方便的调用了。
1 |
|
我们先定义了两个子组件child1和child2,我们希望这两个组件能够直接给对方发送消息。
1 |
|
我们初始化时在child1和child2中分别注册了两个接收事件,然后点击按钮时分别触发这两个自定义的事件,并传入数据,最后两个组件分别能接收到对方发送的消息,最后效果如下:
前面也提到过,如果使用不善,EventBus会是一种灾难,到底是什么样的“灾难”了?大家都知道vue是单页应用,如果你在某一个页面刷新了之后,与之相关的EventBus会被移除,这样就导致业务走不下去。还要就是如果业务有反复操作的页面,EventBus在监听的时候就会触发很多次,也是一个非常大的隐患。这时候我们就需要好好处理EventBus在项目中的关系。通常会用到,在页面或组件销毁时,同时移除EventBus事件监听。
1 |
|
总结:EventBus可以用来很方便的实现兄弟组件和跨级组件的通信,但是使用不当时也会带来很多问题;所以适合逻辑并不复杂的小页面,逻辑复杂时还是建议使用vuex。
在vue组件开发中,经常会遇到需要将当前组件的状态传递给其他非父子组件组件,或者一个状态需要共享给多个组件,这时采用上面的方式就会非常麻烦。vue提供了另一个库vuex来解决数据传递的问题;刚开始上手会感觉vuex非常的麻烦,很多概念也容易混淆,不过不用担心,本文不深入讲解vuex。
vuex实现了单向的数据流,在全局定义了一个State对象用来存储数据,当组件要修改State中的数据时,必须通过Mutation进行操作。
1 |
|
我们首先在全局定义了count.js
模块用来存放数据和修改数据的方法,然后在全局引入。
1 |
|
我们就可以在任何组件中来调用mutations和actions中的方法操作数据了。vuex在数据传值和操作数据维护起来比较方便,但是有一定的学习成本。
有时候我们需要在vue中直接来操作DOM元素,比如获取DIV的高度,或者直接调用子组件的一些函数;虽然原生的JS也能获取到,但是vue为我们提供了更方便的一个属性:$refs
。如果在普通的DOM元素上使用,获取到的就是DOM元素;如果用在子组件上,获取的就是组件的实例对象。
1 |
|
我们首先创建一个简单的子组件,有两个函数用来增减num的值。
1 |
|
我们给子组件增加一个ref属性child,然后通过$refs.child
来获取子组件的实例,通过实例来调用子组件中的函数。
可以看到我们获取到的是一个VueComponent
对象,这个对象包括了子组件的所有数据和函数,可以对子组件进行一些操作。
如果页面有多个相同的子组件需要操作的话,$refs
一个一个操作起来比较繁琐,vue提供了另外的属性:$parent和$children
来统一选择。
1 |
|
我们在父组件中插入了两个相同的子组件,在子组件中通过$parent
调用了父组件的函数,并在父组件通过$children
获取子组件实例的数组。
我们在Parent中打印出$parent
属性看到是最外层#app的实例。
常见使用场景可以分为三类:
JavaScript和HTML之间的交互是通过事件实现的。事件,就是文档或浏览器窗口发生的一些特定的交互瞬间。可以使用监听器(或事件处理程序)来预定事件,以便事件发生时执行相应的代码。通俗的说,这种模型其实就是一个观察者模式。(事件是对象主题,而这一个个的监听器就是一个个观察者)
事件流描述的就是从页面中接收事件的顺序。而早期的IE和Netscape提出了完全相反的事件流概念,IE事件流是事件冒泡,而Netscape的事件流就是事件捕获。
IE提出的事件流是事件冒泡,即从下至上,从目标触发的元素逐级向上传播,直到window对象。
而Netscape的事件流就是事件捕获,即从document逐级向下传播到目标元素。由于IE低版本浏览器不支持,所以很少使用事件捕获。
后来ECMAScript在DOM2中对事件流进行了进一步规范,基本上就是上述二者的结合。
DOM2级事件规定的事件流包括三个阶段:
(1)事件捕获阶段
(2)处于目标阶段
(3)事件冒泡阶段
DOM节点中有了事件,那我们就需要对事件进行处理,而DOM事件处理分为4个级别:DOM0级事件处理,DOM0级事件处理,DOM2级事件处理和DOM3级事件处理。
其中DOM1级事件处理标准中并没有定义相关的内容,所以没有所谓的DOM1事件处理;DOM3级事件在DOM2级事件的基础上添加了更多的事件类型。
DOM0级事件具有极好的跨浏览器优势,会以最快的速度绑定。第一种方式是内联模型(行内绑定),将函数名直接作为html标签中属性的属性值。
1 |
|
内联模型的缺点是不符合w3c中关于内容与行为分离的基本规范。第二种方式是脚本模型(动态绑定),通过在JS中选中某个节点,然后给节点添加onclick属性。
1 |
|
点击输出hello
,没有问题;如果我们给元素添加两个事件
1 |
|
这时候只有输出hello again
,很明显,第一个事件函数被第二个事件函数给覆盖掉,所以脚本模型的缺点是同一个节点只能添加一次同类型事件。让我们把div扩展到3个。
1 |
|
当我们点击btn3的时候输出3,那当我们点击btn1的时候呢?
我们发现最先触发的是最底层btn1的事件,最后才是顶层btn3的事件,因此很明显是事件冒泡。DOM0级只支持冒泡阶段。
进一步规范之后,有了DOM2级事件处理程序,其中定义了两个方法:
函数均有3个参数,
第一个参数是要处理的事件名
第二个参数是作为事件处理程序的函数
第三个参数是一个boolean值,默认false表示使用冒泡机制,true表示捕获机制。
1 |
|
这时候两个事件处理程序都能够成功触发,说明可以绑定多个事件处理程序,但是注意,如果定义了一摸一样时监听方法,是会发生覆盖的,即同样的事件和事件流机制下相同方法只会触发一次,
1 |
|
这时候hello只会执行一次;让我们把div扩展到3个。
1 |
|
这时候看到顺序和DOM0中的顺序反过来了,最外层的btn最先触发,因为addEventListener最后一个参数是true,捕获阶段进行处理。
那么冒泡和捕获阶段谁先执行呢?我们给每个元素分别绑定了冒泡和捕获两个事件。
1 |
|
我们看到先执行捕获阶段的处理程序,后执行冒泡阶段的处理程序,我们把顺序换一下再看运行结果:
1 |
|
我们发现在触发的目标元素上不区分冒泡还是捕获,按绑定的顺序来执行。
有时候我们需要点击事件不再继续向上冒泡,我们在btn2上加上stopPropagation函数,阻止程序冒泡。
1 |
|
可以看到btn2捕获阶段执行后不再继续往下执行。
如果有多个DOM节点需要监听事件的情况下,给每个DOM绑定监听函数,会极大的影响页面的性能,因为我们通过事件委托来进行优化,事件委托利用的就是冒泡的原理。
1 |
|
正常情况我们给每一个li都会绑定一个事件,但是如果这时候li是动态渲染的,数据又特别大的时候,每次渲染后(有新增的情况)我们还需要重新来绑定,又繁琐又耗性能;这时候我们可以将绑定事件委托到li的父级元素,即ul。
1 |
|
上面代码中我们使用了两种获取目标元素的方式,target和currentTarget,那么他们有什么区别呢:
不一定是绑定事件的元素
因此我们总结一下事件委托的优点:
笔者入坑Vue也有一段时间了,对Vue也算了解,Vuex、Vue-Router也用了不少;但是前几天一看到这个面试问题却感觉一下子回答不上了,想来每次写代码也都是拿来就用,也没有仔细的思考过里面的原因;每每报错了就换一种写法,能用就行,仅此而已。
这个问题要从两个方面来说:
当我们实例化Vue的时候,填写一个el选项,来指定我们的SPA入口:
1 |
|
1 |
|
如果我们把代码改造一下,变成两个入口。
1 |
|
1 |
|
这时候会发现只有第一个div被渲染出来,而第二个div还是原封不动。我们简单来看一下Vue的源码是如何实现的
1 |
|
可以看到挂载函数传了一个el参数,这个参数可以是string类型,也可以是一个element元素,也就是dom节点。最重要的是el = el && query(el)
这一行代码,那就继续看一下query函数是做什么的:
1 |
|
首先query函数判断是否是string类型,如果是string类型,就通过querySelector
函数获取页面中的元素,但是querySelector仅仅返回匹配指定选择器的第一个元素,所以这就解释了为什么第二个div会原封不动。
Vue其实并不知道哪一个才是我们的入口,因为对于一个入口来讲,这个入口就是一个Vue类
,Vue需要把这个入口里面的所有东西拿来渲染、处理,最后再重新插入到dom中。如果同时设置了多个入口,那么vue就不知道哪一个才是这个类
。
当我们在vue-cli脚手架搭建的vue开发环境下使用单文件组件时,一般会这么写:
1 |
|
如果我们尝试在template标签下写两个div,那么编辑器会提示我们The template root requires exactly one element
。那这里为什么template下也必须有且只能有一个div呢?
这里我们要先看一看template这个标签,这个标签是HTML5出来的新标签,它有三个特性:
但是我们可以通过innerHTML来获取到里面的内容。
知道了这个,我们再来看.vue的单文件组件。其实本质上,一个单文件组件会被各种各样的loader处理成为.js文件(因为当你import一个单文件组件并打印出来的时候,是一个vue实例),通过template的任意性我们知道,template包裹的HTML可以写在任何地方,那么对于一个.vue来讲,这个template里面的内容就是会被vue处理为虚拟dom并渲染的内容,导致结果又回到了开始 :既然一个.vue单文件组件是一个vue实例,那么这个实例的入口在哪里?
如果在template下有多个div,那么该如何指定这个vue实例的根入口?
为了让组件能够正常的生成一个vue实例,那么这个div会被自然的处理成程序的入口。
通过这个‘根节点’,来递归遍历整个vue‘树’下的所有节点,并处理为vdom,最后再渲染成真正的HTML,插入在正确的位置。
]]>说出下面代码的运行结果,并说明原因:
1 |
|
先贴一下在浏览器里的运行的结果(如果跟你的思路一模一样的话,大佬请直接Ctrl+F4):
1 |
|
如果跟你的思路不一样的话也不用担心,我们从简单的开始一点点剖析这道面试题。
首先我们都知道,JavaScript是一门单线程的语言,所谓单线程指的是在JavaScript引擎中负责解释和执行代码的线程只有一个,通常称为主线程。那么为什么JavaScript必须是单线程的语言,而不能像他的老大哥Java一样,手动开启多个线程呢?
因为这是由于JavaScript所运行的浏览器环境决定,他只能是单线程的。试想一下,如果JavaScript能开启多个线程,页面上有一个div,我们同时在多个线程中来改变这个div中的内容,那么最终这个div会变成什么样子谁也确定不了,最后只能听天由命,看哪个线程是最后一个运行结束的。
因此多线程带来了很多的不确定性,为了避免这种问题,JavaScript必须是单线程。
可能有的同学又会说了,JavaScript不是可以通过Web Worker开启多线程么?是的,Web Worker是可以开启另一个线程,但是这个新开线程的功能被限制了,只能做一些消耗CPU的逻辑运算等,数据传输也是通过回调的方式来进行,不会阻塞主线程的执行;而且最最重要的是,Web Worker不能来操作dom,笔者经过尝试发现,在新开的线程中甚至都不能获取到document和window对象。
所以还是没有改变JavaScript是单线程运行这一核心原则。当然,虽然JavaScript是单线程运行的,但是还是存在其他线程的;例如:处理Ajax请求的线程、定时器的线程、读写文件的线程(nodejs中)等。
因为JavaScript是单线程运行的,所有的任务只能在主线程上排队执行;但是如果某个任务特别耗时,比如Ajax请求一个接口,可能1s返回结果,也可能10s才返回,有很多的不确定因素(网络延迟等);如果这些任务也放到主线程中去,那么会阻塞浏览器(用户除了等,不能进行其他操作)。
于是,浏览器就把这些任务分派到异步任务队列中去,并且跟他们说:你们自己去后台玩儿,等你们好了再过来通知我!先来看简单的例子来理解一下同步和异步任务:
1 |
|
当主线程执行到setTimeout的时候,虽然是延迟了0s,但是并不会马上来运行,而是放到异步任务队列中,等下面的同步任务队列执行完了,再来执行异步队列中的任务,所以运行结果是:start、end、setTimeout。
但如果同步任务中有特别耗时的操作,阻塞了setTimeout
的定时执行,那么setTimeout
就不会按时来完成。来看下面的例子:
1 |
|
虽然我们让setTimeout
1s后执行,但是for循环占用了太多的线程资源,实际执行会在2s后。所以事件循环
的流程大致如下:
如果任务队列中有多个异步任务,那么先执行哪个任务呢?于是在异步任务中,也进行了等级划分,分为宏任务(macrotask)和微任务(microtask);不同的API注册的任务会依次进入自身对应的队列中,然后等待事件循环将它们依次压入执行栈中执行。
宏任务包括:
微任务包括:
我们可以把整体的JS代码也看成是一个宏任务,主线程也是从宏任务开始的。我们把上面事件循环的步骤更新一下:
让我们来看一个例子:
1 |
|
分析一下执行流程:
start
promise
end
,这时整个执行栈清空了,宏任务和微任务队列各有一个回调方法then
timeout
我们把Promise进行一下改变,看一下下面的例子:
1 |
|
刚开始我们会想当然的认为执行顺序是:async1 start
–> async2
–> async1 end
–> script end
。但是当真正理解了async函数的本质后,我们知道async函数还是基于Promise的一些封装,而Promise是属于微任务的一种;因此会把await async2()
后面的所有代码放到Promise的then回调函数中去,因此,如果把上面代码进行如下改写,会好理解很多:
1 |
|
根据上面对微任务的理解,console.log('async1 end')
会放到微任务队列中,所以实际执行顺序是:async1 start
–> async2
–> script end
–> async1 end
。
最后来看那道面试题,相信已经不难理解了。
script start
async1 start
async2
promise1
script end
async1 end
promise2
setTimeOut
事情的起因来源于知乎上的一篇提问,有人提问“学生会退会申请六千字怎么写?”于是乎,各路大神纷纷前来献计献策,有复制粘贴六千字“不干了”的,有复制粘贴六千字《道德经》的,更有提议使用美人计撩学长的;然而其中的一个回答横空出世,答主随手就写了一个开源项目狗屁不通文章生成器;通过该项目,快速生成了一篇相关文章,不仅解决了题主的问题,还得到了广大网友的认同。
文章太长了,这边就不放完整版的了,有兴趣的童鞋可以去知乎观摩一下原文《学生会退会申请六千字怎么写?》,我试了下,果然是滑到底都需要半分钟之久。
该项目一开始为python3版本,后来有网友整理了网页版的,现在我们常用的是由suulnnka修改的在线版本,对页面的样式进行了优化,将生成的主题放入query参数中,使用更加的方便,这里我们随机来生成一篇文章看一下效果:
可以看出整篇文章虽然废话连篇、狗屁不通,但是段段扣题,旁征博引,引用各种名人名言,什么爱迪生曾经提到,什么康德曾经说过,每一段说的貌似都有理有据,令人无法反驳。
那么作者到底是如何来生成这么一长串的长篇大论的呢?最开始我还猜测是不是通过某种神经网络算法来将每一段话拼接起来,但是作者很明确的在README中写道:
鄙人才疏学浅并不会任何自然语言处理相关算法. 而且目前比较偏爱简单有效的方式达到目的方式. 除非撞到了天花板, 否则暂时不会引入任何神经网络等算法. 不过欢迎任何人另开分支实现更复杂, 效果更好的算法. 不过除非效果拔群, 否则鄙人暂时不会融合.
很明显,作者只是通过某种简单有效的
方式来实现这个功能的,那让我来深扒一下源码,看看这种到底是怎么样一种简单有效的
方式。
首先放在项目开始的是定义的一些论述、名人名言、前后垫话以及用到的公用函数等:
1 |
|
在广大网友的帮助下,整理了一百条的名人名言,整理的格式都是固定的:人名+曾经说过+一段话+这不禁令我深思,然后把曾经说过
替换前面垫话,把这不禁令我深思
替换成后面垫话。公用函数定义好了,最核心最精彩的部分就是生成文章的代码了:
1 |
|
首先作者的思路是把一篇文章作为一个数组存起来,数组中的每个元素都是一个章节,这里的章节可以理解为一个自然段,是文章的基本组成部分;最后再把整个数组通过div拼接起来。其中,最重要的就是如何来生成一个章节。
刚开始我对for(let 空 in 主题)
这个遍历感到十分困惑,主题很好理解是一个字符串,但是将字符串中每一个字符遍历出来有什么作用么?经过多次debug,猜测其实是为了多生成几次章节,凑字数而已;原本6000字,经过多次遍历,实际可能会远超6000字,达到好几万的字数(6000*主题字数);因此在suulnnka改版后的函数中,也将这段代码优化成了while( 文章长度 < 12000 )
,控制整篇文章的字数略大于12000字。
随便取一个数
函数用来生成一个0到100的随机数,首先让我们看一下随机数 < 20
的情况,也就是15%的概率(20-5)用来在章节中添加一句名人名言;然后80%的概率(100-5-15)用来在章节中添加一句论述;最后我们回到最难理解的随机数 < 5
的情况,也就是5%的概率来将这段章节给结束掉,通过把章节最后两个字符截取替换成句号,然后把章节push到文章中,最后把章节变量的内容给清空了。
通过对源码的分析,我们看出作者的方法确实很简单有效
,通过预存的名人名言和大段论述来生成文章;也正是因为简单,所以整个生成出来的文章重复率偏高了;因此作者也意识到这个问题,在项目中明确表示下一步计划是防止文章过于内容重复。
广大网友还在此基础上开发了日语版的和嘴臭版的(LOL喷人神器?),更有网友调侃李小璐离婚宣言都是用这个项目生成的。
除此之外,我们还发现项目中的代码使用了大量的中文函数名和中文变量,这也是我第一次知道了编程中还能使用中文变量名,太硬核了。作者也在项目中表示,使用中文变量名只是因为懒得切英文输入法,于是,分支作者还特地帮忙把漏网的英文变量名,也给改成了中文。
]]>CSS盒模型本质上是一个盒子,封装周围的HTML元素,它包括:边距margin,边框border,填充padding,和实际内容content。盒模型允许我们在其它元素和周围元素边框之间的空间放置元素。
box-sizing: content-box
(W3C盒模型,又名标准盒模型):元素的宽高大小表现为内容的大小。 box-sizing: border-box
(IE盒模型,又名怪异盒模型):元素的宽高表现为内容 + 内边距 + 边框的大小。背景会延伸到边框的外沿。
transition和animation的区别:
Animation和transition大部分属性是相同的,他们都是随时间改变元素的属性值,他们的主要区别是transition需要触发一个事件才能改变属性,而animation不需要触发任何事件的情况下才会随时间改变属性值,并且transition为2帧,从from …. to,而animation可以一帧一帧的。
BFC(Block Formatting Context)格式化上下文,是Web页面中盒模型布局的CSS渲染模式,指一个独立的渲染区域或者说是一个隔离的独立容器。
1 |
|
1 |
|
1 |
|
1 |
|
1 |
|
1 |
|
1 |
|
1 |
|
1 |
|
1 |
|
1 |
|
1 |
|
absolute
绝对定位 相对于最近的已定位的祖先元素, 有已定位(指position不是static的元素)祖先元素, 以最近的祖先元素为参考标准。如果无已定位祖先元素, 以body元素为偏移参照基准, 完全脱离了标准文档流。
fixed
固定定位的元素会相对于视窗来定位,这意味着即便页面滚动,它还是会停留在相同的位置。一个固定定位元素不会保留它原本在页面应有的空隙。
共同点:改变行内元素的呈现方式,都脱离了文档流;不同点:absolute的”根元素“是可以设置的,fixed的“根元素”固定为浏览器窗口
采用 Flex 布局的元素,称为 Flex 容器
(flex container),简称”容器”。它的所有子元素自动成为容器成员,称为 Flex 项目
(flex item),简称“项目”。
属性名 | 属性值 | 备注 |
---|---|---|
display | flex | 定义了一个flex容器,它的直接子元素会接受这个flex环境 |
flex-direction | row,row-reverse,column,column-reverse | 决定主轴的方向 |
flex-wrap | nowrap,wrap,wrap-reverse | 如果一条轴线排不下,如何换行 |
flex-flow | [flex-direction] , [flex-wrap] | 是flex-direction 属性和flex-wrap 属性的简写形式,默认值为row nowrap |
justify-content | flex-start,flex-end,center,space-between,space-around | 设置或检索弹性盒子元素在主轴(横轴)方向上的对齐方式 |
align-items | flex-start,flex-end,center,baseline,stretch | 设置或检索弹性盒子元素在侧轴(纵轴)方向上的对齐方式 |
属性名 | 属性值 | 备注 |
---|---|---|
order | [int] | 默认情况下flex order会按照书写顺序呈现,可以通过order属性改变,数值小的在前面,还可以是负数。 |
flex-grow | [number] | 设置或检索弹性盒的扩展比率,根据弹性盒子元素所设置的扩展因子作为比率来分配剩余空间 |
flex-shrink | [number] | 设置或检索弹性盒的收缩比率,根据弹性盒子元素所设置的收缩因子作为比率来收缩空间 |
flex-basis | [length], auto | 设置或检索弹性盒伸缩基准值 |
align-self | auto,flex-start,flex-end,center,baseline,stretch | 设置或检索弹性盒子元素在侧轴(纵轴)方向上的对齐方式,可以覆盖父容器align-items的设置 |
visibility:hidden、display:none、z-index=-1、opacity:0
clear:both
的空 div 元素,1 |
|
overflow:hidden
或者 auto 样式,触发BFC。1 |
|
1 |
|
1 |
|
1 |
|
推荐使用第三种方法,不会在页面新增div,文档结构更加清晰。
calc函数是css3新增的功能,可以使用calc()计算border、margin、pading、font-size和width等属性设置动态值。
1 |
|
注意点:
rem官方定义『The font size of the root element』,即根元素的字体大小。rem是一个相对的CSS单位,1rem等于html元素上font-size的大小。所以,我们只要设置html上font-size的大小,就可以改变1rem所代表的大小。
1 |
|
一般来说,在PC端浏览器中,设备像素比(dpr)等于1,1个css像素就代表1个物理像素;但是在retina屏幕中,dpr普遍是2或3,1个css像素不再等于1个物理像素,因此比实际设计稿看起来粗不少。
1 |
|
1 |
|
圣杯布局和双飞翼布局是前端工程师需要日常掌握的重要布局方式。两者的功能相同,都是为了实现一个两侧宽度固定,中间宽度自适应的三栏布局。
1 |
|
1 |
|
css引入伪类和伪元素概念是为了格式化文档树以外的信息。也就是说,伪类和伪元素都是用来修饰不在文档树中的部分。
伪类存在的意义是为了通过选择器找到那些不存在DOM树中的信息以及不能被常规CSS选择器获取到的信息。
伪元素用于创建一些不在文档树中的元素,并为其添加样式。比如说,我们可以通过:before来在一个元素前增加一些文本,并为这些文本添加样式。虽然用户可以看到这些文本,但是这些文本实际上不在文档树中。常见的伪元素有:::before
,::after
,::first-line
,::first-letter
,::selection
、::placeholder
等
因此,伪类与伪元素的区别在于:有没有创建一个文档树之外的元素。
在实际的开发工作中,我们会看到有人把伪元素写成:after
,这实际是 CSS2 与 CSS3新旧标准的规定不同而导致的。
CSS2 中的伪元素使用1个冒号,在 CSS3 中,为了区分伪类和伪元素,规定伪元素使用2个冒号。所以,对于 CSS2 标准的老伪元素,比如:first-line
,:first-letter
,:before
,:after
,写一个冒号浏览器也能识别,但对于 CSS3 标准的新伪元素,比如::selection,就必须写2个冒号了。
1 |
|
css中white-space这个属性,用来设置元素对内容中的空格的处理方式,有几个可选值:normal,nowrap,pre,pre-wrap,pre-line。没有设置white-space属性,则默认为white-space:normal。其他几个属性区别如下:
demo如下:
1 |
|
属性名 | 源码空格 | 源码换行 | br换行 | 容器边界换行 |
---|---|---|---|---|
normal | 合并 | 忽略 | 换行 | 换行 |
nowrap | 合并 | 忽略 | 换行 | 不换行 |
pre | 保留 | 换行 | 换行 | 不换行 |
pre-wrap | 保留 | 换行 | 换行 | 换行 |
pre-line | 合并 | 换行 | 换行 | 换行 |
CSS选择器的解析是从右向左解析的。若从左向右的匹配,发现不符合规则,需要进行回溯,会损失很多性能。若从右向左匹配,先找到所有的最右节点,对于每一个节点,向上寻找其父节点直到找到根元素或满足条件的匹配规则,则结束这个分支的遍历。比如.box .left p,会在页面中找到所有的p标签,然后在p标签中找其父元素有.left类的p元素,再找祖父元素有.box的p标签。
两种匹配规则的性能差别很大,是因为从右向左的匹配在第一步就筛选掉了大量的不符合条件的最右节点(叶子节点),而从左向右的匹配规则的性能都浪费在了失败的查找上面。
JS基本有5种简单数据类型:String,Number,Boolean,Null,Undefined。引用数据类型:Object,Array,Function。
在写业务逻辑的时候,经常要用到JS数据类型的判断,面试常见的案例深浅拷贝也要用到数据类型的判断。
1 |
|
优点:能够快速区分基本数据类型
缺点:不能将Object、Array和Null区分,都返回object
1 |
|
优点:能够区分Array、Object和Function,适合用于判断自定义的类实例对象
缺点:Number,Boolean,String基本数据类型不能判断
1 |
|
优点:精准判断数据类型
缺点:写法繁琐不容易记,推荐进行封装后使用
let
为 ES6
新添加申明变量的命令,它类似于 var
,但是有以下不同:
var
声明的变量,其作用域为该语句所在的函数内,且存在变量提升现象let
声明的变量,其作用域为该语句所在的代码块内,不存在变量提升const
声明的变量不允许修改Undefined类型只有一个值,即undefined。当声明的变量还未被初始化时,变量的默认值为undefined。用法:
Null类型也只有一个值,即null。null用来表示尚未存在的对象,常用来表示函数企图返回一个不存在的对象。用法
1 |
|
1 |
|
1 |
|
JS中的作用域分为两种:全局作用域和函数作用域。函数作用域中定义的变量,只能在函数中调用,外界无法访问。没有块级作用域导致了if或for这样的逻辑语句中定义的变量可以被外界访问,因此ES6中新增了let和const命令来进行块级作用域的声明。
更多作用域的了解可以看JS作用域
简单来说闭包就是在函数里面声明函数,本质上说就是在函数内部和函数外部搭建起一座桥梁,使得子函数可以访问父函数中所有的局部变量,但是反之不可以,这只是闭包的作用之一,另一个作用,则是保护变量不受外界污染,使其一直存在内存中,在工作中我们还是少使用闭包的好,因为闭包太消耗内存,不到万不得已的时候尽量不使用。
更多闭包的内容可以看JS闭包
1 |
|
更多JS去重的方法JS数组去重
三个函数的作用都是将函数绑定到上下文中,用来改变函数中this的指向;三者的不同点在于语法的不同。
1 |
|
所以apply
和call
的区别是call
方法接受的是若干个参数列表,而apply
接收的是一个包含多个参数的数组。
而bind()方法创建一个新的函数, 当被调用时,将其this关键字设置为提供的值,在调用新函数时,在任何提供之前提供一个给定的参数序列。
1 |
|
Demos:
1 |
|
==类型转换过程:
经典面试题:[] == ![] 为什么是true
转化步骤:
![]
会被转为为false,因此表达式变成了:[] == false
[] == 0
[]
转为空字符串,因此表达式变成了:'' == 0
0 == 0
浅拷贝
1 |
|
深拷贝,遍历对象中的每一个属性
1 |
|
防抖
1 |
|
节流
1 |
|
名称 | 生命期 | 大小限制 | 与服务器通信 |
---|---|---|---|
cookie | 一般由服务器生成,可设置失效时间。如果在浏览器端生成Cookie,默认是关闭浏览器后失效 | 4KB | 每次都会携带在HTTP头中,如果使用cookie保存过多数据会带来性能问题 |
localStorage | 除非被清除,否则永久保存 | 5MB | 仅在浏览器中保存,不与服务器通信 |
sessionStorage | 仅在当前会话下有效,关闭页面或浏览器后被清除 | 5MB | 仅在浏览器中保存,不与服务器通信 |
把需要计算的数字升级(乘以10的n次幂)成计算机能够精确识别的整数,等计算完成后再进行降级(除以10的n次幂),即:
1 |
|
更多关于浮点数精度处理请看JS中浮点数精度问题
首先创建一个父类
1 |
|
new了一个空对象,这个空对象指向Animal并且Cat.prototype指向了这个空对象,这种就是基于原型链的继承。
1 |
|
1 |
|
1 |
|
MVC即Model View Controller,简单来说就是通过controller的控制去操作model层的数据,并且返回给view层展示。
MVVM即Model-View-ViewModel,将其中的 View 的状态和行为抽象化,让我们可以将UI和业务逻辑分开。MVVM的优点是低耦合、可重用性、独立开发。
MVVM模式和MVC有些类似,但有以下不同
概括起来,MVVM是由MVC发展而来,通过在Model
之上而在View
之下增加一个非视觉的组件将来自Model
的数据映射到View
中。
vue采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty
劫持data属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。
组件中的data写成一个函数,数据以函数返回值形式定义,这样每复用一次组件,就会返回一份新的data。如果单纯的写成对象形式,就使得所有组件实例共用了一份data,造成了数据污染。
由于Vue会在初始化实例时对属性执行getter/setter
转化,所以属性必须在data
对象上存在才能让Vue将它转换为响应式的。Vue提供了$set方法用来触发视图更新。
1 |
|
v-if 是真正的条件渲染,因为它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建;也是惰性的:如果在初始渲染时条件为假,则什么也不做——直到条件第一次变为真时,才会开始渲染条件块。
v-show 就简单得多——不管初始条件是什么,元素总是会被渲染,并且只是简单地基于 CSS 的 “display” 属性进行切换。
所以,v-if 适用于在运行时很少改变条件,不需要频繁切换条件的场景;v-show 则适用于需要非常频繁切换条件的场景。
v-model本质上是语法糖,v-model 在内部为不同的输入元素使用不同的属性并抛出不同的事件。
所以我们可以v-model进行如下改写:
1 |
|
这个语法糖必须是固定的,也就是说属性必须为value,方法名必须为:input。
知道了v-model的原理,我们可以在自定义组件上实现v-model。
1 |
|
高效的更新虚拟DOM
。1 |
|
当text改变时,这个元素的key属性就发生了改变,在渲染更新时,Vue会认为这里新产生了一个元素,而老的元素由于key不存在了,所以会被删除,从而触发了过渡。
在Vue文件中的style标签上有一个特殊的属性,scoped。当一个style标签拥有scoped属性时候,它的css样式只能用于当前的Vue组件,可以使组件的样式不相互污染。如果一个项目的所有style标签都加上了scoped属性,相当于实现了样式的模块化。
scoped属性的实现原理是给每一个dom元素添加了一个独一无二的动态属性,给css选择器额外添加一个对应的属性选择器,来选择组件中的dom。
1 |
|
vue将代码转译成如下:
1 |
|
scoped虽然避免了组件间样式污染,但是很多时候我们需要修改组件中的某个样式,但是又不想去除scoped属性。
1 |
|
1 |
|
this.$refs.box
this.$refs.box.msg
this.$refs.box.open()
1.当页面中有某些数据依赖其他数据进行变动的时候,可以使用计算属性computed。
1 |
|
2.watch用于观察和监听页面上的vue实例,如果要在数据变化的同时进行异步操作或者是比较大的开销,那么watch为最佳选择。
1 |
|
即地址栏URL中的#符号,它的特点在于:hash 虽然出现URL中,但不会被包含在HTTP请求中,对后端完全没有影响,不需要后台进行配置,因此改变hash不会重新加载页面。
利用了HTML5 History Interface 中新增的pushState() 和replaceState() 方法(需要特定浏览器支持)。history模式改变了路由地址,因为需要后台配置地址。
1 |
|
1 |
|
重绘(repaint或redraw):当盒子的位置、大小以及其他属性,例如颜色、字体大小等都确定下来之后,浏览器便把这些原色都按照各自的特性绘制一遍,将内容呈现在页面上。重绘是指一个元素外观的改变所触发的浏览器行为,浏览器会根据元素的新属性重新绘制,使元素呈现新的外观。
重绘发生在元素的可见的外观被改变,但并没有影响到布局的时候。比如,仅修改DOM元素的字体颜色(只有Repaint,因为不需要调整布局)
重排(重构/回流/reflow):当渲染树中的一部分(或全部)因为元素的规模尺寸,布局,隐藏等改变而需要重新构建, 这就称为回流(reflow)。每个页面至少需要一次回流,就是在页面第一次加载的时候。
触发重排的条件:任何页面布局和几何属性的改变都会触发重排:
重排必定会引发重绘,但重绘不一定会引发重排。
GET、POST、HEAD、PUT、DELETE、CONNECT、OPTIONS、TRACE
请求方式 | GET | POST |
---|---|---|
参数位置 | 参数拼接到url的后面 | 参数在请求体中 |
参数大小 | 受限于浏览器url大小,一般不超过32K | 1G |
服务器数据接收 | 接收1次 | 根据数据大小,可分多次接收 |
适用场景 | 从服务器端获取数据 | 向服务器提交数据 |
安全性 | 参数携带在url中,安全性低 | 相对于GET请求,安全性更高 |
更多CORS请看彻底读懂前端跨域CORS
由于浏览器的同源策略限制,不允许跨域请求;但是页面中的 script、img、iframe标签是例外,不受同源策略限制。
Jsonp 就是利用script
标签跨域特性进行请求。
JSONP 的原理就是,先在全局注册一个回调函数,定义回调数据的处理;与服务端约定好一个同名回调函数名,服务端接收到请求后,将返回一段 Javascript,在这段 Javascript 代码中调用了约定好的回调函数,并且将数据作为参数进行传递。当网页接收到这段 Javascript 代码后,就会执行这个回调函数。
JSONP缺点:它只支持GET
请求,而不支持POST
请求等其他类型的HTTP请求。
缓存分为强缓存和协商缓存。强缓存不过服务器,协商缓存需要过服务器,协商缓存返回的状态码是304。两类缓存机制可以同时存在,强缓存的优先级高于协商缓存。当执行强缓存时,如若缓存命中,则直接使用缓存数据库中的数据,不再进行缓存协商。
更多缓存内容请看前端也要懂Http缓存机制
区别:
http://
起始且默认使用端口80,而HTTPS的URL由https://
起始且默认使用端口4431xx表示客户端应该继续发送请求
2xx表示成功的请求
3xx表示重定向
4xx表示客户端错误
5xx表示服务器错误
flex
CSS清除浮动
圣杯布局和双飞翼布局的理解与思考
一图秒懂函数防抖和函数节流
web前端面试总结
公司要求会使用框架vue,面试题会被问及哪些
30道Vue面试题
总结了17年初到18年初百场前端面试的面试经验
面试分享:两年工作经验成功面试阿里P6总结
No Access-Control-Allow-Origin header
这样的报错提示感到很头疼,怎么请求又跨域了。文章的开始,让我们从一个小故事开始。。。
在开发中,前端的童鞋们每次看到浏览器下面出现一长串红色的跨域报错就会很恼火,不停的念叨着:那个谁谁谁,又没有给我加跨域头;后端小伙伴又会毫不示弱地反击道:不就是Access-Control-Allow-Origin: *
么?已经有了啊!那为什么还会报错?肯定是你没加好!
于是,一场甩锅大战即将开始…
说实话,每一个前后端开发都应该要了解跨域的用法。
前端的小伙伴可能会觉得跨域问题应该都是后端接口来处理的,但是如果多了解一些HTTP请求响应头的,能够更快的定位问题,更快的解决接口异常,方便排查调试,所以希望能够耐下心把这篇文章看完。
在MDN中,对跨域是这么解释的:
跨域资源共享(CORS) 是一种机制,它使用额外的 HTTP 头来告诉浏览器 让运行在一个 origin (domain) 上的Web应用被准许访问来自不同源服务器上的指定的资源。当一个资源从与该资源本身所在的服务器不同的域、协议或端口请求一个资源时,资源会发起一个跨域 HTTP 请求。
简单来说就是当你向不同“域”的服务器发起网络请求的时候,这个请求就跨域了。这里不同“域”指的是不同的协议、域名、端口,有任何一个不同时,浏览器都视为跨域。我们在使用postman、fiddler等一些工具模拟发起http请求的时候,不会遇到跨域的情况;当我们在浏览器中请求不同域名的时候,虽然请求正常发出了,但是浏览器在请求返回时会进行一系列的校验,判断此次请求是否“合法”;如果不合法,返回结果就被浏览器拦截了。
我们在进行POST或其他跨域请求时,会发现只有一个OPTIONS请求,并没有我们想要的请求方法。
我们没有发送OPTIONS请求,那么它是从哪里来的呢?它的名称叫CORS请求预检,首先来看一下官方对它的定义是:
HTTP的OPTIONS方法用于获取目的资源所支持的通信选项。客户端可以对特定的URL使用OPTIONS方法,也可以对整站(通过将 URL 设置为“*”)使用该方法。
选项 | 是否允许 | 备注 |
---|---|---|
Request has body | No | 没有请求体 |
Successful response has body | No | 成功的响应有响应体 |
Safe | Yes | 安全 |
Idempotent | Yes | 密等性,不变性,同一个接口请求多少次都一样 |
Cacheable | No | 不能缓存 |
Allowed in HTML forms | No | 不能在表单里使用 |
根据官网的文档,我们发现它没有请求体,不能设置data,也不能直接发起OPTIONS请求。简言之,OPTIONS请求是用于请求服务器对于某些接口等资源的支持情况的,包括各种请求方法、头部的支持情况,仅作查询使用。
让我们详细地看一下OPTIONS请求的真实面目吧,我们首先构造一个POST请求:
1 |
|
可以看到OPTIONS请求头很简单,都没有请求的body,有两个字段Access-Control-Request-Headers
和Access-Control-Request-Method
是新出现的,下面会说到这两个字段的用法;那么什么时候会触发OPTIONS请求呢,这里涉及到两种CORS请求。
浏览器将CORS请求分成两类:简单请求(simple request)和非简单请求(not-so-simple request),简单请求不会触发CORS预检请求。
只要同时满足以下两大条件,就属于简单请求:
请求方法是以下三种方法之一
HTTP的头信息不超出以下几种字段
因此我们只要把上面的请求加一个请求头Content-Type
,就能不触发OPTIONS请求。
1 |
|
下面,我们的重点来了,我们在进行ajax请求时,一般都会在请求头加一下自定义的数据,因此大多数请求都是非简单请求。非简单请求涉及以下几个请求和响应的头部的字段:
字段名 | 位置 | 用法 | 备注 |
---|---|---|---|
Origin | 请求头 | origin | 表明预检请求或实际请求的源站 |
Access-Control-Request-Method | 请求头 | method | 将实际请求所使用的 HTTP 方法告诉服务器。 |
Access-Control-Request-Headers | 请求头 | field-name[, field-name]* | 将实际请求所携带的头部字段告诉服务器。 |
Access-Control-Allow-Origin | 响应头 | origin or * | 对于不需要携带身份凭证的请求,服务器可以指定该字段的值为通配符,表示允许来自所有域的请求 |
Access-Control-Allow-Methods | 响应头 | method[, method]* | 指明了实际请求所允许使用的 HTTP 方法。 |
Access-Control-Allow-Headers | 响应头 | field-name[, field-name]* | 指明了实际请求中允许携带的头部字段。 |
Access-Control-Allow-Credentials | 响应头 | true | 指定了当浏览器的credentials设置为true时是否允许浏览器读取response的内容 |
Access-Control-Max-Age | 响应头 | delta-seconds | 指定了请求的结果能够被缓存多久 |
在上面的OPTIONS请求中我们可以发现表格中的三个请求头部都在该次请求中出现了,Access-Control-Request-Method
和Access-Control-Request-Headers
用来询问服务器,下面我要用POST方法和Content-Type头部来请求,你就说你答不答应吧?
在服务器端,我们可以这么写来允许请求跨域:
1 |
|
这里有我们后端小伙伴很熟悉的Access-Control-Allow-Origin: *
,用来表明所有的origin都允许跨域,相当于告诉浏览器:
这样我们就能看到我们期待已久的POST请求,同时返回的头部信息中带上了CORS的响应头;同时我们可以看到axios默认的Content-Type
是application/json;charset=UTF-8
,不在仅限的三个值中,因此会触发OPTIONS请求。
除了content-type,我们还可以在请求头中添加一些自己定义的信息,比如需要传给后台的token之类的。
1 |
|
默认情况下,Cookie是不包括在CORS的请求中,但有时候我们又需要用到Cookie来传输数据,这时候我们的Access-Control-Allow-Credentials
字段就派上用处了,另一方面,需要在AJAX请求中打开withCredentials属性;我们再把代码进行如下改造:
1 |
|
当我们满怀期待打开浏览器准备接收Cookie时,却发现又报错了:
经过对错误信息仔细阅读,发现这次报错跟上面的跨域报错不完全一样,大概的意思是当请求的身份凭证包括的时候,Access-Control-Allow-Origin
不能是通配符’*’(wildcard)。因此我们大概了解到了错误的原因是在通配符上面,我们对代码再进行一下改造:
1 |
|
这时候就能看到我们想要的Cookie了。
CORS内容其实来说不是很多,也比较简单,但是考验动手实践能力,面试时一般也会问到,因此通过express搭建服务器来加深对CORS知识的了解。
]]>非严格模式和严格模式中this都是指向顶层对象
1 |
|
在非严格模式下,普通函数的this指向window
1 |
|
这里的sayName()
相当于调用了window.sayName()
。在严格模式下,普通函数的this指向undefined
1 |
|
在ES5中,var会给顶层对象中(浏览器是window)添加属性,而使用let则不会。
1 |
|
对象中的函数this指向对象本身
1 |
|
将对象中的函数赋值成变量,这样又变成普通函数,this指向顶层对象。
1 |
|
所以,通过上面我们可以看出来,函数的定义位置不影响其this指向,this指向只和调用函数的对象有关。
通过call、apply、bind可以改变函数this的指向
语法:
1 |
|
thisArg是fun函数在运行时指定的this的值。但是指定的this值不一定就是该函数执行时真正的this值。如果这个函数处于非严格模式下,指定为null和undefined的this值会自动指向全局对象(windows中就是window对象);指定为原始值(数字,字符串,布尔值)的this会指向该原始值的自动包装对象;
1 |
|
apply和call类似。只是参数不一样。它的参数是数组(或者类数组)。
1 |
|
严格模式下,指向null和undefined的this值还是指向原来的。
1 |
|
bind和call和apply的调用方式相同,第一个值也是修改this的指向,只不过bind返回一个新的函数。
1 |
|
由此可知,new操作符调用时,this指向生成的新对象。
1 |
|
先看箭头函数和普通函数的重要区别:
箭头函数中没有this绑定,必须通过查找作用域链来决定其值。 如果箭头函数被非箭头函数包含,则this绑定的是最近一层非箭头函数的this,否则this的值则被设置为全局对象。 比如:
1 |
|
如果要判断一个运行中函数的 this 绑定, 就需要找到这个函数的直接调用位置。
]]>浏览器和服务器之间通信是通过HTTP协议,HTTP协议永远都是客户端发起请求,服务器回送响应。模型如下:
HTTP报文就是浏览器和服务器间通信时发送及响应的数据块。浏览器向服务器请求数据,发送请求(request)报文;服务器向浏览器返回数据,返回响应(response)报文。报文信息主要分为两部分:
本文用到的一些报文头如下:
字段名称 | 字段所属 |
---|---|
Pragma | 通用头 |
Expires | 响应头 |
Cache-Control | 通用头 |
Last-Modified | 响应头 |
If-Modified-Sice | 请求头 |
ETag | 响应头 |
If-None-Match | 请求头 |
Http缓存可以分为两大类,强制缓存(也称强缓存)和协商缓存。两类缓存规则不同,强制缓存在缓存数据未失效的情况下,不需要再和服务器发生交互;而协商缓存,顾名思义,需要进行比较判断是否可以使用缓存。
两类缓存规则可以同时存在,强制缓存优先级高于协商缓存,也就是说,当执行强制缓存的规则时,如果缓存生效,直接使用缓存,不再执行协商缓存规则。
我们先简单搭建一个Express的服务器,不加任何缓存信息头。
1 |
|
我们可以看到请求结果如下:
请求过程如下:
看得出来这种请求方式的流量与请求次数有关,同时,缺点也很明显:
接下来我们开始在头信息中添加缓存信息。
强制缓存分为两种情况,Expires和Cache-Control。
Expires的值是服务器告诉浏览器的缓存过期时间(值为GMT时间,即格林尼治时间),即下一次请求时,如果浏览器端的当前时间还没有到达过期时间,则直接使用缓存数据。下面通过我们的Express服务器来设置一下Expires响应头信息。
1 |
|
我们在demo.js中添加了一个Expires响应头,不过由于是格林尼治时间,所以通过momentjs转换一下。第一次请求的时候还是会向服务器发起请求,同时会把过期时间和文件一起返回给我们;但是当我们刷新的时候,才是见证奇迹的时刻:
可以看出文件是直接从缓存(memory cache)中读取的,并没有发起请求。我们在这边设置过期时间为两分钟,两分钟过后可以刷新一下页面看到浏览器再次发送请求了。
虽然这种方式添加了缓存控制,节省流量,但是还是有以下几个问题的:
不过Expires 是HTTP 1.0的东西,现在默认浏览器均默认使用HTTP 1.1,所以它的作用基本忽略。
针对浏览器和服务器时间不同步,加入了新的缓存方案;这次服务器不是直接告诉浏览器过期时间,而是告诉一个相对时间Cache-Control=10秒,意思是10秒内,直接使用浏览器缓存。
1 |
|
强制缓存的弊端很明显,即每次都是根据时间来判断缓存是否过期;但是当到达过期时间后,如果文件没有改动,再次去获取文件就有点浪费服务器的资源了。协商缓存有两组报文结合使用:
为了节省服务器的资源,再次改进方案。浏览器和服务器协商,服务器每次返回文件的同时,告诉浏览器文件在服务器上最近的修改时间。请求过程如下:
If-Modified-Since
(等于上一次请求的Last-Modified)请求服务器If-Modified-Since
和文件的上次修改时间。如果果一致就继续使用本地缓存(304),如果不一致就再次返回文件内容和Last-Modified。代码实现过程如下:
1 |
|
我们多次刷新页面,可以看到请求结果如下:
虽然这个方案比前面三个方案有了进一步的优化,浏览器检测文件是否有修改,如果没有变化就不再发送文件;但是还是有以下缺点:
为了解决文件修改时间不精确带来的问题,服务器和浏览器再次协商,这次不返回时间,返回文件的唯一标识ETag。只有当文件内容改变时,ETag才改变。请求过程如下:
If-None-Match
(等于上一次请求的ETag)请求服务器If-None-Match
和文件的ETag。如果一致就继续使用本地缓存(304),如果不一致就再次返回文件内容和ETag。1 |
|
请求结果如下:
在报文头的表格中我们可以看到有一个字段叫Pragma,这是一段尘封的历史….
在“遥远的”http1.0时代,给客户端设定缓存方式可通过两个字段–Pragma和Expires。虽然这两个字段早可抛弃,但为了做http协议的向下兼容,你还是可以看到很多网站依旧会带上这两个字段。
当该字段值为no-cache
的时候,会告诉浏览器不要对该资源缓存,即每次都得向服务器发一次请求才行。
1 |
|
通过Pragma来禁止缓存,通过Cache-Control设置两分钟缓存,但是重新访问我们会发现浏览器会再次发起一次请求,说明了Pragma的优先级高于Cache-Control
我们看到Cache-Control中有一个属性是public,那么这代表了什么意思呢?其实Cache-Control不光有max-age,它常见的取值private、public、no-cache、max-age,no-store,默认值为private,各个取值的含义如下:
所以我们在刷新页面的时候,如果只按F5只是单纯的发送请求,按Ctrl+F5会发现请求头上多了两个字段Pragma: no-cache和Cache-Control: no-cache。
上面我们说过强制缓存的优先级高于协商缓存,Pragma的优先级高于Cache-Control,那么其他缓存的优先级顺序怎么样呢?网上查阅了资料得出以下顺序(PS:有兴趣的童鞋可以验证一下正确性告诉我):
Pragma > Cache-Control > Expires > ETag > Last-Modified
参考资料:
http缓存优先级问题
彻底弄懂HTTP缓存机制及原理
HTTP缓存控制小结
浅谈浏览器http的缓存机制
通过express框架简单实践几种设置HTTP对缓存的控制
总结了一下,一共有以下两种问题
在计算商品价格加减乘除时,偶尔会出现精度问题,一些常见的例子如下:
1 |
|
在遇到浮点数运算后出现的精度问题时,刚开始我是使用toFixed(2)来解决的,因为在W3school和菜鸟教程(他们均表示这锅不背)上明确写着定义:toFixed()方法可把Number四舍五入为指定小数位数的数字。
但是在chrome下测试结果不太令人满意:
1 |
|
使用IETester在IE下面测试的结果却是正确的。
让我们来看一下为什么0.1+0.2会等于0.30000000000000004,而不是0.3。首先,想要知道为什么会产生这样的问题,让我们回到大学里学的复(ku)杂(zao)的计算机组成原理。虽然已经全部还给大学老师了,但是没关系,我们还有百度嘛。
和其它语言如Java和Python不同,JavaScript中所有数字包括整数和小数都只有一种类型 — Number。它的实现遵循 IEEE 754 标准,使用64位固定长度来表示,也就是标准的 double 双精度浮点数(相关的还有float 32位单精度)。
这样的存储结构优点是可以归一化处理整数和小数,节省存储空间。
64位比特又可分为三个部分:
符号位S:第 1 位是正负数符号位(sign),0代表正数,1代表负数
指数位E:中间的 11 位存储指数(exponent),用来表示次方数
尾数位M:最后的 52 位是尾数(mantissa),超出的部分自动进一舍零
那么JavaScript在计算0.1+0.2时到底发生了什么呢?
首先,十进制的0.1和0.2会被转换成二进制的,但是由于浮点数用二进制表示时是无穷的:
1 |
|
IEEE 754 标准的 64 位双精度浮点数的小数部分最多支持53位二进制位,所以两者相加之后得到二进制为:
1 |
|
因浮点数小数位的限制而截断的二进制数字,再转换为十进制,就成了0.30000000000000004。所以在进行算术计算时会产生误差。
针对以上两个问题,网上搜了一波解决方法,基本都大同小异的,分别来看一下。
针对toFixed的兼容性问题,我们可以把toFix重写一下来解决,代码如下:
1 |
|
我们通过判断最后一位是否大于等于5来决定需不需要进位,如果需要进位先把小数乘以倍数变为整数,加1之后,再除以倍数变为小数,这样就不用一位一位的进行判断。
既然我们发现了浮点数的这个问题,又不能直接让两个浮点数运算,那怎么处理呢?
我们可以把需要计算的数字升级(乘以10的n次幂)成计算机能够精确识别的整数,等计算完成后再进行降级(除以10的n次幂),这是大部分变成语言处理精度问题常用的方法。例如:
1 |
|
但是这样就能完美解决么?细心的读者可能在上面的例子里已经发现了问题:
1 |
|
看来进行数字升级也不是完全的可靠啊(允悲)。
但是魔高一尺道高一丈,这样就能难住我们么,我们可以将浮点数toString后indexOf(‘.’),记录一下小数位的长度,然后将小数点抹掉,完整的代码如下:
1 |
|
如果觉得floatObj调用麻烦,我们可以在Number.prototype上添加对应的运算方法。
参考链接:
MomentJS将时间封装成一个对象,moment对象,这个对象很多种构造方式,可以支持传入字符串、数组和对象的形式来构造。
如果什么都不传,就获取当前的系统时间。
1 |
|
可以传入字符串,首先会检查字符串的格式是否符合ISO 8601
的格式,如果不符合,就调用new Date(string)
来构造。
1 |
|
假如日期的格式不符合ISO 8601
的格式,但是你知道输入的字符串的格式,也可以通过这种方式解析,解析的语法有以下四种格式:
1 |
|
第一种已知某个时间的格式,将格式作为第二个参数传入
1 |
|
格式字母代表的含义如下表
Input | Example | Description |
---|---|---|
YYYY | 2014 | 4位数年份 |
YY | 14 | 2位数年份 |
Q | 1..4 | 季度,将月份设置成季度的第一个月 |
M MM | 1..12 | 月份 |
MMM MMMM | Jan..December | 月份名称 |
D DD | 1..31 | 一个月的第几天 |
DDD DDDD | 1..365 | 一年的第几天 |
H HH | 0..23 | 24小时制 |
h hh | 1..12 | 12小时制 |
m mm | 0..59 | 分钟 |
s ss | 0..59 | 秒 |
第二种,可以将当地区域的关键符作为第三个参数传入。
1 |
|
MomentJS的匹配模式是十分宽松的,并且可能会导致一些我们不想要的行为。从2.3.0
版本开始,我们可以在最后传入一个布尔值来让Moment使用严格模式匹配。严格模式要求输入的字符串和格式要完全相同。
1 |
|
假如你不知道输入的字符串确切是用的哪种格式,但是知道是某些格式中的一种,可以将多种格式用数组的形式传入,将会以最先匹配到的格式为输出结果。
1 |
|
我们也可以通过传入一个对象的形式来创建moment对象,传入的对象中包括一些时间单位的属性。
1 |
|
上面代码中的day和date都表示当前月的第几天。
我们也可以传入JS原生的Date对象来创建moment对象。
1 |
|
我们可以传入一个数字的数组来创建moment对象,数组中每个每个数字代表的含义如下:
1 |
|
需要注意的是:数组中的月、时、分、秒、毫秒都是从0开始的,而年和日都是从1开始的。
MomentJS使用可以重载的get和set方法,跟我们以前在jQuery中的形式很相似。我们可以调用这些方法不传参数作来获取,传入参数作来设置。
获取或者设置毫秒,设置的范围0到999
1 |
|
获取或者设置秒,设置的范围0到59
1 |
|
获取或者设置分钟,设置的范围0到59
1 |
|
获取或者设置小时,设置的范围0到23
1 |
|
获取或者设置日期,设置的范围1到31
1 |
|
获取或者设置星期,设置的范围0(周日)到6(周六)
1 |
|
获取或者设置一年中的天数,设置的范围1到366
1 |
|
获取或者设置一年中的周
1 |
|
获取或者设置一年中的月份,设置的范围0到11
1 |
|
获取或者设置季度,设置的范围1到4
1 |
|
获取或者设置年,设置的范围-270000到270000
1 |
|
除了上面的这么多函数外,MomentJS还有一个用来统一取值和赋值的函数,get和set。
1 |
|
set函数接收单位作为第一个参数,单位的值作为第二个参数。如果要设置多个值的话,也可以通过传入一个对象。
1 |
|
max函数可以返回给定的moment对象中最大的实例,也就是最靠近未来的实例。
1 |
|
min函数可以返回给定的moment对象中最小的实例,也就是最靠近过去的实例。
1 |
|
有时候,我们需要对时间进行一系列的操作,最常见的就是加减计算。MomentJS提供了很多方法给我们来进行调用。
MomentJS使用的模式跟jQuery相似,都是使用的函数的链式调用,可以让我们将操作链式执行下去,代码如下所示:
1 |
|
add函数让我们把Moment对象的时间往后退,它的语法如下:
1 |
|
我们可以传入想要的增加的时间数量和时间单位,比如要往后推迟7天:
1 |
|
当然,add函数也允许我们提供时间单位的缩写:
1 |
|
时间单位 | 缩写 |
---|---|
years | y |
quarters | Q |
months | M |
weeks | W |
days | d |
hours | h |
minutes | m |
seconds | s |
milliseconds | ms |
如果想要同时增加不同时间单位,可以以对象的形式传入:
1 |
|
需要注意的是,如果原始日期的天数比新增后的日期的月份的总天数还要多,就变为该月的最后一天:
1 |
|
subtract函数的用法和add相似,不同的是把时间往前推。
1 |
|
startOf函数将Moment对象的时间设置为传入单位的开始时间。
1 |
|
endOf函数将Moment对象的时间设置为传入单位的结束时间。使用方式和startOf相似。
1 |
|
当我们解析和操作完Moment对象后,我们就需要对最后的结果进行展示。
format函数接收token字符串,并且将token替换成对应的值。
1 |
|
对应的关系如下表:
- | Token | 输入 |
---|---|---|
月 | M | 1 2 .. 11 12 |
Mo | 1st 2nd … 11th 12th | |
MM | 01 02 … 11 12 | |
MMM | Jan Feb … Nov Dec | |
MMMM | January February … November December | |
季度 | Q | 1 2 3 4 |
月份中的天 | D | 1 2 … 30 31 |
Do | 1st 2nd … 30th 31st | |
DD | 01 02 … 30 31 | |
年份中的天 | DDD | 1 2 … 365 366 |
DDDo | st 2nd … 365th 366th | |
DDDD | 001 002 … 365 366 | |
星期中的天 | d | 0 1 … 5 6 |
do | 0th 1st … 5th 6th | |
dd | Su Mo … Fr Sa | |
ddd | Sun Mon … Fri Sat | |
dddd | Sunday Monday … Friday Saturday | |
年中的星期 | w | 1 2 … 52 53 |
wo | 1st 2nd … 52nd 53rd | |
ww | 01 02 … 52 53 | |
年 | YY | 70 71 … 29 30 |
YYYY | 1970 1971 … 2029 2030 | |
AM/PM | A | AM PM |
a | am pm | |
小时 | H | 0 1 … 22 23 |
HH | 00 01 … 22 23 | |
h | 1 2 … 11 12 | |
hh | 01 02 … 11 12 | |
分钟 | m | 0 1 … 58 59 |
mm | 00 01 … 58 59 | |
秒 | s | 0 1 … 58 59 |
ss | 00 01 … 58 59 | |
毫秒 | ms | 000 001 … 998 999 |
语法
1 |
|
diff函数可以帮我们获取到两个Moment对象的时间差,默认的单位是毫秒。
1 |
|
除了得到毫秒为单位,diff函数还支持获取其他的时间单位,将其作为第二个参数传入:
1 |
|
支持的测量单位有years、months、weeks、days、hours、minutes、seconds和milliseconds。默认返回的数值会向下取舍,去掉小数。假如想要精确一点,得到小数类型的数值,第三个参数传入一个true。
1 |
|
daysInMonth获取当前月的总天数
1 |
|
将Moment对象转为js原生的Date对象
返回时间数组,和构造Moment对象时传入的数组代表的含义相同。
1 |
|
将Moment对象转为包含年月日时分秒毫秒的对象。
1 |
|
查询操作主要用来判断Moment是否满足某些条件。
1 |
|
isBefore判断一个moment对象是否在某个时间点之前。
1 |
|
默认的比较单位是毫秒,但是假如我们想要限制到其他的时间单位,我们可以将其作为第二个参数传入。接受的单位和startOf支持的单位一样。
1 |
|
1 |
|
isSame判断一个moment对象是否和另一个moment对象相同。
1 |
|
同样的,我们如果要将比较的单位改为其他的,也可以作为第二个参数传入。接受的单位和startOf支持的单位一样。
1 |
|
当传入第二个参数时,它会匹配所有相同或者更大的单位。比如传入了月份,将会比较年和月,传入了日期,将会比较年月日
1 |
|
isBefore判断一个moment对象是否在某个时间点之后。接受的单位和startOf支持的单位一样。
1 |
|
1 |
|
判断一个moment对象是否在两个其他时间点之间。
1 |
|
传入第二个参数作为限制的单位。接受的单位和startOf支持的单位一样。
1 |
|
是闰年就返回true,不是就返回false。
1 |
|
判断是否Moment对象
1 |
|
判断是否Date对象
1 |
|
multipart/form-data
类型的表单数据,可以很方便的将表单中的文件数据保存到服务器。 multer是一个node.js文件上传中间件,它是在 busboy的基础上开发的,上传的表单数据必须是multipart/form-data
类型,不然会报错。
Multer作为express的一个中间件,我们可以很方便的自定义上传的文件目录以及保存的文件名。先看一个最简单的用法,demo1地址:
1 |
|
我们先创建了一个upload对象,这个对象中destination函数用来定义上传文件的存储的文件夹;filename函数用来修改上传文件存储到服务器的文件名称,这里我们我们加上一个时间戳简单区分一下。这两个函数都是通过回调函数来实现的。每次上传的时候这两个函数都会调用一次,如果是多个文件上传,那个这两个函数就调用多次,调用顺序是先调用destination,然后调用filename。
在两个函数中都会有一个file
对象,表示当前上传的文件对象,有以下几个属性:
1 |
|
在express中定义路由的回调函数时,把定义好了的upload对象作为中间件添加进去。如果是单个文件就用single
方法,如果是多个文件就用array
方法,这两个方法都需要传一个页面上定义好的字段名。
在路由的回调函数中,request对象已经有了file属性(单个文件上传)或files属性(多个文件上传),files属性是一个数组,数组的每一个对象都有以下属性:
我们可以发现在路由的回调函数中的file对象比diskStorage中的file对象多了几个属性,这是因为在diskStorage中文件还没有保存,只能知道文件的大致属性;而路由的回调函数文件已经在服务器上保存好了,文件的保存路径以及文件的大小都是已知的。
有时候我们可能需要用字段名来对上传的文件进行一下划分,比如说上传多个图片的时候可能有身份证还有头像。虽然可以分开放到两个接口中,但是会产生其他一系列的麻烦事。multer支持对图片进行字段名的划分。demo3地址
1 |
|
在这边也有req.files
属性,但是这个属性并不是一个数组,而是一个复杂的对象,这个对象中有多个属性,每个属性名都是一个字段名,每个属性下面又是一个数组,数组下面才是一个个的文件对象,结构大致如下:
1 |
|
在文件上传时,有时候会上传一些我们不需要的文件类型,我们需要把一些不需要的文件给过滤掉。demo2地址。
1 |
|
在定义存储器的时候,新增一个fileFilter函数,用来过滤掉我们不需要的文件,在回调函数中我们传入true/false来代表是否要保存;如果传了false,那么destination函数和filename函数也不会调用了。
1 |
|
在定义存储器的时候,新增一个limits对象,用来控制上传的一些信息,它有以下一些属性:
在这边我们把fileSize的值设置得小一点,设为10kb方便测试看效果,但是如果这个时候会发现有报错。因为上传的文件大小很容易就会超过10KB,导致有报错出现,我们就需要在路由回调里对错误的情况进行捕获。
1 |
|
所有的demo代码都在这个仓库里
]]>什么是Git。百度百科的解释:
那分布式版本控制系统与集中式版本控制系统有何不同呢?首先,分布式版本控制系统根本没有“中央服务器”,每个人的电脑上都是一个完整的版本库,这样,你工作的时候,就不需要联网了,因为版本库就在你自己的电脑上。既然每个人电脑上都有一个完整的版本库,那多个人如何协作呢?比方说你在自己电脑上改了文件A,你的同事也在他的电脑上改了文件A,这时,你们俩之间只需把各自的修改推送给对方,就可以互相看到对方的修改了。
和集中式版本控制系统相比,分布式版本控制系统的安全性要高很多,因为每个人电脑里都有完整的版本库,某一个人的电脑坏掉了不要紧,随便从其他人那里复制一个就可以了。而集中式版本控制系统的中央服务器要是出了问题,所有人都没法干活了。
在实际使用分布式版本控制系统的时候,其实很少在两人之间的电脑上推送版本库的修改,因为可能你们俩不在一个局域网内,两台电脑互相访问不了,也可能今天你的同事病了,他的电脑压根没有开机。因此,分布式版本控制系统通常也有一台充当“中央服务器”的电脑,但这个服务器的作用仅仅是用来方便“交换”大家的修改,没有它大家也一样干活,只是交换修改不方便而已。
什么是版本库?版本库又名仓库,英文名repository,你可以简单的理解一个目录,这个目录里面的所有文件都可以被Git管理起来,每个文件的修改,删除,Git都能跟踪,以便任何时刻都可以追踪历史,或者在将来某个时刻还可以将文件”还原”。
git clone命令会把项目的版本库一起克隆到本地,就是.git目录,这个目录是Git来跟踪管理版本的,没事千万不要手动乱改这个目录里面的文件,否则,会把git仓库给破坏了。
工作区:就是在电脑里能看到的目录,比如react-stage目录就是一个工作区
暂存区:在版本库中。
1 |
|
1 |
|
用来查看当前工作区和暂存区的状态。可以加-s
查看状态的简写形式。
1 |
|
1.颜色区分:
红色表示在工作区,绿色表示在暂存区。
2.字符区分:
查看工作区的修改
1 |
|
查看从近到远的日志信息。
1 |
|
如果觉得信息太多,可以添加--pretty=oneline
查看缩略信息
1 |
|
1、撤销工作区修改
1 |
|
2、撤销分支上的修改
1 |
|
为了理解Git分支的实现方式,我们需要回顾一下Git是如何储存数据的。Git保存的不是文件差异或者变化量,而只是一系列文件快照。
当使用git commit
新建一个提交对象前,Git会先计算每一个子目录(本例中就是项目根目录)的校验和,然后在 Git 仓库中将这些目录保存为树(tree)对象。之后 Git 创建的提交对象,除了包含相关提交信息以外,还包含着指向这个树对象(项目根目录)的指针,如此它就可以在将来需要的时候,重现此次快照的内容了。
Git 中的分支,其实本质上仅仅是个指向 commit 对象的可变指针。Git 会使用 master 作为分支的默认名字。在若干次提交后,你其实已经有了一个指向最后一次提交对象的 master 分支,它在每次提交的时候都会自动向前移动。
1 |
|
通过git branch
这种方式创建的分支仅仅是在本地创建了分支,远程仓库是没有这个分支的。因为没有关联,提交的修改是不能push到远程仓库的。
一般都是从远程仓库拉取已有的分支。
1 |
|
1 |
|
或者
1 |
|
1 |
|
查看本地所有的分支以及所在的分支
1 |
|
查看本地所有的分支以及远程仓库所有的分支
1 |
|
删除本地分支
1 |
|
如果要删除的分支上有提交推送到远程或者merge到其他分支,那么这样删除会失败,可以用强制删除:
1 |
|
删除远程分支
1 |
|
将其他分支commit的内容合并到当前分支。
1 |
|
在clone项目后使用git flow init
初始化git流程,选项全部回车默认就行。这个操作只需要在clone后执行一次,以后都不需要了。
1 |
|
分支说明:
1 |
|
在自己的特性分支开发完成后:
1 |
|
下次开发前:
1 |
|
第一种方法也是最一般、最常用的办法,使用数组的indexOf()
方法。
1 |
|
但是indexOf方法内部实现也是去遍历数组知道找到目标为止,如果待去重的数组很长且重复的元素少,则会耗费了大量的时间。
第二种方法是将数组所有的元素转变成对象的键名,利用对象键名的不可重复的特性来去重。
1 |
|
在时间消耗上来看,这种方法比第一种方法要快很多,因为从对象中取属性值消耗的时间几乎可以不计,但是存在以下两个问题:
第三种方法利用数组原生的sort()
方法,将数组先进行排序,排序后比较相邻两个元素的值。
1 |
|
这种方法比indexOf()
消耗的时间要短,比存放Hash对象占用的内存要小,算是一种折中两者的方法。但是也存在一个问题,就是去重后的数组的顺序发生了改变。
如果你开发环境支持ES6,这个方法是最简洁的。
1 |
|
console.log
将数据打印出来,如果有时候想要修改接口数据还很不方便。针对上面调试的痛点,笔者对Fiddler的用法进行了简单的学习,分享一下学习的心得。首先来介绍一下Fiddler。Fiddler是位于客户端和服务器端的HTTP代理,也是目前最常用的http抓包工具之一 。 它能够记录客户端和服务器之间的所有HTTP请求,可以针对特定的HTTP请求,分析请求数据、设置断点、调试web应用、修改请求的数据,甚至可以修改服务器返回的数据,功能非常强大,是web调试的利器。
是的,你没有看错,Fiddler这货居然还能设置断点。不仅如此,Fiddler还能修改请求数据或者修改返回数据。这样在调试的时候可以随意的将服务器的返回数据修改成我们想要的数据了。除此之外,Fiddler还能够拦截手机端的数据,能够看到手机端发送的请求和请求结果,不过要进行一个小小的设置。好了,介绍了这么多,来看一下Fiddler的原理:
既然Fiddler是客户端和服务器端之间的代理,那么客户端所有发起的请求都会经过Fiddler,然后再发送到对应的服务器;同样,服务器所有的响应数据也会经过Fiddler再发送到客户端。
安装完Fiddler,打开界面如下,整个界面可以分为五个部分:
Fiddler简单介绍完了,下面来看下Fiddler的一些配置。
Fiddler打开后会自动更改IE的代理设置,由于Chrome默认代理设置是跟IE关联在一起的,因此Chrome不用进行配置,但是火狐使用独立的代理设置,因此需要单独配置。
首先查看Fiddler的监听端口。在Fiddler中选择Tools
=> Fiddler Options
=> connections
,打开如下界面:
其中的Fiddler Listens on port
就是Fiddler的监听端口,我们只要代理到这个端口就可以用Fiddler进行监听了。然后把Allow remote computers to connect
这一行前面的勾打上,允许其他电脑来连接。
Firefox手动设置如下,打开工具
=> 选项
=> 高级
=> 网络
=> 设置
,然后进行如下设置。
不过上面的手动设置比较麻烦,我们可以安装一个FiddlerHook插件,安装好后启用这个插件就行。
Fiddler不仅能够代理电脑的请求,也能够代理手机端的请求,当我们开发微信站或者手机站的时候,就可以很方便我们来进行调试。
IOS选择对应的无线网设置,然后找到HTTP代理,服务器一栏填写电脑的IP地址,端口号是Fiddler端口号,默认8888。
在Android中,长按所连接的无线网,然后修改网络,在代理的选项卡里选择手动。同样,服务器一栏填写电脑的IP地址,端口号默认8888.
默认情况下,Fiddler不会捕获HTTPS会话,因此如果不开启HTTPS捕获的话自动应答器是不会替换HTTPS的会话。打开Tools
=> Fiddler Options
=> HTTPS
弹出框需要安装一个证书,然后全程点Yes就可以了。
Fiddler的统计选项卡中显示了当前Session的基本信息,在选项卡的最上方显示的是文本信息,最下方是个饼图,按MIME类型显示流量。使用Statistics页签,用户可以通过选择多个会话来得来这几个会话的总的信息统计,比如多个请求和传输的字节数。
选择第一个请求和最后一个请求,可获得整个页面加载所消耗的总体时间。从条形图表中还可以分别出哪些请求耗时最多,从而对页面的访问进行访问速度优化。
在左侧请求数据列表中选中一条记录会自动切换到Insprctors
面板,这个面板分为上下两个,上面是请求头的一些信息,下面是返回的响应主体。
在日常工作中,有时候脚本样式或者页面有点问题是家常便饭,经常需要对文件进行修改。但是每次都需要发布到测试环境才能看到效果很麻烦,我们希望在自己本机就能看到调试的效果。Fiddler就提供了自动应答器这个功能,能让我们直接看到效果。
打开AutoResponder
面板,我们可以添加URL匹配规则,让请求的URL从本地返回文件而不是从服务器。
例如现在需要将线上地址http://xieyufei.com
替换为本地的一个HTML文件,首先勾选Enable rules
使所有的匹配规则生效,然后勾选Unmatched request passthrough
,让没有匹配到的规则通过(如果不勾选这个,打开其他网页会失败)。
然后点Add rules
来新增一个匹配规则,在x下面的Rule Editor
输入要替换的URL和本地文件的路径,然后Save就添加成功了。
虽然这样添加匹配成功了,但是产生了一个心得问题,由于是匹配URL,所以只要是URL中带有http://xieyufei.com
都会被替换掉,所以该域名下所有的脚本、样式以及子页面都会被替换,这样显然不利于我们调试。但是Fiddler提供了另外的四种匹配规则。
因此修改一下我们的匹配规则,改为EXACT:http://xieyufei.com
就可以了。还有一个小Tips:
AutoResponder
面板会直接生成匹配规则。Rule Editor
的第二个输入框旁边的小三角找到Find a file
可以选择文件路径。Test...
可以测试匹配规则。构造器composer用来创建一个HTTP请求然后发送到服务器。可以自己定义一个请求,也可以讲会话列表中拖拽一个已有的请求过来。
打开Composer面板,第一个就是Parsed选项卡,在表单中我们输入一个HTTP请求,比如对baidu.com发送一个请求。点击Execute
按钮,这个请求就发送出去了。这时候在会话列表中就多了一次请求。
第二个选项卡是Raw,也是原始请求,如果熟悉HTTP请求,可以直接手动输入。
第三个选项卡是ScratchPad,可以同时保存多条原始请求,然后选择性的发送。高亮选中你要发送的请求,然后点击Execute就发送出去了。
几个选项说明:
有时候请求刷新一个页面会有很多的请求,看得眼花缭乱,但是绝大多数请求可能并不是我们想要的,这时候我们就需要对请求进行一些过滤。
在Zone Filter
中有三个选项,分别过滤以下请求:
在Host Filter
中有四个选项,分别过滤以下请求
命令行QuickExec允许我们快速的执行一些脚本命令。
select命令用来选择所有类型为指定类型的HTTP请求,即根据请求的content-type来选择所有同一类型的。常用的select css
选择所有的样式请求,select image
选择所有的图片请求。
allbut命令用于清除除了指定类型之外的其他HTTP请求,仅保留指定类型。例如allbut image
仅保留图片的请求。如果跟一个不存在的类型,执行效果和csl,命令相同,清除所有的请求。
当你在问号后输入一些文本的时候,Fiddler会高亮URL中带有这些文本的所有请求。
大于号小于号命令选择响应主体的大小大于(或者小于)指定大小。
等号命令用于选择状态码等于指定状态码或者指定请求方法的会话。
选择包含指定HOST的全部请求。
如果以后的请求的URL中带有指定的字符串,那么将会被加粗显示。bold /bar.aspx
表示加粗URL带有bar.aspx。再次执行bold
会清除加粗。
这几个命令用于批量设置断点。
清除请求列表。
虽然bpafter和bpu都是用于中断URL包含指定字符的全部会话,但是打断点的时间是不一样的。bpu是在浏览器发送请求的时候进行断点,可以对请求参数进行修改;而bpafter是在服务器响应的后进行断点,可以对响应结果进行修改。
我们使用用express模拟简单的ajax请求。
1 |
|
我们输入命令 bpu /test
,然后Fiddler就会进入等待。在浏览器中输入网址,这时候浏览器就会进入等待的状态。在会话列表中选择进入断点状态的请求,然后修改请求参数,修改完后点击Run to Completion
结束断点。这时候,浏览器页面也结束等待,出现修改后的结果。
调试完后我们不需要Fiddler再进行断点,可以输入bpu
清除所有bpu的断点。
同样,我们输入命令 bpafter /test
,然后Fiddler就会进入等待。在浏览器中输入网址,这时候浏览器就会进入等待的状态。在会话列表中选择进入断点状态的请求,然后修改响应结果,修改完后点击Run to Completion
结束断点。这时候,浏览器页面也结束等待,出现修改后的结果。
调试完后我们不需要Fiddler再进行断点,可以输入bpafter
清除所有bpafter的断点。
AJAX是一种数据请求方式,不需要刷新整个页面就能够更新局部页面的数据。AJAX的技术核心是XMLHttpRequest对象,主要请求过程如下:
不说话直接贴代码
1 |
|
IE7及其以上版本中支持原生的 XHR 对象,因此可以直接用:var oAjax = new XMLHttpRequest();
。IE6及其之前的版本中,XHR对象是通过MSXML库中的一个ActiveXObject对象实现的。
通过xhr的open函数来连接服务器,主要接收三个参数:请求方式、请求地址和是否异步请求(一般都是异步请求)。请求方式有两种,GET和POST,GET是通过URL将数据提交到服务器的,POST则是通过将数据作为send方法的参数发送到服务器。
给xhr绑定状态改变函数onreadystatechange,主要用来检测xhr的readyState的变化,当异步发送成功后,readyState的数值会由0变成4,同时触发onreadystatechange事件。readyState的属性及对应状态如下:
在readystatechange事件中,先判断响应是否接收完成,然后判断服务器是否成功处理请求,xhr.status 是状态码,状态码以2开头的都是成功,304表示从缓存中获取,上面的代码在每次请求的时候都加入了随机数,所以不会从缓存中取值,故该状态不需判断。
如果还是用上面的XMLHttpRequest对象来发送需要跨域的请求,虽然调用了send函数,但是xhr的状态一直都是0,也不会触发onreadystatechange事件,这个时候就要用到JSONP的请求方式了。
JSONP(JSON with Padding) 是一种跨域请求方式。主要原理是利用了script标签可以跨域请求的特点,由其src
属性发送请求到服务器,服务器返回js代码,网页端接受响应,然后就直接执行了,这和通过script标签引用外部文件的原理是一样的。
JSONP由两部分组成:回调函数和数据,回调函数一般是由网页端控制,作为参数发往服务器端,服务器端把该函数和数据拼成字符串返回。
比如网页端创建一个script标签,并给其src赋值为 http://www.test.com/json/?callback=process, 此时网页端就发起一个请求。服务端将要返回的数据拼作为函数的参数传入,服务端返回的数据格式类似”process({‘name:’xieyufei’})”,网页端接收到了响应值,因为请求者是 script,所以相当于直接调用process方法,并且传入了一个参数。
不说话直接贴代码。
1 |
|
给script标签设置src属性时浏览器就会去发送请求,但是只能发送一次请求,导致script标签不能复用,因此每次操作完都需要把script标签移除。在浏览器发送请求之前给全局绑定一个回调函数,当数据请求成功时就会调用这个回调函数。
将两种发送异步数据的方式整合起来,根据dataType来进行判断选用哪种方式。贴上完整的代码
1 |
|
我们先使用构造函数创建一个对象:
1 |
|
在这个例子中,Person就是一个构造函数,我们使用new创建了一个实例对象person。
很简单吧,接下来进入正题:
每个函数都有一个prototype属性,就是我们经常在各种例子中看到的那个prototype,比如:
1 |
|
那这个函数的prototype属性到底指向的是什么呢?是这个函数的原型吗?
其实,函数的prototype属性指向了一个对象,这个对象正是调用该构造函数而创建的实例的原型,也就是这个例子中的person1和person2的原型。
那么什么是原型呢?你可以这样理解:每一个JavaScript对象(null除外)在创建的时候就会与之关联另一个对象,这个对象就是我们所说的原型,每一个对象都会从原型”继承”属性。
让我们用一张图表示构造函数和实例原型之间的关系:
在这张图中我们用Object.prototype表示实例原型
那么我们该怎么表示实例与实例原型,也就是person和Person.prototype之间的关系呢,这时候我们就要讲到第二个属性:
这是每一个JavaScript对象(除了null)都具有的一个属性,叫__proto__,这个属性会指向该对象的原型。
为了证明这一点,我们可以在火狐或者谷歌中输入:
1 |
|
于是我们更新下关系图:
既然实例对象和构造函数都可以指向原型,那么原型是否有属性指向构造函数或者实例呢?
指向实例倒是没有,因为一个构造函数可以生成多个实例,但是原型指向构造函数倒是有的,这就要讲到第三个属性:construcotr,每个原型都有一个constructor属性指向关联的构造函数
为了验证这一点,我们可以尝试:
1 |
|
所以再更新下关系图:
综上我们已经得出:
1 |
|
了解了构造函数、实例原型、和实例之间的关系,接下来我们讲讲实例和原型的关系:
当读取实例的属性时,如果找不到,就会查找与对象关联的原型中的属性,如果还查不到,就去找原型的原型,一直找到最顶层为止。
举个例子:
1 |
|
在这个例子中,我们设置了person的name属性,所以我们可以读取到为’name of this person’,当我们删除了person的name属性时,读取person.name,从person中找不到就会从person的原型也就是person.__proto__ == Person.prototype中查找,幸运的是我们找到了为’name’,但是万一还没有找到呢?原型的原型又是什么呢?
在前面,我们已经讲了原型也是一个对象,既然是对象,我们就可以用最原始的方式创建它,那就是
1 |
|
所以原型对象是通过Object构造函数生成的,结合之前所讲,实例的__proto__指向构造函数的prototype,所以我们再更新下关系图:
那Object.prototype的原型呢?
null,嗯,就是null,所以查到Object.prototype就可以停止查找了
所以最后一张关系图就是
顺便还要说一下,图中由相互关联的原型组成的链状结构就是原型链,也就是蓝色的这条线。
最后,补充和纠正本文中一些不严谨的地方:
首先是constructor,
1 |
|
当获取person.constructor时,其实person中并没有constructor属性,当不能读取到constructor属性时,会从person的原型也就是Person.prototype中读取,正好原型中有该属性,所以
1 |
|
其次是__proto__, 绝大部分浏览器都支持这个非标准的方法访问原型,然而它并不存在与Person.prototype中,实际上,它是来自于Object.prototype,与其说是一个属性,不如说是一个getter/setter,当使用obj.__proto__时,可以理解成返回了Object.getPrototypeOf(obj)
最后是关于继承,前面我们讲到“每一个对象都会从原型”继承”属性”,实际上,继承是一个十分具有迷惑性的说法,引用《你不知道的JavaScript》中的话,就是:继承意味着复制操作,然而JavaScript默认并不会复制对象的属性,相反,JavaScript只是在两个对象之间创建一个关联,这样,一个对象就可以通过委托访问另一个对象的属性和函数,所以与其叫继承,委托的说法反而更准确些。
]]> A-Frame是Mozilla发布的一个全新的开源框架,旨在帮助开发者开发在浏览器中运行的高性能响应式的VR体验。只需要在页面中引入aFrame.min.js
就能够集成支持VR页面所需要的组件了。
我们可以使用传统的JavaScript DOM API来操纵A-Frame场景来添加逻辑,行为和功能。同时,A-Frame是基于DOM的,现在一些流行的框架能够基于A-Frame工作,比如React、Vue、jQuery和Angular。
A-Frame是一个基于three.js的实体组件系统。在A-Frame里一切都是实体,我们插入组件,可以随意撰写外观,行为和功能集成。
A-Frame配备了多个组件,但由于A-Frame在其核心部分是完全可扩展的,社区已经为生态系统填充了许多组件,如物理,粒子系统,音频可视化和Leap Motion控件。这个生态系统是A-Frame的命脉。开发人员可以构建一个组件并发布它,然后其他人可以使用该组件并直接从HTML使用,甚至不必知道任何JavaScript。
可视化编辑器用于检查和编辑A框架场景的可视化工具。与浏览器的DOM检查器类似,您可以进入任何A-Frame场景,本地或Web上,然后点击ctrl+alt+i
键盘。
这将打开视觉检查器,我们可以在其中进行更改。可以在视觉上移动和放置物体,用组件的属性随意的挪动物体,或者围绕相机平移以查看场景的不同视图。
介绍了这么多,让我们来看一下A-Frame是如何来构造组件的。
一个场景是由a-scene创建的,是全景渲染的根对象,所有的元素都需要放在a-scene这个组件里。它会处理3D所需的所有设置:设置WebGL、画布、相机、灯光、渲染器、渲染循环以及开启及时的WebVR支持。
每一个场景都需要一个背景,a-sky
标签用来设置场景的背景,可以直接放置src为全景图片,或者直接渲染color值。
1 |
|
如果直接渲染了color值,那么整个背景就会变成该颜色;如果设置全景图片,可以左右移动来查看。效果链接戳这里。
我们通过a-box
标签来生成一个长方体,有一下几个重要的属性:
1 |
|
最后生成一个长1高1深1颜色为红色的长方体:
但是如果仅仅是红色的外观那么就太单调了。A-Frame允许我们给组件设置纹理图片,虽然可以直接给组件设置src属性,不过不推荐这种做法,推荐通过资源管理系统a-assets
。
一般在游戏等视觉体验丰富的场景中,由于有着大量的图片、模型、声音等资源,都会对这些资源进行一个预加载处理,确保在渲染的时候不会出现缺失的现象。
我们把这些资源放到a-assets
也是为了进行预加载。我们可以存放以下资源:
<a-asset-item>
:其他资产,如3D模型和材料<audio>
:声音文件<img>
:图像纹理<video>
:视频纹理我们通过给资源标志一个唯一的id,然后在组件的src中引用这个id来进行调用。
1 |
|
这样我们的长方体就变成了一个带有图案纹理的长方体。
我们可以通过使用a-light来改变场景的亮度。默认情况下,如果我们没有指定任何指示灯,A-Frame将添加环境光和定向光。如果A-Frame没有为我们添加灯,场景将是黑色的。一旦我们添加了我们自己的灯,默认的照明设置将被删除,并替换为我们的设置。
我们还会添加一个点光源,点光源就像灯泡; 我们可以将它们放在场景周围,点光源对实体的影响取决于它与实体的距离。
1 |
|
我们给环境一个黄色照明的光源,最后的效果是这样的。
我们可以使用A-Frame的内置动画系统<a-animation>
向盒子添加动画。我们可以将<a-animation>
元素作为实体的子代。让我们把盒子上下摆动来给场景添加一些动作。
1 |
|
一些属性说明:
在A-Frame中还可以添加文本组件<a-text>
。
1 |
|
最后添加文字的效果,效果链接戳这里。
圆筒原型是多功能的,可用于创建不同种类的形状:
1 |
|
用于创造一个椎体。
1 |
|
在A-Frame中也有DOM元素,通过querySelector()和querySelectorAll()方法来提供元素的遍历,查询,查找和选择。这个很像jQuery中的选择器。
如果我们想抓住一个元素,我们使用querySelector()返回那一个元素。比如我们来抓住场景元素:
1 |
|
如果元素具有ID,则可以使用ID选择器(即,#
1 |
|
如果我们要抓取一组元素,我们使用querySelector()哪个返回一个元素数组。我们可以查询元素名称、类名、属性名:
1 |
|
如果我们抓住了一组使用的实体querySelectorAll(),我们可以循环使用它们for。我们围绕场景中的每个元素循环遍历。
1 |
|
要创建一个实体,我们可以使用document.createElement。这将给我们一个空白的实体:
1 |
|
但是,在将实体附加到我们的场景之前,该实体将不会被初始化或者成为场景的一部分。
要向DOM添加实体,我们可以使用.appendChild(element)。具体来说,我们想把它添加到我们的场景中。我们抓住现场,创建实体,并将实体附加到我们的场景。
1 |
|
请注意,这appendChild()方法是浏览器中的异步操作。在实体完成附加到DOM之前,我们不能对实体执行许多操作(如调用.getAttribute())。如果我们需要查询刚被追加的实体上的一个属性,我们可以监听loaded该实体上的事件,或者将逻辑放在A-Frame组件中,以便一旦它被准备好就执行:
要从DOM中移除实体,因此从场景中删除一个实体,我们removeChild(element)从父元素调用。如果我们有一个实体,我们必须要用它的parent(parentNode)去除实体。
1 |
|
要更新组件,我们可以使用setAttribute()方法。更新组件需要几种形式。如果组件是单属性组件,则setAttribute其行为与通常情况相同:
1 |
|
但是如果是单属性,它可以处理该值的特殊解析。例如,position组件是单属性组件,但其属性类型解析器允许它占用一个对象:
1 |
|
要设置或替换多属性组件的组件数据,我们可以传递注册组件的属性名称,并将属性对象传递为value:
1 |
|
从DOM中删除属性或者分离组件,调用组件的remove生命周期方法。
1 |
|
在选择文件之前,我们需要对文件类型进行一些过滤的操作。
通过input:file来选择我们需要的文件类型,有两个属性值是我们需要的:
1 |
|
但是在开发时,我们习惯把accept设置为image/*
来过滤所有非图片的文件。虽然这种方式简单粗暴,但是在新版本的chrome中,会出现点击input之后,文件选择框弹出非常慢的问题。将accept="image/*"
改为指定的图片格式,比如指定几种常用格式,就能解决这个问题。
1 |
|
在input选取文件后,我们可以监听chang事件来获取所选取的文件。
1 |
|
这里获取到的this.files是一个FileList对象,也是一个类数组对象,可以通过this.files[index]来获取每一个文件。
这个数组对象中的每个对象有以下几个属性:
在获取到文件后,我们可以根据type和size对文件的类型和大小进行过滤匹配。匹配后需要对文件的内容进行读取。HTML5定义了一个FileReader对象用来读取文件。FileReader使用方式也非常简单,需要创建FileReader对象并调用方法。
1 |
|
FileReader的实例对象有4个方法,三个方法是用来读取文件的,还有一个方法用来中断读取。
方法 | 参数 | 描述 |
---|---|---|
abort | none | 中断读取 |
readAsBinaryString | file | 将文件读取为二进制码 |
readAsDataURL | file | 将文件读取为 DataURL |
readAsText | file, [encoding] | 将文件读取为文本 |
readAsText主要用来读取文本文件的内容,readAsDataURL用来读取文件并将其转为DataUrl格式。FileReader还有一系列完整的事件函数,用来捕获读取文件时的状态。
事件 | 描述 |
---|---|
onabort | 中断时触发 |
onerror | 出错时触发 |
onload | 文件读取成功完成时触发 |
onloadend | 读取完成触发,无论成功或失败 |
onloadstart | 读取开始时触发 |
onprogress | 读取中 |
文件一旦开始读取,无论成功或失败,实例的result
属性都会被填充。如果读取失败,则result
的值为 null,否则即是读取的结果,绝大多数的程序都会在成功读取文件的时候,抓取这个值。
1 |
|
通过readAsDataURL来读取图片变成DataUrl格式。将其字符串嵌入到页面中,我们可以看到读取后的图片,通过这种方式实现选取图片后的预览效果。
1 |
|
在通过readAsDataURL方法读取到文件的DataUrl后,我们可以将这么长的字符串直接放到ajax中作为string类型发送到后台解析(建议使用post方式)。不过会有一定的局限性,就是如果文件很大,服务器可能会拒绝接受这么长的字符串。
FormData对象是HTML5新增的一个对象,目前一些主流的浏览器都已经兼容了,我们可以通过FormData来向服务器传递数据。
1 |
|
但是如果这样直接把FormData对象作为data数据来发送,浏览器会报一个非法调用的错误。
在发送FormData对象时,还需要给ajax加上另外两个属性:
1 |
|
jQuery在发送异步请求的时候会自动将data数据进行序列化处理,转化成key/value格式的字符串,加上processData:false
说明禁止对数据进行序列化处理。contentType用来指定发送至服务器时的内容编码类型。
如果每个表单数据都需要使用append来添加就比较麻烦,FormData还支持直接从html中的表单生成数据,就是在html页面中已经有数据了,然后FormData可以直接把这个表单的数据写入这个对象,然后直接提交给后台。
1 |
|
我们定义了一个form表单,有text类型和file类型的input。
1 |
|
FormData还支持异步的上传文件,以前我们上传文件,需要写一个表单直接刷新提交,现在可以使用FormData,在构造这个对象的时候,把表单的对象,作为一个参数放进去,就可以了,然后FormData,就会得到这个表单对象里面的所有的参数,甚至我们在表单中,都不需要声明enctype ="multipart/form-data"
,就可以直接提交。
使用FormData的优点,第一是在提交表单的时候,不需要写大量的js来获得表单数据,直接把表单对象构造就行了。第二就是可以直接异步上传文件。
Promise的链式调用虽然方便我们不用再写恶心的嵌套回调,但是有一个问题,就是如果第一个异步没有发送成功,进入了reject函数,后面的链式调用的resolve函数的data都是undefined,对后面的then调用造成了很大的问题。
在每个then方法中对data进行非空判断。
如果当前的Promise进入reject函数,对后面的Promise都进行abort操作,该方法适用于jQuery的Promise操作。
1 |
|
chrome保存密码并且自动填充的功能确实能够方便我们在浏览网站的时候登录进去,但是有时候chrome会莫名其妙的抽风,在我们不想要填充的地方自动给input填充上账号,为了不让chrome自动填充,我们采用下面的方式禁止自动填充:
1 |
|
我们在我们需要的input#pwd上面加一个display:none隐藏的div,然后给input#pwd加上一个autocomplete=”off”的属性,这样,这个input#pwd就不会自动填充了。
刚开始,笔者在页面上用jQuery的$.post方法发送一个请求给服务器,然后服务器根据这个参数再生成相应的一个文件流返回给客户端。但是,在$.post方法的回调函数中,只能处理xml, json, script, or html类型,对返回的文件流却没办法弹出对话框让用户下载了。经过百度,看到了很多人采用隐藏form提交的方式,再用response来推就可以。
1 |
|
这种方法发出的请求格式类似于username=username&password=password. 代码中的name就是请求中的key,代码中的value就是请求数据中的value
]]> 在开始写代码前首先让我们来构思一下整体游戏的实现过程:
首先既然是贪吃蛇,那么游戏中肯定要涉及到两个对象,一个是蛇的对象,另一个是食物的对象。食物对象肯定要有一个属性就是食物的坐标点,蛇对象有一个属性是一个数组,用来存放蛇身体所有的坐标点。
另外全局需要有一个定时器来周期性的移动蛇的身体。由于蛇的身体弯弯曲曲有各种不同的形状,因此我们只处理蛇的头部和尾部,每次移动都根据移动的方向的不同来添加新的头部,再把尾部擦去,看起来就像蛇在向前爬行一样。
由于蛇有移动的方向,因此我们也需要在全局定义一个方向对象,对象中有上下左右所代表的值。同时,在蛇对象的属性中我们也需要定义一个方向属性,用来表示当前蛇所移动的方向。
在蛇向前爬行的过程中,会遇到三种不同的情况,需要进行不同的判断检测。第一种情况是吃到了食物,这时候就需要向蛇的数组中添加食物的坐标点;第二种情况是碰到了自己的身体,第三种是碰到了边界,这两种情况都导致游戏结束;如果不是上面的三种情况,蛇就可以正常的移动。
整体构思有了,下面就开始写代码了。
首先整个游戏需要一个搭建活动的场景,我们通过一个表格布局来作为整个游戏的背景。
1 |
|
pannel就是我们的幕布,我们在这个里面用td标签来画上一个个的“像素点”。我们用两种样式来表现不同的对象,.body
表示蛇的身体的样式,.food
表示食物的样式。
1 |
|
我们定义了一个全局的settings用来存放全局性的变量,比如幕布的大小、蛇移动的速度和工作的线程。然后通过一个函数把幕布画了出来,最后的效果就是这样:
既然我们的“舞台”已经搭建完了,怎么来定义我们“演员”的位置和移动的方向呢。首先定义一个全局的方向变量,对应的数值就是我们的上下左右方向键所代表的keyCode。
1 |
|
我们在上面画幕布的时候通过两次遍历画出了一个类似于中学里学的坐标系,有X轴和Y轴。如果每次都用{x:x,y:y}
来表示会很(mei)麻(bi)烦(ge),我们可以定义一个坐标点对象。
1 |
|
既然定义好了坐标点对象,那么可以先来看一下简单的对象,就是我们的食物(Food)对象,上面说了,它有一个重要的属性就是它的坐标点。
1 |
|
既然食物有了坐标点这个属性,那么我们什么时候给他赋值呢?我们知道Food是随机产生的,因此我们定义了一个Create函数用来产生Food的坐标点。但是产生的坐标点又不能在蛇的身体上,所以通过一个while循环来产生坐标点,如果坐标点正确了,就终止循环。此外为了方便我们统一处理坐标点的样式,因此定义了一个handleDot函数。
终于到了我们的主咖,蛇。首先定义一下蛇基本的属性,最重要的肯定是蛇的body属性,每次移动时,都需要对这个数组进行一些操作。其次是蛇的方向,我们给它一个默认向下的方向。然后是食物,在蛇的构造函数中我们传入食物对象,在后续移动时需要判断是否吃到食物。
1 |
|
下面对蛇移动的过程进行处理,由于我们每次都采用添头去尾
的方式移动,因此我们每次只需要关注蛇的头和尾。我们约定数组的第一个元素是头,最后一个元素是尾。
1 |
|
这样我们对蛇身数组就处理完了。但是我们还需要对新的头(newHead)进行一些碰撞检测,判断新头部的位置上是否有其他东西(碰撞检测)。
1 |
|
因此我们需要对Move函数进行一些扩充:
1 |
|
因为在Move函数处理数组的后我们的蛇身还没有重新绘制,因此我们很巧妙地判断如果是吃到食物的情况,在数组中就把原来的尾部添加上,这样就达到了吃食物的效果。同时我们定义一个rePaint函数进行页面的重绘。
我们的“幕布”、“演员”和“动作指导”都已经到位,那么,我们现在就需要一个“摄影机”进行拍摄,让它们都开始“干活”。
1 |
|
我们给document绑定一个keydown事件,当触发按键时改变蛇的移动方向,但是如果和当前蛇移动方向相反时就直接return。最后的效果如下:
可以戳这里查看实现效果
实现了贪吃蛇的一些基本功能,比如移动、吃点、控制速度等,页面也比较的简单,就一个table、select和button。后期可以添加一些其他的功能,比如有计分、关卡等,也可以添加多个点,有的点吃完直接GameOver等等。
]]>doT模板引擎是一个最快速最简洁的JavaScript模板引擎,在浏览器端和Nodejs端都适用。它小巧快速并且没有任何依赖,所有代码才一百多行,压缩后才4k,非常的轻量。
在doT文件中有一个templateSettings属性用来配置doT的定界符(官方文档这么称呼,我们可以理解为模板的语法),我们也可以手动修改使用自己的定界符,但是建议使用默认的:
1 |
|
在配置中有一个属性是varname
,它的值是it
,代表了在模板中传入对象所使用的变量名。
首先介绍一下doT模板中常用的定界符代表的使用和含义:
首先定义要赋值的模板,注意模板的type要写成text/x-dot-template。
1 |
|
在这里我们使用了的赋值定界符,用于在模板中进行赋值操作。这里使用的it就是我们在上面配置中定义好的varname变量。然后在JS中调用模板渲染到页面上去:
1 |
|
在这里我们获取到了模板函数doTemplate,然后将定义好的对象传入函数中,最后返回我们所需要的字符串插入到DOM中。这里我们对doTemplate函数进行了一次复用,定义了两个属性相同字面量传入。
如果传入到模板中的是一个对象,我们还可以通过求值定界符遍历输入对象中的属性:
1 |
|
在求值定界符中,我们可以写类似于js的语法。
1 |
|
有时候我们需要遍历对象中的数组,通过迭代定界符来遍历。但是要在但是需要在后面加上:value:index
表示数组中的每个元素和索引值。
1 |
|
在这里我们传入的是一个对象,所以需要用~it.array
来遍历我们的数组。
1 |
|
我们可以直接传入一个数组,遍历的时候就需要用~it
直接来遍历数组值。
在模板中有时候我们需要对数据进行判断,进行不同的展示,这时我们就需要用到条件定界符。
1 |
|
条件模板前后都用单问号包裹,中间的双问号表示else。
1 |
|
对于条件判断,我们还可以使用求值定界符,对上面的进行如下改写:
1 |
|
在1.0.0之后的版本,doT加入的局部模板的功能,我们在模板中还可以定义一个局部模板。在主模板和子模板中我们都可以通过it变量引用到传入的对象。
1 |
|
首先通过两个#定义一个编译的需要引入局部模板的主模板def.father,在它里面定义了两个子模板def.child1和def.child2,然后通过一个#在编译时输入我们的主模板。如果没有这段输入代码,那么最后在编译时doT不会帮我们输出主模板的。
1 |
|
在JS中我们首先定义需要传入的数据,然后定义一个子模板的对象。这个对象中包含了我们在模板中定义的两个子模板的名称和内容,在子模板内容中,我们还是通过it变量引用到传入的对象。然后在生成模板函数时我们将子模板的对象一起传给template。
]]>这是最原始的 JavaScript 文件加载方式,如果把每一个文件看做是一个模块,那么他们的接口通常是暴露在全局作用域下,也就是定义在 window 对象中,不同模块的接口调用都是一个作用域中,一些复杂的框架,会使用命名空间的概念来组织这些模块的接口。
缺点:
1、污染全局作用域
2、开发人员必须主观解决模块和代码库的依赖关系
3、文件只能按照script标签的书写顺序进行加载
4、在大型项目中各种资源难以管理,长期积累的问题导致代码库混乱不堪
该规范的核心思想是允许模块通过require方法来同步加载所要依赖的其他模块,然后通过exports
或module.exports
来导出需要暴露的接口。
1 |
|
优点:
1、简单并容易使用
2、服务器端模块便于重用
缺点:
1、同步的模块加载方式不适合在浏览器环境中,同步意味着阻塞加载,浏览器资源是异步加载的
2、不能非阻塞的并行加载多个模块
1、exports 是指向的 module.exports 的引用
2、module.exports 初始值为一个空对象 {},所以 exports 初始值也是 {}
3、require() 返回的是 module.exports 而不是 exports
exports示例:
1 |
|
module.exports示例:
1 |
|
错误的情况:
1 |
|
其实是对 exports 进行了覆盖,也就是说 exports 指向了一块新的内存(内容为一个计算圆面积的函数),也就是说 exports 和 module.exports 不再指向同一块内存,也就是说此时 exports 和 module.exports 毫无联系,也就是说 module.exports 指向的那块内存并没有做任何改变,仍然为一个空对象{},也就是说area.js导出了一个空对象,所以我们在 app.js 中调用 area(4) 会报 TypeError: object is not a function 的错误。
总结:当我们想让模块导出的是一个对象时, exports 和 module.exports 均可使用(但 exports 也不能重新覆盖为一个新的对象),而当我们想导出非对象接口时,就必须也只能覆盖 module.exports 。
由于浏览器端的模块不能采用同步的方式加载,会影响后续模块的加载执行,因此AMD(Asynchronous Module Definition异步模块定义)规范诞生了。
AMD标准中定义了以下两个API
1、require([module], callback);
2、define(id, [depends], callback);
require接口用来加载一系列模块,define接口用来定义并暴露一个模块。
示例:
1 |
|
优点:
1、适合在浏览器环境中异步加载模块
2、可以并行加载多个模块
缺点:
1、提高了开发成本,代码的阅读和书写比较困难,模块定义方式的语义不顺畅
2、不符合通用的模块化思维方式,是一种妥协的实现
CMD(Common Module Definition)规范和AMD很相似,尽量保持简单,并与CommonJS和Node.js的 Modules 规范保持了很大的兼容性。在CMD规范中,一个模块就是一个文件。
示例:
1 |
|
优点:
1、依赖就近,延迟执行
2、可以很容易在 Node.js 中运行
缺点:
1、依赖 SPM 打包,模块的加载逻辑偏重
AMD和CMD起来很相似,但是还是有一些细微的差别,让我们来看一下他们的区别在哪里:
1、对于依赖的模块,AMD是提前执行,CMD是延迟执行。
2、AMD推崇依赖前置;CMD推崇依赖就近,只有在用到某个模块的时候再去require。看代码:
1 |
|
3、AMD 的 API 默认是一个当多个用,CMD 的 API 严格区分,推崇职责单一。
EcmaScript6标准增加了JavaScript语言层面的模块体系定义。ES6 模块的设计思想,是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。CommonJS和AMD模块,都只能在运行时确定这些东西。
在 ES6 中,我们使用export关键字来导出模块,使用import关键字引用模块。需要说明的是,ES6的这套标准和目前的标准没有直接关系,目前也很少有JS引擎能直接支持。因此Babel的做法实际上是将不被支持的import翻译成目前已被支持的require。
尽管目前使用import和require的区别不大(本质上是一回事),但依然强烈推荐使用import关键字,因为一旦JS引擎能够解析ES6的import关键字,整个实现方式就会和目前发生比较大的变化。如果目前就开始使用import关键字,将来代码的改动会非常小。
示例:
1 |
|
优点:
1、容易进行静态分析
2、面向未来的 EcmaScript 标准
优点:
1、原生浏览器端还没有实现该标准
2、全新的命令字,新版的 Node.js才支持
在jQuery中,首先要通过Deferred方法获取到Deferred对象,让我们来打印出来看看它有什么方法。
1 |
|
最后输出如下:
我们可以看到$.Deferred()返回是一个对象(Deferred对象),也有resolve、then、reject等一些我们熟悉的方法,让我们来看看它是怎么用的。
1 |
|
感觉跟ES6中Promise的用法很相似。我们首先通过$.Deferred()获取到了Deferred对象,然后在异步成功后返回数据,然后在then方法中对异步数据进行处理。
但是跟ES6中不一样的是,异步没有放到Promise的构造函数中,在异步成功后,调用了Deferred对象的resolve方法。then方法处理回调数据还是一样的。
既然Deferred对象上有resolve()方法,那么是不是在外部就能够调用resolve()方法就能够修改Promise的状态呢。把上面的代码进行如下改写:
1 |
|
输出结果如下:
可以看到我们在函数的外面调用了resolve()方法提前让异步结束并且返回了数据。这样Promise的状态就能够被随意的改变,肯定是不行的。
将代码进行如下改进,在返回的对象上多加一个promise()方法:
1 |
|
输出结果如下:
这时候,如果在函数的外部调用resolve()方法会报错,告诉我们resolve()方法不存在,异步也会“如期”执行完成。如果我们将def对象打印出来看的话会发现并没有resolve()方法。
和ES6中的Promise相似,then也支持接收两个参数,分别是执行成功的回调和执行失败的回调。
1 |
|
除此之外,jQuery还新增了两个函数,done()和fail()分别用来指定成功的回调和失败的回调。因此,上面的代码和下面的代码是等价的:
1 |
|
既然是Promise,那么then()肯定也支持链式调用的,这边也不在赘述,跟ES6中是一样的用法,不太熟悉的可以戳这边《对程序员的一个Promise(一)》
jQuery中没有all和race方法,但是扩展了一些其他的方法。
always方法就是不管执行成功或者失败,都会执行的,有点类似ajax的complete方法。
1 |
|
when方法和ES6中的all方法功能一样,都是并行执行异步,所有异步执行完成后才执行回调函数。不过when方法是挂载在全局中的方法,而且,它接受的参数也是多个对象。
1 |
|
平时我们都是这么请求ajax异步的:
1 |
|
上面的代码,平时的工作中我们肯定也写了无数遍了,已经很熟悉了,但是这个ajax()方法返回的是什么呢,让我们打印出来看下:
难道是巧合么?我们看到了熟悉的always()、done()、fail()、promise()、then()等方法。没错,ajax返回的也是一个Deferred对象,既然是Deferred对象,那么肯定也支持链式调用了。
那么将ajax方法进行如下改写:
1 |
|
既然是Deferred对象,那么done()、fail()、$.when()这些方法也能使用,这里就不再赘述了。
]]> Promise在英文中的解释就是承诺,在爱情中时常用来表示比较罗曼蒂克的憧憬,但是在JS中没有这么浪漫,只是单纯地表示无论操作成功或者失败,一定会给出一个“反馈”。
就好比媳妇喊你去街上打酱油,最后只有两种可能性,一种可能性是你成功的打到了酱油,回来给她了;另一种可能性就是酱油卖光或者其他原因,然后你没有打到酱油,但是你还是会跑去跟你媳妇汇报,然后你媳妇就会在心里默默的想下面这张图:
咳咳,有点扯远了,那么首先让我们用console来揭开Promise的真正面目吧。
通过控制台打印出来,我们看到原来Promise其实是一个构造函数,它的构造函数上有resolve和reject等其他几个方法,原型上也有then、catch等方法。既然是构造函数,那么肯定是能够通过new来创建一个对象的。
介绍了Promise,那么来说一下它的两个特点吧。
对象的状态不受外界影响。Promise 对象代表一个异步操作,有三种状态:Pending(进行中)、Resolved(已完成)和 Rejected(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。
一旦状态改变,就不会再变,任何时候都可以得到这个结果。Promise 对象的状态改变,只有两种可能:从 Pending 变为 Resolved 和从 Pending 变为 Rejected。只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果。就算改变已经发生了,你再对 Promise 对象添加回调函数,也会立即得到这个结果。
说了这么多,相信笔者也迫不及待了,先来构建一个Promise看看吧。
1 |
|
看到这里读者肯定觉得跟以前比没有很大的变化,而且有很多的困惑,比如:
运行上面的代码,控制台就会打印”异步1执行完成”。我们只是构造了一个Promise的对象,并没有调用它里面的方法,就已经执行了,所以这就是为什么要将Promise放到函数中调用获取的原因。同时,使用一个函数返回对象更加符合函数式编程的思想。这边的resolve方法的作用就是将Promise对象从Pending状态置为Resolved状态。
1 |
|
getPromise1方法获取到的就是我们上面返回的Promise对象,直接调用then方法,表示在异步结束后调用此方法。它接受一个参数,是一个函数,这个函数默认会传入一个参数,这个参数就是我们在Promise构造函数中所调用的resolve所传入的异步数据。
感情绕了一大圈,其实就是把原有在异步完成后的业务逻辑单独抽离出一个方法么?其实Promise还能做更多。
如果这个时候来了一个需求,这个异步的数据不够,还需要发另外一个异步。如果按照以前的逻辑肯定是在then回调方法的继续来发异步,然后就陷入了恶(e)性(xin)的嵌套,如果业务逻辑很复杂,而且还需要发异步,那么这个函数里面代码也会越来越庞大,后期维护起来会非常的麻烦。但是Promise的出现拯救了这一切。
Promise的优势在于能够进行链式的调用,将原来嵌套调用转为线性调用。在then方法中继续返回一个新的Promise对象,然后能够继续调用then方法。
1 |
|
我们会看到每隔一秒、两秒、三秒就会打印一组“执行完成n和返回数据n”。在then方法中也可以不返回一个Promise对象,直接返回数据。将上面的代码如下改写:
1 |
|
最后可以看到,一秒之后打印了一次执行完成1和三次返回数据1。这样的then方法没有什么意义。
细心的读者可能发现了,在Promise的构造方法中还有一个reject方法还没有被用到。既然是异步,那么肯定有成功也有失败的时候,reject方法的作用是将Promise对象的状态置为Rejected状态,在then方法中执行失败情况的回调函数。
1 |
|
我们首先获取一个随机数,判断这个随机数是否是偶数,如果是偶数的话就进入成功的回调方法;如果失败了就进入失败的回调方法。在then方法中我们发现多传入了一个方法,第一个方法还是成功情况的回调,第二个方法就是失败情况的回调,可以不传,不传的话就默认只有成功的回调。
但是需要注意的是,如果不传失败的回调函数,但是同时你还调用了reject方法,这时候Promise内部会报错。
在Promise对象上还有一些其他方法。
在Promise的原型上还有一个catch方法,我们知道try/catch方法是用来捕捉异常的,Promise中的catch方法也可以做同样的事情。将上面的方法进行如下改写:
1 |
|
在执行Promise的回调方法时可能会进入到第一个成功的回调函数中也可能会进入到第二个失败的回调函数中,如果回调函数中有抛出异常,并不会因为这个异常而卡死程序,就会进入到catch方法中捕捉到异常。最终运行的效果有以下两种可能性:
Promise中还有一个all方法,all是全部的意思,因此我们能猜测,就是等所有的异步都执行完毕。all方法让Promise有并行执行异步的能力,等所有并行异步执行完成后才执行回调。
1 |
|
可以看到all接受一个Promise的数组,数组中是三个Promise对象。在第一个和第二个异步执行完成后都没有进入then方法,而是等最后一个最慢的异步执行完了才进入then方法。最终,所有异步操作的结果都通过then方法的参数以数组的形式传递进来。最终运行效果如下:
但是问题来了,如果多个异步中有一个异步执行失败了呢?如果这个异步失败是通过reject方法抛出的,那么此时其他Pending中的异步还是会继续去运行,但是这个时候就会提前进入then方法的第二个参数函数中去,这个函数的默认参数也只是这个失败异步reject所发送的数据(不一定还是数组),等其他异步执行完成也不会再去执行then方法了。
all方法执行的效果是等大家都结束了再运行
,但是race是赛跑、竞争的意思,那么就很明显了,就是谁跑的快就有肉吃
。将上面的代码进行改写:
1 |
|
这三个异步同样是并行执行的,但是race的then方法优先执行先完成的异步。第一个异步getPromise1先执行完,因此先进入then方法。此时getPromise2和getPromise3还没有执行完,还会继续执行,但是不会再去执行then方法了。最后执行结果如下:
本文中介绍的所有异步操作均以setTimeout作为例子,如果有不正确的地方欢迎指正。
]]>
gzip是GNUzip的缩写,是一个GUN自由软件的文件压缩程序。刚开始用于UNIX系统的文件压缩。熟悉Linux系统的读者应该了解有一种文件的后缀是.gz,这种文件就是GZIP格式的文件。现在GZIP已经成为网上使用非常普遍的一种数据压缩格式。
既然GIZP是一种数据压缩格式,那么它是如何在浏览器和服务器之间进行数据传输的呢。通过下面一张图片能够很清晰的看出它的工作方式:
笔者模拟了一个GZIP数据请求:
可以看到Request Headers(请求头)中有一个Accept-Encoding属性,里面有一个gzip,表示浏览器能够接受的数据格式有:gzip、deflate、sdch和br。然后在Response Headers(响应头)中有一个Content-Encoding属性,表示返回的数据格式的编码,我们看到只有一种编码,就是gzip。
减小网络传输的数据量,提高网页响应速度。
经过在网上找了很多资料,找到目录下的conf/server.xml文件,将Connector
进行如下改写:
1 |
|
但是。。。当笔者配置完后,满心欢喜的打开服务器以为有效时,发现自己太天真了。很遗憾的是,脚本并没有被压缩。我以为是服务器没有配置成功,又多次重启,但是并没有用,网上查了很多资料,也没有解决。
笔者发现同样是CSS却是有效的,为什么脚本不行呢?但是脚本占了网络流量的很大一部分。
当笔者再次仔细查看Response Headers的时候,发现Content-Type
这个属性竟然是application/javascript
,但是在我们的代码中配置的却是text/javascript
,会不会是这个问题呢。
笔者抱着试一下的态度,在compressableMimeType
属性中添加了application/javascript
。最后再次启动服务器,OK,问题解决了。
Nodejs中笔者常用的库就是express,就用express作为demo。如果你的express的4.0以下的版本,只需要在代码中添加以下代码:
1 |
|
在express 4.0以上的版本中不再集成GZIP功能,需要单独安装中间件,先通过npm i compression
安装compression,然后在项目中引用:
1 |
|
修改nginx安装目录下的conf/nginx.conf配置文件,添加以下代码:
1 |
|
上一篇文章中,我们用到了一种查询数据库的最基本的方法:connection.query(sqlString, callback)
。
第一个参数是一个SQL语句,可以是任意的数据库语句,而第二个参数是一个回调函数,查询结果通过回调参数的方式返回。
1 |
|
这是最简单的查询方式,但是存在着两个问题,一个是需要拼接字符串,比较繁琐;另一个是容易被sql注入攻击,因此我们有了第二种查询方式。
第二种查询方式是采用了占位符的形式connection.query(sqlString, values, callback)
,这样就不需要进行恶心的字符串的拼接了。
1 |
|
第三种查询方式我们将查询语句和查询值组合成一个对象来进行查询。它的形式是这样的:connection.query(object, callback)
。
1 |
|
将第二种和第三种方式可以结合起来使用,查询值作为query方法的一个参数,而不是作为对象中的一个属性。
1 |
|
需要注意的是,如果我们既将查询值作为对象的属性,又将其作为query函数的参数,这个时候函数中的参数将会覆盖对象的属性,也就是说此时只有参数的值生效。
在进行数据库查询时,有一个重要的原则就是永远不要相信用户的输入
。为什么不能相信用户的输入呢,首先让我们来了解一下SQL注入攻击。
所谓的SQL注入攻击,就是通过把SQL命令插入到Web表单递交或输入域名或页面请求的查询字符串,最终达到欺骗服务器执行恶意的SQL命令。由于笔者并不是从事数据库方面的工作,也不是数据库方面的砖家,所以只能通过一个简单的DEMO来演示一下SQL注入攻击。
假如我们拼接的SQL语句是这样的
1 |
|
这里我们期待用户输入的username是Jack或者LiLi之类的,但是用户说我偏不,我就输入一串恶意代码:
1 |
|
最后我们拼接的查询语句就变成了下面这样的:
1 |
|
如果读者对SQL语句有一些基本了解,就会知道如果把这段查询语句放到数据库中进行查询,那么所有用户的信息都被查出来了,但是这并不是我们想要看到的结果。
那么怎么才能避免SQL注入攻击呢?mysql提供了两种方法给我们,第一种方法就是每次查询时都把用户输入的数据都用escape()函数解析一下,有点类似预处理语句。
1 |
|
第二种方法就是在查询时通过上面说到的占位符注入查询
的查询方式来进行查询。但它内部的实现也是通过上面所说的escape()方法将用户输入解析了一下。推荐使用第二种方法来得简单快捷。
mysql还支持多语句查询,但是由于某些安全原因(官方解释是因为如果值没有正确解析会导致SQL注入攻击)默认是被禁止的。那么让我们来打开这个“潘多拉魔盒”把。
在创建数据库连接时首先把这个功能开启。
1 |
|
然后我们就可以使用多语句查询了。
1 |
|
通过查询语句返回的结果以数组的形式返回,如果是单语句查询,数组就是一个纯对象数组[obj1,obj2,...]
,数组中的每一个对象都是数据库中每一行的数据,只是以对象的方式返回。如果没有查询到数据,那么数组的长度就为0。
但是如果是多语句(m条语句)的方式查询,虽然返回也是一个数组,但是数组中嵌套有n个数组,n的取值取决于你查询语句的条数m(即n=m)。
由于官方文档比较零碎,因此整理得不是很到位,有问题的地方希望大家指正。
]]> 首先说来介绍一下MySQL(非广告)。MySQL是由瑞典的MySQL AB公司开发,后来被甲骨文公司收购。和Oracle一样,MySQL是一个典型的关系型数据库,在百度百科中,把MySQL称为是最好的关系数据库管理系统的之一。
说到关系型数据库,大家肯定就会想到另一个词与之对应,非关系型数据库,那么这两者有什么样的区别呢?
关系型数据库是指采用了关系模型(指的是二维表格模型)来组织数据的数据库,有稳定的表结构;而非关系型数据库中的数据没有关系模型,以对象的形式存放到数据库中,对象之间的关系是通过每个对象的属性来决定的,有点类似于一长串json对象。典型的非关系型数据库有MongoDB和Redis。
我在项目中使用MySQL作为数据库主要是因为它体积小,速度快,安装完才几百兆,相比于Oracle好几个G它确实“轻”了不少。而且核心程序采用多线程编程,线程也是轻量级的进程,不会占用太多的系统资源,因此一般的中小型网站都选择MySQL数据库,而且最重要的是MySQL几乎是免费的。
但是也正是由于它的轻量级,因此它也“砍掉”了一些功能,比如存储过程等。
这边不再赘述MySQL的安装过程,有需要的读者可以自行百度安装教程。在我们的项目中通过npm install mysql --save
来安装依赖。
首先,通过一个小的Demo来测试我们的环境是否已经搭建完毕了:
1 |
|
运行程序,如果显示“The solution is: 2”,那么整个连接查询是成功的;如果不成功,读者可以根据打印的错误信息提示来修改。
在查询完数据库后,需要通过end()函数将连接关闭。如果连接一直打开,首先会浪费不必要的系统资源;其次,数据库的连接数量有限制,如果达到上限时,会出现后续连接不上报错的情况。
要想查询数据库,首先就要跟数据库建立连接,上面的Demo给出了一种建立连接的方式。官方文档还给出了另外两种建立连接的方式。
1 |
|
我们并没有像Demo中一样使用connect()函数建立连接,而且直接进行了查询,这时候建立连接将会被隐式地调用。
上面两种连接方式并没有对连接出错的情况进行处理,一旦连接出现错误将带来连锁的多米诺骨牌效应,查询也将会失败,整个程序也会崩溃,为了避免出现这样的情况,我们将查询和关闭连接放到回调函数中。
1 |
|
注:上面的三种建立连接的方式都是可以的,取决于笔者怎么处理连接错误。
打开了数据库的连接我们也需要关闭连接,有两种关闭连接的方式,一种就是我们上面用的end()方法来关闭连接,它可以接收一个回调函数。
1 |
|
通过end()函数关闭连接不会影响队列中的查询。还有一种方式是调用destroy()函数。
1 |
|
destroy()函数确保了没有更多的时间和回调会触发连接。同时destroy()函数也没有回调函数。
数据库连接是一种关键的、有限的、昂贵的资源。 —百度百科
通过上面的数据库连接方式我们会发现直接创建一个数据库连接比较“危险”,因为有很多种可能性导致连接的失败。而且如果我们的程序中随意都可以和数据库建立连接的话,我们的程序就比较得混乱,不能很有效的管理数据库连接。mysql库提供了另一种数据库连接方式给我们。
数据库连接池负责分配、管理和释放数据库连接,它允许应用程序重复使用一个现有的数据库连接,而不是再重新建立一个。这项技术能明显提高对数据库操作的性能。
用一个很生动的例子来形容数据库连接池的工作:以前我们存取钱都需要去银行的柜台交易,银行的柜台数量是有限的,人多的时候还需要排队;现在我们把钱都存在了支付宝上,每次需要用钱的时候都直接跟支付宝“要”,不需要再跑到银行去了,所有和银行“打交道”的业务都交给了支付宝帮我们来管理。
数据库连接池在初始化的时候将一定数量(数量受最小连接数制约)的数据库连接存放到数据库连接池中,不管这些数据库连接是否被使用,连接池一直要存放这么多的连接数量。连接池的最大数据库连接数量限制了连接池最多能同时拥有的连接数,如果超过最大连接数时,请求将会被添加到等待队列中去。
下面就开始创建一个数据库连接池。
1 |
|
首先我们通过createPool()方法创建了一个数据库连接池,它的配置参数和上面的配置基本差不多,只是多了一个最大连接数。每次我们需要和数据库建立连接的时候不再是直接建立连接,而是去连接池中通过pool.getConnection()方法“捞取”已有的连接。这个方法有一个回调,数据库连接作为回调参数返回给我们使用。
每次查询完数据库是都要使用release()方法释放数据库连接,这样数据库连接又回到了连接池中。释放后如果再使用connection将会报错。
一般数据库连接池不需要关闭,但是如果使用完连接池需要将所有的连接关闭,我们可以使用pool.end()方法将其关闭。
1 |
|
end()方法提供一个回调方法,以便在所有连接关闭时进行一些操作。关闭连接池前所有队列中的查询任然会执行完成,所以每次关闭的时间都不一样。一旦end()方法被调用了,getConnection和其他一些获取连接池中连接的方法不会再被执行。
本篇文章主要学习了nodejs连接mysql数据库的一些两种连接方式,直接连接和通过数据库连接池的方式进行连接。直接创建连接的方式比较“危险”,推荐使用连接池,把所有的连接集中管理,既方便又安全。
]]>贾维斯
,协助钢铁侠处理各种事务、计算各种数据和信息,相当的方便,让我欣羡不已。于是我就想着有一个自己的机器人帮我处理事情,正好在网上看到GitHub的一个开源聊天机器人Hubot,学习着用了一下,虽然没有贾维斯那么狂拽酷眩,但是毕竟是属于自己的Hubot。 Hubot是GitHub的开源聊天机器人,前身主要是GitHub用来完成一些自动化的任务,比如部署站点、自动处理任务(别问我,笔者也不知道是什么任务)等。随着使用Hubot使用越来越频繁,它也变得更健壮更智能。为了帮助更多的人,GitHub将它重写并且开源。
Hubot采用CoffeeScript语言开发,这是一套类似于JavaScript的语言,但是更加的简洁,很容易就能够读懂。目前Hubot原生带有一些功能,比如搜索图片、翻译、地图服务,还可以自定义插件脚本,同时还能使用别人开发好的插件。Hubot插件库中大概有一百多个插件。
要运行Hubot,需要对nodejs和npm有一些了解,最好还装有redis服务。推荐使用Git工具进行以下操作。
下面就开始在电脑上安装我们的Hubot,默认读者们把nodejs和npm都安装好了。
由于官方支持使用CoffeeScript语言编写代码,所以先安装脚本编译器和Hubot的框架
1 |
|
接着创建项目目录,这里的目录名建议跟Hubot的名称保持一致,这里我的Hubot叫jarvis
1 |
|
在git命令行中输入如下命令,开始我们的安装过程。
1 |
|
出现了如下安装提示界面,输入我们的所有者、Hubot名称和描述等。
输好后会根据你输入的创建对应的配置文件,然后进行处理,并且安装一些模块文件等。等待安装完成就可以使用了。
安装完后启动Hubot,在命令行输入bin/hubot
,看到有报错:
出现这个报错是因为没有配置heroku和redis服务没有开启。可以忽略,也可以在external-script.json中把hubot-heroku-keepalive和hubot-redis-brain注释掉。
然后把/scripts目录下的example.coffee打开,将下面两行代码中的#
删除:
1 |
|
然后再次启动,这时候就能看到我们的jarvis正常启动。在命令行里输入jarvis ping
,如果能看到jarvis回复pong,Hubot就安装完成了。
首先查看一下我们可以使用哪些命令。在命令行里输入jarvis help
,可以看到所有可以使用的命令以及描述,比如map、pug、time、image命令等。在安装过其他插件后可以使用同样的命令查看新增加的命令。
需要注意的是,这里的jarvis help
中的jarvis是笔者的自定义机器人名称,读者需要替换成自己的机器人名称,比如myhubot help
。
Hubot是基于事件监听机制的,我们可以为他自定义事件发生时的回调,当触发这个事件时执行回调。
在项目目录下有一个/scripts目录,存放的就是我们自己定义的脚本,像之前的example.coffee就是一个小的demo。
我们新建一个greet.coffee,输入一下代码:
1 |
|
每个自定义的脚本都需要导出一个函数function,默认有一个robot的参数。这里的module.exports = (robot) ->就相当于JavaScript中的module.exports = function(rebot){}。
这里的robot的hear方法相当于一个监听事件,它有两个参数,第一个是一个正则表达式,只要匹配了这个正则表达式就执行下面的回调函数,回调函数中我们通过send方法返回一个字符串。
再次重启我们的Hubot,输入jarvis greet
,会看到Hubot跟我们打招呼了。
但是这时候使用help命令,没有看到可以使用greet命令的提示,怎么样将我们自定义的greet命令添加到help中去呢?如下修改我们的greet.coffee脚本,添加头部注释说明。
1 |
|
Hubot不仅能通过命令行监听命令,还能够通过路由监听地址。
我们新建一个文件router.coffee。
1 |
|
这时候我们打开浏览器输入http://localhost:8080/foo
就能看到输出bar
。这里的端口默认识8080,如果需要更改端口可以在启动Hubot的时候通过PORT=8888 bin/hubot
命令设置端口号
Hubot不仅可以静态的设置要回复的内容,还能够动态地通过异步数据返回对应的内容,比如这里我们通过Hubot来查询城市的天气。
我们新建一个weather.coffee脚本(需要申请openweathermap.org的APPID)。
1 |
|
process.env
允许我们设置一个环境变量,这里我们自定义了一个天气url接口的变量。在CoffeeScript中x ||=y
是x = (x != null) ? x : y
的简写方式,代表了如果x没有赋值,就取y的值,保证了x一定有值。
通过调用msg.robot.http()方法来发送异步请求,在回调方法中先解析成JSON格式,然后对数据进行拼接处理,再用msg返回。
再次启动Hubot,输入jarvis weather in Suzhou
就能查到对应城市的天气了。
有时候,我们输入的信息没有被任何脚本的正则捕获到,我们还是希望对这些信息进行处理,那么可以新建一个catchAll.coffee脚本。
1 |
|
虽然我们写了很多的脚本,但是有的功能已经有现成的,可以直接使用。
Hubot自带了几个插件,让我们来看一下。
这个插件有两个命令,一个是hubot pug me
,另一个是hubot pug bomb [number]
。
输入第一个命令看到出现一个链接,在浏览器中打开我们看到了一只可爱的哈巴狗,输入第二个链接,我们把[number]改成随意的一个数字6,我们看到出来6张照片,打开是6张哈巴狗的照片,没错,这个插件就是给你看狗狗的(难道是狗狗爱好者做的?)。
我顿时就忍不住吐槽了,居然还标榜自己是最重要的Hubot插件(Pugme is the most important hubot script),简直鸡肋啊。
这个插件有两个命令,一个是hubot image me
,另一个是hubot animate me
。
官网解释它是用来搜索图片地址的,那么就让我们用官网的例子来尝试一下吧jarvis image me bananas
。
很遗憾的是Hubot提醒我这个谷歌的图片搜索引擎不能用了,要设置自定义的引擎。再看了一下官方的文档,这个谷歌图片搜索引擎还要注册,并且每天只能免费搜索一百次,超过了还要收费,有点坑啊。
这个插件看样子是地图插件,看了一下官方说明,也有两个命令hubot map me
和hubot direction me
。抱着希望再次尝试了一下jarvis map me wuxi
,幸运的是结果出来了,是一个地图的地址,再看了一下是谷歌地图的地址,那不用说了,绝壁要科学上网了。
自带的插件用完了,笔者顿时累觉不爱了,想着去网上找找有没有其他的插件。
笔者在网上看到能够使用npm search hubot-scripts github
命令查看Hubot的插件,但是试了一下老是报错,求好心人告知。
于是又找到了Hubot在线插件库的链接,进去看到确实有不少插件。详细的插件用法本文不再赘述,读者可以自行根据需要下载使用,使用前需要把插件名称添加到/external-scripts.json
文件中去
经过这几天对Hubot的学习,对Hubot我有以下几点感悟:
随着网页内容的越来越丰富,在我们的网页上我们经常要用到很多的脚本文件,比如幻灯模块的脚本、列表模块的脚本和搜索模块的脚本等等。如果不对这些文件进行统一的打包,整个页面就会非常的凌乱。
于是,webpack就诞生了,我们可以把它想象成一台洗衣机+烘干机+叠衣机(据说岛国已经发明类似的机器人了),我们可以把杂七杂八的衣服、裤子、袜子等等都丢进去,然后它就会帮我们洗干净、烘干、叠整齐了,一系列工作全自动完成,不需要我们亲自动手,怎么样,是不是很心动。
借用webpack官网的一张图来解释一下webpack的工作原理。左边就是我们杂乱的页面资源,有脚本文件、样式文件、图片文件等等,各种文件之间互相引用。经过webpack的打包整理,生成静态文件。
webpack的工作方式是:通过一个配置文件找到入口文件,从这个入口文件找到你项目依赖的所有资源文件,使用对应的资源加载器(loaders)来处理这些资源文件,最后打包成静态文件。
使用webpack之前需要安装webpack,在这里我们需要在两个地方安装:全局目录和项目目录,在项目目录下执行以下命令:
1 |
|
在开始上手项目之前首先来搭建我们的目录结构。
我们可以把项目目录搭建成如下,当然只是给大家做一个参考而已:
1 |
|
package.json使用npm init
命令可以自动生成,在这里不过多的阐述;build目录主要是webpack构建的产物,自动生成;这里的核心文件就是我们的webpack.config.js文件,需要自己手动编写。
webpack.config.js
配置文件通过exports导出一个对象,这个对象中有三个模块比较重要:entry、output和module,具体如下:
1 |
|
entry属性是页面的主入口,所有页面的文件都在这个入口文件中进行引用。
当然,一个项目肯定有不止一个页面,需要多个入口,entry属性可以这样配置:
1 |
|
在webpack打包之后,生成的js文件、css文件、图片文件等等就会放到output属性所指定的文件目录下。
path属性就是所在文件夹的路径,需要使用绝对路径,这里用path的resolve方法进行解析;filename属性指定了输出的文件名,[name]表示入口的属性名叫什么就输出对应的文件,比如这里的index输入的文件名就是index.bundle.js。
publicPath是以http方式请求的静态资源的路径,webpack-dev-server(webpack的一个插件)会根据你请求的url来匹配这个publicPath下的文件。
module属性主要存放解析资源文件的各个加载器,每一个对象表示了一个加载器。
test属性表示正则匹配,用来匹配文件的后缀名;loader属性表示如果文件相匹配,则调用对应的加载器来解析文件。比如样式加载器有css-loader、sass-loader。
加载器的后缀都是-loader
,在loader属性中配置加载器不用写后缀,配置不同的加载器需要使用!
分隔并串联起来。
plugins属性是用来放webpack的插件,这个属性下面会用到
在/public/javascript/目录下,编写我们的入口文件index.js,我们的入口文件非常简单,就在页面上打印一句话:
1 |
|
还有我们的模块文件:
1 |
|
这样一个简单的webpack项目就完成了,通过在项目根目录使用webpack命令,在build文件中生成index.bundle.js文件就是我们的构建产物,在页面上直接引用这个js就能看到效果了
在页面上我们肯定会用到很多的样式文件,那么怎么在页面上使用呢?首先需要有对应的加载器,这里我们就要用到样式加载器。
首先安装我们的加载器:
1 |
|
然后改写配置文件中的加载器模块:
1 |
|
css-loader会遍历css文件,找到所有的url(…)并且处理。style-loader会把所有的样式插入到你页面的一个style标签中。
接下来就可以编写我们的样式文件了:
1 |
|
在index.js中我们添加对样式文件的引用:
1 |
|
在根目录我们再次执行webpack命令,再次生成构建的js文件就能看到页面上有颜色了。
有时候多个公共脚本中有公用的部分,如果多写就显得有点多余,我们可以利用webpack的提取公共部分的插件来帮助我们提取。
在plugins属性中添加如下代码:
1 |
|
再次执行webpack命令,我们在build文件夹里看到多出了一个common.js文件,这个就是提取出的公共部分。
通过style
标签引入样式可能会让页面的代码看起来非常的庞大非常的凌乱,有时候我们需要将所有的样式导出到一个独立的样式文件,然后通过link
标签引入样式文件。
这时候我们就需要用新的插件。通过npm install
命令安装extract-text-webpack-plugin插件。
1 |
|
安装完插件我们就需要在webpack.config.js
文件中进行配置了。
1 |
|
我们再使用webpack命令打包一下,看到build
文件夹中看到多了一个index.css文件,就是插件提取出来的所有页面样式,都在一个文件中。
ES6简洁的语法糖、性能的提升都让开发者对其深深的“迷恋”,下面来让我们的项目也来支持ES6的语法。
要想支持ES6首先要安装对应的解码器,es6的解码器就是babel,首先安装babel-loader
:
1 |
|
然后改写配置文件:
1 |
|
配置完成后我们就可以在js中尽情地使用es6的语法糖了。
在这里我们编写了webpack的配置文件,对配置文件的属性进行了详细地说明,也添加了webpack的几个插件。但是还是欠缺以下几点:
本文所涉及到的代码都在我的github仓库,需要的读者可以去fork,喜欢的给个赞啊亲。希望在后续的文章中对代码进行优化,添加更多的插件来丰富功能。
]]>WXML提供模板组件给我们使用,可以在模板定义公用的代码片段,然后在需要引用的地方进行调用。
定义模板使用name属性作为模板的名字,然后在template标签中定义代码片段:
1 |
|
使用模板我们用is属性引用定义好的模板,然后把模板所需要的值通过data属性传给模板。比如需要遍历persons数组,我们可以将整个persons作为对象传给模板,也可以遍历persons后将每个对象传给模板,具体取决于所应用的场景。
1 |
|
需要的数据结构如下
1 |
|
注意:
什么是事件呢,简单来说,事件就是逻辑层到逻辑层的通讯方式。就是在页面上通过触发某个操作(就是我们说的事件),在逻辑层进行一系列的操作,最终来改变数据。
比如在一个输入框中用户输入了一段文字,但是data中的数据并没有随之改变,因此我们需要在输入框上绑定对应的输入事件来更改数据。
事件也有分类,可以分为冒泡事件和非冒泡事件。“冒泡”这个词很形象的表现了事件向上传递的过程,这两种事件的区别也在于是否会向父节点进行传递。
一些常用的冒泡事件,除以下的事件外都是非冒泡事件:
名称 | 触发 |
---|---|
touchstart | 手指开始触摸 |
touchmove | 手指触摸后移动 |
touchend | 手指触摸动作结束 |
touchcancel | 触摸被打断,比如来点,弹框等 |
tap | 触摸后离开,有点像点击click |
longtap | 长按,超过350ms才离开 |
当事件函数被调用时,从逻辑层有一个默认的事件对象传到函数中,不同的事件所包含的事件对象的属性有所区别,一些常用的事件对象的属性如下:
属性 | 类型 | 说明 |
---|---|---|
type | String | 事件类型 |
timeStamp | Int | 从页面加载到事件触发的时间戳 |
target | Object | 触发事件的组件的一些属性值集合 |
currentTarget | Object | 当前组件的属性值集合 |
touches | Array | 触摸点信息的数组 |
detail | Object | 额外的信息 |
当不存在嵌套时,target和currentTarget没有区别。但是当嵌套触发事件是,current和currentTarget的区别就体现出来了。
1 |
|
点击组件B,当触发handle2事件时,收到target和currentTarget对象是一样,都指向组件B;而当点击组件B触发handle1事件时,target对象指向了组件B,currentTarget对象则组件A。总结一下:
在组件中定义数据,当触发事件时,这些数据通过事件对象传给逻辑层。书写规则:以“data-”开头,多个字符用“-”连接,不能含有大写,可以绑定多个data值。例如data-element-name,最终会在event.currentTarget.dataset中转为elementName属性,属性的值就是定义的数据。
小程序还提供发送异步的方法request(object),发起的是https请求。一个小程序,同时只有有5个网络请求链接。object的参数如下:
参数命 | 类型 | 说明 |
---|---|---|
url | Sring | 服务器接口地址 |
data | Object | 请求的参数 |
header | Object | 设置请求头header,header不能设置Referer |
method | String | 请求方式,默认GET |
success | Function | 请求成功的回调方法 |
fail | Function | 请求失败的回调方法 |
complete | Function | 请求完成的回调方法(请求成功、失败都会调用) |
跟jQuery不同的是,小程序请求的数据不是直接在success方法的res中(res是一个对象,还包括请求成功的状态码等),而是在res.data中。示例代码如下:
1 |
|
我们提供了一种新的开放能力,开发者可以快速的开发一个小程序。小程序可以在微信内被便捷地获取和传播,同时具有出色的使用体验。
要开始做项目,首先要安装微信小程序的开发工具。官网的版本比较旧,笔者分享一个下载链接,大家可以去下载,微信小程序开发工具下载,下载完直接安装后新建项目就可以使用了。当然,由于还处在内测阶段,腾讯只邀请了部分企业参与内侧,所以AppID数量很稀少,读者在添加项目的时候可以选择无AppID。添加项目后打开小程序的开发工具是这样的:
调试按钮可以打开调试界面,如果读者用惯了Chrome浏览器,想必对这个界面肯定不会陌生;如果需要切换到其他的项目,可以直接点关闭按钮,而不用关闭整个工具。
整个项目由两部分组成,一个是描述整体程序的app文件和多个框架页面文件组成。
描述整体程序的app文件必须放在文件的跟目录,由三个文件组成,app.js、app.json和app.wxss。
在app.json文件中可以对小程序进行全局配置,比如页面路由、窗口表现、网络超时、多个tab等;在app.js中我们使用App()注册小程序的,它接受一个object对象作为它的参数,这个参数指定了小程序的生命周期函数。object函数说明:
属性 | 描述 | 触发 |
---|---|---|
onLaunch | 小程序初始化 | 初始化完成后 |
onShow | 小程序显示 | 小程序由后台进入前台 |
onHide | 小程序隐藏 | 小程序由前台进入后台 |
注意:
项目框架页面可以配置多个,建议页面的文件名称和文件名保持一致。比如有一个下单页book,其中的文件可以设置为book.js、book.wxml、book.wxss、book.json。
我们在框架页面也需要注册页面,注册页面通过Page()函数,这个函数也接受一个object函数,用来指定函数的生命周期函数和初始化的数据。
属性 | 类型 | 描述 |
---|---|---|
data | object | 页面初始化数据 |
onLoad | Function | 页面加载时 |
onReady | Function | 页面初次渲染完成 |
onShow | Function | 页面显示时 |
onHide | Function | 页面隐藏时 |
onUnload | Function | 页面卸载时 |
onPullDownRefreash | Function | 页面下拉时 |
注意:
和jQuery等其他js框架不同的,小程序不能直接操作DOM元素,只能通过改变数据来控制页面元素的状态,这样有点类似React的思想。所以我觉得小程序的思想是面向数据,而不是面向元素。
上一节我们说过注册页面时需要传入一个object参数,这个参数可以挂载很多页面的生命周期函数,同时,也能将页面的数据挂载进去。页面的数据可以直接挂载到object参数的data对象中去。
1 |
|
这里我们定义了两个数据,一个数据定义在data对象中,另一个数据直接定义Page的参数中。我们可以将data中的数据渲染到页面中,使用Mustache语法(双大括号)将要渲染的变量包起来,如下:
1 |
|
如果需要在组件的属性内渲染数据,也需要用双大括号包起来:
1 |
|
在双大括号中,我们可以进行简单的运算和判断,比如三元运算,算数运算,字符串运算等。
1 |
|
对于data中的数据,我们必须通过this.data.name这种方式来获取,对于data外面的数据,我们可以通过this.myname的方式来获取。
1 |
|
对于data中的数据,要想改变它的值,必须要调用setData()方法来改变,而要改变data外的数据,可以直接给他进行赋值。
1 |
|
注意:
总结一下,两种方式定义的数据区别如下:
对于data中的简单变量,我们可以通过双大括号的方式进行渲染,但是如果对于一些稍微复杂一点的数据结构(比如数组),双大括号就不能满足我们的需求了。我们需要引入另外两种渲染方式,条件渲染和列表渲染。
我们使用wx:if=”“的来判断是否需要渲染该模块,还可以添加wx:elif和wx:else渲染。
1 |
|
也可以直接在大括号中使用布尔类型的值控制页面元素隐藏和显示。如果data中flag为false,那么类名为demo的这个view组件就不会渲染到页面上去。
1 |
|
有时候我们需要将一个数组渲染到页面上,比如几个用户信息,比如点评列表,这时候就需要用到列表循环。
列表循环使用wx:for来绑定一个数组,就可以将数组中的每个数据循环遍历到组件中。默认情况下每个元素的变量名为item,每个变量的索引值为index。
1 |
|
在列表遍历时我们并没有定义item和index,小程序自动为我们添加了wx:for-index=”index”和wx:for-item=”item”。因此在嵌套列表渲染时,注意index和item所代表的值和对象。需要我们自己定义变量名和索引,避免混乱。
微信小程序还在内测阶段就引起了这么多关注,作为一个程序员,尤其是一名前端程序员,上手和熟悉微信小程序的开发无疑会让我们越来越“吃香”。网上也在热议微信小程序是否会颠覆或者代替原生App,我觉得两者还是有以下区别的:
个人感觉微信小程序的开发也存在着不足之处,希望在以后的版本中能够改进:
这部电影在两个小时里其实讲了挺多东西的,亲情、爱情、友情和人性善良还有丑恶。笔者觉得一部好的电影不仅仅在特效和演员方面做好,所要刻画的人物才是整个电影的灵魂所在。那么怎么样才能刻画好电影中的人物形象呢,就是通过人性的善恶来刻画,折射出一类人,折射出社会的通病,这样的电影才能算是成功的电影。
这部电影一共刻画了这么几类人物,一个是主人公,身份是一位父亲,职业是证券基金经纪人;一个是秀安,跟随父亲一起去釜山看妈妈;一个是体格健壮的大叔,身份是一位丈夫,孩子即将出世;一个是快要临盆的孕妇,跟随丈夫一起去釜山;还有是高中生棒球队学生,和同学们去釜山比赛;还有是穿西装的中年大叔,职业是巴士公司常务,属于中产阶级;最后是中年大妈们,是一对姐妹花。
首先是我们的主人公,作为一名证券基金经纪人,他也是一位成功人士,但是由于工作繁忙,他经常不在家陪孩子,和孩子产生了很深的隔阂。在孩子的强烈要求下,才放下工作,进行了这趟釜山行。上车后,秀安去洗手间遇到了体格健壮的大叔,大叔说主人公是“吸血鬼”,秀安非但没有为父亲辩护,反而还赞同大叔的观点。一开始主人公也是比较自私自利的,在大叔面临困难的时候他居然不是救人而是关门。在他得知前面有危险时,他也并没有及时地告诉其他人,而是各自管各自的带着女儿走了。
后来在遇到危险时,还是大叔和孕妇的帮助下,秀安才被救。他也被大叔和孕妇慢慢的感化,最后牺牲了自己,让秀安活了下来,在生命的最后还是惦记的女儿小时候的样子。看到最后这一幕,笔者也不禁为之所感动。主人公所代表的是一个不为女儿所理解的好父亲的形象,虽然他自身也有很多缺点,但是他还是会守护整个家庭,都在默默的为整个家庭所付出。
其次是体格健壮的大叔,一开始,外表看起来不太友善,但是对已怀孕的妻子确实十分的呵护,妻子对他发火的时候他也只是脸上堆满笑容。在整个电影里一直在救人,十分的有正义感。他利用自己健壮的优势,打退僵尸大军,不断地保护自己的妻子,保护身边的人。他也心胸开阔,虽然之前主人公关门不救他,但是后来不计前嫌,在主人公危难的时候还是救了他一把,两人也并肩作战。在最危机的时刻,他主动牺牲了自己,让主人公带自己的妻子先行逃离,自己拖住了僵尸队伍,还不忘记给自己未出世的女儿起名字。他是一个好丈夫,好父亲,好公民的角色。在我们生活中,总能看到这样一群大叔的身影,对生活充满热情,在我们迷路时,他会热情的为我们指路,在有人需要帮助的时候,他总会第一时间站出来。
然后是参加比赛的棒球队学生,他们刚开始在车上打打闹闹,玩的很开心,天真无邪。但是灾难发生的太突然了,除了女高中生和队长,其他的同学都被感染了。面对昔日的好友变成僵尸来攻击他们,队长拿着棒球拍愣住了,迟迟不敢动手,他内心肯定是不敢去相信的,自己的好朋友怎么会变成这样。到最后面对自己心爱的女生变成了僵尸,队长也只有一个劲的痛苦,却无可奈何,只能跟着爱情一起“陪葬”。学生是一群充满活力充满热情的群体,他们敢爱敢恨,能为爱情所牺牲,但是他们涉世未深,在困难面前容易不知所措,需要有人指引他们前进。
还有就是一对中年大妈,一开始,姐姐给妹妹剥鸡蛋吃,对妹妹十分的“溺爱”,但是妹妹一脸嫌弃。他们的衣着也有着鲜明的对比,姐姐素颜素衣,头上带着些许的白发,看得出生活比较艰辛,历经岁月的沧桑。后来从妹妹的评价中得知,姐姐有一个儿子,把所有的都给了儿子,这应该是所有的母亲都会做的吧。妹妹打扮时髦,一身的花衣服,烫着头跟于谦老师一样,手上还涂着靓丽的指甲油。一开始妹妹跟广大的“吃瓜群众”一样冷漠,女学生说跟他们在一起比跟僵尸在一起还可怕。但是后来见到了已经变成了僵尸的姐姐,感叹她辛勤劳动了一辈子都是虚妄的。看到自己的亲人受到了社会不公平的待遇,她内心反人类反社会的性格开始表现,不顾他人的劳动成果,毅然地放僵尸大军进来,拉所有人跟她一起陪葬。
最后是里面穿西装的常务大叔,一开始对秀安说,如果不认真学习也会变成流浪汉,天真无邪的秀安却引用妈妈的话,说他是坏人,感觉这是导演的铺垫?后来到了前面的车站,他主动跟列车长说要抛下其他乘客跑路。列车长还是挺热心肠的,为了更多乘客着想,拒绝了他,不过他的这种热心肠注定是要被常务大叔所利用的。后来主人公一行想要跟“吃瓜群众”汇合,但是常务大叔这个时候却开始热心肠起来了,口口声声说为了所有的乘客的安全着想,不能让他们把危险带过来,其实内心从头到尾都只是为了自己。他还欺骗“吃瓜群众”说主人公已经被感染,煽动其他乘客将主人公一行赶到了其他车厢去。后来当他和乘务长准备一起逃离时,骗乘务长先出去引开了僵尸,自己独自逃离;在被僵尸追上时,将女高中生推了下去,自己又跑了;在热心的列车长救他后,他却没有帮列车长,而是恩将仇报将列车长拉了下去,自己逃生。
到了最后,交代了自己还有亲人在等他,他想要活着回去。很多人都很痛恨常务大叔,害死了这么多人,但是在灾难面前,有多少人会为他人考虑呢。每个人都想要在灾难面前活下去,每个人都有活下去的权利,我们没有权利决定他人的生死。
还记得有一个网友在豆瓣上的评论是这样的:比丧尸更可怕的是人性。确实,只有在危险的情况下才能展示一个人最真实的一面,人性才能够彻底的暴露出来,这才是最真实的。有的人可能会善良的帮助他人,有的人可能就自己只顾着自己逃生,每个人都有自己的选择,孰是孰非我觉得应该更加理性的看到。有的人总喜欢站在道德的至高点,对他人的言行指指点点,但是自己遇到了一样的情况却并没有做的比他人好,这样的人更加的可怕。
最后,国庆马上要结束了,希望坐高铁回来的童鞋们旅途愉快;-)
js中的变量分为两种:全局变量和局部变量。全局变量很好理解,就是在js任何地方都能够调用的变量;而局部变量就只能在函数的内部才能够调用的变量。
1 |
|
在上面的程序中,变量a就是一个全局变量,在函数的内部能够调用。但是这里的变量b就是局部变量,当函数结束调用后,变量b就被回收了,因此在函数外部调用失败。
另外需要特别注意的是:
如果在声明局部变量时不用var声明,那么这个变量自动“提升”为全局变量。
1 |
|
对比两段代码,如果你在声明b=2时没有写var,那么b就隐式地声明为全局变量,在函数外面还是能够被调用到的。
虽然使用全局变量能够在任何地方调用,很方便,但是全局变量的优点也给他带来了缺点:
因此我们在定义变量时一般要尽可能少的定义全局变量。
函数声明优先于变量声明
下面我们通过一段代码来说明.
1 |
|
或许有人是认为函数声明在后面的原因,那么调换一下位置。
1 |
|
调换位置后变量a的类型还是function,这时候声明变量a的语句没有起作用,被函数声明覆盖了。因此函数声明优先于变量的声明。
但是如果我们在声明的同时给a进行赋值。
1 |
|
我们将其调换一下位置再次进行验证。
1 |
|
可以看到,给变量a进行赋值后,不管变量a在哪,其类型变为字符串类型,上面两段代码相当于如下代码:
1 |
|
a最后被赋值为字符串,因此a的类型自然是字符串
js中作用域只有一个函数作用域和全局作用域,一个很大的特点就是js中没有块级作用域。函数作用域是比较容易理解的,那么什么是块级作用域呢?
任何一对花括号({和})中的语句集都属于一个块,在这之中定义的所有变量在代码块外都是不可见的,我们称之为块级作用域。
理解了块级作用域,来看一下下面的小例子。
1 |
|
这段代码很好理解,由于变量v在没有赋值前使用了,所以是undefined。其实这里存在着声明的提前。
当前作用域内的声明都会提升到作用域的最前面,包括变量和函数的声明
由于js作用域中的声明都会被提升到作用域的最前面,所以,上面的代码相当于:
1 |
|
这样就能很清晰地理解为什么变量v是undefined的了。
下面我们把变量v放到一个方法中去:
1 |
|
在这里由于js没有块级作用域,所以if方法没有“形成”一个封闭的作用域,并不能够“阻挡”外面的代码获取里面的变量。
我们再把变量v放到函数中去:
1 |
|
由于show函数是一个函数作用域,“阻挡”外面的代码获取里面变量(并不能阻挡里面的代码获取外面的变量),所以函数外部并不能获取到函数里面的变量v。因此证明了js中只有函数作用域,没有块级作用域。
再来看下面的一段代码:
1 |
|
很多人看到这边都会很疑惑,不是说这边show函数中能够获取到函数外面的变量的么?但是由于这边是一个函数作用域,而函数作用域存在着变量声明的提前,因此,上面的代码相当于下面的代码:
1 |
|
这里把变量v的声明放到了整个函数作用域的最前面,因此显示为undefined。理解了上面的代码,相信下面的代码也不难理解了。
1 |
|
在这里自执行函数形成了函数作用域
变量提升只提升函数的声明,并不提升函数的定义
1 |
|
或许有人有疑问,为什么这边定义的函数就不能执行呢?在这里我们需要明白函数在js中是如何进行定义的。函数有两种定义方式,一种是函数声明,另一种是函数表达式。那么什么是函数声明什么是函数表达式呢?
1 |
|
乍一看,他们长得很像,写法都差不多,但是实际上还是有区别的。js的解析器对函数声明和函数表达式并不是一视同仁的对待的,有点“种族歧视”的意思在里面。
函数声明就像是“一等公民”,js会优先读取,确保在执行前就已经被解析了,所以函数声明放在当前作用域的任何地方都能够被调用,甚至放到调用函数声明之后面。
而函数表达式就显得比较“普通”,和一般的变量一样,只有到执行到该行时才进行解析,因此,调用函数表达式要在定义后进行使用。
对于js闭包,官方的解释是这样的:
一个拥有许多变量和绑定了这些变量的环境的表达式(通常是一个函数),因而这些变量也是该表达式的一部分。
第一次读估计很难理解,什么绑定变量的环境表达式、表达式的一部分,都是些什么鬼。用通俗一点的话来说就是:
一个函数中有许多变量,这些变量变成了函数的一部分。闭包就是能够获取到函数内部变量的函数。
因此,闭包也称为闭包数据,闭包的本质也就是函数。
要理解闭包,首先要来理解两个概念:变量和作用域。在这里不多阐述,可以看笔者的另一篇文章《深入理解js中的变量和作用域》。
由于函数作用域的封闭性,函数外部并不能访问函数内部的变量。
1 |
|
但是有时候我们需要用到函数内部的变量,这时候闭包就派上用处了。我们将上面的代码改造一下,就能够获取到func函数内部的变量。
1 |
|
这里的tempShow函数其实就是一个闭包show函数。它是通过最外层的func函数运行后进行赋值的。tempShow函数运行后就获取到了func函数内部的变量n。
那为什么func函数运行之后变量n没有被垃圾回收机制回收掉呢?在《深入理解js中的变量和作用域》中我们说过了,全局变量是保存在静态存储区的,由于静态存储区中的变量是不会被回收掉的。而在这里我们将函数show赋值给全局变量tempShow,而函数show是函数func的子函数,因此,函数func也保存在静态存储区了。所以我们可以在任何地方调用tempShow方法。
在这段代码中,很巧妙的定义了一个add函数,没有在add前使用var关键字,因此函数add是一个全局变量而不是局部变量,可以在函数的外部调用对变量n进行操作。函数add也是一个闭包函数。
理解了上面的代码,相信下面的代码也不难理解了。
1 |
|
在这里出现了一个新的变量this,读者们可以通过笔者的这篇文章《Js中this的用法》大致的了解this。由于函数temp调用的环境不在object内部进行了调用,因此函数中的this指代了全局变量window。为了达到获取object内部name的效果,我们对上面的代码进行改造:
1 |
|
或者这样改造,使用bind方法,将temp函数的作用域绑定到object上。
1 |
|
闭包不仅能够返回一个函数,还能够返回其他类型的数据,比如下面的代码返回了一个数组对象。
1 |
|
在全局作用域中,this指向了Window对象。
1 |
|
在上面我们把name挂载到了全局作用域Window下面,其实我们在用var声明变量的时候也是把变量挂载到Window下面。所以上面的操作等价于下面的操作
1 |
|
在js中函数分为普通的函数和构造函数,主要的区别就是函数的调用形式。普通函数能够直接调用,而构造函数是不能调用,需要用new实例化。
普通函数的中this指向了Window对象
1 |
|
这时候函数show作为一个普通函数调用,虽然看起来像构造函数,但是内部的this却指向了Window对象,如果你在控制台打印Window对象,它下面挂载了name属性
构造函数中的this则指向了它所实例化的对象
1 |
|
在这里如果你直接调用show(‘xyf’)跟普通函数没有区别,通过new实例化一个myshow对象,这时候this就指向了这个实例化出来的对象
对象中的this指向了当前对象
1 |
|
但是如果对象的函数中嵌套了其他函数,this的指向就被改变了。
1 |
|
这时候自执行函数中的this指向了全局对象Window,所以setName()函数并不能产生作用。
1 |
|
这里的this指向了$(‘.temp’)这个对象。为了避免这些情况,我们先将this赋值给局部变量that,然后使用that。这时候that就指向了我们需要的对象。
1 |
|
如果将一个对象中的函数赋值给一个变量,再通过该变量调用这个函数,此时函数中的this指向Window对象,即使这个操作在回调函数中。
1 |
|
这两个函数都能够手动指定被调用函数内部this指向哪个对象。
1 |
|
当对象p1使用apply函数后,p1对象中的this就指向了对象p2,此时对象p1的setName函数的操作就作用在了p2对象上。
]]>工欲善其事,必先利其器
“工”是指的工作,一件事情要想做好,必定先要让工具“锋利”。
Sublime是我用过的最好的撸码神器,没有之一。那些撸码还在用什么EditPlus、DreamWeaver的,和Sublime比,简直就是拿石器时代的石器武器和二十一世纪的大规模杀伤性武器做比较。Sublime拥有漂亮的用户界面和无比强大的功能,例如代码的缩略图、自定义按键绑定、拼写检查、项目切换、多窗口等等。Sublime的界面如下:
Sublime还能支持多种编程语言的语法高亮,有优秀的代码自动补全的功能。而且Sublime还有非常强大的插件系统作为其功能的补充。一些常见和实用的插件如Emmet(快速编写 HTML/CSS 代码的方案)、ColorPicker(颜色选择器)、MarkdownPreview(markdown预览)、DocBlockr(代码注释规范)、SideBar(侧边栏工具)等等,其他一些强大的插件等待读者去体验。
大家在上学的时候肯定都用过便签纸一类的东西来记一些上课笔记之类的东西,然后贴在课本上,整个课本就贴的花花绿绿的。小孩桌面便签就是这样一个在桌面上贴上便签的工具。正如它的名字一样:DeskNotes(桌面贴纸)。
这是我一个女同事推荐我使用的,刚开始我还很不屑的,没怎么用。后来随着工作越来越多,渐渐的发现……脑子越来越不够用了。然后我就把这个工具用起来了,确实能够提醒我很多事情。它的界面如下:
除了能够添加贴纸外,小孩便签还增加了实用的小功能,如闹钟提醒和定时关机。
由于笔者从事网页的工作,经常要和图片打交道,所以经常需要截图、取色、测量像素等等。这是笔者在上学时一个老师推荐的软件,基本能够解决上述需求。FastStone Capture的界面如下:
很多读者可能会说:那你用PhotoShop啊,功能更强悍。诚然,PhotoShop功能确实相当的强悍,笔者的电脑里也装了,但是体积大,而且又十分的吃内存,每次打开都要耗不少时间(估计是电脑太老了)。
相比于PhotoShop,FastStone Capture就轻巧了不少。整个软件的大小不超过3MB,通过任务管理器看到所占用的内存仅0.3MB,基本上可以忽略不计,每次打开基本上都是秒开的。
它的主要功能有截图、屏幕录像、图像处理(裁切,改变图像效果等)。附带的特色小功能有屏幕放大器、屏幕取色器、屏幕标尺、图像转为PDF,功能可是非常的强大。
有的读者可能会说:欸,上面不是有小孩桌面便签可以用来记事了么,怎么还要有道云笔记呢。诚然,便签是可以记录生活中的琐事,但是要想把记录的内容从一台电脑转移到另一台电脑却比较费时。
笔者的所处的环境就是需要在多个地点记录,比如上班的时候看到有用的东西需要记录下来,下班在家里做一些学习笔记也需要记录下来,虽然小孩桌面便签有强大的导入导出功能,但是频繁的导入导出操作也是比较麻烦的,或许是笔者比较懒。
这个时候使用有道云笔记就可以很方便的在“云上”进行办公,而且还支持Android、iPhone、iPad、Mac、WP和web等平台,让工作摆脱了平台和设备的限制。有道云笔记还支持多种编辑格式,可以用富文本进行编辑也可以采用Markdown语法进行编辑,Markdown支持有预览的功能。读者还可以将写的文章比较好的文章在微博、微信和扣扣中进行分享。
在工作和生活中,笔者经常会将文件添加上版本号以区分,但是时间一长,就会忘记不同的版本号所更改的内容,所以经常需要一一地比对,这就让笔者很苦恼。有一天在上网时无意中发现了这款“神器”,让我们来看看它长什么样:
Beyond Compare主要用途是对比两个文件夹或者文件的不同,并将差异 通过颜色的不同以标识。对的,你没有看错,就是文件夹的不同。它还可以按照你的需要进行比较,比如需要对比文件的不同或者对比文件的相同,都可以显示。
由于笔者的每天工作都会收到很多不同文件,但是笔者又不善于对文件归类存放,都是杂乱的放在桌面,需要用到的时候找起来又相当的麻烦。这时候Everything这款文件快速搜索软件就成为我们这种“懒人”的福音了。他的界面如下:
它的体积也相当的轻巧,才30多MB,和现在动辄几百MB的软件相比确实小了很多;它的界面也很简洁易用,常用的就一个输入框和下面的文件列表,非常方便日常的使用;占用系统资源极低,Everything搜索只基于文件和文件夹的名称,所以建立起搜索数据库非常的快,搜索文件基本上都是秒搜。
Chrome是笔者用过最好用的浏览器,没有之一。不管是日常生活中的使用还是开发中的测试等,Chrome都能够轻松胜任。
Chrome的特点是简洁、快速。它支持多个标签进行浏览,即使有一个标签崩溃,其他标签页不会崩溃。而且,Chrome是基于V8 JavaScript引擎的,页面性能更加优异。
对于像笔者一样的网页开发者来说,Chrome更是开发网页的“利器”。使用F12调出控制台,在这里你能看到Elements(页面元素)、Console(网页运行的提示消息)、Sources(页面运行所加载的源码)、Resources(页面所需要的文件、存储的cookie和session等资源)、Network(可以看到网页加载脚本样式页面的时间还有异步的资源)
]]>1 |
|
1 |
|
目录下多一个.git目录,用来跟踪管理版本库,你也可以把线上的项目克隆到本地,使用下面的命令
1 |
|
1 |
|
或者一次性添加所有未追踪的文件
1 |
|
1 |
|
1 |
|
这个命令用来查看仓库的详细状态,添加-s查看简要的状态(s表示short)
1 |
|
简要状态下前面的符号代表的意思:
简要状态下颜色的不同也有区别。如果是红色,则表示该文件修改后没有追踪;是绿色则表示修改后追踪了改文件。
git diff(difference)此命令比较的是工作目录中当前文件和暂存区域快照之间的差异,也就是修改之后还没有暂存起来的变化内容
1 |
|
如果使用add命令追踪该文件后,diff命令失效。
1 |
|
log命令用于显示从最近到以前的提交日志,commit后面显示的一长串字符数字是该次提交所对应的版本号,每次都不会重复的。log命令显示的信息比较多,可以加上 –pretty=oneline 参数
1 |
|
在下面的版本回退中需要用到提交日志的版本号,这时候就需要复制这个版本号。在windows下复制git窗口中的内容的快捷键是Ctrl+Insert,粘贴是Insert
1 |
|
reset命令用于控制版本回退到之前提交时的状态。这边的LogId就是上一节中复制出来的版本号。
1 |
|
checkout命令让你在工作区做的修改全部撤销,回到上一次commit时的状态。
如果你在工作区删除了一个文件,那么status就会提醒你工作区和暂存区不一致。这时候你有两种选择,一个是git rm命令确认删除,
1 |
|
这个命令相当于同时进行了删除命令和追踪文件命令,其等价命令如下:
1 |
|
另一个是通过checkout命令找回删除的文件
1 |
|
首先从官网上下载CraftyJs的脚本引用到项目中来。然后就可以开始写我们自己的程序了。
1 |
|
这段代码用于初始化整个stage,用官方的话来说就是舞台,所有的元素将在这整个舞台里活动。这个舞台的宽度是this.config.width(px),高度是this.config.height(px)。如果有元素超出了舞台的范围,这个元素将被遮住,因为整个舞台设置了样式overflow:hidden将超出的元素隐藏掉。
1 |
|
你还可以通过background()方法给整个舞台设置背景颜色
当整个舞台初始化后就可以玩游戏了吧?不!你去剧院看戏一入座演员就给你演戏么,当然不是,还需要一些场景的带入和切换。这些场景比如加载动画、菜单选项等一系列。
1 |
|
我们可以使用Crafty.defineScene()方法来定义一个场景。在这里我们定义了一个叫loading的场景,里面只有一个元素就是一行”Loading”的字。但是定义好了场景并没有显示在舞台上,因为这个场景并没有被调用到。
1 |
|
通过enterScene()这个方法来展示刚才我们定义好场景,你会在舞台上看到这个场景。这个展示舞台的方法可以在任何地方被调用。但是需要注意的是这个方法会清除舞台上所有的元素,除了那些有”Persist”组件的元素(组件这一名词下面会解释到)。
现在到了CraftyJS最重要的部分来了,就是CraftyJS独特的实体/组件系统。这个系统有点面向对象编程的意思。整个系统分为两个部分。
所谓的组件,有点类似JAVA中的对象(不是现实里的对象),看不见摸不着,是对实体的抽象。每个组件里封装了对应的方法,可以在实体中直接调用。CraftyJS中有很多已经被预先定义好的组件可以直接拿来使用,而且组件可以被重复地继承。
实体是真正看得见的元素,是对组件的“实例化”。一个单一的实体能够继承多个实体。
1 |
|
这样就通过e()方法定义了几个叫”square”的实体。这个实体继承了三个组件”2D”、”DOM”和”Color”,这三个组件预先在CraftyJS中就已经被定义好了。如果你觉得单单使用这三个组件还不够,你可以后续往”square”这个实体中再添加组件。
1 |
|
通过addComponent()方法向实体中加入”Text”组件,这个方法支持一次添加多个组件。
1 |
|
你还可以通过has()方法判断某个实体中是否含有某个组件
1 |
|
这个方法返回一个boolean类型的值。但是需要注意的是这个方法一次性只能判断一个组件存在,并不支持同时判定多个组件比如:
1 |
|
如果你对某个组件不满意,你还可以把它删掉,这个方法也不支持传入两个以上的组件名称
1 |
|
一些常用的组件是CratyJs帮我们定义好了,我们直接使用就可以了。
2D组件是CraftyJS预先给我们定义好的一个组件,是最常用的组件之一。他提供了一个attr()的方法让我们来设置实体的属性值。
1 |
|
这里的x和y是实体相对于舞台左上角的位置,单位都为像素(px)。w和h是实体的宽度和高度,单位也是像素(px)。alpha是实体的透明度,取值范围是0到1。visible代表实体是否可见,只能接受boolean类型的参数。x、y、w、h如果不设置值,默认为0。
Text组件有四个方法可以使用,分别是text()、textColor()、textFont()和unselectable()。text()方法用于设置组件里面的内容。
1 |
|
text()方法支持传入一个方法,但是这个方法必须要返回一个字符串类型的参数,否则这个组件的内容将会显示undefined(未定义)。
1 |
|
textColor()方法用来设置组件文字的颜色,你可以使用HEX、rgb或者rgba的方式来定义颜色。
1 |
|
textFont()方法用来设置文字的字体。如果有多个字体的属性,传入一个对象的方式进行设置,Crafty支持设置的属性有以下几个:
1 |
|
unselectable()方法设置Text组件中的内容不能被高亮选中。Canvas的Text是不能被高亮选中的,所以这个方法只对DOM的Text组件有效。
1 |
|
JavaScript由于安全性方面的考虑,不允许页面跨域调用其他页面的对象,那么问题来了,什么是跨域问题?
答:这是由于浏览器同源策略的限制,现在所有支持JavaScript的浏览器都使用了这个策略。那么什么是同源呢?所谓的同源是指三个方面“相同”:
下面就举几个例子来帮助更好的理解同源策略。
URL | 说明 | 是否允许通信 |
---|---|---|
http://www.a.com/a.js http://www.a.com/b.js | 同一域名 | 允许 |
http://www.a.com/a.js http://www.b.com/a.js | 不同域名 | 不允许 |
http://www.a.com:8000/a.js http://www.a.com/b.js | 同一域名不同端口 | 不允许 |
https://www.a.com/a.js http://www.a.com/b.js | 同一域名不同协议 | 不允许 |
在JAVA中处理跨域问题,通常有以下两种常用的解决方法。
后台代码在被请求的Servlet中添加Header设置:
1 |
|
Access-Control-Allow-Origin这个Header在W3C标准里用来检查该跨域请求是否可以被通过,如果值为*则表明当前页面可以跨域访问。默认的情况下是不允许的。
在前端JS中需要向Servlet发出请求,请求代码如下所示:
1 |
|
通过jsonp跨域请求的方式。JSONP和JSON虽然只有一个字母的区别,但是他们完全就是两回事,很多人很容易把他们搞混。JSON是一种数据交换的格式,而JSONP则是一种非官方跨域数据交互协议。
首先来说一下前端JS是怎么发送请求。代码如下所示:
1 |
|
这里的callbackparam和success_jsonpCallback可以理解为发送的data数据的键值对,可以自定义,但是callbackparam需要和后台约定好参数名称,因为后台需要获取到这个参数里面的值(即success_jsonpCallback)。
下面,最重要的来了,后台怎么样获取和返回数据呢。代码如下所示:
1 |
|
首先需要获取参数名为callbackparam的值,这里获取到的值就是“success_jsonpCallback”。然后将这个值加上一对小括号。小括号里放入你需要返回的数据内容,比如这里我返回一个JSON对象。当然你也可以返回其他对象,比如只返回一个字符串类型数据也可以。最后前端JS返回的数据就是这样的:
1 |
|
浏览器会自动解析为json对象,这时候你只需要在success回调函数中直接用data.status就可以了。
]]>