连续加班1个月,感觉很久没玩js了。碰到群里同学问前端题,我就写了一下。没想到花了1h时间。
时间怎么使用的?就是这样的时间用掉的
题目:
…… QQ 收藏
看到这样的题目,我自己也能够出很多类似的题目。比如生成随机华容道图形。
data数据如下:
奴隶社会,非洲,古埃及文明,金字塔
,亚洲,两河流域文明,汉谟拉比法典
,,古印度,种姓制度
,,,佛教的创立
,欧洲,希腊,希腊城邦
,,,雅典民主
,,罗马,城邦
,,,帝国的征服与扩展
,,希腊罗马古典文化,建筑艺术
,,,公历
初版本: http://js.jirengu.com/cohur/1
优化版本:http://js.jirengu.com/cohur/4
优化版本,添加了正则,让结果看起来跟题目想更像一些。
后者是不是添足了?
当前在山东第一家民营银行做外包,到了压力巨大的时候。
客户提出要更新页面UI,从来没有适配过IE的我,招了一大堆UI。
今天,不,半个小时之前的昨天,我看到juejin chrome tab页面中推的layui。
看到之后觉得挺合适的。最主要的是,看起来不需要自己配置requireJs等。
在vue中一般不需要直接操作DOM,当我看到自定义指令这章时…
我在写一个server端, 【github repo】
当我向socket读取数据时,希望降低频率。
在android shell中运行minicap「github repo」 ,得到的屏幕刷新数据流。
android 设备屏幕一旦有刷新,minicap就会将屏幕显示的完整画面,通过socket传输。
与常见socket的区别:
和fs.readStream对比,这个socket的读取不是一次性读完的,而是持续产生并输出的。
这个socket的用法,通过websocket Server转发这里的socket,实现通过浏览器显示手机屏幕。
|
|
我想控制这里for循环的fps。
这里stream.read()
将会一直读取socket,如果没有返回则会等待返回,一旦完成,则继续下一个。
于是,for
的大括号里的代码,无法影响到for循环的进程。
|
|
在node中,使用 child_process.spawn 或 fs.readFile 直接拿到的是 Buffer。
使用ws直接传输buffer想来会更高效率。
占坑
node读取到buffer后,设定缺省编码 ‘utf8’.
一般的api中可以手动设定为 ‘binary’ 以二进制方式读取buffer。
读取到的 buffer 有大小。一般的,api中能够设置buffer的单元大小。比如 highWaterMark
|
|
拿到buffer之后,其在内存中的存储就像这样
使用JSON.stringify() 将buffer转换为字符串,再使用JSON.parse() 解析,会得到格式如 { type: 'buffer', buffer: [...]}
的对象。
根据Buffer的基本格式,可以重塑该Buffer
Chrome中没有Buffer对象,有ArrayBuffer
new Uint8Array
]]> Promise 变体。
代码示例脱胎于 execSequence 方法 与 TaskSequence.prototype.executeOne 方法。
与常见的promise相比,变化的地方在于,then(Function) 的参数中的 resolve 被存到了数组中。
项目使用DB2数据库,上线时要将更新的SQL打包。统计SQL时,同样的数据两次INSERT会提示主键冲突。于是,尝试写一个分析SQL语句的功能,用来替代或简便手工检查。
从npmjs.org上,找其他包时,发现其包内的单元测试。决定尝试一些这样的套路。
在 npmjs.org 上选了若干包安装尝试解析项目中的SQL语句,遇到了问题:VALUES中含有单引号转义时,解析不成功,或者拿到结果需要再解析一遍单引号转义。
观察VALUES的结构,手动解析逻辑比较简单。
VALUES中划分每个字段的标示是逗号,当value中有逗号时,一行正则无法区分是value中的逗号,还是外部的逗号。
将划分标示扩展为 ` ‘ , ‘` 依然会遇到标识在value中出现的可能。
尝试先解析转移的单引号 ,未能区分value两侧的单引号,还是转义的单引号。
尝试写了一些正则,代码没成功,丢弃了。
分析按照大脑识别的过程,当遇到单引号时,根据之前的状态,是准备取字段,还是正在取字段中的值确定单引号用来结束或开始转义,还是结束或开始取值。
于是,约定状态
|
|
在不同状态下,读到的特殊符号,完成转义或结束取值行为,并更改到新状态。
状态发生变化时,是在后一个字符确定的,所有识别到变化时候,需要 i– 或者手动补上上一位的操作。
然后,收集错误
解析完毕之后,临时取值池中的值应该为undefined,如果不为空,则判定为异常停止
遇到不预期的字符,可以跳出for循环,设置标示变量,ifBreakFor,用来在switch中跳出外部for
直接阅读mocha文档,不觉得有什么用途。
当写了这个解决其他包不能够的痛点时,用mocha让测试意图非常清晰+固定。
执行结果
这是一个感受到mocha必要的地方。
]]>尝试Socket,使用之中有很多问题。
首先,在图解HTTP一书中,知道http头部有个connection,能够用来将http协议升级到websocet。
然后,在node环境下,使用http模块启动web server,让能够直接处理http头部。
现在,要使用websocket了,那么,是不是将要接触将http 升级到 websocket的细节了?
不,倾向于使用下载量超级多的npm包。这些包之中,应该做了升级协议的细节。
筛选到的包有 ws
,faye-websocket
。 这两个包的下载量最多。
浏览了下包的首页介绍,其中前者可以像浏览器中一样,使用现成的WebSocket API。
后者,在web-dev-server中的hmr相关代码中发现有依赖到。浏览器介绍,能看出是在http模块上进行处理。
ws
的 express 例子README中的express例子简短的展示了与express结合使用的基本法。
未能演示如何使用拿到的ws。
在github repo中的example中,服务器状态 这个例子,展示了前后端使用websokcet通讯的使用。
ws
从前端发送指令,到后端执行代码: https://github.com/songlairui/amarscfpack 中的 server.js (node后端) 与 static(前端)
前端先使用 (connectWS
方法)[https://github.com/songlairui/amarscfpack/blob/master/static/websocket.part.js]建立websokcet连接, 并做了断线重连的操作
然后使用 sendCmd
方法,按照格式发送JSON.stringify处理完成的指令。
... ws.send(JSON.stringify(Object.assign(command, { taskid })))...
后端接受后,使用JSON.parse解析的到指令内容,进行判断并执行相应指令
... ws.on('message', function incomint(message) { console.info(`received: ${message} `) dealWithMsg(ws, message) })...
发送数据时,添加任务id,异步事件处理完成后,通知前端时加上此标示id,执行前端回掉函数。
详细: 想了一下,Promise可以这样写
前端通过ws
发送String简便易行,在后端拿到buffer更为方便。后端往前端传输buffer时,想带上额外的标示信息怎么办?
使用了一种将buffer stringify,然后放在 {} 中再stringify以传输。降低了效率,完成了传输。
如果直接传输buffer,没有标示信息,需要在前端添加机制,约定好buffer的位置。这种机制未尝试,但尝试了前后端传输 buffer 的细节处理
详细: 通过ws包前后端相互传输ArrayBuffer/Buffer
需要了解http请求头的字段,在websocket中解析的使用方式。
]]>仿照网易云音乐播放界面的一个页面: https://songlairui.github.io/NeteaseMusic/static
跟随音乐显示歌词,用了两种方式实现:ontimeupdate事件、循环调用setTimeout。
有gif,流量党慎点。
生成的歌词DOM结构
|
|
基本需求:跟随歌曲播放显示歌词
更多需求:歌词严格跟随播放进度
1. 歌词激活-样式逻辑
当指定歌词激活后,为其添加标识样式 ‘.current’,CSS中为此样式设定显著的颜色和阴影。
计算当前歌词的相对父元素的高度,调节translateY使其在第三行位置(歌词显示区域中间位置)。
关键代码:
显示效果:
2. 使用进度条常用方法
为HTMLMediaElement制作自定义进度条时,会用到 timeupdate 事件。//TODO:MDN
ontimeupdate | - | 控制台 |
---|---|---|
- | ||
为audio设置事件 | - | 点击播放后,控制台会按照audio的频率打印当前播放时间 |
3. 筛选出当前应该激活的歌词
在updateLrc方法中,筛选出来当前激活的歌词。
updateLrc 代码1:
显示效果:
页面 | - | 控制台 |
---|---|---|
- | ||
选取到指定歌词,然后激活 | - | 控制台输出,每出发一次timeupdate,都更新一下要激活的歌词。 |
这儿配合声音听的话,歌词早了一句,代码先修正,再分析。
updateLrc 代码2:
filter
返回的是一个二维数组,末尾加[0]
即取第一个值。 [0][0]
,则在下边判null
的时候,会报错。 activeLrcItem
设计这样的传参用法,能同时兼容另一种歌词策略。 updateLrc 代码3 【添加新方法,增加可读性】:
在filter
写了三行的代码逻辑被简化了。
为其创建额外方法之后,将filter
变成了简写状态,私以为代码可读性提高了。
使用ontimeupdate,依赖audio元素自身事件的机制。如果ontimeupdate的频率太慢,两个事件间隔之间,更新了多个歌词的情况下,歌词的激活就会出现skip。 可以使用30倍速播放音乐,查看ontimeupdate方法等表现。
30倍速播放 DEMO
Step 1 方法 | - | 新方法 |
---|---|---|
- | ||
会出现跳词 | - | 每句歌词都被激活 |
另一个细节是,歌词中空行和下一句歌词的时间间隔很小。使用ontimeupdate方法,激活空行(令其显示在中间位置)的概率很小(是个概率事件)。
而使用settimeout方法,激活空行,是必然事件。
实现细节:
准备全局变量(未进行组件化,粗鲁的使用全局变量了):
开始播放时,执行一个方法操作歌词:
function playLrc() { if (timer) return let currentStamp = audio.currentTime // 获取当前激活的歌词,和下一个要激活的歌词, 此处正向获取。因为下一个歌词还没有播放,等待setTimeout延迟激活, let nextLrc = lrc .filter(v => v[0]) // 清空掉没有时间参数的歌词 .filter(v => lrcTime2Second(v[0]) > currentStamp)[0] if (nextLrc) { console.info(`下一歌词:${nextLrc}`) timer = setTimeout(function() { clearTimeout(timer), timer = null activeLrcItem(nextLrc[0]) playLrc() // 尾调用自身 }, (lrcTime2Second(nextLrc[0]) - (+currentStamp)) * 1000 / audio.playbackRate) }}
timer
,并在setTimeout中函数真正执行时,clear掉,并置null。然后在尾部调用自身。 activeLrcItem(nextLrc[0])
这句。 audio.playbackRate
, 匹配不同播放速度下切换行为。 audio.playbackRate
为‘负’值,就gg了。。好快就说完了,记得做的时候,来回调了特别多遍。
DEMO地址: https://songlairui.github.io/NeteaseMusic/static
上述两方法实现过程,还有很枝外细节,写在最后。
准备全局变量:
let activedLrcEl = new Set() // cache for 激活的歌词
未使用缓存变量 | - | 使用了缓存变量 |
---|---|---|
- | ||
使用filter方法对DOM遍历读取 | - | 每次激活li,都把其放到Set中,并在取消激活时删除掉 |
使用缓存变量之后,很起来很骚的长行代码去掉了。
为什么用Set?因为不用考虑去重了。
使用效果: |
---|
settimeout方法下,使用30倍速度播放,查看控制台。此时因为setTimeout的误差,造成超时时间取得过短,playLrc方法执行频次远超实际需要,造成额外尝试操作DOM。缓存变量的使用,会使得其在需要操作DOM时,真正去执行。 |
歌词播放完之后,想让它跳回头部,而不是滚回头部。
这样处理的想法时,歌曲结束之后,.3s的时间滚回头部,如果看到觉得太黏腻,不利落。
这儿用到void el.clientWidth。算是一种黑魔法?
切换 visibility | - | 切换 display |
---|---|---|
- | ||
无效 | - | 生效 |
使用display:none会达到想要的效果。猜测跟浏览器绘制过程有关。
默认情况,空的一行会有一个小的height,因为字体高度为0。
让空的一行也显示有内容,就可以获得正常的高度。为每个歌词添加伪元素,并设置content: ' '
,可令每一行都最少有一个空格。
如果有强迫症,可以前后各加一个空格,让歌词居中。
这是个人觉得正确使用reduce的一次了,虽然最后注释掉了。
//写的时候挺费脑子的,不舍得删,注释掉吧。let currentStamp = audio.currentTimelet { currentLrc, nextLrc } = lrc.filter(v => v[0]).reduce((prev, current) => { // 如果传入的值有了nextLrc,说明取到了想要的值 // console.info(`reduce 得到的上一个的返回值:${JSON.stringify(prev)}`) let { currentLrc, nextLrc } = prev if (nextLrc) return prev return lrcTime2Second(current[0]) > currentStamp ? { currentLrc, nextLrc: current[0] } : { currentLrc: current[0], nextLrc: '' }}, { currentLrc: '', nextLrc: '' })
根据audio的当前时间,使用reduce取得当前歌词和下一条歌词。
最初没有使用缓存变量时,简单的把上一条歌词取消激活用到了这个方法。虽然这种取消非常不严谨。但这个reduce写起来很爽。
支持方法的切换,通过增加wrap函数和多处使用3元运算符,逗号运算符完成。//有点丧心病狂,就不截图了
]]>上一篇Vue SSR 官方文档实践·一:从零到粗暴混合前后端之后,运行Build Configuration 章节示例代码,比较快了。
不过多加了几个webpack选项,混合起来变得更加简单。大神铺路铺得就是好。。
实践Target: Build Configuration
启用前 | - | 启用后 |
---|---|---|
- | ||
启用前,异步加载的组件,被分割开来 | - | 启用后Server端打包不再进行分割,只输出一个文件 |
客户端打包,对文件分割实现懒加载,可以令浏览器只加载需要的内容,有体验提升。
但服务端打包的文件,给后端程序使用,不通过网络请求获取,分割代码意义不大甚至降低性能,所以打包成一个文件更好。
相关配置文件
对比
启用前 | - | 启用后 |
---|---|---|
- | ||
启用前,前后端输出文件名称一致,会造成覆盖 | - | 启用后,生成 manifest, 可以使用vue-ssr插件自动注入js |
启用前,在页面注入JS,需要手动在 index.template.html 作如下添加
1. 更改renderer
前 | - | 后 |
---|---|---|
- | ||
createRender | - | 更改为 createBundleRenderer,并由打包生成的单文件创建 |
2. 更改express配置
前 | - | 后 |
---|---|---|
- | ||
从打包得到的文件引入createApp | - | render中自动包含app内容,不需要引入。只需要传入context |
添加webpack关键配置
自动注入的script.src 都指向 dist 目录下文件,server.js中强制redirect的中间件逻辑可以删除了。
回顾一下,完成服务端渲染的配置,大部分靠webpack的熟练度。
代码地址: songlairui/vue-playground/demo/chapter5
├── webpack.base.conf.js # 创建baseConfig,方便使用webpack-merge├── webpack.client2.conf.js # 启用vue-ssr-client-bundle 插件,这是个子插件。启用manifest插件。├── webpack.server2.conf.js # 启用vue-ssr-server-plugin 插件,这是个子插件。打包只出一个文件。├── server2.js # 简化 server.js 逻辑。去掉强制redirect逻辑。
]]>Vue 2.3 发布很久了,距离第一次打开ssr.vuejs也很久了。
现在我终于把其中的代码片段运行起来了。
Github Repo: https://github.com/songlairui/vue-playground
vue-server-renderer
实现使用 console.log(html)
将渲染过后的html打印到屏幕上。
即,将 new Vue({…}) 变成输出结果。跳过从浏览器获得结果。
文档第四章Source Code Structure 出现了目录结构,而且js文件中有了import关键字。
这里无法像前面的代码片段一样,直接在node里粘贴代码可执行。这里需要使用webpack。
这时的我:使用vue-cli创建vue项目脚手架是唯一的webpack使用经验。
然后我去 学习webpack
代码示例: songlairui/vue-playground/demo/chapter4
目标: 让这样的目录能够执行
|
|
main.js、entry-client.js、entry-server.js 代码从文档中复制来。
*.vue 文件内容自己补充。
这里需要webpack的概念: 入口文件、输出文件、模块、插件、打包目标平台。
啃了webpack文档后 [跳转…]
deal with 新出现的 import
在基本的配置文件上增加babel-loader
配置即可将import转译为node和浏览器可以支持引入方式了。
这里需要将 import
转译为 commonjs
方式,设置babel-loader的query为
modules 默认为commonjs,可以省略。其他可选umd,amd等。
// TODO 如果可能,会写一篇初试requirejs,commonjs.
deal with vue单文件组件
*.vue
单文件组件通过import引入,为其添加vue-loader
,即可正确引入。
module.target
webpack module默认的target是 web,为服务端代码进行打包时,需要设置target为 node 或 aysnc-node。
我已有的vue使用经验中,在router中,使用ensure即可轻松实现懒加载。
动态引入模块,可以完成懒加载文件的打包
改写router.js
使用vscode中js formatter插件保存时自动格式化代码时,会将import的格式强行换行。可以停用自动格式化。
启用动态加载
webpack配置rules中,为babel-loader启用动态引入插件
dist目录下打包结果如下:
client.conf | - | server.conf |
---|---|---|
0.js | - | 0.js |
client.bundle.js | - | server.bundle.js |
client和server配置文件都会生成0.js,1.js….
在未使用manifest情况下,两个配置文件生成内容不一样(server配置文件中 module.target为’node‘)。
如果同时使用,需要调整输出位置,避免代码覆盖。
我对需求服务端渲染的理解是,使用爬虫获取指定url时,得到的是该url渲染好的html内容。
在浏览器上第一次打开此页面时,请求到的html内容是已渲染好的。
在浏览器上进行后续的交互时,用的是vue框架的交互逻辑,而不重新发起url请求。
客户端使用场景:
客户端build之后,使用方式是在一个body只包含div#app
元素的index.html底部注入script:src
引入build好的client.bundle.js。
然后使用http server提供对这个index.html的访问。
服务端使用场景:
在server.js中 配置express,并require 打包好的entry-server.js,填充逻辑。
然后运行node server.js
, 就能像使用nginx 托管静态html文件夹那样按照url访问指定html页面。
现在,在App.vue文件中根元素上添加 id='app'
, 渲染的到的html中包含此 id='app'
. 对此#app挂载Vue即常规的Vue使用方法。
在2.3版本中对服务端渲染进行了支持,会自动辨识服务端渲染好的dom。
使用服务端生成的内容,替换客户端使用场景中index.html 就是简易的融合前后端。
即,在服务端所使用的index.template.html页面中,添加 script:src
引入客户端build的client.bundle.js。
融合细节:
<script src="./dist/bundle.js"></script>
|
|
代码示例: songlairui/vue-playground/demo/chapter5
按照个人理解,成功使用了下服务端渲染的结果。
过程虽然很粗糙,也是使出了浑身解数。
我想要从零开始构建自己的应用,比如使用ssr-vue。而官方vue-cli没有类似原有template的ssr版。
看来,我需要学会webpack了
vue-cli自带的webpack配置,看过好几次。自己添加一些chunk,自动生成多个页面,觉得自己做了很大一件事情,而客观上并不大。
webpack文档尝试看了好几次,从来觉得太长没看完。
链接地址: http://www.css88.com/doc/webpack2/concepts/module-resolution/
我还是从头开始看了入口文件,配置方法。
目前看来,概念部分,应该是一口气看完的。然后再在指南不烦,联系代码片段。
这儿,我联想到了vimtutor中的练习方式。
啃了webpack文档后,能快速手打webpack的最简单配置文件。所有的配置都在此基础上添加。
|
|
常见选项
|
|
默认逻辑不够准确时,需要手动添加alias。
需要为vue使用 vue.esm.js时,可以通过添加alias更改。
|
|
用来生成source-map文件
entry
可由基本的 值 变成 键-值。
|
|
output
output: {filename: '[chunk].bundle.js',path: path.resolve(__dirname, 'dist')}
需要先 const path = require('path')
, 这里path属性的写法即使用兼容全平台的方式指定当前目录下的dist目录为输出目录。filename
属性的[chunk]
能让entry中的main.js 输出为 main.bundle.js ,类似的还有[name]
、[chunkhash]
、[hash]
。
rules
指定loader。webpack2兼容webpack1等loader写法。
我的Login form存在一多bug。在CSS动画的控制上欠缺。
使用 element.offsetWidth 解决了我的CSS动画重绘问题。
而且效果很稳定。