为了解决前端静态网站统计 PV 数据的问题,需要有个接口返回页面的访问数量。
我们不打算使用写接口来接受用户访问,然后数据库字段++ 的方案,因为需要维护 node 服务器、还有数据库,成本太高。
最后决定直接读取 nginx 的 log 日志信息,来获取用户的访问量。
要实现该功能,可以从以下几步来实现:
下面介绍如何来实现以上功能
借助 nginx 的 log_format 我们可以自定义一个日志输出格式。
1 | http { |
修改我们的 server 配置
1 | server { |
注意 analysis 为我们自己的命名,只要保证相同即可。
这样拿到的请求数据日志就会如下:
1 | 10.232.140.148 - [21/May/2021:16:14:48 +0800] "GET /assets/js/runtime~main.1e8e793d.js HTTP/1.1" "http://10.210.4.154:3000/" "Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1" "-" |
有了以上数据,我们便可以分析 UV/PV 等信息,当前需求是页面访问量,所以我们只需要取 index.html 的请求次数即可。
实践中,index.html 作为请求次数不是一个好的方案,因为 nginx 配置,请求/也会直接返回 index.html 文件,因此数据可能会少很多,因此我们采用统计runtime~main.js的请求次数作为请求次数的统计。
我们使用grep命令从日志中过滤符合条件的文本,然后使用wc(word count)来统计出现的行数,这样就能拿到日志的行数,进而拿到了请求次数。
1 | grep runtime /var/lib/nginx/log/website.log |wc -l |
我们只需要将输出的内容,写入到前端对应的目录下,前端使用ajax请求对应的文件即可,假设我们的前端项目部署在:/var/lib/nginx/html/目录下,则可以这样输出:
1 | grep runtime /var/lib/nginx/log/website.log |wc -l > /var/lib/nginx/html/pv.json |
这样,在代码中我们便可以直接请求该文件即可
1 | fetch("/pv.json") |
只有一次的数据输出,是无法满足不断统计用户请求数量的,因此我们可以借助crontab来执行定时任务。
创建名为pvcron的任务文件,编辑内容
1 | * * * * * grep runtime /var/lib/nginx/log/website.log |wc -l > /var/lib/nginx/html/pv.json |
前面的* * * * *
的意思是每分钟执行一次该写入任务,也可以根据需要定制cron任务,比如:
1 | * 8-22 * 5 * |
该内容的意思是在5月份的每天8点-22点之间,每分钟执行一次
接下来执行
1 | crontab pvcron |
现在该文件已经提交给cron进程,它将每分钟运行一次来将PV数写入json文件供前端请求。
该方案有多个点会导致数据不够精确
因此,在以上问题都可接受的情况下,才可以采用此方案。
React 中常常会用到 Ref 对组件进行命令式的调用,官方对不同 ref 值的介绍如下:
ref 的值根据节点的类型而有所不同:
但是函数式组件,可以使用 forwardRef 与 useImperativeHandle 组合使用,来实现对外暴露组件调用命令的效果。
1 | // cool-input.tsx |
同时,使用 forwardRef,可以实现将函数式组件中的子组件转发给父组件使用,让父组件直接调用对应的命令。
1 | // cool-input.tsx |
1 | // index.tsx |
这样就可以在父组件中,直接调用函数组件中的子组件的事件了。但是有一种特殊的情况:函数组件中在转发子组件的同时,也需要使用该组件的 Ref。我们期望实现的效果是,父组件(index.tsx)点击「聚焦输入框」可以正确的使 input 触发 focus 事件,同时,函数组件也可以监听 input 的 focus 事件,打印输入框的值。
这个时候,我们便需要将 input 元素同时赋给多个 Ref,因此需要根据 ref 的类型来进行批量赋值。ref 赋值有三种方式:
不建议使用它,因为 string 类型的 refs 存在 一些问题。它已过时并可能会在未来的版本被移除
1 | <input type='text' ref='textInput' />; |
1 | this.setTextInputRef = (element) => { |
在 React 16.3 版本之后,使用 createRef 使得对象引用变得更加方便。因此正常情况下,推荐使用该方式。
1 | // 声明 |
在函数组件中,一样可以在函数组件内部使用 ref 属性,使用方法便是 useRef,创建一个 Ref 对象。
1 | const inputRef = useRef<HTMLInputElement>(null); |
回归问题,我们只需要判断 ref 的类型,然后根据当前类型,依次赋值即可。
1 | const CoolInput = forwardRef<HTMLInputElement, CoolInputProps>((props, forwardedRef) => { |
思路便是以上的内容,即将示例依次赋值给对应的 ref 即可,但是这样编写起来比较麻烦,我可以将该功能提取为一个 hooks 使用,这样就不需要单独去赋值了。
1 | import { MutableRefObject, Ref, useCallback } from "react"; |
这样我们即可在页面中直接使用即可合并所有相同的 Ref 为一个,直接赋值到元素上即可。
1 | const CoolInput = forwardRef<HTMLInputElement, CoolInputProps>((props, forwardedRef) => { |
这时,父组件也可以直接对 input 元素使用.focus()方法,函数组件中也可以随时在 onFocus 中使用 ref 打印当前输入框的值了。
在React开发中经常会遇到controlled和uncontrolled组件的问题处理,最近仔细阅读了官方文档以及相关的介绍,整理一份基础笔记。
当用户将数据输入到表单字段(例如 input,dropdown 等)时,React 不需要做任何事情就可以映射更新后的信息
非受控组件就像是我们常见的DOM元素,Input节点元素内部保存输入的内容,然后在需要的时候可以通过 ref 获取它们的值。如下:
1 | import React, { useCallback, useRef } from 'react'; |
非受控组建的特点就是,不会实时的向父组件发送数据的变化,只有在父组件需要的时候,才会使用ref主动的从该组件中「提取」最终结果数据。
非受控组件的应用场景比如一个应用场景比较单一的Form表单,它的特点如下:
1 | import React, { useRef } from "react"; |
以上便是一个非常简单的非受控组件的例子,它通过forwardRef和useImperativeHandle 暴露出获取全部Form数据的接口,父组件在使用时,可以直接通过该接口「拉取」数据即可。
1 | // index.tsx |
在开发上,该组件的优点便是快速,完全不用考虑可能传递进来的参数的问题,快速完成功能。
如果一个 input 表单元素的值是由 React 控制,就其称为受控组件
与非受控组件对应的,便是受控组件,特点也是与之对应,受控组件有以下特点:
参考「Controlled and uncontrolled form inputs in React don’t have to be complicated」文章介绍,它的流程如下:
1 | import React from "react"; |
受控组件的当前值(value)与更新事件(onChange)一般是成对出现的,否则会导致组件变为readOnly组件,无法更新数据。在React开发模式可能会报warning:
Warning: Failed prop type: You provided a value prop to a form field without an onChange handler. This will render a read-only field. If the field should be mutable use defaultValue.
受控组件是React官方推荐的开发风格,大部分的组件都应该遵循该模式来实现。在实际使用中,因为父组件可以实时获取、控制数据,因此对处理表单的错误提醒、其他子组件的disabled等状态变化都有非常好的体验。同时,对于异步初始化更新子组件数据也会变得非常方便。
1 | const [name, setName] = useState<string>(""); |
大部分的组件都应该使用受控组件。但是对于一些指令式的操作,使用非受控组件可以更好实现,比如:
之前有遇到过这样的业务:同一个页面是由多个Form组成,它们初始化数据是由多个接口请求到的数据来初始化,但是修改后使用同一个提交按钮同一个接口进行提交。
这样使用非受控组件的方案可以是每个组件单独去请求该数据,然后决定是否可以编辑,父组件点击提交时,从子组件中拉取全部数据,然后统一提交。
一般非受控组件,也会有一个defaultValue可以用来初始化组件的值,但是之后该值的改变,便不会实时更新。实现方法如下。
1 | const UncontrolledInput: React.FC<UncontrolledInputProps> = ({ defaultValue: initialValue }) => { |
利用了useState函数的参数值,只会在第一次创建的时候使用,之后再修改该props,并不会再对state的值引起改变的特点。
当对比两个相同类型的 React 元素时,React 会保留 DOM 节点,仅比对及更新有改变的属性。
1 | <div className="before" title="stuff" /> |
通过对比这两个元素,React 知道只需要修改 DOM 元素上的 className 属性。
这时,非受控组件的问题就凸显出来了,再次更新的defaultValue值并不会更新组件的值,在一些特殊场景,比如切换Tab时,会出现将第一个Tab组件里的值,带入到第二个组件中去的问题。
1 | <div> |
在该案例中,我们期待的是切换后,Input组件会切换两个默认值hi1和hi2的,但是实际效果并不如意,不仅如此,我们修改其中一个输入内容之后,点击切换,输入内容会带入到第二个中去。
我们通过对非受控组件中模拟mount和unmount事件,来查看切换时的效果。
1 | interface UncontrolledInputProps { |
观察结果发现,组件并没有被销毁,而是被重用了,这样就导致出现了状态被重用的问题。参考官方文档介绍,我们可以通过两种方案修复:
当根节点为不同类型的元素时,React 会拆卸原有的树并且建立起新的树。因此使用不同的父节点元素可以解决该问题。
1 | { |
通过观察可以发现,每次切换,都会卸载上一个组件,重新创建一个新的组件。这样就解决了之前的问题。但是,使用不同的父组件会让代码变得很难理解,因此,更推荐使用官方提供的添加key的方式。
1 | { |
当子元素拥有 key 时,React 使用 key 来匹配原有树上的子元素以及最新树上的子元素,这样key不相同,便不会复用原来的元素,这样就会保证了正常的切换。
受控组件,在表现上不会存在该问题,因为其value值的变化,都会实时的同步到元素状态中。但是,受控组件一样会复用该组件,就会导致组件内的状态(比如:定时器)会被复用,同样有一些隐患。因此,在切换受控组件时,也同样推荐增加key属性。
浏览器端的下载,广泛应用于文件下载、导出等操作,前端发起下载的方案有各种的方案,但是都与后端的协议息息相关,比如最简单的甚至可以直接使用链接即可发起下载。
1 | <a href="https://url.com/file" download="logo-image">下载</a> |
这样既可在高版本浏览器中,指定一次下载,download属性便是指定该链接为下载的属性,属性值有三种形式:
以下为浏览器的支持情况:
在实际开发中,更多的情况可能会是使用JavaScript来发起下载,而不是直接写好一个连接。因此我们可以通过动态的创建a标签,并触发点击事件,来完成下载。一个标准的下载工具函数如下:
1 | const download = (url: string, fileName: string) => { |
直接使用a标签来发起下载,在不同的浏览器端,会有一些不符合预期的表现形式,比如在测试中遇到的情况就是如果返回的文件是PDF格式,并且指定了’Content-Type’ Header为 ‘application/pdf’,则在不同的浏览器上有不同的表现。
以下为不同浏览器表现的视频效果:
经测试,在使用JavaScript动态创建的标签下载时,不会出现该问题。和使用a标签链接对应的文件类似,我们同样可以使用其他的方式打开链接,让浏览器决定下载内容。
两种实现方法比较类似,都是使用浏览器直接打开对应的链接,然后浏览器根据Header中的content-disposition 的信息来决定是展示还是下载。
对于普通的二进制文件正常都是直接触发下载,但是对于图片、video等浏览器可以直接解析的内容,就可能会出现直接在浏览器展示,而不是下载。这其中的主要原因依旧是content-disposition Header的缘故。
content-disposition在作为response header时候,可以有两种类型,分别是:
对于这两种文件类型的效果区别,可以分别点击以下链接查看Header信息里的区别:
inline https://blog-demos.vercel.app/api/download/inline-image
attachement https://blog-demos.vercel.app/api/download/attachment-image
因此,如果希望浏览器直接打开的方式下载文件,需要服务端配置好对应的content-disposition为attachment值。
使用XHR下载主要可以处理以下几种情况,其中前两项为比较常见的场景:
使用XHR下载的思路是,请求的responseType为blob格式,在获取到文件之后,使用createObjectURL转换为对象地址,赋值给a标签的href属性,完成下载。
1 | // 触发下载blob文件 |
以上便是一个较为完整的使用XHR下载的方法,基于此,我们为其增加错误信息回调。
1 | const onDownload = useCallback( |
使用fileReader将后端返回的错误JSON解析出来,这样在使用该函数的过程中,就可以获取到成功或者错误的回调信息了。
取消网络请求可以使用xhr.abort,对应Axios的实现为cancelToken,取消网络请求相关的介绍内容可以参照之前内容:「JavaScript网络请求(一):处理race condition竞态问题#abort上一次网络请求」
1 | const onDownload = useCallback( |
在线测试效果地址:https://blog-demos.vercel.app/admin/blog-demos/user-download
完整代码地址:https://gist.github.com/mrxf/14091139d710a7e4d9d79b71e1472243
filesaver.js是非常强大的前端文件保存方案,不仅限于下载文件,包括canvas等内容都可以直接保存,是非常成熟的方案。
下载文件也非常的方便,如果有其他的保存需求,可以使用该库。
1 | FileSaver.saveAs("https://httpbin.org/image", "image.jpg"); |
🤔 Q: 在一些返回中,并不能看到content-disposition Header信息,导致无法获取文件名,应该怎么办呢?
✅ 解决思路:检查Access-Control-Expose-Headers 的配置信息,该Header控制响应Header暴露那些Header到外部,默认不展示content-disposition。
🤔 Q: XHR的responseType还有哪些格式?
🙋♂️ Answer:text(省缺值)、json、document、arraybuffer、blob、ms-stream(IE支持的下载类型)
🤓 Q:content-disposition的attachment类型的规范是怎么样的呢?
🙋♂️ Answer:除了可以直接设置attachment值之外,还可以指明文件名,格式如下:
因此在获取content-disposition的文件名的时候,直接用正则匹配可能效果并不好,尤其是在出现了 filename*的时候,处理的条件就更加复杂了。建议直接使用第三方工具包 content-disposition
1 | $ npm install content-disposition |
竞争危害(race hazard)又名竞态条件、竞争条件(race condition),它旨在描述一个系统或者进程的输出依赖于不受控制的事件出现顺序或者出现时机
在前端中的表现常见于异步操作的结果返回的时间顺序和预期不同,比如常见的网络请求结果的顺序。
首先看一个异步请求返回结果顺序不同,导致数据不符合预期的场景。
以上功能的完整实现代码如下,我们接下来需要一点点的对其进行改动。
1 | const UserSearch: React.FC = () => { |
最常见的缓解该状况的方案便是使用debounce,在用户输入完成之后,再发起请求。
1 | const run = useCallback( |
这样,就可以极大的缓解了请求返回顺序不同导致的错误问题。
但是这样已经改变了需求的内容,原本期望的是实时更新,现在变成了等输入完成之后再更新,如果用户输入的内容比较长,界面就会一直空白,因此还需要考虑其他的解决方案。
我们在每次请求的时候,都生成一个随机的字符串数据(自增ID,时间戳、nanoID)带入请求中去,后端在返回数据的同时,再原封不动的将该requestID返回;本地也有一个全局变量,记录最新的requestID。
在数据返回之后,我们拿数据中的requestID与本地的最新的requestID做比对,如果相等,则展示数据,不相等,则舍掉数据。
1 | // 只记录最新的请求ID |
后端返回的数据格式:
1 | { |
这样,根据请求ID的比对,我们就可以将旧的请求舍弃掉。
这种方案的弊端太明显,需要依赖后端的配合,因此尝试寻找完全由前端处理的方案。
在使用ahooks的useRequest的时候,官方的文档中提到了这样一个API介绍
默认情况下,新请求会覆盖旧请求。如果设置了 fetchKey,则可以实现多个请求并行,fetches 存储了多个请求的状态。外层的状态为最新触发的 fetches 数据。
那么意味着useRequest是完全可以处理竞态问题的。我们用useRequest来尝试一下。
1 | const { run, data, loading } = useRequest( |
这个时候再次进行请求之后发现,完全符合我们的预期。
那么我们如果也期望自己去实现该效果的话,则可以借助React的useEffect来实现。
首先我们看一下React Hooks的执行流程。
可以发现,在每次Update阶段,都会先执行一次cleanUp Effects,然后再执行Effects函数。借助此效果,我们可以定义一个Ref外部变量,每次Effect clean阶段自增该值。在effect阶段,将该值赋给局部变量。在请求结束之后,比对局部变量与自增的Ref值是否相等,然后即可对数据做取舍。
1 | const [name, setName] = useState<string>(""); |
同时,Dan Abramov 在A Complete Guide to useEffect 也提到了该问题的解决方案,基于此,我们可以取消掉Ref这个外部变量来实现。这个可以实现的原因和该情况类似。
因为useEffect的cleanup函数不是和effects一一对应执行的,并不是Mount阶段执行的Effect,一定在UnMount阶段执行对应的cleanup。而是在下一次effect执行之前,执行上一个effect的cleanup函数。
1 | useEffect(() => { |
可以看到,使用react useEffect是可以满足我们的需求的。但是,针对每一个请求都要写一次这样的代码,会做很多重复性的操作,因此我们可以将该功能提取为一个单独的hooks使用。
1 | import { useEffect } from "react"; |
使用时,直接获取最新的current值即可。
1 | useCurrentEffect( |
还有一种实现思路,我们可以在下一次请求的时候,直接将上一次未结束的网络请求取消掉。取消网络请求,针对不同的协议对应的api是
Axios中的具体实现,对应的为 CancelToken。使用CancelToken的方式官方介绍地址 https://github.com/axios/axios#cancellation ,被取消的请求会执行Promise的reject,错误内容为cancel的内容。
1 | const cancelSource = useRef<CancelTokenSource | null>(null); |
看效果这样也满足了我们的需求,这样先请求的数据,如果还没有完成,就不会再回来了,也就不会出现覆盖的情况了。
Axios的cancelToken的实现方式是利用了一个cancel token的Promise,如果存在该Promise,并且该promise resolve了,则执行xhr.abort()方法来取消掉请求。
A reactive programming library for JavaScript
使用rxjs处理异步操作,非常的轻松,因此处理该问题时,在不依赖react hooks直接使用rxjs的switchMap 操作符即可完成。
switchMap特点是在每次发出时,会取消前一个内部 observable 的订阅,然后订阅一个新的 observable。这个思想和react的useEffect的clearup类似,因此也是一样的实现思路。
1 | import { map, switchMap } from "rxjs/operators"; |
Avoiding Race Conditions when Fetching Data with React Hooks
]]>我们将我们的需求汇总一下,我们希望的功能有:
Docker的官方化介绍,可以在其官网[1]中了解到,对于我们来说Docker就是一个将项目运行所需所有环境打包到一个虚拟化容器中的工具,
同时,借助Docker,我们也可以非常方便的获取所需的依赖环境。
只需要在命令行使用Docker使用mariadb镜像启动一个容器即可。
1 | docker run \ |
使用用户名root
,密码:password
即可成功连接MySQL。
如果想停止,直接按下Ctrl+\,服务又从电脑中移除,非常干净。
备注:为何这里不是Ctrl+C来终止命令,可以参阅本文[3]
该命令中有几个参数具体分享一下:
接着启动数据库的例子,我们会发现,在结束掉服务后,下次再启动会发现数据全没了,我们如果希望将自己的数据保存在本地,那么可以增加-v参数。
1 | docker run \ |
这样本地路径/you/data/path
就会将MySQL产生的数据持久化保存了,每次启动MariaDB都会还原之前的数据了。
以上我们便体验了Docker的一大特点快速部署,即快速将MySQL服务部署到你的电脑上,无论是Windows/OSX/Linux系统,都是一段命令搞定,大大节省了时间和精力。
这样在我们的开发服务器上,只需要存储一些配置和数据文件,执行文件完全交由Docker管理。
我们需要在开发中需要用到该接口:https://yapi.thisjs.com/mock/21/antd/games
,我需要使用nginx对其进行一些代理配置,这样我们只需要创建一个.conf文件,然后使用Docker启动一个Nginx挂载该conf文件到conf.d/
目录下即可。
1 | # proxy.conf |
在配置目录下执行该命令
1 | docker run \ |
这时候,我们即可访问 http://localhost:8080/games
,即可获取到所需要的数据。
当我们开发/调试结束后,如果需要将nginx容器停止并移除可以参考以下命令。
1 | # 查看正在运行中的container |
至此,Docker已经基本满足了我们的两个需求:
但是还没有完全达到,我们希望所需环境一键配置,而不是一键又一键的配置,因此希望能在下一个篇章里介绍Docker更轻松维护服务的相关内容。
看到这,你可能还没有安装Docker,其实Docker安装非常的简单。比如Mac,只需要下载镜像然后直接安装即可:
其他系统的Docker安装都非常的简单,可以参考这篇文章介绍:
安装完之后,不如尝试启动一个普通的前端项目。
1 | docker run \ |
这是一个基于create react app的前端项目,启动之后直接访问 localhost
即可看到页面。
不能为了用Docker而用Docker,否则会出现Docker也成了我们开发中的一项没有必要的服务。遇到问题,还是要以前端的方式解决。
以下以2个场景进行分析:
如果是单一的代理请求来解决跨域问题的场景,那么最佳方案是以前端的方案解决
在现代前端框架脚手架中都集成了非常方便的proxy方案,以Create React App[2]为例,就提供了http和https代理功能,在未eject的项目的package.json
文件中增加一项配置即可。1
"proxy": "http://url.to.poxy",
Creact React App在执行eject之后,与其他基于Webpack的项目都可以通过配置webpack.config.js
,在devServer
中新增proxy即可。1
2
3
4
5
6proxy: {
'/api': {
target: 'http://localhost:3000',
pathRewrite: {'^/api' : ''}
}
}
只有在遇到比较特殊的情况下,我们才会去配置Nginx
对于场景1,我们需要Nginx + Hosts的配合来实现。首先修改Hosts将原来的访问地址转发到本地,然后使用nginx来将其指向目标地址。1
2
3
4
5
6
7
8{
listen 80;
server_name api.serverurl.com;
location / {
...其他配置...
proxy_pass http://127.0.0.1:4300;
}
}
前端线上部署到底要不要用Docker?纯静态前端项目完全不需要,因为前端项目更依赖网络资源和浏览器资源,因此常规场景下,CDN才是纯静态项目的部署方案。
[1]Docker: Empowering App Development for Developers
]]>在开发的过程中,常常会遇到需要抓包,查看请求数据的情况。
本来各自负责各自的平台,非常的和平。但是我们会遇到在一个平台上调试其他设备的数据请求情况。
比如在Windows上调试手机设备,我们可以在Fiddler中开启允许其他设备远程连接,然后在手机设备中设置VPN为电脑IP,这样手机的数据会通过电脑进行请求,这样我们就可以在Fiddler中抓取手机中的数据包了。
这项操作其实还可以简化,那就是不需要手机进行任何设置,我们就可以直接直接获取手机上的数据包。这时候,我们就可以使用神奇的 Ettercap 了,该软件可以实现一个中间人攻击的思路,进行抓包分析。
中间人攻击是指攻击者与通讯的两端分别建立独立的联系,并交换其所收到的数据,使通讯的两端认为他们正在通过一个私密的连接与对方直接对话,但事实上整个会话都被攻击者完全控制
基于此,我们便不需要通过手机端的设置或者允许,我们在这个环节中,扮演攻击者,就可以快速的开始对其抓包分析了。
以下介绍在os X系统中进行中间人攻击抓包的方式。
我们需要的几个工具如下:
1 | $ brew install nmap |
1 | $ brew install ettercap |
在安装Ettercap的时候可以选择带GUI界面的,只需要在后面追加--with-gtk+
参数即可。
1 | $ brew install wireshark --with-qt |
首先,电脑要与手机在同一个局域网中。接下来,通过IP查看局域网使用的网段。在终端中,使用以下其中一个命令,查看IP地址。
1 | ipconfig getifaddr en0 # 使用无线网连接 |
接下来我们使用namp查看同一网段下,有哪些设备在连接。会得到类似以下结果。
1 | $ nmap -sP 192.168.199.0/24 |
当然,如果你通过手机的链接信息中,直接获取到手机IP的话,该步骤可以省略。
可以看到这里有多个设备在连接,而我本次需要测试的是android…….lan (192.168.199.153)
这一个IP。
这里使用curses图形化界面启动,参数为-C
,如果使用GUI界面的话,参数为-G
1 | sudo ettercap -C |
进入该界面后,依次选择Sniff
-> Unified sniffing...U
-> 输入网络类型值(参考上面查询IP的参数,默认en0) -> Hosts
-> Scan for hosts
-> Hosts list
这里可以看到扫描出来的同网段IP,在编写该文章的时候,有些其他设备已经离线了,因此本列表中扫描到的与使用namp扫描出来数量不同。但是如果记住了对应设备的IP,依旧可以使用。
这里,192.168.199.1
为网关,本次中间人攻击就是要实现欺骗设备 192.168.199.153
与网关 192.168.199.1
之间的通讯。
接下来,需要将这两个IP分别加入嗅探的目标中,依次进入Targets
-> Select TARGET(s)
-> 在TARGET1中输入/192.168.199.153//
TARGET1中输入/192.168.199.1//
备注:这里的Target格式为 MAC/IPs/PORTs/
这时,查看Current targets可以看到当前的目标列表。
执行MiTM
-> ARP poisoning...
-> Parameters为空即可
这时,已经通过ARP欺骗的方式,成功开始了中间人攻击。可以通过View
-> Statistics
查看该设备的数据情况。
现在,我们已经成功监听了设备和网关之间数据。现在需要试着分析这些数据了。那么就要使用Wireshark了。
1 | sudo wireshark |
我们简单做一下筛选,只展示IP地址为192.168.199.153的POST请求。1
ip.addr == 192.168.199.153 && http.request.method == "POST"
我在手机的一个非https网站(www.div.io)中进行了登录测试。可以在wireshark中获取到了POST的JSON数据信息。
可以看到登录的用户名密码都是以明文的方式传输的,非常方便的进行了数据抓包调试。
我们一直使用POST方式来获取该设备的登录信息,但是如果该设备已经登录过了,我们应该如何抓取到可以使用的信息呢?——当然是Cookie信息了。
将过滤的请求方式改为GET,在随便找到一个HTML页面之后,会发现其中带有Cookie信息。
将该Cookie信息,保存下来,在任意浏览器中导入该Cookie信息,即可实现『登录』的效果。
至此,我们已经利用非常古老的的中间人攻击的方式,实现了不需要手机任何操作,就可以抓取手机数据包的功能。该方法在设置好之后,非常方便,可以快速切换设备,也可以多个设备同时抓包测试。
当然,我们也发现了其中的问题,那就是如果使用该方式对其他人的手机进行渗入,是不是就会导致数据泄露呢?理论上是会出现这种情况的,但是前面也提到,这是比较古老的攻击方式,只要设备上安装了任意的『XX安全卫士』『xx管家』,不要随便连接公共的WIFI,都可以保证我们的设备安全。
看到这个题目,立马想到的就是 element.classList.contains() 和 $(element).hasClass() 方法。
但是,在一些低版本浏览器中,classList无法使用,这个时候就可以自己实现类似jQuery的hasClass()函数。
classList的兼容性
假如我们有如下测试元素
1 | <div id="LL" class="a b hello-world"></div> |
最开始我们找到的方法如下,即使用正则判断单词边界的方式判断是否存在className
1 | function hasClass(element, className) { |
在样式的名字为hello-world之类的带有-连接符的情况,测试hello和world都会返回true,这并不满足我们的预期。
同理,还有简单的使用 indexOf() 方法判断,也会导致这样的问题。
1 | function hasClass(element, className) { |
这时候,不仅hello和world返回true,h/e/l/等单个字符都会返回true。
我们改进一下该方法:
1 | function hasClass(element, className) { |
现在根据样式名称加” “的方式,判断一个元素是否含有该样式。在大部分的测试中,已经没有了问题。
但是!!!我们遇到了这样的神奇代码:
1 | <div id="tab" class="hello-worldabworld"></div> |
看上去和正常的代码没有太大区别,然而样式名称间的分隔符居然 是TAB,不是空格!
那么使用空格作为分隔符检索的方式就失效了。不过我们可以在检索之前将内容格式化一下即可。将tab和多余的空格一起替换为空格即可。
1 | function hasClass(element, className) { |
这样,无论是遇到tab键,还是-连接符问题都可以很好的解决了。好方法 get√
另外,在查阅YOU MIGHT NOT NEED JQUERY时,遇到了如下方法。
1 | function hasClass(element, className) { |
该方法也是基于样式左右的空格,使用正则进行匹配,同时考虑到了样式开头如果没有空格的问题。但是依旧没有考虑到 TAB 作为分隔符的问题,我们可以将格式化的字符串作为匹配内容,也可以直接将该情况添加到正则中即可。
1 | function hasClass(element, className) { |
好啦,现在又有一个新方法get√
现在,非常方便的方法我们已经拥有了2个,那么我们开始天马行空(不考虑性能)的部分吧。
同样是利用了样式边界的思路。我们将className字符串进行分割,然后使用for循环,进行查找。
1 | function hasClass(element, className) { |
至于为什么不用filter,includes等高阶函数,主要是为了照顾低版本浏览器,如果浏览器版本支持的话,还是用 classList.contains 吧。
思路:根据className匹配元素数组,然后查找其中是否含有对应的元素。
1 | function hasClass(element, className) { |
好了,这样在判断一个元素是否含有某个样式的时候,就有不同的方法可以用了。如果要做兼容性的话,只需要在前面加个判断if(element.classList),在有classList方法的浏览器中使用classList.contains()方法,其他浏览器使用这些方法中的一个即可。
本文介绍如何利用云服务器,为OneDrive增加离线下载功能。在充分利用云服务器空闲资源的同时,享受OneDrive强大的在线影音、文档编辑功能。
离线下载
功能一般有如下两个目的添加下载任务 => 将资源保存到服务器中 => 在服务器上将资源同步到OneDrive中 => 在OneDrive中查看资源
备注: 本次使用的云服务器安装的是CentOs 7.2系统
我们采用了Linux OneDrive的开源项目。
1 | yum install git |
1 | # 安装依赖 |
如果你在make过程中遇到了dmd:命令未找到
错误,请先激活dmd,方法如下
1 | # 激活 |
安装完成之后,需要配置一下需要同步的内容,因为Onedrive默认会将服务器上所有的内容都同步下来,这样非常慢。
在onedrive 目录下执行以下三行命令,创建OneDrive配置文件1
2
3mkdir -p ~/.config/onedrive
cp ./config ~/.config/onedrive/config
vim ~/.config/onedrive/config
配置信息可以参考如下1
2
3
4# 本地同步的位置
sync_dir = "/home/download/onedrive"
# 符合以下规则的目录或者内容,将跳过同步
skip_file = "影视|软件工具"
/home/download/onedrive
作为同步目录,是为了给Aria2留出下载目录,可以根据自己需要随便修改接下来为OneDrive执行授权,在命令行中执行
1 | onedrive |
会输出一个授权地址,复制授权地址到本地浏览器中打开,授权登录之后,将授权后的全部地址拷贝过来粘贴即可
从现在开始,只要执行OneDrive即可将本地的资源与服务端的内容同步。
但是我们希望在关闭SSH终端之后,依然可以自动同步。
官方推荐的方案是:
1 | systemctl --user enable onedrive |
但是在Centos 7.2中会出现错误,因此可以使用nohup
、screen
等命令允许在关闭SSH终端之后,继续执行,执行以下命令即可
1 | nohup onedrive -m & |
现在,我们在服务器上的文件操作,都会同步到OneDrive中了。
如果需要结束后台同步,找到ID,结束即可1
2
3[root@onedrive ~]# ps -ef|grep onedrive
root 40504 1 0 12:21 ? 00:00:02 onedrive -m
[root@onedrive ~]# kill 40504
首先安装Aria2
1 | yum install aria2 |
配置1
2
3mkdir /home/soft/aria2c -p
touch /home/soft/aria2c/aria2.session
vim /home/soft/aria2c/aria2.conf
配置内容参考如下
1 | #用户名 |
几个关键内容:
rpc-secret
用于设置访问tokendir
设置到OneDrive的目录启动Aria2服务:1
aria2c --conf-path=/home/soft/aria2c/aria2.conf -D
接下来安装UI界面
UI界面采用webui-aria2
进入/home/wwwroot
目录,克隆项目
1 | git clone https://github.com/ziahamza/webui-aria2.git |
使用Nginx启动界面服务
安装nginx1
2
3
4
5# 安装
sudo yum install nginx
# 作为服务启动
sudo systemctl start nginx
1 | vim vim /etc/nginx/nginx.conf |
修改root目录到项目所在位置
1 | server { |
重启Nginx
1 | nginx -s reload |
打开对应地址,发现已经成功了
测试下载文件
注意设置dir为OneDrive下的目录
过一会儿在Onedrive上,就会发现文件已经成功转存。
随着前端单页面APP的发展,前后端分离成为了现在开发的一种趋势,用户身份认证,发生了一系列的变化。传统的Cookie, Session验证方式存在跨域、扩展性的限制,Token验证方式成为了一个很好的替代选择。
这是一篇前导文章,之后会发布一系列关于JSON WEB TOKEN的项目实践。因此,这里将自己了解的相关知识和自己的一些观点汇集于此,以供查阅。
当然,传统验证方式并不是一文不值的,这里只是列出其中的不足,然后使用JSON WEB TOKEN来弥补其中的缺点。
JWT包含3部分数据信息,使用”.”分割,格式示例如下1
hhhhhhh.pppppp.sssss
三部分信息分别为:
Signature
: 签名
Header中一般包含Token类型和哈希算法,例如:1
{"alg":"HS256","typ":"JWT"}
Payload中包含声明信息,例如1
2
3
4
5{
"username": "admin",
"iat":1506320911, // 创建时间
"exp":1506324511 // 过期时间
}
注意: Payload和Header中的信息是BASE64编码,不是加密,因此不要再payload中添加敏感信息
签名用来校验JWT的发送方属实,以及确认消息在传递途中没有被更改。
例如,使用HS256算法,签名将采用如下方式创建:1
2
3
4HS256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
这里对于jwt的介绍只是简单介绍,详细关于JWT的信息可以参阅[2],[3]这两篇文章。
Header
,body
,参数
中,因此只要服务器允许跨域请求,那么带有授权Token的客户端,可以任意访问不同服务器下的服务,因此,非常适合SSO单点登录系统。[1] 前后端分离之JWT用户认证
[3] 适用于前后端分离的下一代认证机制 —— JSON Web Token
]]>大学时期,每学习一门新编程语言,就会被要求重新实现一遍斐波那契数列算法。那时,常用的方法即递归法和递推法。那时只对结果感兴趣,只要结果出来了,其他的仿佛就无所谓了。
现在,成为一名前端工程师之后,再看这个问题,要考虑的情况,也变得更广泛了,可以用的方法也更多了。所以现在希望应用自己了解的知识,再计算一次斐波那契数列。
首先,斐波那契数列从第0个开始,分别是1
0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233……
因此要根据该规则,返回第n个斐波那契数
首先,先把之前的递归方法再再再实现一遍。1
2
3
4function fibonacci(n){
if(n === 1 || n === 0 ) return n;
return fibonacci(n-1) + fibonacci(n-2);
}
递归的思路很简单,即不断调用自身方法,直到n为1或0之后,开始一层层返回数据。
使用递归计算大数字时,性能会特别低,原因有以下2点:
① 在递归过程中,每创建一个新函数,解释器都会创建一个新的函数栈帧,并且压在当前函数的栈帧上,这就形成了调用栈。因而,当递归层数过大之后,就可能造成调用栈占用内存过大或者溢出。
另外,我们在return前加入以下语句,打印一下递归的计算过程。1
console.log(`fibonacci(${n-1}) + fibonacci(${n-2})`)
当,n为6时,得到的结果为1
2
3
4
5
6
7
8
9
10
11
12fibonacci(5) + fibonacci(4)
fibonacci(4) + fibonacci(3)
fibonacci(3) + fibonacci(2)
fibonacci(2) + fibonacci(1)
fibonacci(1) + fibonacci(0)
fibonacci(1) + fibonacci(0)
fibonacci(2) + fibonacci(1)
fibonacci(1) + fibonacci(0)
fibonacci(3) + fibonacci(2)
fibonacci(2) + fibonacci(1)
fibonacci(1) + fibonacci(0)
fibonacci(1) + fibonacci(0)
② 分析可以发现,递归造成了大量的重复计算。
递归的以上两种缺点,我们可以使用尾调用优化和递推法来解决。
首先,什么是尾调用。
尾调用是指一个函数里的最后一个动作是一个函数调用的情形:即这个调用的返回值直接被当前函数返回的情形。WikiPad[1]
用代码来说,就是B函数的返回值被A函数返回了。1
2
3
4
5
6function B() {
return 1;
}
function A() {
return B(); // return 1
}
什么时候会执行尾调用优化呢?
在ES6中,strict模式下,满足以下条件,尾调用优化会开启,此时引擎不会创建一个新的栈帧,而是清除当前栈帧的数据并复用:[2]
尾调用函数不需要访问当前栈帧中的变量
尾调用返回后,函数没有语句需要继续执行
尾调用的结果就是函数的返回值
举例说明:
以下函数即可开启尾调用优化1
2
3
4 ;
function doA() {
return doB();
}
以下函数无法开启尾调用优化1
2
3
4 ;
function doC() {
doD(); // 尾调用的结果不是函数的返回值
}
1 | ; |
我们使用尾调用优化,重写函数。1
2
3
4
5
6
function fibonacci(n, current, next) {
if(n === 1) return next;
if(n === 0) return 0;
return fibonacci(n - 1, next, current + next);
}
我们可以使用如下方法调用该函数1
fibonacci(6, 0, 1);
这时,在执行该函数时,由于引擎不会创建一个新的栈帧,而是清除当前栈帧的数据并复用
,就不会出现内存占用过大的情况了。
得益于ES2015的默认参数
特性,我们可以将以上函数改写。1
2
3
4
5
6
function fibonacci(n, current = 0, next = 1) {
if(n === 1) return next;
if(n === 0) return 0;
return fibonacci(n - 1, next, current + next);
}
这样在调用时,只需要传递一个参数即可1
fibonacci(6);
这时,我们在return语句之前,打印其调用过程1
console.log(`fibonacci(${n}, ${next}, ${current + next})`);
会发现调用过程大大减少1
2
3
4
5fibonacci(6, 1, 1)
fibonacci(5, 1, 2)
fibonacci(4, 2, 3)
fibonacci(3, 3, 5)
fibonacci(2, 5, 8)
注意: 在ES 2015中,只有在strict模式下,才会开启尾调用优化
递推法的思路非常符合计算思路,即,f(0)开始,一个个计算下去,直到加到我们需要的值。1
2
3
4
5
6
7
8
9function fibonacci(n) {
const aFi = new Array(n+1);
aFi[0] = 0; aFi[1] = 1;
for(let i=2; i<= n; i++){
aFi[i] = aFi[i-1] + aFi[i-2];
}
return aFi[n];
}
这里我们定义了一个数组来容纳所有的计算结果,但是实际上,我们仅仅需要f(n-1)
和f(n-2)
两个值,因此我们可以用两个变量存储这两个值来减少内存开销。1
2
3
4
5
6
7
8
9
10
11function fibonacci(n) {
let current = 0;
let next = 1;
let temp;
for(let i = 0; i < n; i++){
temp = current;
current = next;
next += temp;
}
return current;
}
基于此思路,我们对此使用不同的方法进行改写。
结构赋值[3]允许我们将值直接从数组中提取到不同变量中。因此我们可以用结构赋值,省略temp中间变量。1
2
3
4
5
6
7
8function fibonacci(n) {
let current = 0;
let next = 1;
for(let i = 0; i < n; i++){
[current, next] = [next, current + next];
}
return current;
}
1 | function fibonacci(n) { |
这里的-->
并不是limit运算符,这只是两个操作符的缩写。即–和>。
这里的1
while(n --> 0){}
可以改写为1
while(n>0) {n--}
这里解释一下为什么是这样。
n先进行–操作,n自身的值变为n-1。
然后使用n–的返回值与0进行比较大小,而n–的返回值是n。
所以,只要n>0
,那么就会执行n--
1 | function fibonacci(n){ |
这里利用Reduce高级函数[5]的特性,第一个参数为上一次计算的值,因此这里的pp保存F(n-1)值,而seed则保存F(n-2)的值。
Generator是ES2015的新特性,得益于该特性,我们可以使用生成器方法,制作一个斐波那契数列生成器。1
2
3
4
5
6
7
8
9
10
11function* fibonacci(){
let current = 0;
let next = 1;
yield current;
yield next;
while(true) {
[current, next] = [next, current + next];
yield next;
}
}
使用方法即1
2
3
4const fibo = fibonacci();
for(let i=0; i< 10;i ++){
console.log(fibo.next().value);
}
但是这一个生成器并不是可以生成指定n的函数,详细实现方法,以及可能遇到的坑可以参阅这篇文章我从用 JavaScript 写斐波那契生成器中学到的令人惊讶的 7 件事。
斐波那契的通项公式证明,可以参阅百度百科。比照该公式,可以实现如下代码[8]:
1 | function fibonacci(n) { |
以上,便是我当前学习到的解决方案。如果你有更好的解决方案,或者对一些方法有异议,也希望可以在评论区不吝赐教。
[2] 《理解 ES6》阅读整理:函数(Functions)(八)Tail Call Optimization
[4] 关于–>的运算顺序问题
[5] Array.prototype.reduce() - JavaScript | MDN
[6] 斐波那契数列_百度百科
[7] 我从用 JavaScript 写斐波那契生成器中学到的令人惊讶的 7 件事
[9] 尾调用优化 - 阮一峰的网络日志
[10] 斐波那契数列算法优化
]]>Headless browser会带给我非常亲切的感觉,因为总能让我回想起按键精灵和AutoHotKey这两款非常实用的小工具。
能有这样的感觉,大概是因为它们都操作基于用户界面,但是在运行时,可以让用户忽略用户界面吧。
无头浏览器有哪些实用的使用场景呢?
常用的E2E测试工具如nightwatch
,Karma
,都支持无头浏览器,这样在测试时,无需打开UI界面,即可完成对应的测试内容。
在使用一些网站API时,会遇到一些网站需要先登录的情况。
标准的网站,允许使用Post方法发送用户名及密码,返回对应的Token,之后的请求即可使用该Token,这时候我们可以直接使用Request包即可。
但是遇到一些网站,并没有对外开放API接口,每次请求数据是通过登录后的Cookis进行判断。这时候我们也可以使用Request,截取Set-Cookie
头部信息即可。
但是,还有一些网站,在登录时候,需要添加服务器发送给客户端的安全码,这个时候如果单单使用Request
就有些费力了。
这时,使用无头浏览器可以很好的解决这个问题,这里使用Google Chrome的puppeteer编写例子。
1 | const puppeteer = require('puppeteer'); |
简单几步,就可以获取到对应的Cookie信息,将该Cookie信息保存起来,就可以在其他请求中使用了。
在爬取一些网页时,对于普通的网页,我们可以直接使用Request, 发送GET请求,获取页面内容,然后进行分析,获取其中的数据。
但是这里有一个缺陷,即我们只能获取到网页的HTML内容,无法获取到页面XHR获取到的内容,即我们无法执行页面的JS。
这就导致我们无法获取那些动态加载的数据,甚至大部分单页面APP。
这时无头浏览器的作用就非常明显了,无头浏览器即没有用户界面的浏览器,浏览器功能全部存在,因此执行JS也不在话下。
我们使用Request,get请求知乎某用户的关注列表https://www.zhihu.com/people/zhang-shu-yuan-18/following,然后使用Cheerio获取关注的用户名。1
2
3
4
5
6
7
8
9const request = require('request');
const cheerio = require('cheerio');
request.get('https://www.zhihu.com/people/zhang-shu-yuan-18/following', (error, res, body) => {
const $ = cheerio.load(body);
$('.UserLink-link').each((index, item) => {
console.log($(item).text());
})
})
会发现只有三个结果1
2
3柳佳
李沫霖
Jim Liu
这是因为剩余的内容需要Ajax加载,这时,我们使用puppeteer进行获取。
1 | const puppeteer = require('puppeteer'); |
这时,一整页的数据全部加在进来了,打印$('.UserLink-link').length
会发现有40条数据。
当然,如果找到了该页面加载用户的API,直接使用该API请求数据是最方便的了
由于有些搜索引擎在抓取页面的时候,并不执行页面里的JS,因此会导致很多单页面APP的内容无法被搜索引擎更好的收录。
这时,可以使用无头浏览器,做服务端渲染。在判断访问来路为XXX-spider
之后,将页面内容,在服务端使用无头浏览器执行一遍,将执行后的HTML内容,返回给搜索引擎,这样搜索引擎就可以获取到执行JS后的内容了。
最后,这里收集了一些常用的无头浏览器
最近在阅读Codrops时,遇到了一个不错的手机APP效果,想着可以用在视差滚动宣传页中,便尝试着也制作了一下。
整体思路不是很复杂,即旋转整体,展示图片
主要用到的CSS3属性有
1 | transform: rotateY(50deg) rotateX(20deg) translateZ(-$dv-height/2 + $depth); |
1 | @for $i from 1 through 5 { |
在页面展开之后,鼠标滑过每个图层,其他图层透明度为0.1
1 | Array.prototype.filter.call(el.parentNode.children, function(child){ |
active
Class,通过css :not()选择器,选择非.active
Class的元素,设置他们的透明度这里采用的是第二种方法。
在使用Node进行开发的时候,有时候需要在不同的Node版本中进行切换。首先,跨平台的NVM(Node Version Manager)可以帮助我们很好的进行版本管理。
在OS X系统中,HomeBrew也是一个很方便的Node版本切换工具。
以下是使用HomeBrew管理Node的一些操作
1 | $ brew search node |
即可看到当前可用的版本
1 | $ brew install node@6 |
如果需要6.x.x中最新版本,可以使用
1 | $ brew install node6-lts |
1 | $ brew unlink node |
1 | $ brew link node@6 [--force] |
注意:在切换版本的时候,可能会需要用到 –force命令,强制执行。
在切换版本时,可能会有一些文件需要删除,注意提示内容,执行即可
例如:
1 | $ rm '/usr/local/include/node/pthread-fixes.h' |
1 | $ node -v |
1 | $ which node # => /usr/local/bin/node |
1 | $ brew uninstall node |
出现这个问题是在安装Yarn的时候遇到的。在使用HomeBrew
安装Yarn
的时候,需要brew link node
,但是link之后的版本是最新的7.9。
本来新版本的Node带来了更多的特性,然而在使用ng-cli
生成的项目中,打包的时候,node-sass
一直出问题,因此需要工具来管理Node版本,固有此总结。
同时,Yarn也是一个很方便的包管理器,推荐在安装包时尝试一下Yarn
How do I completely uninstall Node.js, and reinstall from beginning (Mac OS X)
]]>How do I downgrade node or install a specific previous version using homebrew?
在开发过程中,有时候需要安装墙外一些包文件,前端常用的包管理工具有node/bower/sass,以及需要git发布内容,解决方案一般有三种:
使用国内镜像的好处是省去搭建梯子的过程,利用国内连接速度优势,快速下载
使用淘宝镜像
https://registry.npm.taobao.org
安装时启用
1 | npm install --registry=https://registry.npm.taobao.org |
设置全局镜像
1 | npm config set registry < registry url > |
cnpm可以很快的安装完包,但是有些项目,比如Angular,有些包可能会安装出现问题。
设置镜像
1 | gem sources --add https://gems.ruby-china.org/ --remove https://rubygems.org/ |
设置代理需要有代理服务,保证可以访问到对应的地址
设置代理
1 | npm config set proxy http://server:port |
取消代理
1 | npm config delete proxy |
查看代理
1 | npm config list |
如果代理不支持https,修改npm存放package的网站地址为非https地址
1 | $ npm config set registry "http://registry.npmjs.org/" |
设置代理
1 | $ git config --global http.proxy http://server:port |
删除代理
1 | git config --global --unset http.proxy |
查看代理
1 | git config --global --get http.proxy |
设置代理
安装时加上 –http-proxy 参数
1 | gem install --http-proxy http://proxy:port sass |
设置代理
1 | # 修改 .bowerrc 文件(如无则新增): |
apm是github出品的Atom编辑器的包管理器,它默认使用npm的设置,如果需要单独设置
设置代理
1 | $ apm config set https-proxy https://server:port |
查看设置
1 | $ apm config list |
可以将命令行直接设置代理,这样命令行里的数据链接都会通过代理
这种设置只对本命令行窗口启用
1 | set http_proxy=http://proxy:port |
1 | sudo networksetup -setwebproxy "Ethernet" http://proxy port |
对于有些几乎没有依赖的包,可以通过直接从node_modules文件夹中拷贝的方法实现安装
1 | gem install --local sass.gem |
OS X上有很多其他的下载需要代理,那么我们可以使用Proxychains
配合 shadowsocks
实现每个命令都可以使用代理
1 | brew install proxychains-ng |
proxychains.conf
文件1 | vim /usr/local/etc/proxychains.conf |
在[ProxyList]
下加入1
socks5 127.0.0.1 1080
proxychains4
为命令代理1 | proxychains4 curl https://www.twitter.com/ |
在使用Hexo创建博客的时候,所有的博客内容为了与主题内容相同,使用了中文命名,导致生成的链接也是中文目录。
最近将博客迁移到Centos中之后,由于中文文件名导致404问题。所以决定将所有的中文文件名改为对应的英文名,希望仿照W3CPlus的命名方式
最初使用手动方式将文件名拷贝到谷歌翻译,得到翻译结果之后,将翻译结果变为小写,将空格替换为”-“,由于重复操作太多,所以决定写个小工具,来进行后面的2步操作。
由于Vue
的双向数据绑定的便利,所以使用Vue对数据进行监听修改,采用loadash来进行数据处理
1 | _.lowerCase(str) |
使用正则表达式替换即可
1 | str.replace(/\s+/g, "-") |
但是这时候可能会出现一个问题,在字符串前后都有空格的时候" Hello World "
,会生成"-hello-world-"
,这不是我们需要的
所以在替换之前将首尾空格去掉即可
1 | _.trim(str) |
所以初版就是这样子的
但是这样还是需要切换页面进行复制粘贴,因此可以直接将翻译过程省略,首先想到的是有道翻译api,申请完key之后,发现如果使用json方式获取数据,那么会有跨域问题,只能使用jsonp方式,但是vue官方推荐的axios并不支持jsonp,所以采用vue-resource。
但是每次输入框发生变化,就会触发一次数据请求,而有道翻译每天提供1000次请求,所以,使用lodash的debounce方法,减少请求次数,在输入结束500ms之后,再发起请求。
1 | getTrans:_.debounce(function(){},500) |
最后使用clipboardjs为格式化的结果提供一个复制功能。这样就更加方便了。
]]>但是这样还是有些不方便,因为仍然需要选择文件名,然后粘贴,再复制粘贴,多了很多重复操作,所以可以使用Node的文档读取与操作功能实现该功能。
在企业开发项目时候,有时需要通过外网网络访问内部服务器,这时候可以通过搭建一个shadowsocks服务器,然后通过shadowsocks客户端连接服务器,访问内网内容。
安装过程如下,服务器已经安装好node服务,并且可以使用npm
命令
全局安装shadowsocks模块
1 | npm install -g shadowsocks |
找到安装目录C:\Users\Administrator\AppData\Roaming\npm\node_modules\shadowsocks
,编辑config.json
文件
1 | { |
启动服务,执行命令
1 | ssserver |
在使用vs2015开发前端项目的时候,将整个网站项目引用进解决方案之后,软件会扫描全部的文件夹。
但是node_modules
,bower_components
的文件夹嵌套,会严重影响扫描的速度
暂时的解决方案是,将不需要被扫描的文件夹隐藏即可,但是要取消掉隐藏二级目录
]]>在页面布局中,在使用float布局大量相同属性元素的时候,如果元素的高度不固定,某个元素的高度过高,可能会阻挡元素的“流动”,会出现如图的情况。
这时我们只要保证后面的高度也大于或等于该元素高度,即可让后面的元素流动到前面
所以一种常见的解决方案是
瀑布流的实现方法,网上已经有大量教程,详情参阅
有时候,我们可能并不需要瀑布流的布局,因为在展示某些数据的时候,会显得比较混乱。
要实现该效果,只需如下几步
如果遇到使用ng-repeat生成的元素无法获取自动高度问题,可以参考如下文章
]]>
在使用Angular进行开发的过程中,使用ng-repeat生成多个元素之后,如果元素的宽高是auto,那么我们在使用css()
、getComputedStyle
、offsetHeight
或者clientHeight
都会获取到0,我们无法获取到元素的实际高度。
这是因为DOM的渲染是异步的,导致计算元素属性信息在DOM渲染完成之前就已经完成了,因此无法获取到DOM真正渲染结束之后属性。
在Angular中,我们可以使用以下几种方法进行处理
当元素信息发生改变之后,将最新的数据赋值给变量即可
例如:
Directive
1 | myApp.directive('heightBind', function() { |
HTML1
< div height-bind height-value="containerHeight"></div>
$apply
来完成同样的事情1 | setTimeout(function(){ |
我们知道,angular的一些方法会自动进行脏值检查,因此我们可以将上面的方法稍微改动一下即可
1 | $timeout(function () { |
]]>参考资料 http://stackoverflow.com/questions/25108780/height-of-container-with-ng-repeat-directive-is-zero