wangyuanqi

wangyuanqi 查看完整档案

杭州编辑温州大学  |  计算机技术 编辑深圳法本信息  |  web前端 编辑 blog.wangyuanqi.com 编辑
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 该用户太懒什么也没留下

个人动态

wangyuanqi 发布了文章 · 9月27日

前端Mac装机推荐软件(~附链接~)

日常必备

搜狗

https://pinyin.sogou.com/mac/

Chrome

https://www.google.cn/chrome/

eZip

https://ezip.awehunt.com/?loc...

效率工具

Alfred

https://github.com/zenorocha/...

思维导图

1、XMind8
https://www.xmind.cn/download...

2、MindMaster
http://www.edrawsoft.cn/mindm...

番茄土豆

(好用程度:3星;重点可以免费用)
https://pomotodo.com/

ProcessOn:在线绘图

https://www.processon.com/

开发工具

终端:iTerm 2

https://www.iterm2.com/downlo...

IDE:vsCode

https://code.visualstudio.com/

插件参考:

Chinese (Simplified) Language Pack for Visual Studio Code
CSS Formatter
HTML Snippets
Reactjs code snippets
TODO Highlight
View In Browser
vsc-commitizen

Homebrew

https://juemuren4449.com/arch...

SwitchHosts

通过Homebrew安装:brew cask install switchhosts

sublime (个人习惯的文档辅助工具)

http://www.sublimetext.com/

测试工具

接口调试:Postman

https://www.postman.com/products

抓包工具Charles

https://pan.baidu.com/s/1qXoC...

UI

Sketch

链接:https://pan.baidu.com/s/1CXrt... 密码:abpn

注:安装后执行:xattr -r -d com.apple.quarantine /Applications/Sketch.app/
查看原文

赞 0 收藏 0 评论 0

wangyuanqi 回答了问题 · 9月24日

解决求助下关于axios请求下载excel的问题

完整代码如下,命名的后缀注意和后端给的文件后缀一致;

exportData () {
        const form = this.getSearchForm() // 要发送到后台的数据
        axios({ // 用axios发送post请求
          method: 'post',
          url: '/user/12345', // 请求地址
          data: form, // 参数
          responseType: 'blob', // 表明返回服务器返回的数据类型
          responseType: 'arraybuffer' // 这样就不会让表格出现乱码现象
        })
          .then((res) => { // 处理返回的文件流
            const content = res
            const blob = new Blob([content], { type: 'application/vnd.ms-excel' })
            const fileName = '文件下载.xls'
            if ('download' in document.createElement('a')) { // 非IE下载
              const elink = document.createElement('a')
              elink.download = fileName
              elink.style.display = 'none'
              elink.href = URL.createObjectURL(blob)
              document.body.appendChild(elink)
              elink.click()
              URL.revokeObjectURL(elink.href) // 释放URL 对象
              document.body.removeChild(elink)
            } else { // IE10+下载
              navigator.msSaveBlob(blob, fileName)
            }
        })
      }

关注 4 回答 3

wangyuanqi 回答了问题 · 9月16日

解决vue axios 在设置了全局请求头下,单一请求更换不了请求头

可以封装一下axios,伪代码如下:

function request(path, param, method){
    const config = {
        ...param,
        headers: {'Content-Type' : 'multipart/form-data'}
    }
    if (//...是上传文件){
        config.headers = //...
    }
    return axios(config)
}

关注 4 回答 4

wangyuanqi 关注了用户 · 6月16日

九瑶 @sparkmorry

专注前端、专注WebAssembly在图形、视频领域的应用
淘系鹿班智能设计前端团队各种技术栈
图形学、计算机动画、音视频剪辑、AI设计…
WebAssembly、WebGL、C++、Javascript…
长期招人,有兴趣的疯狂私聊呀~~

关注 15

wangyuanqi 赞了文章 · 6月11日

10分钟白嫖我的常用的在线工具网站清单

大家好,我是 Guide 哥,一个三观比主角还正的技术人。

简单整理了一下自己日常经常使用的工具网站,分享给小伙伴们!其他推荐阅读:

  1. 完结撒花!JavaGuide面试突击版来啦!
  2. 「Java面试题精华集」Java基础知识篇(2020最新版)附PDF版 !
  3. 【Java后端面试经历】我和阿里面试官的“又”一次“邂逅”(附问题详解)

1.奶牛快传:用户体验更好的网盘工具。

https://cowtransfer.com/

最近开始使用的一款网盘工具,和百度网盘类似,不过没有下载速度的限制,并且可以支持自定义分享文件的下载次数(需要开会员)。

还有一点让我觉得比较舒服的是,你给别人分享文件,别人无需登录即可直接下载。

就传输速度和体验感来说,奶牛快传真的没话说。缺点也比较明显,免费用户的容量容量只有 5 g并且单次传输上限是 2g,所以,可能需要付费才能更好的使用。

2.docsmall:压缩工具

https://docsmall.com/

我经常用来压缩图片、GIF、PDF,你们平时看到我放的 Gif 图片都是在这里压缩过一波的。

并且,还支持PDF合并和分割。不得不说这个网站做的简直不要太美观,体验感和好感 Max!!!

3.创客贴:平面设计作图神器

https://www.chuangkit.com/

我的公众号首页封面图就是通过这个网站制作的。通过这个网站你可以制作好看的海报、简历、新媒体文章的首页图等等,这个网站甚至还有很多免费且好看的 PPT插件,简直是神器。

4.Dimmy.club:手机电脑等设备的展示模型

https://dimmy.club/

可以让你的图片放在电脑、手机、ipad等模型中展示,大大提升了图片的档次。

5.BrowserFrame:浏览器展示模型

https://browserframe.com/

正如你们所看到的,本文所有的图片都是他通过这个网站在线转换的,相比直接展示要更加美观一些,节省了很多自己手动调整图片的时间。

6.Flourish:数据可视化

https://flourish.studio/

通过这个网站,你可以快速地把表格数据转换为各种各样好看的图表,并且还支持动态可视化。

7.PDF派:20个免费好用的PDF在线工具

https://www.pdfpai.com/

PDF在线工具挺多的,PDF派是我最喜欢的一个,功能强大稳定。

8.小码短连接:简单易用的渠道短链接统计工具

https://xiaomark.com/

非常好用的长链接转短链接工具,能够让链接看起来更简洁。并且,转换为短链接之后,还能在后台监测访问数据,如访问次数、访问人数。

9.Kapwing:一个用于创建图像,视频和GIF的协作平台

https://www.kapwing.com/

神器网站!强烈推荐!

Kapwing 是一个在线视频编辑网站,集成了很多在线小工具,当有视频编辑的需求手头却没有什么趁手的小工具的话,不妨用来应应急。网站并没有复杂的操作界面,已经把常见的需求做成了单独的小功能,即使没有视频编辑经验的小白,也能三秒上手。

10.removebg:抠图神器

https://www.remove.bg/zh

抠图神器,绝对的神器!

11.今日热榜:你关心的热点

https://tophub.today/

今日热榜提供各站热榜聚合:微信、今日头条、百度、知乎、V2EX、微博、贴吧、豆瓣、天涯、虎扑、Github、抖音...。
追踪全网热点、简单高效阅读。

12.Apkpure:安卓安装包下载

https://apkpure.com/cn/

因为我的手机无法正常访问 Google Store,所以,很多安装包我都是通过这个网站来下载的。

13.crx4chrome:下载Chrome浏览器插件

https://www.crx4chrome.com/

如果你无法访问谷歌商店的话,可以通过这个网站来下载你想要的Chrome浏览器插件,毕竟没有插件的 Chrome浏览器 ,失去了很大一项乐趣。

14.ProcessOn:在线绘图

https://www.processon.com/

画图工具,支持流程图、思维导图、原型图、UML、网络拓扑图、组织结构图等。

15.draw.io:又一个画图工具

https://www.draw.io/

相比于 ProcessOn 我更喜欢这块画图工具,不光有在线版还有电脑版,并且可以将文件保存到多个位置。

16.其他

  1. Carbon :生成漂亮的代码图片。
  2. 图壳 :免费好用稳定的图床网站。
  3. 龙轩导航 :究极好用的导航网站。
  4. excalidraw :简洁大方的在线画图工具。
  5. mdnice :支持自定义样式的 Markdown 编辑器。支持微信公众号、知乎和稀土掘金的排版。
  6. ......

今天就分享到这里吧!后面再想起来其他的在线工具的话,我就直接补充在评论区了,也欢迎大家补充自己觉得不错的工具,不论是技术类还是非技术类都可以!

2020-05-25 23:57

查看原文

赞 70 收藏 50 评论 3

wangyuanqi 赞了文章 · 5月27日

React专题:react,redux以及react-redux常见一些面试题

面试中问框架,经常会问到一些原理性的东西,明明一直在用,也知道怎么用,
但面试时却答不上来,也是挺尴尬的,就干脆把react相关的问题查了下资料,再按自己的理解整理了下这些答案。

react生命周期有哪些

组件载入阶段:
componentWillMount:组件即将被装载、渲染到页面上,只调用1次
componentDidMount:组件真正在被装载之后,这里可以拿到真实DOM执行操作,只调用1次

运行中状态:
componentWillReceiveProps(nextProps):组件将要接收到新属性的时候调用,在这时setState不会触发额外的render,因为此时已经有一次来自父组件引发的render了。

shouldComponentUpdate:组件接受到新属性或者新状态的时候(返回 false,接收数据后不更新,阻止 render ,后面的函数不会继续执行)
componentWillUpdate:组件即将更新不能修改属性和状态
componentDidUpdate:组件已经更新

销毁阶段:
componentWillUnmount:组件即将销毁,这时候可以销毁绑定的事件监听或者定时器什么的。

有些好像把render也算进生命周期了:
render:组件在这里生成虚拟的 DOM 节点

react在哪个生命周期做优化

shouldComponentUpdate,这个方法用来判断是否需要调用 render 方法重绘 dom。
因为 dom 的描绘非常消耗性能,如果我们能在这个方法中能够写出更优化的 dom diff 算法,可以极大的提高性能。

react的diff算法是怎么完成的

1.把树形结构按照层级分解,只比较同级元素。
2.通过给列表结构的每个单元添加的唯一 key值进行区分同层次的子节点的比较。
3.React 只会匹配相同 class 的 component(这里面的 class 指的是组件的名字)
4.合并操作,调用 component 的 setState 方法的时候, React 将其标记为 dirty.
    到每一个事件循环结束, React 检查所有标记 dirty 的 component 重新绘制。
5.选择性渲染。开发人员可以重写 shouldComponentUpdate 提高 diff 的性能。

clipboard.png
图片源自:react精髓之一---diff算法

react虚拟DOM实现原理,以及为什么虚拟 dom 会提高性能

实现原理:

1. 用 js对象结构表示 DOM 树的结构;然后用这个树构建一个真正的 DOM 树,插到文档当中。
2. 当状态变更的时候,重新构造一棵新的对象树。然后对比新旧虚拟DOM树,记录两棵树差异。
3. 把 2 所记录的差异应用到步骤 1 所构建的真正的 DOM 树上,视图就更新了。

原因:虚拟 dom 相当于在 js 和真实 dom 中间加了一个缓存,利用 dom diff 算法减少了对真实DOM的操作次数,从而提高性能。

react怎么从虚拟dom中拿出真实dom

Refs 是 React 提供给我们的安全访问 DOM 元素或者某个组件实例的句柄。
我们可以为元素添加 ref 属性然后在回调函数中接受该元素在 DOM 树中的句柄,该值会作为回调函数的第一个参数返回。或者ref可以传字符串。

例如:<input ref=((input)=>{return this.name=input}) />, this.name.value取值
或者 <input ref="name" />,this.refs.name取值

React中的props和state的用法

state 是一种数据结构,用于组件挂载时所需数据的默认值。state 可能会随着时间的推移而发生突变,但多数时候是作为用户事件行为的结果。
Props则是组件的配置。props 由父组件传递给子组件,并且就子组件而言,props 是不可变的(immutable)

react组件之间如何通信

父子:父传子:props; 子传父:子调用父组件中的函数并传参;
兄弟:利用redux实现。
所有关系都通用的方法:利用PubSub.js订阅

react的setState的原理及用法

原理:当调用setState时,它并不会立即改变,而是会把要修改的状态放入一个任务队列,等到事件循环结束时,再合并指更新。
因此,setState有 异步,合并更新更新两个特性。

这里应该需要了解下Batch Update

clipboard.png

使用:
1.最常见的用法就是传入一个对象。

    this.setState({
        isLoading:false
    })

2.还可以接收一个函数

    this.setState((prevState,props)=>{
        // 要做的事件
        return {isLoading:false};
    })

3.因为setState是异步的,所以它还可以接收第二个参数,一个回调函数

    this.setState({count:2},()=>{
        isLoading:this.state.count===2 ? true : false
    })

setState为什么是异步的

参考链接:React 中 setState() 为什么是异步的?react的setstate原理

1.保证内部的一致性

因为props是要等到父组件渲染过后才能拿到,也就是不能同步更新,state出于统一性设成异步更新。

2.性能优化

举例说你正在一个聊天窗口输入,如果来了一条新消息又要render,那就会阻塞你的当前操作,导致延迟什么的。

3.支持state在幕后渲染

异步可以使state在幕后更新,而不影响你当前旧的页面的交互,提升用户体验。

详情可以点击上面的参考链接,写的很详细的。

另外:setstate在原生事件,setTimeout,setInterval,promise等异步操作中,state会同步更新

react的优势以及特点

优势:

1. 实现对虚拟DOM的操作,使得它速度快,提高了Web性能。
2. 组件化,模块化。react里每一个模块都是一个组件,组件化开发,可维护性高。
3. 单向数据流,比较有序,有便于管理,它随着React视图库的开发而被Facebook概念化。
4. 跨浏览器兼容:虚拟DOM帮助我们解决了跨浏览器问题,它为我们提供了标准化的API,甚至在IE8中都是没问题的。

不足:

1. react中只是MVC模式的View部分,要依赖引入很多其他模块开发。、
2. 当父组件进行重新渲染操作时,即使子组件的props或state没有做出任何改变,也会同样进行重新渲染。

特点: 

1. 声明式设计:React采用声明范式,可以轻松描述应用。
2. 高效:React通过对DOM的模拟,最大限度地减少与DOM的交互。
3. 灵活:React可以与已知的库或框架很好地配合。

React如何性能优化

讲一些项目中用到的小的点:

1. 充分利用shouldComponentUpdate函数,不过这需要你的组件尽量最小化,如果当前组件数据过于复杂,其实是很难优化的。
2. 给你的DOM遍历上加上唯一的key,注意尽量不要用index,因为如果你新DOM中删了某一个节点,它会重新排列index,
那跟原来同层级一比就都会完全不一样,而重新渲染了,所以最好使用id值什么的作key值。

3. 能用const声明的就用const。
4. DOM里少用箭头函数,当然其实要传参时也还是得用。再者,函数bind尽量写在constructor,避免每次render重新bind。
5. 减少对真实DOM的操作。
6. 如果是用webpack搭建环境的话,当一个包过大加载过慢时,可分打成多个包来优化。
react-perf性能查看工具,可自行了解下:react-perf

react与vue的对比

有些是个人意见,仅供参考。

相同点:

1. 都用虚拟DOM实现快速渲染
2. 我觉得父子,兄弟通信这些都挺像的,也都有自己的状态管理器:react=>redux, vue=>vuex
3. 都是轻量级框架
4. 现在vue也在渐渐吸收react中的一些语法,比如JSX语法,类式声明写法等

不同点:

1. React属于单向数据流——MVC模式,vue则属于双向——MVVM模式。
2. react兼容性比vue好,vue不兼容IE8.
3. react采用JSX语法,vue采用的则是html模板语法。
4. vue的css可以有组件的私有作用域,react则没有。
5. react比vue好的另一点是,它是团队维护,而vue属于个人,一般来说,大型项目更倾向于react,小型则用vue,当然这也不是绝对。

Redux的实现流程

用户页面行为触发一个Action,然后,Store 自动调用 Reducer,并且传入两个参数:当前 State 和收到的 Action。Reducer 会返回新的 State 。每当state更新之后,view会根据state触发重新渲染。

react-redux的实现原理

Redux作为一个通用模块,主要还是用来处理应用中state的变更,通过react-redux做连接,可以在React+Redux的项目中将两者结合的更好。
react-redux是一个轻量级的封装库,它主要通过两个核心方法实现:

Provider:从最外部封装了整个应用,并向connect模块传递store。
Connect: 
    1、包装原组件,将state和action通过props的方式传入到原组件内部。
    2、监听store tree变化,使其包装的原组件可以响应state变化

redux中间件的理解,以及用过哪些中间件

理解:中间件就是要对redux的store.dispatch方法做一些改造,以实现其他的功能。

我用过redux-thunk,就拿它举例。

背景:Redux 的基本做法,是用户发出 Action,Reducer 函数立刻算出新的 State,View 重新渲染,但这是做同步。

而如果有异步请求时,那就不能知道什么时候获取的数据有存进store里面,因此此时需要在请求成功时返回一个标识或状态,并在此时再触发action给reducer传值。
因此,为了解决异步的问题,就引入了中间件的概念。

作用: redux-thunk 帮助你统一了异步和同步 action 的调用方式,把异步过程放在 action 级别解决,对 component 调用没有影响。

引入使用可参照:理解redux和redux的中间件redux-thunk的认识

redux-thunk VS redux-saga对比 异步处理方案中间件

原文链接:异步方案选型redux-saga 和 redux-thunk

redux-thunk
缺点:

(1).一个异步请求的action代码过于复杂,且异步操作太分散,相对比saga只要调用一个call方法就显得简单多了。
(2).action形式不统一,如果不一样的异步操作,就要写多个了。

优点:学习成本低

redux-saga:
优点:

(1)集中处理了所有的异步操作,异步接口部分一目了然(有提供自己的方法)
(2)action是普通对象,这跟redux同步的action一模一样({type:XXX})
(3)通过Effect,方便异步接口的测试
(4)通过worker和watcher可以实现非阻塞异步调用,并且同时可以实现非阻塞调用下的事件监听
(5) 异步操作的流程是可以控制的,可以随时取消相应的异步操作。

缺点:学习成本高。

比较redux和vuex的区别

原文链接不记得了(囧...)
相同点:

1.数据驱动视图,提供响应式的视图组件
2.都有virtual DOM, 组件化开发,通过props参数进行父子组件数据的传递,都实现webComponents规范
3.都支持服务端渲染  
4.都有native解决方案,reactnative(facebook团队) vs weex(阿里团队)

不同点:

1.vuex是一个针对VUE优化的状态管理系统,而redux仅是一个常规的状态管理系统(Redux)与React框架的结合版本。
2.开发模式:React本身,是严格的view层,MVC模式;Vue则是MVVM模式的一种方式实现
3.数据绑定:Vue借鉴了angular,采取双向数据绑定的方式;React,则采取单向数据流的方式
4.数据更新:Vue采取依赖追踪,默认是优化状态:按需更新;
    React在则有两种选择:
    1)手动添加shouldComponentUpdate,来避免冗余的vdom,re-render的情况
    2)Components 尽可能都用 pureRenderMixin,然后采用 redux 结构 + Immutable.js
5.社区:react相比来讲还是要大于vue,毕竟背后支撑团队不同。
    facebook vs 个人!当然目前vue的增长速度是高于react的增速,不知道未来的发展趋势是如何。

总之:期待构建一个大型应用程序——选择React,期待应用尽可能的小和快——选择Vue

react-router的实现原理

原理:实现URL与UI界面的同步。其中在react-router中,URL对应Location对象,
而UI是由react components来决定的,这样就转变成location与components之间的同步问题。

优点:

1.风格: 与React融为一体,专为react量身打造,编码风格与react保持一致,例如路由的配置可以通过component来实现
2.简单: 不需要手工维护路由state,使代码变得简单
3.强大: 强大的路由管理机制,体现在如下方面
4.路由配置: 可以通过组件、配置对象来进行路由的配置
5.路由切换: 可以通过<Link> Redirect进行路由的切换
6.路由加载: 可以同步记载,也可以异步加载,这样就可以实现按需加载
7.使用方式: 不仅可以在浏览器端的使用,而且可以在服务器端的使用

缺点:API不太稳定,在升级版本的时候需要进行代码变动。

react router3到4有什么改变

我只挑了一部分。
原文链接:https://blog.csdn.net/qq_3548...

1. V4不再使用V3里的{props.children}(代表所有路由-个人理解),而V4丢给 DOM 的是我们的应用程序本身.
2. V4还同时支持同时渲染多个路由,1和2都可参照下面代码,当访问 /user 时,两个组件都会被渲染。(V3可实现但过程复杂)
  <div className="primary-layout">
    <header>
      Our React Router 4 App
      <Route path="/user" component={UsersMenu} />
    </header>
    <main>
      <Route path="/" exact component={HomePage} />
      <Route path="/user" component={UsersPage} />
    </main>
  </div>;
3.获取路由的路径匹配,可以使用props.match.path获取,match上还有match.params,match.url等属性。
注:url是浏览器的url的一部分,path是给router写的路径
4.多了一个限制未登录的用户访问某些路由功能,可以在应用程序顶端中设置一个主入口,区别登录和未登录UI展示界面。

对webpack的理解:

参考链接:webpack配置整理

概念: webpack是一个预编译模块方案,它会分析你的项目结构将其打包成适合浏览器加载的模块。
打包原理:把所有依赖打包成一个bundle.js文件,通过代码分割成单元片段并按需加载。
核心思想:

1.一切皆模块,一个js,或者一个css文件也都看成一个模块,
2.按需加载,传统的模块打包工具(module bundlers)最终将所有的模块编译生成一个庞大的bundle.js文件。
    但是在真实的app里边,“bundle.js”文件可能有10M到15M之大可能会导致应用一直处于加载中状态。
    因此Webpack使用许多特性来分割代码然后生成多个“bundle”文件,而且异步加载部分代码以实现按需加载。

基础配置项:

1. entry:{} 入口,支持多入口
2. output 出口
3. resolve 处理依赖模块路径的解析
4. module 处理多种文件格式的loader
5. plugins 除了文件格式转化由loader来处理,其他大多数由plugin来处理
6. devServer 配置 webpack-dev-server
7. 搭配package.json配置环境变量,以及脚本配置。
"scripts": {
    "build": "webpack --mode production",
    "start": "webpack-dev-server --mode development"
}

"scripts": {
    "build_": "NODE_ENV=production webpack",
    "start_": "NODE_ENV=development webpack-dev-server"
}

react高阶组件

参考资料:浅谈React高阶组件
通俗理解 React 高阶函数
深入浅出React高阶组件
定义:js里的高阶函数的定义是接收一个函数作为参数,并返回一个函数。redux的connect就是一个高阶函数。
那react高阶组件就是指接收一个react组件作为入参,并返回一个新react组件的组件。

好处:它不用关心组件从哪来,也就是不用自己去引入很多个组件了。
一个简单的高阶组件:(写法不是唯一)

    export default function withHeader(WrappedComponent){
        return class HOC extends component{
            return (
                <div className="wrap">
                    <div>这是一段普通的文字</div>
                    <WrappedComponent {...this.props} />
                </div>
            )
        }
    }

直接引入:import withHeader from 'withHeader'

高阶组件部分还有待补充。

查看原文

赞 52 收藏 38 评论 2

wangyuanqi 关注了用户 · 5月15日

leexiaoran @li1076629390

关注 536

wangyuanqi 赞了文章 · 2019-12-23

「万字整理 」这里有一份Node.js入门指南和实践,请注意查收 ❤️

前言

什么是 Node.js 呢 ?

JS 是脚本语言,脚本语言都需要一个解析器才能运行。对于写在 HTML 页面里的 JS,浏览器充当了解析器的角色。而对于需要独立运行的 JS,NodeJS 就是一个解析器。

解析器需要运行引擎才能对 JavaScript 进行解析,Node.js 采用了 V8 引擎,Google 开源的 JavaScript 引擎。

所以,Node.js 就是一个基于 Chrome V8 引擎的 JavaScript 运行环境。

Node.js 事件驱动机制 + 异步 IO + 高性能 V8 引擎 ,也让它成为编写高性能 Web 服务一个非常好的选择。

Node.js 能做什么呢 ?

马上 2020 年了,距离 2009 年 Node.js 开源以来,已经 10 个年头了。

这么长时间的迭代,Node.js 生态圈已经非常成熟,有了很多优秀的实践和轮子,比如 express,koa 等 web 开发框架。

Node.js 无疑也带动了前端生态的发展,比如前端工程化领域。

说 Node.js 能做什么,不如说说我用 Node.js 做了什么吧。

工作中:

  • 基于 express 做了一个活动页生成工具
  • 基于 koa + sequelize 做了一个监控系统平台
  • 用 Node.js 撸了一些自动化脚本,优化重复性劳作

工作之余:

null-cli 来啦 , 一行命令提高你的效率

5 个有趣的 Node.js 库,带你走进 彩色 Node.js 世界

nodejs + docker + github pages 定制自己的 「今日头条」

说了这么多废话,我要干嘛呢~

如果你最近刚好想要了解,学习 Node.js,那希望这篇文章能帮到你~

本文通过了解 Node.js 13 个 基础核心模块 和 一个基于 原生 Node.js 的 TodoList 实践 ,带你上手 Node.js !

13 个基础核心模块

1. 事件触发器 events 模块

2. 本地路径 path 模块

3. 文件操作系统 fs 模块

4. 全局对象 process 进程

5. http 模块

6. 统一资源定位符 url 模块

7. 压缩 zlib 模块

8. 流 stream 模块

9. 逐行读取 readline 模块

10. 查询字符串 querystring 模块

11. module 模块

12. 缓冲器 Buffer 模块

13. 域名服务器 dns 模块

Node.js 内置模块远不止 13 个,入门阶段我们了解一些常用的基础核心模块,就可以上手 实践啦~

如果不想看通篇长文,我在github 博客 将 13 个模块拆分成了 13 个小节,方便阅读,每个模块的 demo 代码也能在博客中找到~

TodoList 实现了什么?

为了对 Node.js 核心模块进一步加深理解,这个 demo 采用原生 api 实现,脱离 express,koa 等一些 web 框架和库 。

  • RESTful API 实践
  • 静态资源映射及 gzip 压缩
  • 后端路由 Router 简易实现
  • Node.js 核心模块方法实践

实现了一个简单的任务管理,前端采用的是 vue + element-ui ,

TodoList
└───app             // 前端代码
│   │   ...
└───controllers     // 控制器
│   │   list.js     // api 逻辑实现
└───router
│   │   index.js    // 注册路由
│   │   router.js   // 路由实现
└───utils           // 工具类
    │   index.js
|   data.json       // 数据存放
│   index.js        // 工程入口

实现没有借助任何库,不用安装任何依赖

node index.js

就可以启动服务,自己想要开发或者调试的话,这里推荐使用nodemon,它实现了热更新,可以自动重启.

npm install -g nodemon

nodemon
#or
nodemon index.js

TodoList 代码地址

实现效果如下:

todolist

<h2 id="1"> 1. 事件触发器 events 模块</h2>

Node.js 使用了一个事件驱动、非阻塞式 I/O 的模型,使其轻量又高效。

大多数 Node.js 核心 API 都采用惯用的事件驱动架构,其中某些类型的对象(触发器)会周期性地触发命名事件来调用函数对象(监听器),那么 Node.js 是如何实现事件驱动的呢?

events 模块是 Node.js 实现事件驱动的核心,在 node 中大部分的模块的实现都继承了 Events 类。比如 fs 的 readstream,net 的 server 模块。

events 模块只提供了一个对象: events.EventEmitter。EventEmitter 的核心就是事件触发与事件监听器功能的封装,EventEmitter 本质上是一个观察者模式的实现。

所有能触发事件的对象都是 EventEmitter 类的实例。 这些对象有一个 eventEmitter.on() 函数,用于将一个或多个函数绑定到命名事件上。 事件的命名通常是驼峰式的字符串,但也可以使用任何有效的 JavaScript 属性键。

EventEmitter 对象使用 eventEmitter.emit()触发事件,当 EventEmitter 对象触发一个事件时,所有绑定在该事件上的函数都会被同步地调用。 被调用的监听器返回的任何值都将会被忽略并丢弃。

下面我们通过几个简单的例子来学习 events 模块

1. 基础例子

注册 Application 实例,继承 EventEmitter 类,通过继承而来的 eventEmitter.on() 函数监听事件,eventEmitter.emit()触发事件

const EventEmitter = require('events')
/**
 * Expose `Application` class.
 * Inherits from `EventEmitter.prototype`.
 */
class Application extends EventEmitter {}
const app = new Application()
//  监听hello事件
app.on('hello', data => {
  console.log(data) // hello nodeJs
})
//  触发hello事件
app.emit('hello', 'hello nodeJs')

2. 多个事件监听器及 this 指向

绑定多个事件监听器时,事件监听器按照注册的顺序执行。

当监听器函数被调用时, this 关键词会被指向监听器所绑定的 EventEmitter 实例。也可以使用 ES6 的箭头函数作为监听器,但 this 关键词不会指向 EventEmitter 实例。

const EventEmitter = require('events')

class Person extends EventEmitter {
  constructor() {
    super()
  }
}
const mrNull = new Person()
//  监听play事件
mrNull.on('play', function(data) {
  console.log(this)
  // Person {
  //   _events:
  //   [Object: null prototype] { play: [[Function], [Function]] },
  //   _eventsCount: 1,
  //     _maxListeners: undefined
  // }
  console.log(`play`)
})
//  监听play事件
mrNull.on('play', data => {
  console.log(this) // {}
  console.log(`play again`)
})
//  触发play事件
mrNull.emit('play', 'hello nodeJs')

3. 同步 VS 异步

EventEmitter 以注册的顺序同步地调用所有监听器。

const EventEmitter = require('events')

class Person extends EventEmitter {
  constructor() {
    super()
  }
}
const mrNull = new Person()
mrNull.on('play', function(data) {
  console.log(data)
})

mrNull.emit('play', 'hello nodeJs')

console.log(`hello MrNull`)

// hello nodeJs
// hello MrNull

监听器函数可以使用 setImmediate() 和 process.nextTick() 方法切换到异步的操作模式

const developer = new Person()
developer.on('dev', function(data) {
  setImmediate(() => {
    console.log(data)
  })
})
developer.on('dev', function(data) {
  process.nextTick(() => {
    console.log(data)
  })
})
developer.emit('dev', 'hello nodeJs')

console.log(`hello developer`)

// hello developer
// hello nodeJs
// hello nodeJs

4. 只调用一次的事件监听器

使用 eventEmitter.once() 可以注册最多可调用一次的监听器。 当事件被触发时,监听器会被注销,然后再调用。

const EventEmitter = require('events')

class Person extends EventEmitter {
  constructor() {
    super()
  }
}
const mrNull = new Person()
mrNull.once('play', () => {
  console.log('play !')
})

mrNull.emit('play')
mrNull.emit('play')

// play ! 只输出一次

5. 事件触发顺序

在注册事件前,触发该事件,不会被触发 !!

const EventEmitter = require('events')

class Person extends EventEmitter {
  constructor() {
    super()
  }
}
const mrNull = new Person()

mrNull.emit('play')

mrNull.on('play', () => {
  console.log('play !')
})

// 无任何输出

6. 移除事件监听器

const EventEmitter = require('events')

class Person extends EventEmitter {
  constructor() {
    super()
  }
}
const mrNull = new Person()

function play() {
  console.log('play !')
}
mrNull.on('play', play)

mrNull.emit('play')

// mrNull.off("play", play); v10.0.0版本新增,emitter.removeListener() 的别名。
//  or
mrNull.removeListener('play', play)

mrNull.emit('play')

// play !  移除后不再触发

<h2 id="2"> 2. 本地路径 path 模块</h2>

Node.js 提供了 path 模块,用于处理文件路径和目录路径 . 不同操作系统 表现有所差异 !

1. 获取路径的目录名

const path = require('path')

path.dirname('/path/example/index.js') // /path/example

2. 获取路径的扩展名

const path = require('path')

path.extname('/path/example/index.js') // .js

3. 是否是绝对路径

const path = require('path')

path.isAbsolute('/path/example/index.js') // true

path.isAbsolute('.') // false

4. 拼接路径片段

path.join('/path', 'example', './index.js') // /path/example/index.js

5. 将路径或路径片段的序列解析为绝对路径。

path.resolve('/foo/bar', './baz')
// 返回: '/foo/bar/baz'

path.resolve('/foo/bar', '/tmp/file/')
// 返回: '/tmp/file'

path.resolve('wwwroot', 'static_files/png/', '../gif/image.gif')
// 如果当前工作目录是 /home/myself/node,
// 则返回 '/home/myself/node/wwwroot/static_files/gif/image.gif'

6. 规范化路径

path.normalize('/path///example/index.js') //  /path/example/index.js

7. 解析路径

path.parse('/path/example/index.js')

/*
 { root: '/',
  dir: '/path/example',
  base: 'index.js',
  ext: '.js',
  name: 'index' }
*/

8. 序列化路径

path.format({
  root: '/',
  dir: '/path/example',
  base: 'index.js',
  ext: '.js',
  name: 'index'
}) // /path/example/index.js

9. 获取 from 到 to 的相对路径

path.relative('/path/example/index.js', '/path') // ../..

<h2 id="3">3 .文件操作系统 fs 模块</h2>

在一些场景下,我们需要对文件进行 增删改查等操作, Nodejs 提供了 fs 模块,让我们对文件进行操作.

下面我们来介绍几个经常用的 API

1. 读取文件

const fs = require('fs')
const fs = require('fs')

// 异步读取
fs.readFile('./index.txt', 'utf8', (err, data) => {
  console.log(data) //  Hello Nodejs
})

// 同步读取
const data = fs.readFileSync('./index.txt', 'utf8')

console.log(data) //  Hello Nodejs

// 创建读取流
const stream = fs.createReadStream('./index.txt', 'utf8')

// 这里可以看到fs.createReadStream用到了我们前面介绍的events eventEmitter.on() 方法来监听事件
stream.on('data', data => {
  console.log(data) // Hello Nodejs
})

2. 写入/修改文件

写入文件时,如果文件不存在,则会创建并写入,如果文件存在,会覆盖文件内容.

const fs = require('fs')
// 异步写入
fs.writeFile('./write.txt', 'Hello Nodejs', 'utf8', err => {
  if (err) throw err
})
// 同步写入
fs.writeFileSync('./writeSync.txt', 'Hello Nodejs')
// 文件流写入
const ws = fs.createWriteStream('./writeStream.txt', 'utf8')
ws.write('Hello Nodejs')
ws.end()

3. 删除文件/文件夹

  • 删除文件
// 异步删除文件
fs.unlink('./delete.txt', err => {
  if (err) throw err
})

// 同步删除文件
fs.unlinkSync('./deleteSync.txt')
  • 删除文件夹
// 异步删除文件夹
fs.rmdir('./rmdir', err => {
  if (err) throw err
})

// 同步删除文件夹
fs.rmdirSync('./rmdirSync')

4. 创建文件夹

// 异步创建文件夹
fs.mkdir('./mkdir', err => {
  if (err) throw err
})

// 同步创建文件夹
fs.mkdirSync('./mkdirSync')

5. 重命名文件/文件夹

const fs = require('fs')

// 异步重命名文件
fs.rename('./rename.txt', './rename-r.txt', err => {
  if (err) throw err
})

// 同步重命名文件夹
fs.renameSync('./renameSync', './renameSync-r')

6. 复制文件/文件夹

const fs = require('fs')

// 异步复制文件
fs.copyFile('./copy.txt', './copy-c.txt', (err, copyFiles) => {
  if (err) throw err
})

// 同步复制文件夹
fs.copyFileSync('./null', 'null-c')

7. 文件夹状态- 文件/文件夹

const fs = require('fs')

// 异步获取文件状态
fs.stat('./dir', (err, stats) => {
  if (err) throw err
  // 是否是文件类型
  console.log(stats.isFile()) // false
  // 是否是文件夹类型
  console.log(stats.isDirectory()) // true
})

// 同步获取文件状态
const stats = fs.statSync('./stats.txt')

// 是否是文件类型
console.log(stats.isFile()) // true
// 是否是文件夹类型
console.log(stats.isDirectory()) // false

在一些复杂的操作场景下,fs 模块要做很多判断与处理 ,这里我推荐大家使用 fs-extra,它在 fs 的基础上扩展了一些方法,让一些复杂操作更简便!

<h2 id="4">4. 全局对象 process 进程</h2>

process 对象是一个 Global 全局对象,你可以在任何地方使用它,而无需 require。process 是 EventEmitter 的一个实例,所以 process 中也有相关事件的监听。使用 process 对象,可以方便处理进程相关操作。

process 常用属性

进程命令行参数: process.argv

process.argv 是一个当前执行进程折参数组,第一个参数是 node,第二个参数是当前执行的.js 文件名,之后是执行时设置的参数列表。

node index.js --tips="hello nodejs"

/*
[ '/usr/local/bin/node',
  'xxx/process/index.js',
  '--tips=hello nodejs' ]
*/

Node 的命令行参数数组:process.execArgv

process.execArgv 属性会返回 Node 的命令行参数数组。

node --harmony index.js --version

console.log(process.execArgv);  // [ '--harmony' ]

console.log(process.argv);

/*
[ '/usr/local/bin/node',
  'xxx/process/index.js',
  '--version' ]
*/

Node 编译时的版本: process.version

process.version 属性会返回 Node 编译时的版本号,版本号保存于 Node 的内置变量 NODE_VERSION 中。

console.log(process.version) // v10.15.3

当前进程的 PID process.pid

process.pid 属性会返回当前进程的 PID。

console.log('process PID: %d', process.pid)

//process PID: 10086

process 常用方法

当前工作目录 process.cwd()

process.cwd()方法返回进程当前的工作目录

console.log(process.cwd()) // /Users/null/nodejs/process

终止当前进程:process.exit([code])

process.exit()方法终止当前进程,此方法可接收一个退出状态的可选参数 code,不传入时,会返回表示成功的状态码 0。

process.on('exit', function(code) {
  console.log('进程退出码是:%d', code) // 进程退出码是:886
})

process.exit(886)

nodejs 微任务: process.nextTick()

process.nextTick()方法用于延迟回调函数的执行, nextTick 方法会将 callback 中的回调函数延迟到事件循环的下一次循环中,与 setTimeout(fn, 0)相比 nextTick 方法效率高很多,该方法能在任何 I/O 之前调用我们的回调函数。

console.log('start')
process.nextTick(() => {
  console.log('nextTick cb')
})
console.log('end')

// start
// end
// nextTick cb

process 标准流对象

process 中有三个标准备流的操作,与 其他 streams 流操作不同的是,process 中流操作是同步写,阻塞的。

标准错误流: process.stderr

process.stderr 是一个指向标准错误流的可写流 Writable Stream。console.error 就是通过 process.stderr 实现的。

标准输入流:process.stdin

process.stdin 是一个指向标准输入流的可读流 Readable Stream。

process.stdin.setEncoding('utf8')

process.stdin.on('readable', () => {
  let chunk
  // 使用循环确保我们读取所有的可用数据。
  while ((chunk = process.stdin.read()) !== null) {
    if (chunk === '\n') {
      process.stdin.emit('end')
      return
    }
    process.stdout.write(`收到数据: ${chunk}`)
  }
})

process.stdin.on('end', () => {
  process.stdout.write('结束监听')
})

process-stdin

标准输出流:process.stdout

process.stdout 是一个指向标准输出流的可写流 Writable Stream。console.log 就是通过 process.stdout 实现的

console.log = function(d) {
  process.stdout.write(d + '\n')
}

console.log('Hello Nodejs') // Hello Nodejs

<h2 id="5">5. http 模块</h2>

http 模块是 Node.js 中非常重要的一个核心模块。通过 http 模块,你可以使用其 http.createServer 方法创建一个 http 服务器,也可以使用其 http.request 方法创建一个 http 客户端。(本文先不说),Node 对 HTTP 协议及相关 API 的封装比较底层,其仅能处理流和消息,对于消息的处理,也仅解析成报文头和报文体,但是不解析实际的报文头和报文体内容。这样不仅解决了 HTTP 原本比较难用的特性,也可以支持更多的 HTTP 应用.

http.IncomingMessage 对象

IncomingMessage 对象是由 http.Server 或 http.ClientRequest 创建的,并作为第一参数分别传递给 http.Server 的'request'事件和 http.ClientRequest 的'response'事件。

它也可以用来访问应答的状态、头文件和数据等。 IncomingMessage 对象实现了 Readable Stream 接口,对象中还有一些事件,方法和属性。

在 http.Server 或 http.ClientRequest 中略有不同。

http.createServer([requestListener])创建 HTTP 服务器

实现 HTTP 服务端功能,要通过 http.createServer 方法创建一个服务端对象 http.Server。

这个方法接收一个可选传入参数 requestListener,该参数是一个函数,传入后将做为 http.Server 的 request 事件监听。不传入时,则需要通过在 http.Server 对象的 request 事件中单独添加。

var http = require('http')

// 创建server对象,并添加request事件监听器
var server = http.createServer(function(req, res) {
  res.writeHeader(200, { 'Content-Type': 'text/plain' })
  res.end('Hello Nodejs')
})

// 创建server对象,通过server对象的request事件添加事件事件监听器
var server = new http.Server()
server.on('request', function(req, res) {
  res.writeHeader(200, { 'Content-Type': 'text/plain' })
  res.end('Hello Nodejs')
})

http.Server 服务器对象

http.Server 对象是一个事件发射器 EventEmitter,会发射:request、connection、close、checkContinue、connect、upgrade、clientError 事件。

其中 request 事件监听函数为 function (request, response) { },该方法有两个参数:request 是一个 http.IncomingMessage 实例,response 是一个 http.ServerResponse 实例。

http.Server 对象中还有一些方法,调用 server.listen 后 http.Server 就可以接收客户端传入连接。

http.ServerResponse

http.ServerResponse 对象用于响应处理客户端请求。

http.ServerResponse 是 HTTP 服务器(http.Server)内部创建的对象,作为第二个参数传递给 'request'事件的监听函数。

http.ServerResponse 实现了 Writable Stream 接口,其对于客户端的响应,本质上是对这个可写流的操作。它还是一个 EventEmitter,包含:close、finish 事件。

创建一个 http.Server

创建 http.Server 使用 http.createServer()方法,为了处理客户端请求,需要在服务端监听来自客户的'request'事件。

'request'事件的回调函数中,会返回一个 http.IncomingMessage 实例和一个 http.ServerResponse。

const http = require('http')
/**
 * @param {Object} req 是一个http.IncomingMessag实例
 * @param {Object} res 是一个http.ServerResponse实例
 */
const server = http.createServer((req, res) => {
  console.log(req.headers)
  res.end(`Hello Nodejs`)
})

server.listen(3000)

http.ServerResponse 实例是一个可写流,所以可以将一个文件流转接到 res 响应流中。下面示例就是将一张图片流传送到 HTTP 响应中:

const http = require('http')
/**
 * @param {Object} req 是一个http.IncomingMessag实例
 * @param {Object} res 是一个http.ServerResponse实例
 */
const server = http.createServer((req, res) => {
  res.writeHead(200, { 'Content-Type': 'image/jpg' })
  const r = require('fs').createReadStream('./kobe.jpg')
  r.pipe(res)
})

server.listen(3000)

<h2 id="6">6. 统一资源定位符 url 模块</h2>

Node.js 提供了 url 模块,用于处理与解析 URL。

1. URL 对象都有哪些属性 ?

const { URL } = require("url");

const myURL = new URL("https://github.com/webfansplz#hello");
console.log(myURL);
{
  href: 'https://github.com/webfansplz#hello',  // 序列化的 URL
  origin: 'https://github.com', // 序列化的 URL 的 origin
  protocol: 'https:', // URL 的协议
  username: '', // URL 的用户名
  password: '', //  URL 的密码
  host: 'github.com', // URL 的主机
  hostname: 'github.com',   // URL 的主机名
  port: '',  // URL 的端口
  pathname: '/webfansplz',  // URL 的路径
  search: '', // URL 的序列化查询参数
  searchParams: URLSearchParams {}, //  URL 查询参数的 URLSearchParams 对象
  hash: '#hello'  // URL 的片段
}

URL 对象属性 除了 origin 和 searchParams 是只读的,其他都是可写的.

2. 序列化 URL

const { URL } = require('url')

const myURL = new URL('https://github.com/webfansplz#hello')

console.log(myURL.href) //  https://github.com/webfansplz#hello

console.log(myURL.toString()) // https://github.com/webfansplz#hello

console.log(myURL.toJSON()) //  https://github.com/webfansplz#hello

<h2 id="7">7. 压缩 zlib 模块</h2>

在流传输过程中,为减少传输数据加快传输速度,往往会对流进行压缩。

HTTP 流就是如此,为提高网站响应速度,会在服务端进行压缩,客户端收到数据后再进行相应的解压。

Node.js 中的 Zlib 模块提供了流压缩与解压缩功能,Zlib 模块提供了对 Gzip/Gunzip、Deflate/Inflate、DeflateRaw/InflateRaw 类的绑定,这些类可以实现对可读流/可写流的压缩与解压。

<!-- 做过 web 性能优化的同学,应该对 gzip 神器很熟悉. -->

关于 gzip 与 deflate

deflate(RFC1951)是一种压缩算法,使用 LZ77 和哈弗曼进行编码。gzip(RFC1952)一种压缩格式,是对 deflate 的简单封装,gzip = gzip 头(10 字节) + deflate 编码的实际内容 + gzip 尾(8 字节)。在 HTTP 传输中,gzip 是一种常用的压缩算法,使用 gzip 压缩的 HTTP 数据流,会在 HTTP 头中使用 Content-Encoding:gzip 进行标识。

HTTP Request Header 中 Accept-Encoding 是浏览器发给服务器,声明浏览器支持的解压类型

Accept-Encoding: gzip, deflate, br

HTTP Response Header 中 Content-Encoding 是服务器告诉浏览器 使用了哪种压缩类型

Content-Encoding: gzip

对 web 性能优化有所了解的同学,相信对 gzip 都不陌生,我们就通过 gzip 来了解 zlib 模块.

1. 文件压缩/解压

文件压缩

const zlib = require('zlib')
const fs = require('fs')
const gzip = zlib.createGzip()
const inp = fs.createReadStream('zlib.txt')
const out = fs.createWriteStream('zlib.txt.gz')
inp.pipe(gzip).pipe(out)

文件解压

const zlib = require('zlib')
const fs = require('fs')
const gunzip = zlib.createGunzip()
const inp = fs.createReadStream('./un-zlib.txt.gz')
const out = fs.createWriteStream('un-zlib.txt')
inp.pipe(gunzip).pipe(out)

2. 服务端 gzip 压缩

const fs = require('fs')
const http = require('http')
const zlib = require('zlib')
const filepath = './index.html'

const server = http.createServer((req, res) => {
  const acceptEncoding = req.headers['accept-encoding']
  if (acceptEncoding.includes('gzip')) {
    const gzip = zlib.createGzip()
    res.writeHead(200, {
      'Content-Encoding': 'gzip'
    })
    fs.createReadStream(filepath)
      .pipe(gzip)
      .pipe(res)
  } else {
    fs.createReadStream(filepath).pipe(res)
  }
})

server.listen(4396)

<h2 id="8">8. 流 stream 模块</h2>

流(stream)是 Node.js 中处理流式数据的抽象接口。 stream 模块用于构建实现了流接口的对象。

Node.js 提供了多种流对象。 例如,HTTP 服务器的请求和 process.stdout 都是流的实例。

流可以是可读的、可写的、或者可读可写的。 所有的流都是 EventEmitter 的实例。

尽管理解流的工作方式很重要,但是 stream 模块主要用于开发者创建新类型的流实例。 对于以消费流对象为主的开发者,极少需要直接使用 stream 模块。

stream 类型

Node.js 中有四种基本的流类型:

  • Writable - 可写入数据的流(例如 fs.createWriteStream())。
  • Readable - 可读取数据的流(例如 fs.createReadStream())。
  • Duplex - 可读又可写的流(例如 net.Socket)。
  • Transform - 在读写过程中可以修改或转换数据的 Duplex 流(例如 zlib.createDeflate())。

用于消费流的 API

const http = require('http')

const server = http.createServer((req, res) => {
  // req 是一个 http.IncomingMessage 实例,它是可读流。
  // res 是一个 http.ServerResponse 实例,它是可写流。

  let body = ''
  // 接收数据为 utf8 字符串,
  // 如果没有设置字符编码,则会接收到 Buffer 对象。
  req.setEncoding('utf8')

  // 如果添加了监听器,则可读流会触发 'data' 事件。
  req.on('data', chunk => {
    body += chunk
  })

  // 'end' 事件表明整个请求体已被接收。
  req.on('end', () => {
    try {
      const data = JSON.parse(body)
      // 响应信息给用户。
      res.write(typeof data)
      res.end()
    } catch (er) {
      // json 解析失败。
      res.statusCode = 400
      return res.end(`错误: ${er.message}`)
    }
  })
})

server.listen(1337)

// curl localhost:1337 -d "{}"
// object
// curl localhost:1337 -d "\"foo\""
// string
// curl localhost:1337 -d "not json"
// 错误: Unexpected token o in JSON at position 1

当数据可以从流读取时,可读流会使用 EventEmitter API 来通知应用程序 (比如例子中的 req data 事件)。 从流读取数据的方式有很多种。

可写流(比如例子中的 res)会暴露了一些方法,比如 write() 和 end() 用于写入数据到流。

可写流和可读流都通过多种方式使用 EventEmitter API 来通讯流的当前状态。Duplex 流和 Transform 流都是可写又可读的。

对于只需写入数据到流或从流消费数据的应用程序,并不需要直接实现流的接口,通常也不需要调用 require('stream')。

对于大部分的 nodejs 开发者来说,平常并不会直接用到 stream 模块,但是理解 stream 流的运行机制却是尤其重要的.

<h2 id="9">9. 逐行读取 readline 模块</h2>

readline 模块是一个流内容的逐行读取模块,通过 require('readline')引用模块。你可以用 readline 模块来读取 stdin,可以用来逐行读取文件流,也可用它来在控制台和用户进行一些交互。

const readline = require('readline')

const rl = readline.createInterface({
  //  监听的可读流
  input: process.stdin,
  //  逐行读取(Readline)数据要写入的可写流
  output: process.stdout
})

rl.question('你如何看待 null-cli ?', answer => {
  console.log(`感谢您的宝贵意见:${answer}`)
  rl.close()
})

readline

很多有趣的 CLI 工具是基于 readline 造的哦,有兴趣的同学也可以尝试~

<h2 id="10">10. 查询字符串 querystring 模块</h2>

querystring 模块是 Node.js 中的工具模块之一,用于处理 URL 中的查询字符串,即:querystring 部分。查询字符串指:URL 字符串中,从问号"?"(不包括?)开始到锚点"#"或者到 URL 字符串的结束(存在#,则到#结束,不存在则到 URL 字符串结束)的部分叫做查询字符串。querystring 模块可将 URL 查询字符串解析为对象,或将对象序列化为查询字符串。

1. 对象序列化为查询字符串

querystring.stringify(obj, sep[, options])

const querystring = require('querystring')

const obj = {
  url: 'github.com/webfansplz',
  name: 'null'
}

console.log(querystring.stringify(obj)) // url=github.com%2Fwebfansplz&name=null

2. 查询字符串解析为对象

const querystring = require('querystring')

const o = querystring.parse(`url=github.com%2Fwebfansplz&name=null`)

console.log(o.url) // github.com/webfansplz

3. 编码查询字符串中的参数

querystring.escape 方法会对查询字符串进行编码,在使用 querystring.stringify 方法时可能会用到.

const str = querystring.escape(`url=github.com%2Fwebfansplz&name=null`)

console.log(str) // url%3Dgithub.com%252Fwebfansplz%26name%3Dnull

4. 解码查询字符串中的参数

querystring.unescape 方法是和 querystring.escape 相逆的方法,在使用 querystring.parse 方法时可能会用到。

const str = querystring.escape(`url=github.com%2Fwebfansplz&name=null`)

console.log(querystring.parse(str)) // { 'url=github.com%2Fwebfansplz&name=null': '' } ✖️

console.log(querystring.parse(querystring.unescape(str))) // { url: 'github.com/webfansplz', name: 'null' }

<h2 id="11">11. module 模块</h2>

Node.js 实现了一个简单的模块加载系统。在 Node.js 中,文件和模块是一一对应的关系,可以理解为一个文件就是一个模块。其模块系统的实现主要依赖于全局对象 module,其中实现了 exports(导出)、require()(加载)等机制。

1. 模块加载

Node.js 中一个文件就是一个模块。如,在 index.js 中加载同目录下的 circle.js:

// circle.js
const PI = Math.PI

exports.area = r => PI * r * r

exports.circumference = r => 2 * PI * r
// index.js
const circle = require('./circle.js')

console.log(`半径为 4 的圆面积为 ${circle.area(4)}`) // 半径为 4 的圆面积为 50.26548245743669

circle.js 中通过 exports 导出了 area()和 circumference 两个方法,这两个方法可以其它模块中调用。

exports 与 module.exports

exports 是对 module.exports 的一个简单引用。如果你需要将模块导出为一个函数(如:构造函数),或者想导出一个完整的出口对象而不是做为属性导出,这时应该使用 module.exports。

// square.js

module.exports = width => {
  return {
    area: () => width * width
  }
}
// index.js

const square = require('./square.js')
const mySquare = square(2)
console.log(`The area of my square is ${mySquare.area()}`) // The area of my square is 4

2. 访问主模块

当 Node.js 直接运行一个文件时,require.main 属性会被设置为 module 本身。这样,就可通过这个属性判断模块是否被直接运行:

require.main === module

比如,对于上面例子的 index.js 来说, node index.js 上面值就是 true, 而通过 require('./index')时, 值却是 false.

module 提供了一个 filename 属性,其值通常等于__filename。 所以,当前程序的入口点可以通过 require.main.filename 来获取。

console.log(require.main.filename === __filename) // true

3. 解析模块路径

使用 require.resolve()函数,可以获取 require 加载的模块的确切文件名,此操作只返回解析后的文件名,不会加载该模块。

console.log(require.resolve('./square.js')) // /Users/null/meet-nodejs/module/square.js

require.resolve 的工作过程:

require(X) from module at path Y
1. If X is a core module,
   a. return the core module
   b. STOP
2. If X begins with './' or '/' or '../'
   a. LOAD_AS_FILE(Y + X)
   b. LOAD_AS_DIRECTORY(Y + X)
3. LOAD_NODE_MODULES(X, dirname(Y))
4. THROW "not found"

LOAD_AS_FILE(X)
1. If X is a file, load X as JavaScript text.  STOP
2. If X.js is a file, load X.js as JavaScript text.  STOP
3. If X.json is a file, parse X.json to a JavaScript Object.  STOP
4. If X.node is a file, load X.node as binary addon.  STOP

LOAD_AS_DIRECTORY(X)
1. If X/package.json is a file,
   a. Parse X/package.json, and look for "main" field.
   b. let M = X + (json main field)
   c. LOAD_AS_FILE(M)
2. If X/index.js is a file, load X/index.js as JavaScript text.  STOP
3. If X/index.json is a file, parse X/index.json to a JavaScript object. STOP
4. If X/index.node is a file, load X/index.node as binary addon.  STOP

LOAD_NODE_MODULES(X, START)
1. let DIRS=NODE_MODULES_PATHS(START)
2. for each DIR in DIRS:
   a. LOAD_AS_FILE(DIR/X)
   b. LOAD_AS_DIRECTORY(DIR/X)

NODE_MODULES_PATHS(START)
1. let PARTS = path split(START)
2. let I = count of PARTS - 1
3. let DIRS = []
4. while I >= 0,
   a. if PARTS[I] = "node_modules" CONTINUE
   c. DIR = path join(PARTS[0 .. I] + "node_modules")
   b. DIRS = DIRS + DIR
   c. let I = I - 1
5. return DIRS

4. 模块缓存

模块在第一次加载后会被缓存到 require.cache 对象中, 从此对象中删除键值对将会导致下一次 require 重新加载被删除的模块。

多次调用 require('index'),未必会导致模块中代码的多次执行。这是一个重要的功能,借助这一功能,可以返回部分完成的对象;这样,传递依赖也能被加载,即使它们可能导致循环依赖。

如果你希望一个模块多次执行,那么就应该输出一个函数,然后调用这个函数。

模块缓存的注意事项

模块的基于其解析后的文件名进行缓存。由于调用的位置不同,可能会解析到不同的文件(如,需要从 node_modules 文件夹加载的情况)。所以,当解析到其它文件时,就不能保证 require('index')总是会返回确切的同一对象。

另外,在不区分大小写的文件系统或系统中,不同的文件名可能解析到相同的文件,但缓存仍会将它们视为不同的模块,会多次加载文件。如:require('./index')和 require('./INDEX')会返回两个不同的对象,无论'./index'和'./INDEX'是否是同一个文件。

5. 循环依赖

当 require()存在循环调用时,模块在返回时可能并不会被执行。

// a.js
console.log('a starting')
exports.done = false
const b = require('./b.js')
console.log('in a, b.done = %j', b.done)
exports.done = true
console.log('a done')
// b.js
console.log('b starting')
exports.done = false
const a = require('./a.js')
console.log('in b, a.done = %j', a.done)
exports.done = true
console.log('b done')
// main.js
console.log('main starting')
const a = require('./a.js')
const b = require('./b.js')
console.log('in main, a.done=%j, b.done=%j', a.done, b.done)

首先 main.js 会加载 a.js,接着 a.js 又会加载 b.js。这时,b.js 又会尝试去加载 a.js。

为了防止无限的循环,a.js 会返回一个 unfinished copy 给 b.js。然后 b.js 就会停止加载,并将其 exports 对象返回给 a.js 模块。

这样 main.js 就完成了 a.js、b.js 两个文件的加载。输出如下:

$ node main.js
main starting
a starting
b starting
in b, a.done = false
b done
in a, b.done = true
a done
in main, a.done=true, b.done=true

6. 文件模块

当加载文件模块时,如果按文件名查找未找到。那么 Node.js 会尝试添加.js 和.json 的扩展名,并再次尝试查找。如果仍未找到,那么会添加.node 扩展名再次尝试查找。

对于.js 文件,会将其解析为 JavaScript 文本文件;而.json 会解析为 JOSN 文件文件;.node 会尝试解析为编译后的插件文件,并由 dlopen 进行加载。

路径解析

当加载的文件模块使用'/'前缀时,则表示绝对路径。如,require('/home/null/index.js')会加载/home/null/index.js 文件。

而使用'./'前缀时,表示相对路径。如,在 index.js 中 require('./circle')引用时,circle.js 必须在相同的目录下才能加载成功。

当没有'/'或'./'前缀时,所引用的模块必须是“核心模块”或是 node_modules 中的模块。

如果所加载的模块不存在,require()会抛出一个 code 属性为'MODULE_NOT_FOUND'的错误。

7. __dirname

当前模块的目录名。 与 __filename 的 path.dirname() 相同。

console.log(__dirname) // /Users/null/meet-nodejs/module

console.log(require('path').dirname(__filename)) // /Users/null/meet-nodejs/module

console.log(__dirname === require('path').dirname(__filename)) // true

8. module 对象

module 在每个模块中表示对当前模块的引用。 而 module.exports 又可以通过全局对象 exports 来引用。module 并不是一个全局对象,而更像一个模块内部对象。

module.children

这个模块引入的所有模块对象

module.exports

module.exports 通过模块系统创建。有时它的工作方式与我们所想的并不一致,有时我们希望模块是一些类的实例。因此,要将导出对象赋值给 module.exports,但是导出所需的对象将分配绑定本地导出变量,这可能不是我们想要的结果。

// a.js

const EventEmitter = require('events')

module.exports = new EventEmitter()

// Do some work, and after some time emit
// the 'ready' event from the module itself.
setTimeout(() => {
  module.exports.emit('ready')
}, 1000)
const a = require('./a')
a.on('ready', () => {
  console.log('module a is ready')
})

需要注意,分配给 module.exports 的导出值必须能立刻获取到,当使用回调时其不能正常执行。

exports 别名

exports 可以做为 module.exports 的一个引用。和任何变量一样,如果为它分配新值,其旧值将会失效:

function require(...) {
  // ...
  ((module, exports) => {
    // Your module code here
    exports = some_func;        // re-assigns exports, exports is no longer
                                // a shortcut, and nothing is exported.
    module.exports = some_func; // makes your module export 0
  })(module, module.exports);
  return module;
}
  • module.filename - 模块解析后的完整文件名
  • module.id - 用于区别模块的标识符,通常是完全解析后的文件名。
  • module.loaded - 模块是否加载完毕
  • module.parent - 父模块,即:引入这个模块的模块
  • module.require(id)
  • module.require 提供了类似 require()的功能,可以从最初的模块加载一个模块

<h2 id="12">12. 缓冲器 Buffer 模块</h2>

在引入 TypedArray 之前,JavaScript 语言没有用于读取或操作二进制数据流的机制。 Buffer 类是作为 Node.js API 的一部分引入的,用于在 TCP 流、文件系统操作、以及其他上下文中与八位字节流进行交互。

创建缓冲区

console.log(Buffer.from([1, 2, 3, 4, 5])) // <Buffer 01 02 03 04 05>

console.log(Buffer.from(new ArrayBuffer(8))) // <Buffer 00 00 00 00 00 00 00 00>

console.log(Buffer.from('Hello world')) // <Buffer 48 65 6c 6c 6f 20 77 6f 72 6c 64>

Buffer 与字符编码

当字符串数据被存储入 Buffer 实例或从 Buffer 实例中被提取时,可以指定一个字符编码。

// 缓冲区转换为 UTF-8 格式的字符串

const buffer = Buffer.from('Hello world')

console.log(buffer.toString()) // Hello world
// 缓冲区数据转换为base64格式字符串

const buffer = Buffer.from('Hello world')

console.log(buffer.toString('base64')) // SGVsbG8gd29ybGQ=
// 将base64编码的字符串,转换为UTF-8编码

const buffer = Buffer.from('Hello world')

const base64Str = buffer.toString('base64')

const buf = Buffer.from(base64Str, 'base64')

console.log(buf.toString('utf8')) // Hello world

<h2 id="13">13. 域名服务器 dns 模块</h2>

DNS(Domain Name System,域名系统),DNS 协议运行在 UDP 协议之上,使用端口号 53。DNS 是因特网上作为域名和 IP 地址相互映射的一个分布式数据库,能够使用户更方便的访问互联网,而不用去记住能够被机器直接读取的 IP 数串。简单的说,就是把域名(网址)解析成对应的 IP 地址。Node.js 的 dns 模块,提供了 DNS 解析功能。当使用 dns 模块中的 net.connect(80, 'github.com/webfansplz')方法 或 http 模块的 http.get({ host: 'github.com/webfansplz' })方法时,在其底层会使用 dns 模块中的 dns.lookup 方法进行域名解析。

dns 模块的两种域名解析方式

1.使用操作系统底层的 DNS 服务解析

使用操作系统底层的 DNS 服务进行域名解析时,不需要连接到网络仅使用系统自带 DNS 解析功能。这个功能由 dns.lookup()方法实现。

dns.lookup(hostname[, options], callback):将一个域名(如:'www.baidu.com')解析为第一个找到的 A 记录(IPv4)或 AAAA 记录(IPv6)

hostname 表示要解析的域名。

options 可以是一个对象或整数。如果没有提供 options 参数,则 IP v4 和 v6 地址都可以。如果 options 是整数,则必须是 4 或 6。如果 options 是对象时,会包含以下两个可选参数:

  • family:可选,IP 版本。如果提供,必须是 4 或 6。不提供则,IP v4 和 v6 地址都可以
  • hints:可选。如果提供,可以是一个或者多个 getaddrinfo 标志。若不提供,则没有标志会传给 getaddrinfo。

callback 回调函数,参数包含(err, address, family)。出错时,参数 err 是 Error 对象。address 参数表示 IP v4 或 v6 地址。family 参数是 4 或 6,表示 address 协议版本。

const dns = require('dns')

dns.lookup(`www.github.com`, (err, address, family) => {
  if (err) throw err
  console.log('地址: %j 地址族: IPv%s', address, family) // 地址: "13.229.188.59" 地址族: IPv4
})

2.连接到 DNS 服务器解析域名

在 dns 模块中,除 dns.lookup()方法外都是使用 DNS 服务器进行域名解析,解析时需要连接到网络。

dns.resolve(hostname[, rrtype], callback):将一个域名(如 'www.baidu.com')解析为一个 rrtype 指定类型的数组

hostname 表示要解析的域名。

rrtype 有以下可用值:

rrtyperecords 包含结果的类型快捷方法
'A'IPv4 地址 (默认)stringdns.resolve4()
'AAAA'IPv6 地址stringdns.resolve6()
'ANY'任何记录Objectdns.resolveAny()
'CNAME'规范名称记录stringdns.resolveCname()
'MX'邮件交换记录Objectdns.resolveMx()
'NAPTR'名称权限指针记录Objectdns.resolveNaptr()
'NS'名称服务器记录stringdns.resolveNs()
'PTR'指针记录stringdns.resolvePtr()
'SOA'开始授权记录Objectdns.resolveSoa()
'SRV'服务记录Objectdns.resolveSrv()
'TXT'文本记录string[]dns.resolveTxt()

callback 回调函数,参数包含(err, addresses)。出错时,参数 err 是 Error 对象。addresses 根据记录类型的不同返回值也不同。

const dns = require('dns')

dns.resolve('www.baidu.com', 'A', (err, addresses) => {
  if (err) throw err
  console.log(`IP地址 : ${JSON.stringify(addresses)}`) // IP地址 : ["163.177.151.110","163.177.151.109"]
})

// or

dns.resolve4('www.baidu.com', (err, addresses) => {
  if (err) throw err
  console.log(`IP地址 : ${JSON.stringify(addresses)}`) // IP地址 : ["163.177.151.110","163.177.151.109"]
})

反向 DNS 查询

将 IPv4 或 IPv6 地址解析为主机名数组。

使用 getnameinfo 方法将传入的地址和端口解析为域名和服务

dns.reverse(ip, callback)

ip 表示要反向解析的 IP 地址。

callback 回调函数,参数包含(err, domains)。出错时,参数 err 是 Error 对象。domains 解析后的域名数组。

dns.reverse('8.8.8.8', (err, domains) => {
  if (err) throw err
  console.log(domains) // [ 'dns.google' ]
})

dns.lookupService(address, port, callback)

address 表示要解析的 IP 地址字符串。

port 表示要解析的端口号。

callback 回调函数,参数包含(err, hostname, service)。出错时,参数 err 是 Error 对象。

dns.lookupService('127.0.0.1', 80, function(err, hostname, service) {
  if (err) throw err
  console.log('主机名:%s,服务类型:%s', hostname, service) // 主机名:localhost,服务类型:http
})

参考

Node.js 中文网

IT 笔录

后记

如果你和我一样喜欢前端,也爱动手折腾,欢迎关注我一起玩耍啊~ ❤️

博客

我的博客

公众号

前端时刻

公众号

查看原文

赞 52 收藏 42 评论 0

wangyuanqi 发布了文章 · 2019-12-13

tweenJs的使用及源码分析

xmind

tweenJs与css动画相比

优势

  • 更加灵活(链式补间...)
  • 可以定义多个动画,循环调用
  • 应用场景更广阔

弊端

  • 动画的的更新需要主动调用更新方法(依赖定时器或者动画主循环函数)
  • 性能没css更优

CSS3中transition和animation的属性

1) transition(过渡动画)
示例

  • 用法:transition: property duration timing-function delay
属性含义描述
transition-property指定哪个CSS属性需要应用到transition效果
transition-duration指定transition效果的持续时间
transition-timing-function指定transition效果的速度曲线
transition-delay指定transition效果的延迟时间

2) animation(关键帧动画)
示例

  • 用法:animation: name duration timing-function delay iteration-count direction fill-mode play-state
属性含义描述
animation-name指定要绑定到选择器的关键帧的名称
animation-duration指定动画的持续时间
animation-timing-function指定动画的速度曲线
animation-delay指定动画的延迟时间
animation-iteration-count指定动画的播放次数
animation-direction指定是否应该轮流反向播放动画
animation-fill-mode规定当动画不播放时(当动画完成时,或当动画有一个延迟未开始播放时),要应用到元素的样式
animation-play-state指定动画是否正在运行或已暂停

tweenjs介绍

tweenjs 是使用 JavaScript 的一个简单的补间动画库,支持数字、对象的属性和 CSS 样式属性的赋值。

tweenjs 以 <mark>平滑</mark> 的方式修改元素的属性值,需要传递给 tween 要修改的值、动画结束时的最终值和动画花费时间(duration),之后 tween 引擎就可以计算从开始动画点到结束动画点之间值,从而产生平滑的动画效果。

示例

var box = document.createElement('div');
box.style.setProperty('background-color', '#008800');
box.style.setProperty('width', '100px');
box.style.setProperty('height', '100px');
document.body.appendChild(box);

function animate() {
    requestAnimationFrame(animate);
    TWEEN.update();
}
requestAnimationFrame(animate);

var coords = { x: 0, y: 0 };
var tween = new TWEEN.Tween(coords) 
    .to({ x: 300, y: 200 }, 1000) 
    .easing(TWEEN.Easing.Quadratic.Out) 
    .onUpdate(function() { 
      box.style.setProperty('transform','translate('+coords.x+'px,'+ coords.y+'px)');
    })
    .start(); 

demo

示例说明

1、 假设有一个对象 position ,它的坐标为 x 和 y

    var position = { x: 100, y: 0 }

2、 假设有一个对象 position ,它的坐标为 x 和 y

    var tween = new TWEEN.Tween(position)
    tween.to({x: 200}, 1000)

3、 创建 tween 对象后,激活它,从而让它开始动画

    tween.start();

4、 为了平滑的动画效果,需要在同一个循环动画中调用 TWEEN.update 方法

    animate();
    function animate(){
        requestAnimationFrame(animate);
        TWEEN.update();
    }

这个动作将会更新所有被激活的 tweens ,在 1s 内 position.x 将变为 200 。

5、 可以使用 onUpdate 回调函数将结果打印到控制台上

    tween.onUpdate(function(){
        console.log( this.x );
    })

这个函数在每次 tween 被更新时都会被调用

tweenjs 动画

Tween.js 本身不会运行,你需要通过 update 方法明确告诉它什么时候开始运行,推荐在动画主循环中使用该动画,可以调用 requestAnimationFrame 方法来获得良好的图像性能。

深入理解 requestAnimationFrame

使用无参数的调用方法,update 将明确当前时间。也可以为 update 方法法明确一个时间。

    TWEEN.update(100);

意思是"更新时间 = 100 毫秒"。你可以使用它来确保代码中的所有时间相关函数都使用相同的时间值。例如,假设你有一个播放器,并希望同步运行补间。

控制 tween 动画

方法名功能参数
Tween.start控制动画开始time (延迟一段时间后触发;可选)
Tween.stop控制动画结束-
TWEEN.update手动执行动画的更新time (更新动画位于time时间点;可选)
  • chain ==> 制作多个动画,例如一个动画在另一个动画结束后开始,可以通过 chain 来实现

示例

    tweenA.chain(tweenB);  //tweenB 在 tweenA 之后开始动画,故可以制作一个无线循环的动画
    tweenB.chain(tweenA);
  • repeat ==> 制作循环动画,优于 chain,接收一个用于描述循环次数的参数
    tween.repeat(10);
    tween.repeat(infinity);
  • delay ==> 用于控制动画之间的延迟
    tween.delay(1000);
    tween.start()
  • to ==> 用于控制动画之间的延迟
/** Tween传入参数可以读取当前属性值并应用相对值来找出新的最终值 **/
    // 绝对值
    Tween(absoluteObj).to({ x: 100 });
    // 相对值
    Tween(relativeObj).to({ x: "+100" });
    // 数组
    Tween(relativeObj).to({ x: [0, -100, 100] });

回调函数

可以在每次 tween 循环周期的指定时间点调用自定义的函数

  • onStart ==> tween 动画开始前的回调函数
  • onStop ==> tween 动画结束后的回调函数
  • onUpdate ==> 在 tween 动画每次更新后执行
  • onComplete ==> 在 tween 动画全部结束后执行
    var tween = new TWEEN.Tween({
    
    }).to({
    
    }).onStart(function(){
    
    }).onUpdate(function(){
    
    })

easing函数

函数示例

函数名效果
Linear线性匀速运动效果
Quadratic二次方的缓动(t^2)
Cubic三次方的缓动
Quartic四次方的缓动
Sinusoidal正弦曲线的缓动
Exponential指数曲线的缓动
Circular圆形曲线的缓动
Elastic指数衰减的正弦曲线缓动
Back超过范围的三次方的缓动
Bounce指数衰减的反弹缓动

easing类型

  • easeIn(In) ==> 加速,先慢后快
  • easeOut(Out) ==> 减速,先快后慢
  • easeInOut(InOut) ==> 前半段加速,后半段减速

使用公式

    .easing(TWEEN.Easing.easing函数.easing类型)

tweenjs 源码分析

源码地址

控制方法

缓动函数

插值函数

应用场景总结

动画类型应用场景
CSS动画或过度动画动画需求非常简单
Tween.js动画需要涉及复杂的布局,如:需要将多个补间同步到一起,在完成一些动作之后,循环多次等等

使用它们来计算平滑曲线作为输入数据

tip

保持你的onUpdate回调非常轻量级,因为这个函数每秒钟会被调用很多次,所以如果每次更新都要花费很多的代价,那么你可能会阻塞主线程并导致可怕的结果。

查看原文

赞 0 收藏 0 评论 0

wangyuanqi 赞了回答 · 2019-12-04

解决构造的File对象,内容不正确, 最明显的就是文件大小只有几个字节

是谁教你这种写法的?你想传入一个 URL 然后获得这个 URL 所代表的 File 对象?那你方向完全是错的。

new File(['第一行', '第二行', '第三行'], '文件名');

这么构建出来的只能是一个文本文件(可以理解为 .txt),其中第一个参数表示这个文本文件每一行的内容。

你那个 size: 9 实际就是你传进去的 abff 变量的文本值字节长度。

关注 2 回答 3

认证与成就

  • 获得 9 次点赞
  • 获得 18 枚徽章 获得 0 枚金徽章, 获得 2 枚银徽章, 获得 16 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

注册于 2017-07-27
个人主页被 364 人浏览