242019.3

手机端真机调试

https://segmentfault.com/a/1190000018613578

212019.3

文件名绝对不能使用驼峰命名方式,原因很简单,不同操作系统对大小写敏感程度不同,导致修改文件名之后无法被识别,git不对这些文件进行任何处理,但是你在代码里面又是使用新的文件名。等你部署的时候,代码报错说找不到这些文件。正确的文件名命名方式是使用-或_连接单词,但是也又一些命名为了省空间不使用连接符,全部小写,这样会导致一些单词连在一起之后,有歧义,而且说实话,工程项目,有的时候为了提升团队协作的整体效率而牺牲小部分性能,也是可以接受的,成熟的现代编程,会平衡代码开发时和运行时的体验,而非一味追求运行时体验。

15:24:15 已有0条回复
202019.3

js数据模型驱动器

在一次表单重构中,我逐渐意识到,在数据和UI分开的前端开发模式中,UI和数据越来越需要解耦,UI要完完全全和数据本身无关,做到无状态,做到完全靠数据驱动,并且提供响应接口(更新UI的接口)。而数据层面,如果考虑到自身将被用于渲染,那么就要配置一个驱动器,将原始的数据结构转化为便于UI使用的数据结构,这就是独立于UI的数据模型驱动器。

拿表单来说,市面上有一些基于jquery的表单框架,虽然说是框架,实际上,还是一套基于js配置的ui组件,并非真正意义上的框架。而我的思考是,我们同一套表单,它背后的业务逻辑、数据提交的验证、编辑时的数据回溯等等问题,也就是脱离了UI层面的其他逻辑问题,都可以抽象出来。现在的开发全部是多端开发,同一个表单,在UI界面上可能不同,使用的UI技术也可能不同,例如在公众号网页和在小程序中,甚至在ReactNative构建的原生app中,他们抛开UI、交互层面的东西,背后的业务逻辑几乎是一摸一样的,为什么不可以共享?而且这三种场景,背后都支持js原生的计算能力和语言特性,所以,很明显是一定可以共享的,所不同的是,在将模型和UI框架进行拼接时,拼接方式不同。

表单模型只是一个例子。这种将模型抽离出UI的方案,适用于大部分带有业务逻辑的中后台应用中,在都使用了web技术的前提下,想多端共享同一套业务逻辑,就应该用这种思路去写。

现在的问题是,有没有一套完整方案(框架),去像React那样,编写模型。我认为还没有,或者说,还没有一个UI框架是基于这种考虑去实现的,因此,也就没有模型框架可以被应用到实际开发中。如果有一种UI框架(特别是React),可以基于“模型-视图”的方式去开发自己,那么就更有利于模型框架的出现。就目前而言,最接近这种形态的是rxjs作为模型框架,react作为UI框架。

01:12:27 已有0条回复
132019.3

第一步,在鼠标setting里面激活secondary click,并选择click on left side。这样就可以左右两个键互换了。接下来就是修改鼠标指针,参考如下步骤。

1. Download Mousecape.zip containing the 0.0.5 pre-release and install.
2. Install the helper tool from the menu bar by clicking Mousecape > Install Helper Tool.
3. From Mousecape preferences (Command-,) select "I am... left handed"
4. From the menu bar click Capes > Dump Cursors.
5. Click the cursor dump to highlight. Command-Enter to apply. Command-R to restore the default cursors.

16:19:27 已有0条回复

npm依赖应该放在哪一个字段?

在一个前端项目中,我们把依赖通过特有的pacakge.json,其中的dependencies字段提供给npm来安装依赖。自从npm升级之后,就增加了依赖的控制,例如增加package-lock.json来锁定开发时使用的依赖版本。而我们的项目,很多会上到CI系统去构建,然后通过CD系统来部署到我们的生产环境中。这就会带来一个问题,我们在开发环境中使用的依赖,应不应该在CI系统中安装,因为大部分CI系统都不具备开发环境那么宽松的编译环境。那么到底要怎么来安排我们的开发环境依赖、CI环境依赖和生产环境依赖?

首先理解三种依赖:

dependencies

本包的依赖。即当另外一个包/项目依赖你这个包的时候,你的包必须依赖的其他包。npm在安装一个包的时候,它不单单只安装了这个包本身,同时还安装了dependencies里面指定的依赖。例如:

npm i databaxe

你会发现,不单单databaxe这个包被安装了,databaxe这个包的package.json里面的dependencies字段里面的包也被安装了。(devDependencies和optionalDependencies里面的包没有被安装。)

由于我们大部分前端项目,在现阶段还是需要通过构建工具构建的,因此,对于一个完整的上线项目而言,dependecies里面只需要放置node端的modules即可,例如express等服务器依赖包,而不需要安装jquery等前端代码需要的包,因为,前端代码的依赖包会被webpack等工具打包进去。这些在构建阶段用到的包,可以放在devDependencies里面。

但是,对于一个组件,或者一个第三方前端包,你需要考虑的是,你的包被其他项目引用时,会由其他项目去自己实现构建过程。你的代码里面的依赖可以和该项目其他依赖的依赖共用,例如前面提到的jquery,有可能你的包和别人的包都用到了,是一个公用的依赖,因此,你不应该把jquery的代码打包到你自己发布的包代码中,而是作为依赖的形式,添加到dependencies中去。这样,当该项目在构建时,会同时使用你的包、别人的包、jquery去进行构建。这样保证了代码的复用和代码量的减少。

devDependencies

本包的开发依赖。当你在开发一个包的时候,你可能需要用到一些用于开发的依赖,例如webpack,babel等。这些依赖只会在开发构建阶段(也包括你上到CI系统进行构建时)用到,而不会在生产环境用到。npm在安装一个包时,这个包pacakge.json里面的devDependencies里面的包不会被同时安装。但你在把这个包的源码克隆到本地,并准备开发时,你在项目目录下执行

npm i

你就会发现,这些包被安装了。而且,这些包里面的package.json里面的规定,也完全和本文所讲的规则一致。也就是说,虽然这些包是放在devDependencies里面,但是,它们被安装时,它们自己package.json里面的devDependencies里面的包不会被安装,因为,这些是它们自己的开发依赖。当它们被安装时,是被当作一个正式的第三方产品来使用。

optionalDependencies

本包的可选依赖。当你在开发一个包的时候,你可能希望有些包是可选的,比如,一个包由AB两位同学在开发。其中A开发时由于自己的习惯,使用到了一个包,但是B同学没有这个习惯,他不希望自己在开发时这个多余的包被下载浪费时间,这个时候,这个包应该被放在optionalDependencies里面。npm在安装一个包的时候,该包的optionalDependencies里面的依赖也不会被安装。但是,在开发时,你执行

npm i

这些包还是会被安装。你可能会问,那它和devDependencies有啥区别?区别在于,你可以使用参数让它们不被安装:

npm i --no-optional

通过这个参数,在执行npm i的时候,optionalDependencies就不会被安装了。

不同的开发环境如何正确选择一个包应该放在哪里?

  • 但凡用于构建的、测试的、质量检查的,都放到devDependencies里面
  • 但凡仅用于本地开发,例如预览等,都放到optionalDependencies里面

例如,我们经常会在本地开发时,起一个devserver来对开发结果进行预览,但是在上到CI系统时,这个功能根本不会用到,所以,我们把这些依赖放到optionalDependencies里面,而在CI系统构建脚本中,一定要加上--no-optional参数。

所以,最终总结下来,我们大致可以这么认为:dependencies是给生产环境用的,devDependencies是给CI构建用的,optionalDependencies是给本地开发用的。虽然这个理解有偏差,但对于对这个知识点刚刚了解的同学来说,非常有用。

13:46:41 已有0条回复
062019.3

创建可缓存计算过程的函数

受reselect启发,实际上,对于某类特定函数——输入相同的情况下,输出一定相同的纯函数,实际上,对于相同的输入参数,没有必要进行多次计算,而是可以将结果缓存起来,在识别到相同的参数时,直接取出该参数对应的结果即可。因此,我编写了computex:

/**
 * 创建一个可缓存的纯函数。
 * 使用场景:纯函数,相同的参数永远得到相同的结果。
 * @param {*} fn 原始纯函数
 * @param {*} expire 缓存过期时间
 * @example
 * const fn = computex((state) => {
 *   return state.name + ':' + state.age
 * })
 * var a1 = fn({})
 * var a2 = fn({})
 * // a1 === a2 // 因为参数相同,所以结果使用了缓存,因此也相同(引用相同)
 */
export function computex(fn, expire = 60000) {
  const cache = {}

  // 创建一个循环任务,检查缓存是否过期,如果过期,则删除缓存,释放内存
  const recycle = () => {
    const keys = Object.keys(cache)
    keys.forEach((key) => {
      const { time } = cache[key]
      if (time + expire <= Date.now()) {
        delete cache[key]
      }
    })
    setTimeout(recycle, 1000)
  }

  // 如果expire设置为0,则缓存永久生效
  if (expire) {
    recycle()
  }

  return function(...args) {
    const hash = getObjectHash(args)
    if (hash in cache) {
      const item = cache[hash]
      return item.result
    }

    const result = fn.apply(this, args)
    const time = Date.now()
    cache[hash] = { result, time }

    return result
  }
}

其中computex本身是一个函数,它接收一个函数,返回一个函数,返回的函数是接收到函数的复刻版,运行它得到的结果和运行传入函数是一样的。这样就可以做到缓存计算过程。

/**
 * 创建一个在同一个同步进程中只获取一次的函数。
 * 使用场景:反复调用一个函数,而该函数在本批次运行时,实际执行结果应该相同。
 * @param {*} fn
 * @param {number} expire 缓存过期时间,根据实际的情况来看,如果一个运算比较大,可以设大一点
 * @example
 * const get = getx(() => Date.now())
 * var a1 = get()
 * var a2 = get()
 * // a1 === a2 // 在同步进程中,直接使用缓存
 */
export function getx(fn, expire = 10) {
  var iscalling = false
  var cache = null
  var timer = null

  return function() {
    clearTimeout(timer)
    timer = setTimeout(() => {
      iscalling = false
      cache = null
    }, expire)

    if (iscalling) {
      return cache
    }

    iscalling = true

    const result = fn.call(this)
    cache = result
    return result
  }
}

getx是另外一个逻辑。当在同一个同步进程中,我们可能会反复调用一个函数,而这些函数在这一次反复调用时,实际上是执行了同一个过程,所以,得到的结果也是一样的,但是在执行过程中消耗了资源。因此,我们可以将这些结果缓存起来,在这一个同步进程结束后再清理掉即可。

/**
 * 创建一个缓存Promise的异步函数
 * @param {*} fn
 * @param {*} expire 缓存的过期时间,不设置的时候,当Promise结束缓存就会被清空,而如果设置了expire,完全按照expire清空缓存,而不会依赖Promise的结束
 */
export function asyncx(fn, expire = 0) {
	const cache = {}

	// 创建一个循环任务,检查缓存是否过期,如果过期,则删除缓存,释放内存
	const recycle = () => {
		const keys = Object.keys(cache)
		keys.forEach((key) => {
			const { time } = cache[key]
			if (time + expire <= Date.now()) {
				delete cache[key]
			}
		})
		setTimeout(recycle, 1000)
	}

	// 如果expire设置为0,则缓存永久生效
	if (expire > 0) {
		recycle()
	}

	return function(...args) {
		const hash = getObjectHash(args)
		if (hash in cache) {
			const item = cache[hash]
			return item.deferer
		}

		const deferer = new Promise((resolve, reject) => {
			Promise.resolve().then(() => fn.apply(this, args)).then(resolve).catch(reject).then(() => {
				if (expire > 0) {
					return
				}
				delete cache[hash]
			})
		})
		const time = Date.now()
		cache[hash] = { deferer, time }

		return deferer
	}
}

而对于基于Promise的异步操作,则通过asyncx来实现缓存。缓存的并非Promise最终返回的结果,而是缓存Promise本身,在一个Promise进行中的时候,如果再次调用这个函数,得到的会是未结束的Promise,而非重新发起一个Promise。这有助于解决短时间内反复请求某个api接口的情况。

20:27:44 已有2条回复
  1. hello,有个问题要请教一下你,之前你说过网易云的播放器音质挺烂,你说过用了什么耳机再来用苹果的,iphone的音质很差。 我想要你帮我推荐一个手机耳机,或者说是便捷式耳机。不不太想要太大,如果说在体验上很好也是可以接受的。


    一直关注你。
    #735 苦瓜 2019-03-08 21:48 回复
  2. 谢谢关注,其实我对耳机要求不高,所以没有太多研究。你可以试试airpod,时尚感比较强,如果是追求音质,还是推荐索尼的耳机,如果希望续航长一点,可以考虑项圈的,我买的JBL虽然音质我还觉得不错,但是续航弱。
    #742 回复给#735 否子戈 2019-03-11 10:15 回复

获取一个对象的hash值

我们比较两个对象,比较好的一种办法是直接比较两个对象的hash值,对于相同结构和内容的对象,hash值应该相等。怎么获取对象的hash值?

/**
 * 获取一个字符串的hash
 * @param {*} str
 */
function getStringHash(str) {
	let hash = 5381
	let i = str.length

	while(i) {
		hash = (hash * 33) ^ str.charCodeAt(--i)
	}

	return hash >>> 0
}

/**
 * 格式化对象,类似JSON.stringify,但是支持自引用嵌套
 * @param {*} obj
 */
export function stringifyObject(obj) {
	const exists = [obj] // 存储已经处理过的,避免死循环
	const used = [] // 记录被用到的引用标记
	const stringifyObjectByKeys = (obj) => {
		if (isArray(obj)) {
			let items = obj.map((item) => {
				if (item && typeof item === 'object') {
					return stringifyObjectByKeys(item)
				}
				else {
					return JSON.stringify(item)
				}
			})
			let str = '[' + items.join(',') + ']'
			return str
		}

		let str = '{'
		let keys = Object.keys(obj)
		let total = keys.length
		keys.sort()
		keys.forEach((key, i) => {
			let value = obj[key]
			str += key + ':'

			if (value && typeof value === 'object') {
				let index = exists.indexOf(value)
				if (index > -1) {
					str += '#' + index
					used.push(index)
				}
				else {
					exists.push(value)
					let num = exists.length - 1
					str += '#' + num + stringifyObjectByKeys(value)
				}
			}
			else {
				str += JSON.stringify(value)
			}

			if (i < total - 1) {
				str += ','
			}
		})
		str += '}'
		return str
	}
	let str = stringifyObjectByKeys(obj)

	exists.forEach((item, i) => {
		if (!used.includes(i)) {
			str = str.replace(new RegExp(`:#${i}`, 'g'), ':')
		}
	})

	if (used.includes(0)) {
		str = '#0' + str
	}

	return str
}

/**
 * 获取一个对象的hash值
 * @param {*} obj
 */
export function getObjectHash(obj) {
	if (typeof obj !== 'object') {
		return
	}

	let str = stringifyObject(obj)
	let hash = getStringHash(str)
	return hash
}

这里面考虑到了一个非常严重的问题,就是当一个对象如果存在自引用的情况,普通的遍历会引起死循环而导致程序挂掉,但这个函数不会,它会用一个临时变量去记录已经处理过的对象,如果在下一次处理到一个对象时,首先会去临时变量里面查找,如果找到的话,会把这个变量的索引值作为参考系数,直接用于hash计算。

14:11:38 已有0条回复
052019.3

给博客加上内置二维码功能

之前用了qr.liantu.com的接口生存二维码,并且通过curl抓取到接口生成的二维码图片之后,再保存到本地的一个cache目录下。之前用着挺爽的,但是最近这个网站开始抽风,生成二维码非常慢,最后完全无法忍受了,于是决定还是自己生成二维码得了。

我的目标是给每一篇文章生成一个二维码,放在cache目录下,在我的首页和路迹栏目都需要用到。于是下载了知名的二维码库phpqrcode,然后集成到我自己的工具函数中。自己写的代码如下:

<?php

include_once(__DIR__.'/phpqrcode/qrlib.php');

function get_qr_code_url($text) {
  $path = "/cache/qrcode-".md5($text).".png";
  $file = WP_CONTENT_DIR.$path;

  if (!file_exists($file)) {
    QRcode::png($text, $file, QR_ECLEVEL_L, 4);
  }

  $url = home_url("/wp-content".$path);
  return $url;
}

然后在主题文件中调用get_qr_code_url函数即可。$text则通过get_the_permalink()得到,这样就快速把二维码集成到自己到博客中了。

22:51:18 已有0条回复