Fastify Swagger 文档为空?加个await试试看

最近在使用 Fastify@fastify/swagger 为项目生成 OpenAPI 文档时,遇到一个奇怪的问题:明明每个路由都写好了 schema,但访问 /swagger-ui 时页面却提示 No operations defined in spec!,文档内容完全是空的。

发现问题

我按照官方文档的示例,先注册了 Swagger 插件,然后注册路由,启动服务。代码大致如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const fastify = require('fastify')();

fastify.register(require('@fastify/swagger'), {
mode: 'dynamic',
openapi: { info: { title: 'My API', version: '1.0.0' } },
exposeRoute: true,
});

fastify.get('/users', {
schema: {
response: { 200: { type: 'array', items: { type: 'object' } } }
}
}, async () => [{ id: 1 }]);

fastify.listen({ port: 3000 });

但无论怎么调整配置,文档页面始终是空的,查原始JSON 中的 paths 对象也为 {}。我检查了插件顺序、schema 格式、路由注册时机,甚至写了一个最小化示例,问题依旧。

处理问题

我尝试了所有常见排查手段:

  1. 确认插件注册顺序 – 确保 Swagger 在路由之前注册。

  2. 检查 schema 格式 – 确认 response 字段完整,且 tagsdescription 等无缺失。

  3. 调整配置选项 – 补充 openapi.info 中的必要字段,开启 exposeRoute

  4. 查看原始 JSON – 访问 /swagger-ui/json,确认 paths 为空。

  5. 写一个简化版本 – 仅保留 Swagger 和一个简单接口,但结果依然失败。

这些步骤都没能解决问题,文档依然空空如也。

找到原因

在反复测试简化示例时,我忽然意识到:fastify.register 是一个异步方法,它会返回一个 Promise。如果我不等待它完成就直接注册路由,那么 Swagger 插件的初始化可能尚未结束,导致后续路由无法被正确捕获。

于是我将代码修改为:

1
2
await fastify.register(require('@fastify/swagger'), options);
fastify.get(...);

再次运行,文档终于正常显示了!原来正是缺少了 await 导致的问题。

总结

根本原因:
@fastify/swagger 插件在注册时需要进行一些异步初始化(如构建内部状态),如果注册后立即注册路由而不等待插件完成,这些路由的 schema 信息就无法被插件收集,最终生成的文档中 paths 为空。

解决方案:
在注册 Swagger 插件时,务必加上 await(或在回调中注册路由):

1
2
3
// 正确方式
await fastify.register(require('@fastify/swagger'), options);
fastify.get(...);

如果你使用了 fastify-plugin 或模块化路由,也要确保外层已经 await 了 Swagger 的注册。

预防建议:

  • 牢记 register 是异步的,尤其是在插件有初始化逻辑时。

  • 如果不想使用 await,可以在 register 的回调中注册路由,但这样会破坏代码的线性结构,推荐直接使用 await

  • 养成编写最小化测试用例的习惯,能更快定位问题。

希望这篇文章能帮助遇到同样问题的开发者少走弯路!

一个简单的敏感信息加密方案

开发一些企业级项目时,经常会遇到“安全扫描”提醒你不能明文发送手机号和密码这类敏感信息。这个时候,最简单的混淆方法就是base64。
但base64随便拿个解码网页就能解出来了,所以还要加一点装饰……

给加密过程加个装饰

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function randomChar(length, safeChar = "ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678") {
let result = "";
for (let i = length; i > 0; i--) {
result += safeChar.charAt(Math.floor(Math.random() * safeChar.length));
}
return result
}
function sensitiveMask(length) {
// 没有传长度时用随机长度
if (!length || length < 1) {
length = Math.floor(Math.random() * 16) % 16
}
// 最长不能超过15,因为16会被4整除,且超过16后需要用更多字符表示长度
if (length > 15) {
length = 15
}
// 长度为1时,取值固定容易暴露装饰内容格式;
// 长度为4的倍数时,用普通base64解码页面会暴露原始内容
if (!(length % 4) || length === 1) {
length++
}
return (16 - length).toString(16) + randomChar(length - 1)
}

这是一个带长度前缀的随机字符串生成函数,作为装饰添加到base64的字符串前面:

1
2
sensitiveMask(5) + btoa('18812345678');
// 返回结果 bpRDYMTg4MTIzNDU2Nzg=

这个拼接后的结果,标准的base64解码页面就不认识了。
真正需要解密的后端人员,只要知道第一位是随机字符串的长度,然后从这个长度开始截取后面的字符进行base64解码就行。

解密也很简单

1
2
3
4
5
6
function descypt(word) {
let length = 16 - parseInt(word.charAt(0), 16);
return atob(word.substring(length));
}
descypt('bpRDYMTg4MTIzNDU2Nzg=');
// 返回结果 18812345678

总结

上面这个加密方案有以下优点:

  • 后端解密简单:读取第一位的装饰长度就能截取需要解密的base64部分。不需要逐字计算也没有密钥需要同步;
  • 随机幅度大,找不到规律:如果不传递length参数,这个装饰结果会让对应字段取值变化巨大,难以猜测规则;
  • 随机字符生成函数可在其他代码中使用,不容易被认定为加密解密算法。

Hello!这里是low-key.cn

Hello, world! 一个新域名,一个新开始!这里是low-key.cn,一个很低调的web前端程序员的工作经验分享博客。

首先,自我介绍一下:我的网名是Dr490n,目前在福州某个私企做web前端开发工作。狠低调的前端是本站的名称,也是我更新工作经验相关内容时使用的ID。

这个新域名low-key就是低调两个字的英文。也体现了这个网站的特色,简单、低调……但所有的内容都是本人整理出来的工作经验!就算是转载别人的内容,我也会根据自己的理解作出一些修改。添加一些我认为有意义的拓展,或者改掉一些冗余的描述。我希望这里的文章可以在对应知识点上直击痛处,让各位读者有醍醐灌顶之感。

搭建这个网站的契机

其实,这个网站已经存在十几年的时间了,之前的域名dr490n.cn我准备转成自己的生活日常分享,所以才把这个工作经验分享博客转到了这个域名。
要说这个网站的契机,就是工作的过程中我搜索过很多项目上需要的知识碎片,如果不找个地方整理成自己的内容,过一段时间之后就会忘记掉。而再次搜索时,可能之前留意过的网站已经无法访问了,只剩一堆图片报错、代码格式混乱的转载版本。
别看这个网站目前公开的文章还不多,我有很多知识碎片都是存成草稿的。就是太零碎了,感觉不做好解释发出来有点雨里雾里。等有空补好一下上下文才会发。所以,各位可能看到新的文章出现在某个过去的归档中,别见怪!那个创建时间是收集到这个知识点的时间,既然能发出来就说明在目前的环境下,这条经验还可以用。

比如,nodejs的LTS现在已经更新到22.11,但很多的项目还是基于14-18的版本搭建的。如果不注明这个经验适用于14-18左右的版本,很多人就会以为对所有版本都适用,就误入死胡同了。但现在网络上很多博客分享的经验都不会解释环境问题的……我提取的知识就必须在各种可能环境里面验证一遍,至少确认几个可以正常执行的版本,才会把这条经验分享出来。

最后,感谢各位对这个新网站的支持!希望各位都能在这个网站上收获需要的知识!共同进步!

浅谈<a>标签

给web开发的初学者们,浅显介绍下HTML最基础的元素<a>标签,以及<a>标签使用时经常遇到的问题!

<a>标签

首先,<a>标签是anchor的缩写。正确的描述应该是“锚标签”。它有两种使用形式:

  • 一种是配合name属性,作为一个“锚点”来使用。但现在已经废弃了。
  • 另一种是配合href属性,作为一个“超链接”来使用。从一个页面“链接”到另一个页面;从页面的某个部分(比如目录),跳转到另一个部分(比如正文中的某个章节)。这是目前主要使用的方式,也是后面主要说明的部分。

href超链接

我们先看下href这个属性。不意外的,它也是缩写,原词是Hypertext Reference(超文本引用)。

我们写文章的时候,都喜欢“引用”一些他人的文章,来旁证自己的观点。如果你阅读的是一些科学相关领域的书籍。你都会看到引用的段落结尾有个小记号,然后在这一页的页尾会有一个“引用了某某作者某某文章”的说明。想象一下只有书本的时代,作者可能刚好手头有这本引用的作品。但阅读这篇文章的你呢?为了查这个引用,可能要到好几家图书馆检索这个文章的信息。而“超文本链接”就是可以把引用的相关内容,直接在文章里面标出。你点击了就可以跳转到引用的地址,减少了你再到搜索引擎上查找一遍的时间。相信这样解释一下,你至少知道了href这个属性为啥不太好用英语读出来,也不会将这个标签和src(来源)弄混了。

目前href属性主要用于引用以下几种:

  • <a href="#anchor"></a> 链接到文章中的一个锚点,旧标准中锚点一定是<a name="anchor"></a>定义的。但现在可以用任意的标签的id属性作为锚点了。
  • <a href="../"></a> 链接到相对于此页面的另一个页面地址。这种不带协议或者不是/开头的地址,都是相对于当前网页的“相对地址”。如果这个链接以/开头的话,就是基于这个网站根目录的绝对地址了。
  • <a href="mailto:xxx@mail.com"></a> 链接到某个邮箱的地址,这个协议大部分浏览器都支持并且会打开系统预设的邮件收发程序,你填写的邮箱地址xxx@mail.com会被自动添加到邮件的收件人列表中。
  • <a href="tel:136****1234"></a> 链接到指定电话号码,如果你是手机访问,点击这个链接会打开手机的“通话”界面,并且将你填写的手机号136****1234作为待拨打的电话号码输入。类似这样带协议的还有sms:发送短信;javascript:执行脚本代码等各种自定义的协议。

target目标

接着,我们经常使用的属性还有target,这个属性指定在哪里显示超链接里面的内容。取值可以以下几种可能:

  • _self 用当前页面显示超链接里面的内容;
  • _blank 在空白的新标签页(或窗口,取决于浏览器设置,下同)打开超链接里面的内容;
  • _parent 用当前页面的父框架显示超链接里面的内容,如果当前页面没有父框架,则等同于_self
  • _top 用当前页面最顶级的框架显示超链接里面的内容,如果当前页面没有多级框架,则等同于_self
  • 取值为某个存在的框架<frame>或者<iframe>容器的ID,则在这个框架里打开超链接里面的内容;
  • 取值为存在的标签页(或窗口)名称(通过window.open方法或者window.name属性设置),则直接在指定的标签页(或窗口)里打开超链接里面的内容;
  • 取值为不存在的名称时,将在空白的新标签页(或窗口)打开超链接里面的内容,并且用这个取值命名这个标签页(或窗口)。

其他属性

其他常用的属性还有:

  • download 点击链接时将链接目标视为下载链接进行下载。如果有取值,则将默认下载的文件的文件名为这个值。前提是这个下载的内容安全,且与当前页面同源。
  • referrerpolicy 链接跟踪策略,指定点击这个链接时,目标站点是否需要知道点击来源于当前页面。具体取值参考MDN文档中的相关说明。

样式问题

<a>标签在作为锚点使用时,默认是隐藏的。但是作为超链接使用时,会展示出4种状态:

  • :link 超链接未被访问前的样式。如果页面上没有将<a>标签作为锚点使用,则这个伪类可以忽略,直接对a标签设定样式即可。
  • :visited 超链接地址已经被访问过的样式。注意只要浏览器历史记录中有访问过目标地址,都会设定为这个样式,就算你之前不是通过当前页面访问到目标地址的。
  • :hover 鼠标悬停在链接上时的样式。鼠标悬停只会出现在PC端之类有鼠标指针的设备上。
  • :active 超链接被用户激活时的样式。这个状态如果你不太理解的话,可以试试不用鼠标,用键盘的TabEnter、方向键来控制这个网页。就会发现要“激活”这个超链接后按Enter键,才能访问这个超链接。

但是,在实际使用中,我们会发现定义这四个伪类时的顺序会导致链接显示的效果不同。原因可能在于浏览器解释CSS时遵循的“就近原则”。这里举几例子来说:

第一种情况:

1
2
3
a:visited { color: red; }
a:hover { color: green; }
a:link { color: blue; }

我们测试一个未访问的超链接和一个已访问的超链接的鼠标移入状态(下同)会发现:把鼠标放到未访问过的蓝色链接上时,它并不变成绿色,只有放在已访问的红色链接上,链接才会变绿。

改了下顺序,出现了第二种情况:

1
2
3
a:link { color: blue; }
a:visited { color: red; }
a:hover { color: green; }

这时,无论你鼠标经过的链接有没有被访问过,它都会变成绿色啦。

其实这个CSS问题早已有高人解释过了,总结就是一个“爱恨原则”(LoVe/HAte)巧妙的将四个伪类的首字母LVHA按顺序嵌入到爱恨两个单词中。也就是正确的顺序为:

a:link、a:visited、a:hover、a:active

所以:

  • 鼠标经过的“未访问链接”同时拥有a:link、a:hover两种样式,后面的样式定义会覆盖前面的样式定义;
  • 鼠标经过的“已访问链接”同时拥有a:visited、a:hover两种样式,后面的样式定义会覆盖前面的样式定义;

引用说明

本文部分段落节选自CSDN博主「IronLavender」的原创文章谈一谈a:link、a:visited、a:hover、a:active的正确使用顺序。由于原作是“自学前端”相关的分享,部分内容可能与实际情况不符,我这边略作修改和补充。

Electron访问Webview标签里的网页全局

从Electron版本12.0.1开始,BrowserWindow默认的webPreferences配置里contextIsolation的默认值从false改成true了。先看看这个参数的描述:

是否在独立 JavaScript 环境中运行 Electron API和指定的 preload 脚本. 默认为 truepreload 脚本所运行的上下文环境只能访问其自身专用的 documentglobal window,其自身一系列内置的JavaScript (Array, Object, JSON等等) 也是如此,这些对于已加载的内容都是不可见的。 Electron API 将只在 preload 脚本中可用,在已加载页面中不可用。 这个选项应被用于加载可能不被信任的远程内容时来确保加载的内容无法篡改 preload 脚本和任何正在使用的Electron API。 该选项使用的是与Chrome内容脚本相同的技术。 你可以在开发者工具Console选项卡内顶部组合框中选择 ‘Electron Isolated Context’条目来访问这个上下文。

把文档里面翻译成中文的内容适当改回来之后,应该容易理解吧?就是 contextIsolation = true 时你预加载的 preload 脚本和窗口加载的第三方网站内容是两个隔离的 window 对象preload 脚本没办法访问第三方网站页面里的全局window对象!

当然这个修改对 <webview> 标签也是生效的,但因为Eelecton本身不鼓励使用 <webview> 标签的关系,文档里面没有明显的提示。如果你也遇到升级Electron版本后 <webview> 标签用的 preload 读取不到第三方页面的全局 window 对象的问题。可以在 <webview> 标签增加上webpreferences这个属性:

1
2
3
<webview id="viewFrame" ref="webview" :src="pageUrl" :preload="preload" :useragent="userAgent" :partition="partition"></webview>
<!-- 改成 -->
<webview id="viewFrame" ref="webview" :src="pageUrl" :preload="preload" :useragent="userAgent" :partition="partition" webpreferences="allowRunningInsecureContent, contextIsolation=0" disablewebsecurity></webview>

这样旧版的preload脚本就能正常的在 <webview> 里面执行了!

Element UI修改组件默认参数的方法

最近手上用Element UI的项目有点多,很多像 el-tableel-pagination这样覆盖默认参数要写一大堆的组件,复制粘贴虽然很方便。但如果要统一修改某个属性,比如给el-dialog对话框都加一个禁用点击遮罩关闭窗口的功能,搜索起来一个一个添加就还是很吃力。

其实这些默认参数都是可以在注入Vue前批量修改的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import Vue from 'vue'
import Element from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'

function changeDefault (e, p, v) {
Element[e].props[p].default = v
}
// 对话框默认都附加到body上,没有层级问题
changeDefault('Dialog', 'appendToBody', true)
// 禁用点击遮罩关闭对话框
changeDefault('Dialog', 'closeOnClickModal', false)
// 修改分页区域的默认显示顺序
changeDefault('Pagination', 'layout', '->,total,prev,pager,next,jumper,sizes')
// 修改分页区域每页显示几行的默认选项
changeDefault('Pagination', 'pageSizes', () => [30, 50, 100])
// 修改完再注入到Vue里面
Vue.use(Element)

注意一下格式!
第一个参数“组件名称”的首字母大写
第二个参数“属性名”需要把“-”连接的格式修改成驼峰格式
不然……会报错!!
第三个参数“属性值”为Array或者Object时,尽量用函数返回的形式!这个地方有一个警告信息,原因的话经常写Vue的估计都已经烂熟于心了。

另外,如果你修改了Element UI组件的默认值,最好在项目文档的显著位置注明一下哦!不然,后面接手的人,会惊叹于你那简单的标签格式的。

当然,还有一种自己给Element UI组件加一层包装的方案。相对于这种更加麻烦一点,这里就不赘述了。

Vue2.0没有unmounted!

重要的事情说三遍:
Vue2.0没有unmounted!
Vue2.0没有unmounted!
Vue2.0没有unmounted!

mounted里面注册的监听器要销毁的话,请用beforeDestroy

unmountedbeforeUnmount是Vue3.0版本才加入的钩子!

Vue项目里面写错名字的钩子函数不会提示,这个确实有点麻烦……
一般情况下,只会注意到mounted函数没被调用,然后去查问题发现函数名写成mount了。
类似的还有一些带s的属性,例如propsmixins。写错了这些属性名或者函数名,报错的都不是这些属性定义的位置,而是你使用到这些属性里面的元素时才会……
嘛,谁让你不认真核对变量名呢?

你也许不需要 jQuery

来源:GitHub - You-Dont-Need-jQuery

前端发展很快,现代浏览器原生 API 已经足够好用。我们并不需要为了操作 DOM、Event 等再学习一下 jQuery 的 API。同时由于 React、Angular、Vue 等框架的流行,直接操作 DOM 不再是好的模式,jQuery 使用场景大大减少。本项目总结了大部分 jQuery API 替代的方法,暂时只支持 IE10 以上浏览器。

Query 选择器

常用的 class、id、属性 选择器都可以使用 document.querySelectordocument.querySelectorAll 替代。区别是

  • document.querySelector 返回第一个匹配的 Element
  • document.querySelectorAll 返回所有匹配的 Element 组成的 NodeList。它可以通过 [].slice.call() 把它转成 Array
  • 如果匹配不到任何 Element,jQuery 返回空数组 [],但 document.querySelector 返回 null,注意空指针异常。当找不到时,也可以使用 || 设置默认的值,如 document.querySelectorAll(selector) || []

注意:document.querySelectordocument.querySelectorAll 性能很。如果想提高性能,尽量使用 document.getElementByIddocument.getElementsByClassNamedocument.getElementsByTagName

  • 1.0 选择器查询

    1
    2
    3
    4
    5
    // jQuery
    $('selector');

    // Native
    document.querySelectorAll('selector');
  • 1.1 class 查询

    1
    2
    3
    4
    5
    6
    7
    8
    // jQuery
    $('.class');

    // Native
    document.querySelectorAll('.class');

    // or
    document.getElementsByClassName('class');
  • 1.2 id 查询

    1
    2
    3
    4
    5
    6
    7
    8
    // jQuery
    $('#id');

    // Native
    document.querySelector('#id');

    // or
    document.getElementById('id');
  • 1.3 属性查询

    1
    2
    3
    4
    5
    // jQuery
    $('a[target=_blank]');

    // Native
    document.querySelectorAll('a[target=_blank]');
  • 1.4 后代查询

    1
    2
    3
    4
    5
    // jQuery
    $el.find('li');

    // Native
    el.querySelectorAll('li');
  • 1.5 兄弟及上下元素

    • 兄弟元素

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      // jQuery
      $el.siblings();

      // Native - latest, Edge13+
      [...el.parentNode.children].filter((child) =>
      child !== el
      );
      // Native (alternative) - latest, Edge13+
      Array.from(el.parentNode.children).filter((child) =>
      child !== el
      );
      // Native - IE10+
      Array.prototype.filter.call(el.parentNode.children, (child) =>
      child !== el
      );
    • 上一个元素

      1
      2
      3
      4
      5
      6
      // jQuery
      $el.prev();

      // Native
      el.previousElementSibling;

    • 下一个元素

      1
      2
      3
      4
      5
      // next
      $el.next();

      // Native
      el.nextElementSibling;
  • 1.6 最近的祖先元素

    Closest 获得匹配选择器的第一个祖先元素,从当前元素开始沿 DOM 树向上。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    // jQuery
    $el.closest(queryString);

    // Native - Only latest, NO IE
    el.closest(selector);

    // Native - IE10+
    function closest(el, selector) {
    const matchesSelector = el.matches || el.webkitMatchesSelector || el.mozMatchesSelector || el.msMatchesSelector;

    while (el) {
    if (matchesSelector.call(el, selector)) {
    return el;
    } else {
    el = el.parentElement;
    }
    }
    return null;
    }
  • 1.7 匹配元素的祖先集

    获取当前匹配元素的每层祖先的集合,不包括匹配元素的本身。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    // jQuery
    $el.parentsUntil(selector, filter);

    // Native
    function parentsUntil(el, selector, filter) {
    const result = [];
    const matchesSelector = el.matches || el.webkitMatchesSelector || el.mozMatchesSelector || el.msMatchesSelector;

    // match start from parent
    el = el.parentElement;
    while (el && !matchesSelector.call(el, selector)) {
    if (!filter) {
    result.push(el);
    } else {
    if (matchesSelector.call(el, filter)) {
    result.push(el);
    }
    }
    el = el.parentElement;
    }
    return result;
    }
  • 1.8 表单

    • Input/Textarea

      1
      2
      3
      4
      5
      // jQuery
      $('#my-input').val();

      // Native
      document.querySelector('#my-input').value;
    • 获取 e.currentTarget 在 .radio 中的数组索引

      1
      2
      3
      4
      5
      // jQuery
      $('.radio').index(e.currentTarget);

      // Native
      Array.prototype.indexOf.call(document.querySelectorAll('.radio'), e.currentTarget);
  • 1.9 Iframe里的内容

    jQuery 对象的 iframe contents() 返回的是 iframe 内的 document

    • Iframe里的内容

      1
      2
      3
      4
      5
      // jQuery
      $iframe.contents();

      // Native
      iframe.contentDocument;
    • Iframe里的查询

      1
      2
      3
      4
      5
      // jQuery
      $iframe.contents().find('.css');

      // Native
      iframe.contentDocument.querySelectorAll('.css');
  • 1.10 获取 body

    1
    2
    3
    4
    5
    // jQuery
    $('body');

    // Native
    document.body;
  • 1.11 获取或设置属性

    • 获取属性

      1
      2
      3
      4
      5
      // jQuery
      $el.attr('foo');

      // Native
      el.getAttribute('foo');
    • 设置属性

      1
      2
      3
      4
      5
      // jQuery, note that this works in memory without change the DOM
      $el.attr('foo', 'bar');

      // Native
      el.setAttribute('foo', 'bar');
    • 获取 data- 属性

      1
      2
      3
      4
      5
      6
      7
      8
      // jQuery
      $el.data('foo');

      // Native (use `getAttribute`)
      el.getAttribute('data-foo');

      // Native (use `dataset` if only need to support IE 11+)
      el.dataset['foo'];

CSS和样式

  • 2.1 CSS

    • 获取样式

      1
      2
      3
      4
      5
      6
      7
      8
      9
      // jQuery
      $el.css("color");

      // Native
      // 注意:此处为了解决当 style 值为 auto 时,返回 auto 的问题
      const win = el.ownerDocument.defaultView;

      // null 的意思是不返回伪类元素
      win.getComputedStyle(el, null).color;
    • 设置样式

      1
      2
      3
      4
      5
      // jQuery
      $el.css({ color: "#ff0011" });

      // Native
      el.style.color = '#ff0011';
    • 同时操作多个样式

      注意,如果想一次设置多个 style,可以参考 oui-dom-utils 中 setStyles 方法

    • 添加class

      1
      2
      3
      4
      5
      // jQuery
      $el.addClass(className);

      // Native
      el.classList.add(className);
    • 移除class

      1
      2
      3
      4
      5
      // jQuery
      $el.removeClass(className);

      // Native
      el.classList.remove(className);
    • 是否包含class

      1
      2
      3
      4
      5
      // jQuery
      $el.hasClass(className);

      // Native
      el.classList.contains(className);
    • 切换class

      1
      2
      3
      4
      5
      // jQuery
      $el.toggleClass(className);

      // Native
      el.classList.toggle(className);
  • 2.2 获取宽高

    Width 与 Height 获取方法相同,下面以 Height 为例:

    • 窗口高度

      1
      2
      3
      4
      5
      6
      7
      8
      // window height
      $(window).height();

      // 含 scrollbar
      window.document.documentElement.clientHeight;

      // 不含 scrollbar,与 jQuery 行为一致
      window.innerHeight;
    • 文档高度

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      // jQuery
      $(document).height();

      // Native
      const body = document.body;
      const html = document.documentElement;
      const height = Math.max(
      body.offsetHeight,
      body.scrollHeight,
      html.clientHeight,
      html.offsetHeight,
      html.scrollHeight
      );
    • 元素高度

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      // jQuery
      $el.height();

      // Native
      function getHeight(el) {
      const styles = this.getComputedStyle(el);
      const height = el.offsetHeight;
      const borderTopWidth = parseFloat(styles.borderTopWidth);
      const borderBottomWidth = parseFloat(styles.borderBottomWidth);
      const paddingTop = parseFloat(styles.paddingTop);
      const paddingBottom = parseFloat(styles.paddingBottom);
      return height - borderBottomWidth - borderTopWidth - paddingTop - paddingBottom;
      }

      // 精确到整数(border-box 时为 height - border 值,content-box 时为 height + padding 值)
      el.clientHeight;

      // 精确到小数(border-box 时为 height 值,content-box 时为 height + padding + border 值)
      el.getBoundingClientRect().height;
  • 2.3 位置和偏移

    • 位置

      获得匹配元素相对父元素的偏移

      1
      2
      3
      4
      5
      // jQuery
      $el.position();

      // Native
      { left: el.offsetLeft, top: el.offsetTop }
    • 偏移

      获得匹配元素相对文档的偏移

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      // jQuery
      $el.offset();

      // Native
      function getOffset (el) {
      const box = el.getBoundingClientRect();

      return {
      top: box.top + window.pageYOffset - document.documentElement.clientTop,
      left: box.left + window.pageXOffset - document.documentElement.clientLeft
      }
      }
  • 2.4 滚动条垂直位置

    获取元素滚动条垂直位置。

    1
    2
    3
    4
    5
    // jQuery
    $(window).scrollTop();

    // Native
    (document.documentElement && document.documentElement.scrollTop) || document.body.scrollTop;

DOM 操作

  • 3.1 移除元素

    从 DOM 中移除元素。

    1
    2
    3
    4
    5
    // jQuery
    $el.remove();

    // Native
    el.parentNode.removeChild(el);
  • 3.2 元素的文本内容

    • 获取文本内容

      返回指定元素及其后代的文本内容。

      1
      2
      3
      4
      5
      // jQuery
      $el.text();

      // Native
      el.textContent;
    • 设置文本内容

      设置元素的文本内容。

      1
      2
      3
      4
      5
      // jQuery
      $el.text(string);

      // Native
      el.textContent = string;
  • 3.3 HTML

    • 获取HTML

      1
      2
      3
      4
      5
      // jQuery
      $el.html();

      // Native
      el.innerHTML;
    • 设置HTML

      1
      2
      3
      4
      5
      // jQuery
      $el.html(htmlString);

      // Native
      el.innerHTML = htmlString;
  • 3.4 插入到末尾

    Append 插入到子节点的末尾

    1
    2
    3
    4
    5
    6
    7
    8
    // jQuery
    $el.append("<div id='container'>hello</div>");

    // Native (HTML string)
    el.insertAdjacentHTML('beforeend', '<div id="container">Hello World</div>');

    // Native (Element)
    el.appendChild(newEl);
  • 3.5 插入到开头

    1
    2
    3
    4
    5
    6
    7
    8
    // jQuery
    $el.prepend("<div id='container'>hello</div>");

    // Native (HTML string)
    el.insertAdjacentHTML('afterbegin', '<div id="container">Hello World</div>');

    // Native (Element)
    el.insertBefore(newEl, el.firstChild);
  • 3.6 在元素前插入

    在选中元素前插入新节点

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // jQuery
    $newEl.insertBefore(queryString);

    // Native (HTML string)
    el.insertAdjacentHTML('beforebegin ', '<div id="container">Hello World</div>');

    // Native (Element)
    const el = document.querySelector(selector);
    if (el.parentNode) {
    el.parentNode.insertBefore(newEl, el);
    }
  • 3.7 在元素后插入

    在选中元素后插入新节点

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // jQuery
    $newEl.insertAfter(queryString);

    // Native (HTML string)
    el.insertAdjacentHTML('afterend', '<div id="container">Hello World</div>');

    // Native (Element)
    const el = document.querySelector(selector);
    if (el.parentNode) {
    el.parentNode.insertBefore(newEl, el.nextSibling);
    }
  • 3.8 匹配

    如果匹配给定的选择器,返回true

    1
    2
    3
    4
    5
    // jQuery
    $el.is(selector);

    // Native
    el.matches(selector);
  • 3.9 克隆/深拷贝

    深拷贝被选元素。(生成被选元素的副本,包含子节点、文本和属性。)

    1
    2
    3
    4
    5
    6
    7
    //jQuery
    $el.clone();

    //Native
    //深拷贝添加参数‘true’
    el.cloneNode();

  • 3.10 清空

    移除所有子节点

1
2
3
4
5
//jQuery
$el.empty();

//Native
el.innerHTML = '';
  • 3.11 包装

把每个被选元素放置在指定的HTML结构中。

1
2
3
4
5
6
7
8
9
10
11
12
//jQuery
$(".inner").wrap('<div class="wrapper"></div>');

//Native
Array.prototype.forEach.call(document.querySelector('.inner'), (el) => {
const wrapper = document.createElement('div');
wrapper.className = 'wrapper';
el.parentNode.insertBefore(wrapper, el);
el.parentNode.removeChild(el);
wrapper.appendChild(el);
});

  • 3.12 拆包装

    移除被选元素的父元素的DOM结构

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // jQuery
    $('.inner').unwrap();

    // Native
    Array.prototype.forEach.call(document.querySelectorAll('.inner'), (el) => {
    let elParentNode = el.parentNode

    if(elParentNode !== document.body) {
    elParentNode.parentNode.insertBefore(el, elParentNode)
    elParentNode.parentNode.removeChild(elParentNode)
    }
    });
  • 3.13 替换

    用指定的元素替换被选的元素

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    //jQuery
    $('.inner').replaceWith('<div class="outer"></div>');

    //Native
    Array.prototype.forEach.call(document.querySelectorAll('.inner'),(el) => {
    const outer = document.createElement("div");
    outer.className = "outer";
    el.parentNode.insertBefore(outer, el);
    el.parentNode.removeChild(el);
    });
  • 3.14 解析HTML

 解析 HTML/SVG/XML 字符串

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// jQuery
$(`<ol>
<li>a</li>
<li>b</li>
</ol>
<ol>
<li>c</li>
<li>d</li>
</ol>`);

// Native
range = document.createRange();
parse = range.createContextualFragment.bind(range);

parse(`<ol>
<li>a</li>
<li>b</li>
</ol>
<ol>
<li>c</li>
<li>d</li>
</ol>`);

Ajax

Fetch API 是用于替换 XMLHttpRequest 处理 ajax 的新标准,Chrome 和 Firefox 均支持,旧浏览器可以使用 polyfills 提供支持。

IE9+ 请使用 github/fetch,IE8+ 请使用 fetch-ie8,JSONP 请使用 fetch-jsonp

  • 4.1 从服务器读取数据并替换匹配元素的内容。

    1
    2
    3
    4
    5
    6
    7
    // jQuery
    $(selector).load(url, completeCallback)

    // Native
    fetch(url).then(data => data.text()).then(data => {
    document.querySelector(selector).innerHTML = data
    }).then(completeCallback)

事件

完整地替代命名空间和事件代理,链接到 https://github.com/oneuijs/oui-dom-events

  • 5.0 Document ready by DOMContentLoaded

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // jQuery
    $(document).ready(eventHandler);

    // Native
    // 检测 DOMContentLoaded 是否已完成
    if (document.readyState !== 'loading') {
    eventHandler();
    } else {
    document.addEventListener('DOMContentLoaded', eventHandler);
    }
  • 5.1 使用 on 绑定事件

    1
    2
    3
    4
    5
    // jQuery
    $el.on(eventName, eventHandler);

    // Native
    el.addEventListener(eventName, eventHandler);
  • 5.2 使用 off 解绑事件

    1
    2
    3
    4
    5
    // jQuery
    $el.off(eventName, eventHandler);

    // Native
    el.removeEventListener(eventName, eventHandler);
  • 5.3 Trigger

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // jQuery
    $(el).trigger('custom-event', {key1: 'data'});

    // Native
    if (window.CustomEvent) {
    const event = new CustomEvent('custom-event', {detail: {key1: 'data'}});
    } else {
    const event = document.createEvent('CustomEvent');
    event.initCustomEvent('custom-event', true, true, {key1: 'data'});
    }

    el.dispatchEvent(event);

实用工具

大部分实用工具都能在 native API 中找到. 其他高级功能可以选用专注于该领域的稳定性和性能都更好的库来代替,推荐 lodash

  • 6.1 基本工具

    • isArray

    检测参数是不是数组。

    1
    2
    3
    4
    5
    // jQuery
    $.isArray(range);

    // Native
    Array.isArray(range);
    • isWindow

    检测参数是不是 window。

    1
    2
    3
    4
    5
    6
    7
    // jQuery
    $.isWindow(obj);

    // Native
    function isWindow(obj) {
    return obj !== null && obj !== undefined && obj === obj.window;
    }
    • inArray

    在数组中搜索指定值并返回索引 (找不到则返回 -1)。

    1
    2
    3
    4
    5
    6
    7
    8
    // jQuery
    $.inArray(item, array);

    // Native
    array.indexOf(item) > -1;

    // ES6-way
    array.includes(item);
    • isNumeric

    检测传入的参数是不是数字。
    Use typeof to decide the type or the type example for better accuracy.

    1
    2
    3
    4
    5
    6
    7
    // jQuery
    $.isNumeric(item);

    // Native
    function isNumeric(n) {
    return !isNaN(parseFloat(n)) && isFinite(n);
    }
    • isFunction

    检测传入的参数是不是 JavaScript 函数对象。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // jQuery
    $.isFunction(item);

    // Native
    function isFunction(item) {
    if (typeof item === 'function') {
    return true;
    }
    var type = Object.prototype.toString(item);
    return type === '[object Function]' || type === '[object GeneratorFunction]';
    }
    • isEmptyObject

    检测对象是否为空 (包括不可枚举属性).

    1
    2
    3
    4
    5
    6
    7
    // jQuery
    $.isEmptyObject(obj);

    // Native
    function isEmptyObject(obj) {
    return Object.keys(obj).length === 0;
    }
    • isPlainObject

    检测是不是扁平对象 (使用 “{}” 或 “new Object” 创建).

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    // jQuery
    $.isPlainObject(obj);

    // Native
    function isPlainObject(obj) {
    if (typeof (obj) !== 'object' || obj.nodeType || obj !== null && obj !== undefined && obj === obj.window) {
    return false;
    }

    if (obj.constructor &&
    !Object.prototype.hasOwnProperty.call(obj.constructor.prototype, 'isPrototypeOf')) {
    return false;
    }

    return true;
    }
    • extend

    合并多个对象的内容到第一个对象。
    object.assign 是 ES6 API,也可以使用 polyfill

    1
    2
    3
    4
    5
    // jQuery
    $.extend({}, defaultOpts, opts);

    // Native
    Object.assign({}, defaultOpts, opts);
    • trim

    移除字符串头尾空白。

    1
    2
    3
    4
    5
    // jQuery
    $.trim(string);

    // Native
    string.trim();
    • map

    将数组或对象转化为包含新内容的数组。

    1
    2
    3
    4
    5
    6
    7
    // jQuery
    $.map(array, (value, index) => {
    });

    // Native
    array.map((value, index) => {
    });
    • each

    轮询函数,可用于平滑的轮询对象和数组。

    1
    2
    3
    4
    5
    6
    7
    // jQuery
    $.each(array, (index, value) => {
    });

    // Native
    array.forEach((value, index) => {
    });
    • grep

    找到数组中符合过滤函数的元素。

    1
    2
    3
    4
    5
    6
    7
    // jQuery
    $.grep(array, (value, index) => {
    });

    // Native
    array.filter((value, index) => {
    });
    • type

    检测对象的 JavaScript [Class] 内部类型。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // jQuery
    $.type(obj);

    // Native
    function type(item) {
    const reTypeOf = /(?:^\[object\s(.*?)\]$)/;
    return Object.prototype.toString.call(item)
    .replace(reTypeOf, '$1')
    .toLowerCase();
    }
    • merge

    合并第二个数组内容到第一个数组。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    // jQuery
    $.merge(array1, array2);

    // Native
    // 使用 concat,不能去除重复值
    function merge(...args) {
    return [].concat(...args)
    }

    // ES6,同样不能去除重复值
    array1 = [...array1, ...array2]

    // 使用 Set,可以去除重复值
    function merge(...args) {
    return Array.from(new Set([].concat(...args)))
    }
    • now

    返回当前时间的数字呈现。

    1
    2
    3
    4
    5
    // jQuery
    $.now();

    // Native
    Date.now();
    • proxy

    传入函数并返回一个新函数,该函数绑定指定上下文。

    1
    2
    3
    4
    5
    // jQuery
    $.proxy(fn, context);

    // Native
    fn.bind(context);
    • makeArray

    类数组对象转化为真正的 JavaScript 数组。

    1
    2
    3
    4
    5
    6
    7
    8
    // jQuery
    $.makeArray(arrayLike);

    // Native
    Array.prototype.slice.call(arrayLike);

    // ES6-way
    Array.from(arrayLike);
  • 6.2 包含

    检测 DOM 元素是不是其他 DOM 元素的后代.

    1
    2
    3
    4
    5
    // jQuery
    $.contains(el, child);

    // Native
    el !== child && el.contains(child);
  • 6.3 Globaleval

    全局执行 JavaScript 代码。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // jQuery
    $.globaleval(code);

    // Native
    function Globaleval(code) {
    const script = document.createElement('script');
    script.text = code;

    document.head.appendChild(script).parentNode.removeChild(script);
    }

    // Use eval, but context of eval is current, context of $.Globaleval is global.
    eval(code);
  • 6.4 解析

    • parseHTML

    解析字符串为 DOM 节点数组.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    // jQuery
    $.parseHTML(htmlString);

    // Native
    function parseHTML(string) {
    const context = document.implementation.createHTMLDocument();

    // Set the base href for the created document so any parsed elements with URLs
    // are based on the document's URL
    const base = context.createElement('base');
    base.href = document.location.href;
    context.head.appendChild(base);

    context.body.innerHTML = string;
    return context.body.children;
    }
    • parseJSON

    传入格式正确的 JSON 字符串并返回 JavaScript 值.

    1
    2
    3
    4
    5
    // jQuery
    $.parseJSON(str);

    // Native
    JSON.parse(str);

Promises

Promise 代表异步操作的最终结果。jQuery 用它自己的方式处理 promises,原生 JavaScript 遵循 Promises/A+ 标准实现了最小 API 来处理 promises。

  • 7.1 done, fail, always

    done 会在 promise 解决时调用,fail 会在 promise 拒绝时调用,always 总会调用。

    1
    2
    3
    4
    5
    // jQuery
    $promise.done(doneCallback).fail(failCallback).always(alwaysCallback)

    // Native
    promise.then(doneCallback, failCallback).then(alwaysCallback, alwaysCallback)
  • 7.2 when

    when 用于处理多个 promises。当全部 promises 被解决时返回,当任一 promise 被拒绝时拒绝。

    1
    2
    3
    4
    5
    6
    // jQuery
    $.when($promise1, $promise2).done((promise1Result, promise2Result) => {
    });

    // Native
    Promise.all([$promise1, $promise2]).then([promise1Result, promise2Result] => {});
  • 7.3 Deferred

    Deferred 是创建 promises 的一种方式。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    // jQuery
    function asyncFunc() {
    const defer = new $.Deferred();
    setTimeout(() => {
    if(true) {
    defer.resolve('some_value_computed_asynchronously');
    } else {
    defer.reject('failed');
    }
    }, 1000);

    return defer.promise();
    }

    // Native
    function asyncFunc() {
    return new Promise((resolve, reject) => {
    setTimeout(() => {
    if (true) {
    resolve('some_value_computed_asynchronously');
    } else {
    reject('failed');
    }
    }, 1000);
    });
    }

    // Deferred way
    function defer() {
    const deferred = {};
    const promise = new Promise((resolve, reject) => {
    deferred.resolve = resolve;
    deferred.reject = reject;
    });

    deferred.promise = () => {
    return promise;
    };

    return deferred;
    }

    function asyncFunc() {
    const defer = defer();
    setTimeout(() => {
    if(true) {
    defer.resolve('some_value_computed_asynchronously');
    } else {
    defer.reject('failed');
    }
    }, 1000);

    return defer.promise();
    }

动画

  • 8.1 Show & Hide

    1
    2
    3
    4
    5
    6
    7
    8
    // jQuery
    $el.show();
    $el.hide();

    // Native
    // 更多 show 方法的细节详见 https://github.com/oneuijs/oui-dom-utils/blob/master/src/index.js#L363
    el.style.display = ''|'inline'|'inline-block'|'inline-table'|'block';
    el.style.display = 'none';
  • 8.2 Toggle

    显示或隐藏元素。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // jQuery
    $el.toggle();

    // Native
    if (el.ownerDocument.defaultView.getComputedStyle(el, null).display === 'none') {
    el.style.display = ''|'inline'|'inline-block'|'inline-table'|'block';
    } else {
    el.style.display = 'none';
    }
  • 8.3 FadeIn & FadeOut

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // jQuery
    $el.fadeIn(3000);
    $el.fadeOut(3000);

    // Native
    el.style.transition = 'opacity 3s';
    // fadeIn
    el.style.opacity = '1';
    // fadeOut
    el.style.opacity = '0';
  • 8.4 FadeTo

    调整元素透明度。

    1
    2
    3
    4
    5
    // jQuery
    $el.fadeTo('slow',0.15);
    // Native
    el.style.transition = 'opacity 3s'; // 假设 'slow' 等于 3 秒
    el.style.opacity = '0.15';
  • 8.5 FadeToggle

    动画调整透明度用来显示或隐藏。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // jQuery
    $el.fadeToggle();

    // Native
    el.style.transition = 'opacity 3s';
    const { opacity } = el.ownerDocument.defaultView.getComputedStyle(el, null);
    if (opacity === '1') {
    el.style.opacity = '0';
    } else {
    el.style.opacity = '1';
    }
  • 8.6 SlideUp & SlideDown

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // jQuery
    $el.slideUp();
    $el.slideDown();

    // Native
    const originHeight = '100px';
    el.style.transition = 'height 3s';
    // slideUp
    el.style.height = '0px';
    // slideDown
    el.style.height = originHeight;
  • 8.7 SlideToggle

    滑动切换显示或隐藏。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // jQuery
    $el.slideToggle();

    // Native
    const originHeight = '100px';
    el.style.transition = 'height 3s';
    const { height } = el.ownerDocument.defaultView.getComputedStyle(el, null);
    if (parseInt(height, 10) === 0) {
    el.style.height = originHeight;
    }
    else {
    el.style.height = '0px';
    }
  • 8.8 Animate

    执行一系列 CSS 属性动画。

    1
    2
    3
    4
    5
    6
    7
    8
    // jQuery
    $el.animate({ params }, speed);

    // Native
    el.style.transition = 'all ' + speed;
    Object.keys(params).forEach((key) =>
    el.style[key] = params[key];
    )

替代品

浏览器支持

Chrome Firefox IE Opera Safari
Latest ✔ Latest ✔ 10+ ✔ Latest ✔ 6.1+ ✔

License

MIT

Lodash的一些小坑

记录几个Lodash在实际运用中遇到的小坑,提醒自己和看到这篇文章的所有人。
有时候一定要等BUG被别人发现了,才注意到是你没有理解开源库文档的含义……

你需要 _.clone 还是 _.cloneDeep

最简单的理解方法就是看代码!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
let a = {
aaa: "Hello,",
bbb: "My",
ccc: "Friend!",
array: [{a:3}, {a:5}, {a:6}, {a:8}],
object: { b:12, c:22}
}
let b = _.clone(a)
let c = _.cloneDeep(a)
// 我们直接给a添加属性
a.kkk = 789
console.log(b)
// >>> {"aaa":"Hello,","bbb":"My","ccc":"Friend!","array":[{"a":3},{"a":5},{"a":6},{"a":8}],"object":{"b":12,"c":22}}
// 好像没啥问题
// 但是修改了a.object里面的属性之后……
a.object.a = 7
console.log(b)
// >>> {"aaa":"Hello,","bbb":"My","ccc":"Friend!","array":[{"a":3},{"a":5},{"a":6},{"a":8}],"object":{"b":12,"c":22,"a":7}}
// 看到object里面的属性a=7了么?

// 同理,改数组array的操作也……
a.array.push({a:233})
a.array[0].b = 996
console.log(b)
// >>> {"aaa":"Hello,","bbb":"My","ccc":"Friend!","array":[{"a":3,"b":996},{"a":5},{"a":6},{"a":8},{"a":233}],"object":{"b":12,"c":22,"a":7}}
// 我只想克隆一份,但为啥内层的值都变了?
// 这时候我们再看看c
console.log(c)
// >>> {"aaa":"Hello,","bbb":"My","ccc":"Friend!","array":[{"a":3},{"a":5},{"a":6},{"a":8}],"object":{"b":12,"c":22}}

为啥要用深克隆(cloneDeep)就很明显了吧?

_.extend_.merge 也有深浅的关系!

_.extend来对配置进行合并,结果……

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
let a = {
aaa: 1,
bbb: 2,
ccc: 3,
object: {
ddd: 4,
eee: 5,
fff: 6,
sub: {
ggg: 7,
hhh: 8,
iii: 9
}
}
}

let b = {
ccc: 15,
bbb: 19,
kkk: 13,
object: {
eee: 26,
zzz: 32,
sub: {
ccc: 19,
kkk: 26
}
}
}
console.log(_.extend({}, a, b))
// >>> {"aaa":1,"bbb":19,"ccc":15,"object":{"eee":26,"zzz":32,"sub":{"ccc":19,"kkk":26}},"kkk":13}
// object里面的ddd和fff属性都没了?

如果要保留a的默认值,又要用b等子集去覆盖,建议使用_.merge

1
2
3
console.log(_.merge({}, a, b))
// >>> {"aaa":1,"bbb":19,"ccc":15,"object":{"ddd":4,"eee":26,"fff":6,"sub":{"ggg":7,"hhh":8,"iii":9,"ccc":19,"kkk":26},"zzz":32},"kkk":13}
// 这样才符合我的需求

80端口被System进程占用的问题

发现问题

今天准备测试一些PHP脚本,于是打开了久违的XMAPP环境
但是XMAPP环境并没有像以往的那样正常地启动起来,而是在日志区域显示了这样的一段提示:

XMAPP环境无法启动

XMAPP环境无法启动,红色提示说80口被PID为4的进程占用。

然后我看了下Netstat的记录,确实是被PID为4的进程占用了:
从NETSTAT里面查找占用端口的进程

说明一下netstat的指令:
我使用的指令是netstat -ano,其中:
-a是显示所有连接和监听端口;
-n是以数字IP地址的方式显示地址和端口号,如果不写这一项会看到“外部地址”一列有的会显示为域名或主机名;
-o是显示连接对应进程的ID
所以,要查明对应关系,必须至少输入“netstat -ao”。
当然,如果你发现列表太长找不到你要找的80口数据,你可以附加一个处理字符串的“|findstr 80”就可以精确定位出几条80口的连接记录了。

解决方法

遇到这种情况,首先要确定占用进程是哪个。
我们用到的是每个电脑都有的软件——“Windows任务管理器”。

什么?你不知道怎么打开“任务管理器”?
……同时按住Ctrl+Alt+Delete,就能看到“启动任务管理器”的选项(Win 7)或者直接打开了“任务管理器”(Windows XP)。
……或者在任务栏上面打开右键菜单也能找到“启动任务管理器”选项。

将“任务管理器”切换到“进程”面板,查找一下PID为4的进程。一般显示的都是System。这是一个系统进程,使用右下角的“结束进程”没有办法关闭这个进程。

在任务管理器中找到PID为4的进程

如果你的“Windows任务管理器”里面看不到“PID”一列,你可以将其显示出来。方法如下:

  • 打开“查看”菜单,点击“选择列…”菜单项。
    “查看”菜单中的“选择列...”菜单项
  • 在打开的新窗口中勾选“PID”一行,点击“确定”即可。
    “选择进程页列”窗口

遇到这种情况就只能重启么?不,不一定!

可以去看下你最近安装的服务,将他们关闭一下试试看!

比如说:
我前段时间为了测试IIS下面的伪静态路由,在这部Windows 7下面安装了IIS套件。而这个套件的万维网服务(W3SVC)也是隐藏在System进程中占用80口的罪魁祸首。

在Win XP中IIS相关的服务都是使用独立的inetinfo.exe进程,比较容易看出是IIS在占用,
而到了我使用的Win 7的时候,IIS服务的启动指令已经变成了
C:\\Windows\\system32\\svchost.exe -k iissvcs
这个“Windows服务主进程”的模式来运行,这个时候看到的监听端口的进程很有可能被显示成System。

这时候,只要关闭对应的服务就能释放出被监听的80端口了。
从服务列表中关闭W3SVC服务项目

打开“服务”管理页的方法:

  • “计算机”(或者“我的电脑”)右键菜单选择“管理”打开的窗口中展开左侧树的“服务和应用程序”节点,找到“服务”一项。
  • “控制面板”里面找到“系统和安全”(如果显示的是分类界面)里的“管理工具”,双击“服务”快捷方式打开。
  • 打开“开始”菜单,选择“运行…”,然后输入“services.msc”即可打开。

当然,并不是所有人都像我这样被IIS这样“愚蠢”的占用问题折腾地到处搜索答案。除了IIS以外,我搜索到的情况中有人是被SQL Server 2008中的报表服务所占用的。
所以,别因为多次重启后80口还是被占用而准备重装系统,很有可能是一些你前些时间安装的服务搞的鬼。