音名小工具
因为最近在学电吉他,需要用随机的音名来检查自己对指板的掌握程度,所以写了个小工具用来给自己提音名。
点这里打开小工具的页面。
原文: Authentication · Everything curl - HTTP authentication
每个 HTTP 请求, 都可以被认证. 如果一个服务器或者一个代理想要让用户证明自己有权限去访问某个 URL 或者执行某个操作, 它可以返回一个 HTTP 请求, 让客户端提供一个包含了正确的 HTTP 认证头的请求, 以便认证和允许访问.
如果一个服务器需要认证才能进行访问的话, 会返回 401 码, 以及一个 WWW-Authenticate:
头, 这个头里包含了服务器支持的所有类型的认证方式.
如果一个 HTTP 代理需要认证的话, 会返回 407 码, 以及一个 Proxy-Authenticate:
头, 这个头里包含了代理支持的所有类型的认证方式.
值得一提的是, 当今的绝大多数网站已经不再需要 HTTP 认证来进行登录等等操作了, 但是取而代之的是, 网站会要求用户在网站的登录页上进行登录, 然后网站会把用户输入的用户名和密码以 POST 请求的方式发送给服务器, 随后客户端只需要维护着 cookie 来维护 session 即可.
为了让 curl 命令发起一个带有 HTTP 认证的请求, 你需要加上 -u
或者 --user
参数来提供用户名和密码(以冒号分隔). 就像下面这样:
1 | curl --user daniel:secret http://example.com/ |
这样一来, curl 就会以 HTTP 认证中的 “Basic” 方式发起一个认证请求. Basic 方法就像它的名字一样, 真的是一种非常基础的认证. 如果你想非常明确的发出一个 Basic 方法的认证的话, 只需要加上 --basic
参数就可以了.
Basic 认证方法直接以文本的格式通过网络来发送用户名和密码了(不过是用 base64 编码过了), 然而这种 HTTP 明文发送用户名和密码的方式, 是应该避免的.
如果一定要通过 HTTP 传输、单认证方法的方式来进行认证的话, curl 会在第一个 HTTP 请求的头部里加上认证信息.
如果你想让 curl 试一下 服务器是否需要认证, 可以给 curl 加上一个 --anyauth
参数. 这样一来, curl 就会先发送一个请求, 看看是否需要认证, 如果需要认证, 再自动地选择服务器所支持的最为安全的方式进行认证:1
curl --anyauth --user daniel:secret http://example.com/
这样的思路在其他类型的可能会需要认证的 HTTP 操作上, 也行得通:1
2curl --proxy-anyauth --proxy-user daniel:secret http://example.com/ \
--proxy http://proxy.example.com:80/
curl 一般来说会同时支持好几种认证方式(取决于你在用的 curl 是如何实现的), 包括 Digest, Negotiate 和 NTLM. 如果你想用这些方式的话, 可以像这样加上参数来使用:1
2
3curl --digest --user daniel:secret http://example.com/
curl --negotiate --user daniel:secret http://example.com/
curl --ntlm --user daniel:secret http://example.com/
因为译者还没有写过这个插件, 而且英文水平有限, 所以这篇文章里还存在很多错误, 也有部分看不懂而没法翻译的地方, 读者如果愿意斧正的话, 请发邮件到我的邮箱 olafcheng@gmail.com
后续我在发现文章里的更正确的翻译后, 也会更新这篇译文.
原文地址:https://eslint.org/docs/developer-guide/working-with-rules
注意: 这个文章里,主要覆盖率 ESLint >= 3.0.0 版本的规则,如果想要看已废弃的规则,请点这里.
每个规则,在 ESLint 里都有以这个规则为标识符为开头命名的三个文件(比如, no-extra-semi
).
lib/rules
目录下: 一个源文件(例如,no-extra-semi.js
)tests/lib/rules
目录下: 一个测试文件 (例如, no-extra-semi.js
)docs/rules
目录: 一个 markdown 文档文件 (例如, no-extra-semi
)需要注意的重点:如果你想要给 ESLint 仓库提交一个核心
规则,你必须遵循下列约定.
下面是规则源文件的基本格式: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/**
* @fileoverview Rule to disallow unnecessary semicolons
* @author Nicholas C. Zakas
*/
"use strict";
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
docs: {
description: "disallow unnecessary semicolons",
category: "Possible Errors",
recommended: true,
url: "https://eslint.org/docs/rules/no-extra-semi"
},
fixable: "code",
schema: [] // no options
},
create: function(context) {
return {
// callback functions
};
}
};
规则的源文件会 exports 出一个带有下列字段的 object:
meta
(object) 包含了规则的元数据:
docs
(object) 对于 ESLint 的核心规则来说,是必须的:
description
(string) 提供了会显示在规则目录里的简短描述category
(string) 规定了这个规则在规则目录中分数哪个部分recommended
(boolean) 是否需要包含在配置文件的 “extends”: “eslint:recommended” 这个属性默认开启的规则里.url
(string) 规定了可以在哪个地方可以访问这个规则的详细信息在自定义规则或者插件里,可以不包含 docs
字段,其实其他字段也是一样的,可以去除掉.
fixable
(string) 可选值有 “code” 和 “whitespace”, 这个字段决定当在命令行里加上--fix
参数的时候, 要不要自动的纠正报错的地方
注意: 如果没有 fixable
字段, 就算在实现规则的时候写了 fix
函数, ESLint 也不会对报错的地方进行纠正. 如果规则是不可自动纠正的话,请不要写 fixable
这个字段.
schema
(array) 这个字段可以约束规则的写法, 以便 ESLint 发现有无效的规则配置文件 时进行报错.
deprecated
(boolean) 用来表明这个规则是否已经被废弃. 如果规则还正在使用的话,请不要写这个字段.create
(function) 返回一个用来遍历(visit
) JavaScript 代码的抽象语法树(一个按照ES 树来定义的 AST)节点的对象,这个对象上会挂有一些方法,以便 ESLint 在 AST 上调用:
规则可以利用当前的代码片段和附近的 AST 来报错或者纠正问题.
下面是 array-call-back 规则的一些方法
1 | function checkLastSegment (node) { |
context
对象包含了一些额外的函数,可以帮助规则来完成工作. 顾名思义,context
对象包含了跟规则相关的一些上下文的信息.context
对象包含如下的字段:
parserOptions
- 在运行的时候,将要用到的解析规则的参数(更多细节看这里)id
- 规则 ID.options
- 给这个规则传递的配置信息. 这个数组不包含规则本身. 详细信息看这里.parserServices
来访问自定义解释器提供的各种结果. (例如, TypeScript 的解释器可以计算出来一个给定的节点的数据类型)除此之外, context
对象还含有如下的方法:
getAncestors()
- 返回一个包含了所有遍历过的父节点的数组, 这些父节点从 AST 的根节点开始, 直到这个节点的直接父节点. 这个数组不包含这个节点本身.getDeclaredVariables(node)
- 返回给定节点定义的变量的列表. 这个信息可以用来跟踪变量的引用信息.VariableDeclaration
, 返回这个变量声明里声明的所有变量.VariableDeclarator
, 返回这个变量声明里声明的所有变量(译者注: 两者的区别可以看这里).FunctionDeclaration
或者 FunctionExpression
, 返回给参数传递的参数以及函数的 name
属性的值.ArrowFunctionExpression
, 会返回传递给这个函数的参数.ClassDeclaration
或者 ClassDeclaration
, 返回类的 name 属性的值.CatchClause
, 返回异常处理函数里的变量.ImportDeclaration
, 返回这个语句定义的变量.ImportSpecifier
, ImportDefaultSpecifier
或者 ImportNamespaceSpecifier
, 返回定义的变量.getFilename()
- 返回源文件的名称.getScope()
- 返回给定节点的作用域. 作用域的信息可以用来跟踪变量.getSourceCode()
- 返回基于源文件处理好的, 可以让 ESLint 进行工作的一个源文件对象.markVariableAsUsed(name)
- 把给定的作用域中的一个变量标记为已使用过. 这个会影响no-unused-vars规则. 如果这个变量在被标记以前的状态已经是被使用过了, 就返回 true
, 否则返回 false
.report(descriptor)
- 报告代码中存在的问题 (点这里看对应的部分).注意: 在 ESLint 的早期版本, context
对象还支持额外的一些方法. 这些方法都已经在新的格式里被移除了, 并且不应该再被依赖了.
写自定义规则的过程里, 最常用的一个方法是 context.report()
, 这个方法可以报告一个 warning 或者 error(取决于你的配置文件怎么用的). 这个方法只接受一个对象作为参数, 这个对象, 包含如下的字段:
message
- 问题的描述.node
- (可选) 与问题相关的 AST. 如果这个选项和 loc
都没有被指定, 这个节点的开始的地方, 就会被被当作问题的定位.loc
- (可选) 一个表示问题定位信息的对象. 如果 loc
和 node
都被指定了, ESLint 会用 loc
参数来定位问题, 而不是 node
.start
- 一个对象, 定位了问题从哪里开始.line
- 从 1 开始数的行数, 用于表示问题在第几行开始发生.column
- 从 0 开始数的列数, 用于表示问题在第几列开始发生.end
- 一个对象, 定位了问题在哪里结束.line
- 从 1 开始数的行数, 用于表示问题在第几行结束.column
- 从 0 开始数的列数, 用于表示问题在第几列开始结束.data
- (可选) 错误信息的占位符数据.fix
- (可选) 一个可以用来纠正问题的函数.node
与 loc
至少需要配置一个.最简单的报错的例子就是直接用 node
和 message
:1
2
3
4context.report({
node: node,
message: "Unexpected identifier"
});
node 变量包括了查错所需要的必要信息, 包括行列信息, 还有简单的源代码的文本.
你也可以用占位符:1
2
3
4
5
6
7context.report({
node: node,
message: "Unexpected identifier: {{ identifier }}",
data: {
identifier: node.name
}
});
注意 message 里变量前后的空格是可选的.
node 里包含的信息, 同上.
messageId
s你可以在 context.report()
和测试文件里都打出报告信息, 也可以用一个 messageIds
来代替.
这样你就能避免重复打印信息了. It also prevents errors reported in different sections of your rule from having out-of-date messages.
1 | // in your rule |
如果你想让 ESLint 在发现报告问题的时候, 尝试自动修正问题, 可以给 context.report()
指定一个 fix
函数. fix
函数接受一个用来修正的 fixer
对象作为参数, 用来修正问题. 比如:
1 | context.report({ |
这个 fix()
函数可以在给定节点后面插入一个分号. 要注意的是, 修正不是立即就进行的, 而且如果跟其他的修正函数有冲突的话, 这个修正可能不会进行. 在修正完以后, ESLint 会在修正完的代码上重新运行所有启用的校验规则, 所以可能会引发更多的修正. 在所有能被修正的问题都被修正完以前, 这个过程最多会重复 10 次. 在这以后, 问题会和平常一样被报告出来, 而不会自动修正.
Important: Unless the rule exports the meta.fixable property, ESLint does not apply fixes even if the rule implements fix functions.
重要: 即便实现了 fix
函数, ESLint 也不会启用修正功能, 除非规则 exports 了 meta.fixable
字段.
fixer
对象包含以下方法:
insertTextAfter(nodeOrToken, text)
- 在给定节点或者 token 后, 插入文本insertTextAfterRange(range, text)
- 在给定的 range 后插入文本insertTextBefore(nodeOrToken, text)
- 在给定节点或者 token 前, 插入文本insertTextBeforeRange(range, text)
- 在给定的 range 前插入文本remove(nodeOrToken)
- 移除给定节点或者 tokenremoveRange(range)
- 在指定节点或者 token 中, 移除给定文本replaceText(nodeOrToken, text)
- 替换给定节点或者 token 中的文本replaceTextRange(range, text)
- 替换给定 range 中的文本上面这些方法返回了一个 fixing
对象. fix()
函数可以返回如下值:
fixing
对象.fixing
对象的数组.fixing
对象的对象. 也就是说, fix()
对象可以是一个生成器.如果你让 fix()
函数返回了多个 fixing
对象, 这些 fixing
对象不能是重复的.
有关修正的最佳实践:
fix()
函数执行完以后, 你都需要返回修正操作的结果.1 | ({ foo : 1 }) |
quotes
规则自动修正.有些规则为了正常的工作, 需要配置选项. 这些选项会出现在配置信息里(.eslintrc
, 命令行, 后者注释中). 比如:1
2
3{
"quotes": ["error", "double"]
}
quotes
规则在这个例子里有一个选项, "double"
(error
是错误级别). 在写修正函数的时候, 可以用context.options
这个属性来得到一个这个规则的配置信息的数组. 在这个例子里, 访问 context.options[0]
会得到 "double"
:1
2
3
4
5
6
7module.exports = {
create: function(context) {
var isDouble = (context.options[0] === "double");
// ...
}
};
由于context.options
刚好是一个数组, 你在拿到配置信息的时候, 可以直接得到到底有多少条配置被应用在这个规则上. 记住, 错误级别不属于 context.options
, 错误级别在一个规则里也是不可以被获取或者被修改的.
当你的代码要读取配置信息的时候, 确保设置好了一些默认值, 以防使用这个规则的人没有传递任何配置信息.
如果你想要得到跟你正在 lint 的源代码的更多相关信息, 需要访问 SourceCode
这个对象. 通过调用 getSourceCode()
方法, 你可以随时获得 SourceCode
这个对象:
1 | module.exports = { |
一旦拿到了 SourceCode
的实例, 就可以调用这个对象上的方法来操作代码了:
getText(node)
- 返回给定节点的源代码. 如果不传递 node
参数, 会得到整份源代码.getAllComments()
- 返回一个包含源代码中所有注释的数组.getCommentsBefore(nodeOrToken)
- returns an array of comment tokens that occur directly before the given node or token.getCommentsAfter(nodeOrToken)
- returns an array of comment tokens that occur directly after the given node or token.getCommentsInside(node)
- 返回给定节点中的所有注释.getJSDocComment(node)
- 返回给定节点中的 JSDoc 注释节点, 如果没有的话, 就返回 null
.isSpaceBetweenTokens(first, second)
- 如果在给定的两个节点之间有空格, 返回 true
.getFirstToken(node, skipOptions)
- 返回给定节点中包含的第一个分词.getFirstTokens(node, countOptions)
- returns the first count tokens representing the given node.getLastToken(node, skipOptions)
- 返回给定节点里的最后一个分词.getLastTokens(node, countOptions)
- returns the last count tokens representing the given node.getTokenAfter(nodeOrToken, skipOptions)
- 返回给定节点或者分词后的第一个分词.getTokensAfter(nodeOrToken, countOptions)
- returns count tokens after the given node or token.getTokenBefore(nodeOrToken, skipOptions)
- 返回给定节点或者分词前的第一个分词.getTokensBefore(nodeOrToken, countOptions)
- returns count tokens before the given node or token.getFirstTokenBetween(nodeOrToken1, nodeOrToken2, skipOptions)
- 返回两个给定节点或者分词间的第一个分词.getFirstTokensBetween(nodeOrToken1, nodeOrToken2, countOptions)
- returns the first count tokens between two nodes or tokens.getLastTokenBetween(nodeOrToken1, nodeOrToken2, skipOptions)
- 返回两个给定节点或者分词间的最后一个分词.getLastTokensBetween(nodeOrToken1, nodeOrToken2, countOptions)
- returns the last count tokens between two nodes or tokens.getTokens(node)
- 返回给定节点的所有分词.getTokensBetween(nodeOrToken1, nodeOrToken2)
- 返回两个给定节点间的所有分词.getTokenByRangeStart(index, rangeOptions)
- 返回以给定索引为开始的所有分词.getNodeByRangeIndex(index)
- 返回包含给定源代码索引的层次最深的一个节点.getLocFromIndex(index)
- 返回给定源代码索引的坐标信息, 包含以 1 开始的行坐标 line
与以 0 开始的列坐标 column
.getIndexFromLoc(loc)
- 返回给定坐标定位到的源代码的坐标对象, loc
是一个坐标对象, 其包含以 1 开始的行坐标 line
与以 0 开始的列坐标 column
.commentsExistBetween(nodeOrToken1, nodeOrToken2)
- 如果给定节点或分词间, 存在注释, 返回 true
.
skipOptions
是一个有 3 个字段的对象, 包括skip
,includeComments
和filter
. 默认值为{skip: 0, includeComments: false, filter: null}
.
skip
是一个正整数, 表示跳过的分词的数量. 如果filter
参数也有数值, 那么filter
过滤掉的分词不会被计数在需要跳过的分词里.includeComments
是一个布尔值, 是一个标志位, 用来标记返回结果里要不要出现注释分词.filter
是一个第一个参数为分词的函数, 如果函数返回false
, 那么结果里就会排除掉这个分词.
countOptions
是一个拥有 3 个字段的对象;count
,includeComments
, 以及filter
. 默认值是{count: 0, includeComments: false, filter: null}
.
count
是一个正整数, 值为返回的所有分词的数量.includeComments
是一个布尔值, 是一个标志位, 用来标记返回结果里要不要出现注释分词.filter
是一个第一个参数为分词的函数, 如果函数返回false
, 那么结果里就会排除掉这个分词.rangeOptions
是包含一个字段的对象:includeComments
.includeComments
是一个布尔值, 是一个标志位, 用来标记返回结果里要不要出现注释分词.
还有一些别的你可以访问的字段:
hasBOM
- 用来表示源代码是否含有 Unicode BOM 头text
- 需要被 lint 的完整的文本. Unicode BOM 头已经被去掉了.ast
- 需要被 lint 的代码的 AST.scopeManager
- 代码的作用域管理对象.visitorKeys
- 用于遍历这个 AST 的访问者 key.lines
- 代码分成行的数组, 分行是根据指定的换行符来进行的.通过 SourceCode
对象, 你可以获得更多有关你要 lint 的代码的信息.
下列这些方法已经被废弃了, 并且在未来的版本里, 会从 ESLint 中移除掉:
getComments()
- 被 getCommentsBefore()
, getCommentsAfter()
, 以及 getCommentsInside()
方法取代了getTokenOrCommentBefore()
- 被带有 { includeComments: true }
参数的 getTokenBefore()
方法取代了getTokenOrCommentAfter()
- 被带有 { includeComments: true }
参数的 getTokenAfter()
方法取代了自定义的规则可能导出 schema
字段, 这个字段是一个基于 JSON 格式的 schema 描述, 这个 schema 会被用于校验用户提供给 ESLint 的参数的有效性, 避免用户给 context.options
传递一些无效或者异常的输入.
有两种导出 schema
的方式. 第一种是, 导出完整的 JSON schema 对象, 这个对象枚举了所有可能的规则参数的情况, 包括了错误级别(error level)第一个参数, 后面再加上几个额外的参数的情况.
然而, 为了简化 schema 的编写, 规则也可以在可选参数的后面跟上一个表示可以作为参数值的数组, ESLint 会首先对需要的错误级别进行校验. 比如, yoda
规则接收主要参数, 以及带有命名好的字段的额外的参数对象.
1 | // "yoda": [2, "never", { "exceptRange": true }] |
在上面这个例子里, 错误级别被假定为了第一个参数. 紧随其后的, 是第一个可选参数, 参数的值可能是 "always"
或者 "never"
. 最后跟着的是一个对象形式的可选的参数, 这个对象表示的是, 这个参数名为 exceptRange
, 值为布尔型.
为了进一步了解 JSON schema, 我们建议你先从看一些例子开始, 并且读一下 Understanding JSON Schema 这本书(免费的).
Note: 目前你需要使用完整的 JSON Schema 对象而不是数组, 以免你的 schema 里含有引用($ref), 因为在这种情况下, ESLint 会把数组转换为单个的 schema 而不更新引用, 结果就会导致出错(引用会被忽略掉).
如果你需要对 JavaScript 源代码进行操作, 可以调用 sourceCode.getText()
来得到源代码. 这个方法的工作方式如下:1
2
3
4
5
6
7
8
9
10
11// get all source
var source = sourceCode.getText();
// get source for just this AST node
var nodeSource = sourceCode.getText(node);
// get source for AST node plus previous two characters
var nodeSourceWithPrev = sourceCode.getText(node, 2);
// get source for AST node plus following two characters
var nodeSourceWithFollowing = sourceCode.getText(node, 0, 2);
如果 AST 没能提供正确的数据的话(比如逗号、分号、圆括号的定位信息等等), 你就可以直接对 JavaScript 文本进行操作来得到这些.
因为注释在技术层面上来讲, 并不属于 AST 的一部分, 所以 ESlint 提供了一些访问注释的方法:
sourceCode.getAllComments()
这个方法返回一个数组, 数组里包含了从程序里找到的那些注释. 当开发者不关心注释的定位信息, 而是需要查看一下所有的注释的时候, 这个方法尤为有用.
sourceCode.getCommentsBefore(), sourceCode.getCommentsAfter(), and sourceCode.getCommentsInside()
这些方法依次返回在给定节点之前、之后以及节点内部的注释. 当开发者想要查看跟给定节点或者分词相关的注释的时候, 这个方法尤为有用.
记住, 这些方法的结果是需要经过计算才能得到的.
其实, 注释也可以通过很多的 sourceCode
对象的方法, 加上 includeCommecnts
参数来调用, 来进行访问.
Shebangs 是类型为 "Shebang"
的分词. 其会被上面提到的这些方法, 当作注释来识别.
ESLint 在遍历 AST 的时候会分析代码路径. 你可以通过跟代码路径相关的 5 个事件, 来访问代码路径.
细节看这里
在提交 ESLint 的核心规则的时候, 为了被 ESLint 库接受, 必须对每个规则增加一系列的单测文件. 单测文件跟规则的源文件同名, 不过放在 tests/lib/
文件夹下. 比如, 如果源文件是 lib/rules/foo.js
, 那测试文件就是 tests/lib/rules/foo.js
.
ESLint 提供了 RuleTester
工具让写单测变得更简单.
为了保持 lint 进程的高效性, 以及对开发者造成尽可能小的影响, 测试一下新规则以及对现有规则的修改对性能的影响, 是很有用的.
当开发 ESLint 核心库的时候, npm run perf
命令会显示出来所有规则都执行的时候, 运行所需时间的概括情况.1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23$ git checkout master
Switched to branch 'master'
$ npm run perf
CPU Speed is 2200 with multiplier 7500000
Performance Run #1: 1394.689313ms
Performance Run #2: 1423.295351ms
Performance Run #3: 1385.09515ms
Performance Run #4: 1382.406982ms
Performance Run #5: 1409.68566ms
Performance budget ok: 1394.689313ms (limit: 3409.090909090909ms)
$ git checkout my-rule-branch
Switched to branch 'my-rule-branch'
$ npm run perf
CPU Speed is 2200 with multiplier 7500000
Performance Run #1: 1443.736547ms
Performance Run #2: 1419.193291ms
Performance Run #3: 1436.018228ms
Performance Run #4: 1473.605485ms
Performance Run #5: 1457.455283ms
Performance budget ok: 1443.736547ms (limit: 3409.090909090909ms)
ESLint 有一个内置的方法来对单个的规则的性能进行跟踪. 设置一下 TIMING
环境变量, 就会触发跟踪结果的显示, 这些内容收集了耗时最长的十个规则里, 每个规则单独的耗时, 并且通过计算占总的规则的时间的百分比, 来标示出对性能的影响的相关性.1
2
3
4
5
6
7
8
9
10
11
12
13$ TIMING=1 eslint lib
Rule | Time (ms) | Relative
:-----------------------|----------:|--------:
no-multi-spaces | 52.472 | 6.1%
camelcase | 48.684 | 5.7%
no-irregular-whitespace | 43.847 | 5.1%
valid-jsdoc | 40.346 | 4.7%
handle-callback-err | 39.153 | 4.6%
space-infix-ops | 35.444 | 4.1%
no-undefined | 25.693 | 3.0%
no-shadow | 22.759 | 2.7%
no-empty-class | 21.976 | 2.6%
semi | 19.359 | 2.3%
如果想要明确的测试某个规则, 可以对命令加上 --no-eslintrc
和 --rule
参数:1
2
3
4$ TIMING=1 eslint --no-eslintrc --rule "quotes: [2, 'double']" lib
Rule | Time (ms) | Relative
:------|----------:|--------:
quotes | 18.066 | 100.0%
ESLint 的规则命名约定, 是相当简单的:
no-
作为前缀, 比如 no-eval
表示禁止使用 eval()
, no-debugger
表示禁止使用 debugger
.之所以 ESLint 独一无二, 是因为 ESLint 能够自定义运行时规则. 当你需要给你自己的项目或者公司制定某个规则, 但是有不需要发给 ESLint 官方的仓库的时候, 这个特性就很完美. 有了运行时规则, 你就不需要等下个版本的 ESLint 了, 或者因为你自己写的规则并不适合发表到大型 JavaScript 社区而感到沮丧, 只管写就是了, 然后在运行时里引入.
运行时规则写起来和其他格式并无二致. 写的时候就跟写其他规则一样, 写完了以后, 再执行下面这些步骤:
原文地址:https://www.valentinog.com/blog/ui-testing-jest-puppetteer/#jest-puppeteer-visual-debug
原文作者: Valentino Gagliardi
自从 Puppeteer 这个库出现以来,我就开始考虑用 Jest 和 Puppeteer 进行测试了。 Puppeteer 有很多有意思的 API。
接下来的文章我将会以联系人表单为例,简单的讲解如何对其进行 UI Test。
测试的技术选型,我们会使用 Jest 和 Puppeteer。
昨天在写测试的时候,刚好看到了 Kent C.Dodds 写的文章。
“让 UI 测试脚本更耐用一些”这篇文章里,阐述了怎么样通过使用 data-*
属性来让 UI 测试脚本更不容易失效。
data-*
属性可以在几乎任何 HTML 元素上定义一个 data 属性。当需要利用 JavaScript 操作 HTML 进行数据交换的时候,这个属性特别有用。
Kent 的文章来的很是时候,因为当时我恰好也要写类似的东西了。
1 | await page.waitForSelector("#contact-form"); |
尽管我不太喜欢用自定义 data-*
这种方法来进行测试,但是不得不承认,这是一个好方法。不管你的程序是大型工程还是小型项目,这个方法都适用,但是在文章里,我还是用比较经典的元素选择的例子来写一下 demo。
号外,你对持续集成感兴趣么?感兴趣的话可以看一下 Cypress >> 用 Cypress 进行更好的 JavaScript E2E 测试。
我的目标是测试一个联系人表单。
如下:
它有如下元素:
如果我想测试这个联系人列表,我应该怎么做呢?
测试上面这个表单,意味着要断言用户可以提交一个网络请求。
我们先来小窥一下要用到的工具。
Jest: Facebook 开源的一个测试框架。Jest 通过一个基本的断言库(Except)提供了一个自动化测试的平台。
Puppeteer: 一个用来控制 headless Chrome 的 Node.js 库。虽然这个库还很新,但是现在正是试试这个库是适于加入你的工作流的好时候。
Faker: 一个用于产生随机数据的 Node.js 库。姓名、电话号码、地址。这个库很像 PHP 里的那个 Faker 库。
如果你已经弄好工程目录了,可以用下面的命令来安装上述的库:1
npm i jest puppeteer faker --save-dev
因为 Puppeteer 需要下载它自己所需要的版本的 Chromium,所以安装 Puppeteer 会花一些时间。
Chromium 是一个在 Chrome 之后的开源浏览器。Chromium 和 Chrome 几乎共享了所有的功能,两者的差异只在部分协议的细节上。
一旦安装完成了,你需要在 package.json
里配置一下 Jest。test
命令应该写成一个可执行的 Jest 命令:1
2
3"scripts": {
"test": "jest"
}
在 Jest 脚本里,别忘了引入 Puppeteer:1
import puppeteer from "puppeteer";
为了能用 ES6 的语法,我们需要为 Jest 配置 Babel。用下面这个命令安装跟 Babel 相关的东西:1
npm i babel-core babel-jest babel-preset-env --save-dev
安装完以后,在你的工程目录里创建一个文件名为 .babelrc
的文件:1
2
3{
"presets": ["env"]
}
上述这些都配置好后,我们就可以开始写一个简单的测试脚本了。
在你的工程目录下新建一个文件夹,可以命名为 test
或者 spec
。然后在这个文件夹里建一个新的文件,名字是 form.spec.js
。
接下来我会把测试脚本拆成好几部分,把重点拿出来让你看看。在文章的末尾,我们能看到完整的代码。
按照顺序引入 Fake 和 Puppeteer:1
2import faker from "faker";
import puppeteer from "puppeteer";
配置表单的 URL(也许你想测试的是本地启动服务的开发版本,而不是真实的网站的生产版本):1
const APP = "https://www.change-this-to-your-website.com/contact-form.html";
用 Faker 创建一个假的用户信息:1
2
3
4
5
6const lead = {
name: faker.name.firstName(),
email: faker.internet.email(),
phone: faker.phone.phoneNumber(),
message: faker.random.words()
};
给 Puppeteer 定义一些变量:1
2
3
4let page;
let browser;
const width = 1920;
const height = 1080;
定义 Puppeteer 应该表现出怎么样的行为:1
2
3
4
5
6
7
8
9
10
11
12beforeAll(async () => {
browser = await puppeteer.launch({
headless: false,
slowMo: 80,
args: [`--window-size=${width},${height}`]
});
page = await browser.newPage();
await page.setViewport({ width, height });
});
afterAll(() => {
browser.close();
});
beforeAll
和 afterAll
都是 Jest 方法。简单来说就是,在开始跑测试以前,我们必须用 Puppeteer 启动一个浏览器核心,然后用 browser.newPage()
启动打开一个新的页面。
当测试用例完成了以后,浏览器核心必须通过 browser.close()
来进行关闭。
除了 beforeAll
和 afterAll
以外,Jest 还有很多其他 API,详情可以看一下 Jest 文档。毕竟,整个测试只用一个浏览器来核心完成要比每个测试用例都重新启动和关闭浏览器核心要方便的多。
上面的代码里有一些需要注意的地方:
我在代码里用了 headless: false
来启动(lanuch)浏览器核心,以便把 Chromuim 真实的显示出来。这其实只是因为我需要录像来给你展示测试是如何工作的而已,并不是测试必须这样写。
在实际测试过程中,并不需要打开一个实际的浏览器页面,所以到时候,只需要把跟 lanuch
方法相关的代码删除掉就可以了。
setViewPort()
也同样,可以删掉了。你也可以设置根据不同的环境来进行不同的 lanuch
操作,比如在开发环境中禁用 headless
模式以便可视化调试,在跑测试的时候只启动核心而不显示窗口。点这里去了解一下怎么做。
现在我们可以写实际的测试脚本了:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17describe("Contact form", () => {
test("lead can submit a contact request", async () => {
await page.goto(APP);
await page.waitForSelector("[data-test=contact-form]");
await page.click("input[name=name]");
await page.type("input[name=name]", lead.name);
await page.click("input[name=email]");
await page.type("input[name=email]", lead.email);
await page.click("input[name=tel]");
await page.type("input[name=tel]", lead.phone);
await page.click("textarea[name=message]");
await page.type("textarea[name=message]", lead.message);
await page.click("input[type=checkbox]");
await page.click("button[type=submit]");
await page.waitForSelector(".modal");
}, 16000);
});
注意,上面的代码里在 Jest 里使用 async/await 了。即假设你在用最新版本的 Node.js 了。
我们来看看当使用 Jest 和 Puppeteer 的时候,headless Chrome 都做了些什么:
.modal
这个 DOM 元素出现。注意:注意以第二参数形式在 test()
方法里传递给 Jasmine 的超时时间(16000)。当你像看 Chrome 浏览器跟页面的交互的时候,这个设置是很有必要的。
如果不是 headless 模式,并且没有配置超时时间的话,会得到如下的错误:1
Timeout - Async callback was not invoked within timeout specified by jasmine.DEFAULT_TIMEOUT_INTERVAL
总之,当 Chrome 运行在 headless 模式的时候,的确可以移除超时设置的,只是会报错。
然后用下面的命令来执行测试:1
npm test
然后你就可以看见下面这样的魔法了:
ok,现在联系人表单已经测试好了,我可以继续测试这个页面里其他部分的内容了。
每个网页,应该都有一个有意义的标题对吧,那么怎么测试呢?
下面的代码是用来测试 <title></title>
是否正确的:1
2
3
4
5
6
7
8
9describe("Testing the frontend", () => {
test("assert that <title> is correct", async () => {
const title = await page.title();
expect(title).toBe(
"Gestione Server Dedicati | Full Managed | Assistenza Sistemistica"
);
});
// 在下面你可以添加更多的测试用例!
});
导航栏呢?页面里至少应该有一个导航栏吧!
用 Jest 与 Puppeteer 测试导航栏是否存在:1
2
3
4
5
6//
test("assert that a div named navbar exists", async () => {
const navbar = await page.$eval(".navbar", el => (el ? true : false));
expect(navbar).toBe(true);
});
//
或者测试一下,某个特定的元素里是否有期望的文本信息:1
2
3
4
5
6//
test("assert that main title contains the correct text", async () => {
const mainTitleText = await page.$eval("[data-test=main-title]", el => el.textContent);
expect(mainTitleText).toEqual("GESTIONE SERVER, Full Managed");
});
//
那么有关 SEO 应该怎么测试呢?
用 Jest 与 Puppeteer 测试一下 SEO 关注的重点,比如一个友情链接是否存在:1
2
3
4
5
6
7describe("SEO", () => {
test("canonical must be present", async () => {
await page.goto(`${APP}`);
const canonical = await page.$eval("link[rel=canonical]", el => el.href);
expect(canonical).toEqual("https://www.servermanaged.it/");
});
});
当然,还可以测试更多的内容。
在那天写完 UI 测试后,看见那些绿色的标记,我很开心:
Puppeteer 给了你无限的可能。现在有很多新的测试框架正在基于 Puppeteer 进行开发。当然,API 还会继续改善,但是要知道,基础条件是必不可少的。
并且 Puppeteer 还能和 Jest 很好的结合在一起。
可能你会觉得 Puppeteer 本身或者 Puppeteer 的 API 并不够方便。我也懂你的感受。
这个库还很新,但是现在是检验这个库是否适合融入你的工作流的好时候。
Puppeteer 仍然在开发阶段中,并且接下来会有很多的改善。与此同时,你可以看看那些用了 Cypress 的例子。
你是怎么样测试你的程序的呢?有很多人在用 Puppeteer 做 E2E 测试。你呢?
Puppeteer 在使用的时候,你可以选择 Chromium 的 headless 和 非 headless 两种模式。
就像我们之前看到的那样:1
2
3
4
5
6
7
8
9
10beforeAll(async () => {
browser = await puppeteer.launch({
// Debug mode !
headless: false,
slowMo: 80,
args: [`--window-size=1920,1080`]
});
page = await browser.newPage();
///
});
如果你想进行可视化的 debug,那就必须要给 Jasmine 设置一个超时时间用来启动浏览器核心。否则测试会很快的中止掉,因为异步请求全部被忽略掉了。这个超时时间被定义为 test()
函数的第二个参数。1
2
3
4
5
6
7
8
9describe("Contact form", () => {
test(
"lead can submit a contact request",
async () => {
///// some assertions
},
16000 // <<< Jasmine timeout
);
});
但是当在自动化测试环境的时候,就不需要真的启动浏览器窗口了。如果你启动了,这将会导致这个测试永远的持续下去。所以怎么样在 headless 模式和普通模式之间轻松的进行切换呢?
写点辅助函数,把这些函数放在文件 testingInit.js
里:1
2
3
4
5
6
7
8
9
10
11export const isDebugging = () => {
let debugging_mode = {
puppeteer: {
headless: false,
slowMo: 80,
args: [`--window-size=1920,1080`]
},
jasmine: 16000
};
return process.env.NODE_ENV === "debug" ? debugging_mode : false;
};
然后你就可以在你的测试脚本里引用这个辅助函数了:
先是1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18///
import { isDebugging } from "./testingInit.js";
///
beforeAll(async () => {
browser = await puppeteer.launch(isDebugging().puppeteer)); // <<< 可视化模式
page = await browser.newPage();
///
});
/// 一些脚本
describe("Contact form", () => {
test(
"lead can submit a contact request",
async () => {
///// 一些断言
}, isDebugging().jasmine // <<< Jasmine timeout
);
然后你就可以用命令分别用这两种模式来启动了,如果你想用 headless 模式启动的话:1
npm test
下面是如何用 debug 模式启动:1
NODE_ENV=debug npm test
感谢阅读!
React 与 ES6 - 第三部分,为 React 类绑定方法(ES7 同理)
=
这篇文章,是我们探索在 ECMAScript6 和 ECMAScript7 中 React 的使用方法的系列文章的第三篇。
想看这个系列的其他文章,请点如下链接:
React | JavaScript |
---|---|
这个文章中的相关代码片段,也可以在 GitHub 中找到
最后更新日期: 2016年6月18日,更新内容包含了 React15 以及 Babel6。
这个系列的旧文章里,有一篇讲到了“CartItem 渲染方法”,如果你看过的话,可能会对 {this.increaseQty.bind(this)}
这种写法有点疑惑。
如果我们在 ES6 的代码里,对同样的 demo 用 {this.increaseQty}
来绑定一个组件的事件处理函数,浏览器会报 Uncaught TypeError: Cannot read property 'setState' of undefined
错误:
这是因为在 ES6 中,函数的 this
绑定规则已经发生了变化,我们在调用 this
的时候,调用的并不是类本身,而是 undefined
。但是如果你在写 React 的时候用的是 React.createClass()
这种方法, React 会自动把所有类的方法的 this
绑定到对应的实例上。
在 React 组件开始支持用 ES6 class
来实现的时候,React 小组决定不再支持自动绑定。详细的原因,可以看这篇文章。
下面来看看在用 ES6 class
写 JSX 文件的时候,怎么给类的方法绑定 this
值。
如下:1
2
3
4
5export default class CartItem extends React.Component {
render() {
<button onClick={this.increaseQty.bind(this)} className="button success">+</button>
}
}
由于 ES6 中类的方法本质上是 JavaScript 函数,因此继承了来自于 Function 原型上的 bind()
方法。现在,再调用 JSX 里的 increaseQty()
方法的时候,this
就会指向类的实例。如果对 Function.prototype.bind() 有疑惑,可以看这篇 MDN 文章。
1 | export default class CartItem extends React.Component { |
这样就不需要在 JSX 里用 bind()
方法了,但是增加了构造函数里的代码。
在 ES6 的箭头函数 被调用的时候,this
是函数执行的上下文。我们可以利用这个特性,在构造函数里重新定义 increaseQty()
:1
2
3
4
5
6
7
8
9
10
11export default class CartItem extends React.Component {
constructor(props) {
super(props);
this._increaseQty = () => this.increaseQty();
}
render() {
<button onClick={_this.increaseQty} className="button success">+</button>
}
}
(译注:是不是写错了)
除了上面提到的 3 种方法,还可以把箭头函数跟 ES2015+ 的类属性组合起来写:1
2
3
4
5
6
7
8export default class CartItem extends React.Component {
increaseQty = () => this.increaseQty();
render() {
<button onClick={this.increaseQty} className="button success">+</button>
}
}
哈哈,这次我们没有用长的构造函数代码来实现我们的需求了,而是巧妙的利用了类的属性初始化。
警告: 类属性现在还不是当前的 JavaScript 标准,但是可以用 Babel 的实验版本标记(也就是 stage 0)来解决这个问题。关于 Babel 的使用方法,可以查看 Babel
文档。
这个系列的文章 React and ES6 - Part 2, React Classes and ES7 Property Initializers 里就已经在用 stage 0 了,所以在这篇文章里,应该不是什么问题。
最近 Babel 增加了一个语法糖,用 ::
来表示 Function.prototype.bind()
,这个内容的细节不再展开。当然,如果你想了解细节,有些人已经在 Babel 官方文章 里对这个作了很好的解释。
下面是用了 ES2015+ 绑定语法的代码:1
2
3
4
5
6
7
8
9
10
11
12export default class CartItem extends React.Component {
constructor(props) {
super(props);
this.increaseQty = ::this.increaseQty;
// line above is an equivalent to this.increaseQty = this.increaseQty.bind(this);
}
render() {
<button onClick={this.increaseQty} className="button success">+</button>
}
}
友情提示,这是一个实验特性,如果想用的话,先考虑一下风险问题。
直接在 JSX 里用 ES2015+ 的语法糖,就不用再写构造函数的代码了:1
2
3
4
5export default class CartItem extends React.Component {
render() {
<button onClick={::this.increaseQty} className="button success">+</button>
}
}
看看,代码写起来很简洁,但是,这样会导致每次子组件被渲染的时候,重新初始化一个函数(译注:有关这个问题的扩展阅读请看这里),所以性能上是存在问题的。如果你想用纯渲染函数(或者 ES2016 的类)的时候,这样写会导致更严重的问题。
这篇文章里我们写了若干种给 React 组件的类的方法绑定 this
值的方式。这些代码我已经在第二部分的基础上写了一些测试用例。
在下篇文章中,我们要讲的是用 ES2015 写 React 的时候,有关 state 的问题。
假设存在如下需求:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15let app1 = new Observer({
name: 'youngwind',
age: 25
});
let app2 = new Observer({
university: 'bupt',
major: 'computer'
});
// 要实现的结果如下:
app1.data.name // 你访问了 name
app1.data.age = 100; // 你设置了 age,新的值为100
app2.data.university // 你访问了 university
app2.data.major = 'science' // 你设置了 major,新的值为 science
这个功能的实现, 要用到 Object
中 setter
和 getter
的劫持。
结合着看过的对 Vue 的源码分析和自己理解的部分, 写一段简单的代码,功能并不完善。
一共分为 5 步来实现, 这里只实现第 1 步, 最简单的原理解释。
1 | class Observer { |
用题目要求的数据进行测试, 测试结果如下:1
2
3
4你访问了 name, 值为 youngwind
你设置了 age, 新的值为 100
你访问了 university, 值为 bupt
你设置了 major, 新的值为 science
示例可以点击这里进行查看。
程序很简单, 比较有意思的是作用域这一部分, 猜猜看, bind()
原型方法中会不会形成闭包?
答案: 会,因为 bind()
在执行的时候, 内部变量 Object.defineProperty
中定义的两个匿名函数被全局变量 app1
的 getter
和 setter
给分别引用了。
CSS3
flex
布局中, 为什么没有justify-self
和 justify-items
属性?flex
容器内,主轴上元素的排列方法, 是如下定义的:
To align flex items along the main axis there is one property: justify-content
To align flex items along the cross axis there are three properties: align-content, align-items and align-self.
译文:
为了排列主轴上的
flex
元素, 我们有这样的一个属性:justify-content
而为了排列交叉轴上的
flex
元素, 我们却有三个属性:align-content
,align-items
还有align-self
.
于是问题就出现了:
为什么在
CSS3 flex
布局中, 没有justify-self
和justify-items
属性?
可以这样回答: 这两个额外的属性, 没有进行规范的必要。
flexbox
的规范指出了两种排列主轴 flex
元素的方法:
justify-content
属性, 还有auto margins
justify-content 属性用来排布 flex
容器中, 主轴上的元素。
这个元素作用在 flex
的容器元素上, 但是会对所有 flex
容器里的子元素产生影响。
下面是 5 种排列的选项:
flex-start
~ 元素靠近一行的开头flex-end
~ 元素靠近一行的末尾center
~ 元素靠近一行的中间space-between
~ 元素被均匀的分散开, 其中, 第一个元素位于一行的开头, 而最后一个元素位于一行的末尾. 而一行的开头和末尾, 则由 flex-direction
还有 writting mode
(ltr
或者 rtl
)来决定。space-around
~ 除了每个元素的开头和结尾都有一半的间隔以外, 和 space-between
是一样的。auto
margin(恰当的利用外边距的 auto
属性) 可以让元素居中排列、均匀分散或者是分组排列。
auto
margin 是作用在 flex
容器的子元素本身上的, 不像 justify-content
属性是作用在父级容器上的。
问题发生的情景:
让一组
flex
元素靠右排列(justify-content: flex-end
), 而其中的第一个元素, 靠左排列(justify-self: flex-start
)想象一下一个带有 logo 的导航条。如果有了
justify-content
和justify-self
属性, 那么这个导航条就可以完美的用flex
布局来实现, 从而如丝般顺滑的自适应各种尺寸的屏幕(译注: 由于大量 ie8/6 的存在, 国内现状并不是这样)。
其他的可能会用到的场景:
把一个 flex
元素放到 flex
容器的一角:
比如下面的场景:
把一个
flex
元素放到flex
容器的一角 .box { align-self: flex-end; justify-self: flex-end; }
水平垂直居中一个 flex
元素
margin: auto;
可以用来替代 justify-content: center; align-items: center;
, 就像下面这样:
从1
2
3
4.container {
justify-content: center;
align-items: center;
}
换成1
2
3.box56 {
margin: auto;
}
或许你会问, 这两个有什么区别呢?当你想水平垂直居中一个大小超过 flex
容器 的 flex
元素时, 就会知道了。
居中一个 flex
元素, 然后在第一个 flex
元素和底边缘的中间再放置一个水平居中的 flex
元素
flex
容器内元素的排列是通过分配剩余的空间来实现的。
因此,为了在一个独立元素的旁边, 再居中放置一个元素, 必须对某些属性进行抵销。
In the examples below, invisible third flex items (boxes 61 & 68) are introduced to balance out the “real” items (box 63 & 66).
在下面的例子中, 不可见的第三个 flex
元素(boxes 61 和 68), 被用来实现居中布局, 以便让显示出来的元素(boxes 63 和 66)符合居中要求。
不过,这种布局方法在语义上并无可取之处。
我们也可以用伪元素来替代真实的 DOM 元素。也可以用绝对定位来实现这个要求,参见flexbox 中的绝对定位 中介绍的三种办法。
注意: 上面的例子只适用于部分居中的要求 ———— 如果容器元素是等宽等高的(when the outermost items are equal height/width)或者 flex
容器的子元素是不同长度的,再看看接下来的例子。
当相邻的元素尺寸不相同时,如何居中 flex
元素
看下面的案例要求:
在某一行上有三个元素,想实现第二个元素居中显示(
justify-content: center
),而第一个和第三个分别靠左(justify-self: flex-start
)和靠右显示(justify-self: flex-end
)。注意:
space-around
和space-between
在这里是无法达到想要的那种效果的,因为需要进行均匀排列的元素本身的宽度是不一样的(看这个例子)。
正如上面提到的注意事项所说,在 DOM 结构上处于中间的那个元素,只有在相邻的元素等高或者等宽(取决于 flex-direction
)的时候,才能真正的居中。这时候,我们就特别需要 justify-self
属性。
1 | #container { |
1 | <div id="center"> |
(点击这里, 前往原答案查看代码的效果)
解决这个问题的办法有两种。
flexbox
规定,允许其子元素用绝对定位进行布局。这样的话,相邻元素尺寸不相同的中间元素进行居中布局,也成为了有可能的事情。
别忘了,绝对定位的元素会脱离文本流。这也就意味着它不再占据其容器元素的空间,并且可以和其他兄弟元素重叠在一起。
在下面的例子中,三个子元素中间的那个元素用绝对定位,居中显示在了容器的正中央,而两个相邻的兄弟元素则仍然保持在文本流中。
其实这个方法也可以反过来用:对三个字元素中间的那个元素使用 justify-content: center
属性,对它的两个兄弟元素用绝对定位。
flex
容器(不使用绝对定位)1 | .container { |
1 | <div class="container"> |
(点击这里, 前往原答案查看代码的效果)
这种方法的工作原理:
.container
) 是一个 flex
容器.box
) 元素, 都是一个 flex
元素.box
元素都被赋了 flex: 1
属性flex
元素都加上属性,变成 flex
容器(译注:就叫二级容器好了), 然后再在其中嵌套 flex
元素,并且给这些新的 flex
容器加上 justify-content: center
属性span
元素是一个居中的 flex
元素了之所以上面的方法可行,是因为在根据规范, 外边距的布局优先级是高于 justify-content
的:
8.1. Aligning with auto margins
Prior to alignment via justify-content and align-self, any positive free space is distributed to auto margins in that dimension.
译文:
8.1 用自动外边距来排列元素
对应任何剩余的空间分配上, 自动外边距都有高于
justify-content
和align-self
的分配权。
再回到 justify-content
这个属性上, 其实这个属性已经有了一个新的构想的值:
space-between
和 space-around
的混合体。这个属性就像 space-between
一样能让元素均匀分布,只不过这次不是让两头的元素在外边缘之能分配到一半的空间了,而是分配到和两两元素中间一样大小的空间。这种布局效果也能通过在 flex
容器上设置 ::after
和 ::before
两个伪元素来实现。
答案作者: Michael_B
并没有取得原作者的授权,直接翻译并发布了,本来是想联系作者的,但是找不到邮箱,StackOverflow 荣誉也不够,无法留言。
先看一道面试题, 如何实现下面这个函数?1
2
3
4add(1);// 1
add(1, 2);// 3
add(1)(2);// 3
add(1, 2, 3)(4, 10);// 20
先是 ES6 版本的答案:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15const add = (...args) => {
const calculate = (arr) => {
return arr.length === 0 ? 0 : arr.length === 1 ? arr[0] : arr.reduce((ac, cv) => ac + cv);
}
let result = calculate(args);
const func = (...args) => {
result += calculate(args);
return func;
}
func.toString = func.valueOf = () => result;
return func;
}
测试1
add(1, 2, 3)(1)()(3); //10
ES5 的版本: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
27function add() {
function calculate() {
var arr = [].slice.call(arguments);
switch(arr.length) {
case 0:
return 0;
case 1:
return arr[0];
default:
return arr.reduce(function(ac, cv) {
return ac + cv;
})
}
}
var result = calculate.apply(null, arguments);
function func() {
result += calculate.apply(null, arguments);
return func;
}
func.toString = func.valueOf = function() {
return result;
}
return func;
}
测试1
add(1, 2, 3)(1)()(3); //10
这道题和柯里化有什么关系呢?
把上面的函数简化一下就可以看出来了:1
2
3
4
5
6
7
8
9
10
11const add_currying = (num) => {
let result = num;
const func = (num) => {
result += num;
return func;
}
func.toString = func.valueOf = () => result;
return func;
}
测试1
add_currying(1)(2); //3
先看维基百科中, 对柯里化的定义:
在计算机科学中,柯里化(英语:Currying),又译为卡瑞化或加里化,是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。
即:
- 需要保持事先传入的参数, 或者第一次传入的参数, 并且返回一个由这个参数构成的新的函数
- 接下来的计算都通过第 0 步返回的函数执行
在 JavaScript 中即为:
在实现的技巧上, 用到了 Function.toString()
方法和 Object.valueOf()
方法, 其中 func.toString
是当这个函数需要在 console
面板中显示时调用的方法,
而 func.valueOf
是在需要当做值进行传递时调用的方法, 而柯里化本身, 并与此无关, 正因为返回值是函数, 才能称作是柯里化。
可以对 add_currying
或者 add
的值进行 typeof
1
2typeof add_currying(1)(2);// "function"
typeof add(1, 2, 3)(1, 2);// "function"
在实现的过程中, 写了一个错误的答案: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
27function add() {
function calculate() {
var arr = [].slice.call(arguments);
switch(arr.length) {
case 0:
return 0;
case 1:
return arr[0];
default:
return arr.reduce(function(ac, cv) {
return ac + cv;
})
}
}
var result = calculate(arguments);
function func() {
result += calculate(arguments);
return func;
}
func.toString = func.valueOf = function() {
return result;
}
return func;
}
测试1
add(1, 2, 3)(1)()(3); //6[object Arguments][object Arguments][object Arguments]
错在哪里了呢?calculate(arguments)
的传参上, arguments
是 array-like, 而不是 array, 直接当做实参传进去的时候, 会被调用 toString()
方法, 形参得到的就是 '[object Arguments]'
, 而不是一个 arguments
或者 Array
了。而在 JavaScript 中, 能直接接受 arguments
当做参数的, 只有 apply
, 所以正确的传值方式是 calculate.apply(null, arguments)
。
任务1-10, 练习flex 布局
任务1-11, 练习响应式开发, 请用移动端打开
任务2-13, 练习简单的 DOM 操作
任务2-14, 练习数据排序
任务2-16, 练习DOM 操作、表格
任务2-17, 练习用 DOM 进行数据展示考查数据操作、DOM操作
任务2-18, 练习数据结构中的双向链表、DOM 操作
任务2-19, 练习DOM 生成、排序,用到了插入排序、CSS3 KeyFrame、任务队列
任务2-20, 练习字符串操作,并对查找到的内容进行高亮显示
任务2-21, 练习字符串操作, DOM 操作
任务2-22, 练习二叉树的三种遍历, 并用 DOM 动画显示出来
任务2-23, 练习数据的深度优先遍历、广度优先遍历、以及前序遍历、中序遍历、后序遍历和查找
任务2-24, 在任务2-23的基础上, 增加了 update 和 delete 两个按钮, 方便交互
任务2-26, 练习Canvas、mediator 模式、Pub-Sub 模式
任务2-27, 在任务2-26的基础上, 对 OOP 中的对象配置进行了抽象, 改善代码的组织方式
任务2-28, 在任务2-27点基础上, 用 Adapter 模式, 增加后续的功能
任务2-29, 最简单的表单验证
任务2-30, 练习策略模式、正则表达式, 简单的表单验证功能