小团子

小团子 查看完整档案

其它编辑  |  填写毕业院校  |  填写所在公司/组织 example.com 编辑
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 个人简介什么都没有

个人动态

小团子 赞了回答 · 2018-12-13

webpack2报错module not found error

我也遇到了同样的问题,google了解决方案是在webpack.config.js中加入

 node: {
             fs: 'empty'
        }

完整如下

{
        output:{
            filename: 'vendor.js',
            path: path.resolve(__dirname, 'dist'),
            libraryTarget: 'umd'
        },
        entry: './src/vendor.js',
        devtool: 'source-map',
        mode: 'development',
        node: {
             fs: 'empty'
        }
    },

我照做了,虽然build是成功了,但是事后引用build好的包却是依然报run time error,不知道lz后来解决了没有

补充!经过一番research,我的问题解决了,因为我用webpack打包了,非nodejs写的包(就是那些需要用node-gyp prebuild的那种modules)。解决方案如下

{
        output:{
            filename: 'vendor.js',
            path: path.resolve(__dirname, 'dist'),
            libraryTarget: 'umd'
        },
        entry: './src/vendor.js',
        devtool: 'source-map',
        mode: 'development',
        resolve: {
            alias: {
                'indy-sdk': path.join(__dirname,'node_modules/indy-sdk/build/Release/indynodejs.node')
            }
        },
        module: {
            rules: [
                {test: /\.node$/, use: 'node-loader'}
            ]
        }
    },

关注 5 回答 4

小团子 收藏了文章 · 2018-09-30

使用 javascript 替换 jQuery

使用 javascript 替换 jQuery

jQuery 曾风靡一个时代,大大降低了前端开发的门槛,丰富的插件也是前端开发者得心应手的武器库,但是,这个时代终于要落幕了。随着 JS 标准和浏览器的进步,jQuery 的很多精华被原生 JS 吸收,我们直接使用原生 API 就可以用类似手法来处理以前需要 jQuery 的问题。在新的 Web 项目中,如果不需要支持过于陈旧的浏览器版本,那么的确没有必要使用 jQuery。

下面就探讨如何用JavaScript(ES6)标准语法,取代jQuery的一些主要功能。

选取元素

选择器查询

常用的 class、id、属性 选择器都可以使用 document.querySelector 或 document.querySelectorAll 替代。

  • document.querySelector 返回第一个匹配的 Element
  • document.querySelectorAll 返回所有匹配的 Element 组成的 NodeList。

jQuery:

var $ele = $("selector");

Native:

let ele = document.querySelectorAll("selector");

选择器模式

选择器示例示例说明
.class.intro选择所有class="intro"的元素
#id#firstname选择所有id="firstname"的元素
**选择所有元素
elementp选择所有<p>元素
element,elementdiv,p选择所有<div>元素和<p>元素
element elementdiv p选择<div>元素内的所有<p>元素
element>elementdiv>p选择所有父级是<div>元素的 <p>元素
element+elementdiv+p选择所有紧接着<div>元素之后的<p>元素
[attribute=value]a[target=_blank]选择所有使用target="_blank"的<a>元素
[attribute^=value]a[src^="http"]选择每一个src属性的值以"http"开头的<a>元素
[attribute$=value]a[src$=".jpg"]选择每一个src属性的值以".jpg"结尾的<a>元素
:first-childul li:first-child选择<ul>元素下的首个<li>元素
:nth-child(n)ul li:nth-child(3)选择<ul>元素下的第三个<li>元素
:last-childul li:last-child选择<ul>元素下的最后一个<li>元素

DOM 树查询

jQueryNative方法说明
$ele.parent()ele.parentNode元素的直接父元素
$ele.children()ele.childNodes元素的所有直接子元素
$ele.find("a")ele.querySelectorAll("a")元素的后代元素
$ele.prev()ele.previousElementSibling元素的上一个同胞元素
$ele.next()ele.nextElementSibling元素的下一个同胞元素

DOM 操作

DOM本身就具有很丰富的操作方法,可以取代jQuery提供的操作方法。

内容和属性

jQueryNative方法说明
var text = $ele.text()let text = ele.innerText获取所选元素的文本内容
$ele.text("text")ele.innerText = "text"设置所选元素的文本内容
var html = $ele.html()let html = ele.innerHTML获取所选元素的HTML内容
$ele.html("<div>html</div>")ele.innerHTML = "<div>html</div>"设置所选元素的HTML内容
var input = $ele.val()let input = ele.value获取表单字段的值
$ele.val("input")ele.value = "input"设置表单字段的值
var href = $ele.attr("href")let href = ele.getAttribute("href")获取元素的属性值
$ele.attr("href", "/")ele.setAttribute("href", "/")设置元素的属性值

修改 DOM 树

jQueryNative方法说明
$parent.append($ele)parent.appendChild(ele)在被选元素的结尾插入内容
$parent.prepend($ele)parent.insertBefore(ele, parent.firstChild)在被选元素的开头插入内容
$ele.after(html)ele.insertAdjacentHTML("afterend", html)在被选元素之后插入内容
$ele.before(html)ele.insertAdjacentHTML("beforebegin", html)在被选元素之前插入内容
$ele.remove()ele.parentNode.removeChild(ele)删除被选元素及其子元素
$ele.empty()ele.innerHTML = null从被选元素中删除子元素
$ele.clone()ele.cloneNode(true)拷贝被选元素
$ele.replaceWith(html)ele.outerHTML = html指定HTML替换被选元素

CSS 样式

设置 Style

HTML DOM 允许 JavaScript 改变 HTML 元素的样式,Native API 提供了如下几种方式:

  • ele.setAttribute 直接修改 DOM style 属性改变样式
  • ele.style.cssText 通过 cssText 修改 Style 属性
  • ele.style.property 通过 style 对象读写行内 CSS 样式

jQuery:

var size = $ele.css("font-size"); // 返回第一个匹配元素的 CSS 属性值
$ele.css("font-size", "2rem"); // 为所有元素设置指定的 CSS 属性值

Native:

let size = getComputedStyle(ele)["font-size"]; // 获取当前元素计算后的 CSS 属性值
ele.style.setProperty("font-size", "2rem"); // 设置当前元素的某个内联样式
ele.style.removeProperty("font-size");  // 移除当前元素的某个内联样式

设置 Class

jQueryNative方法说明
$ele.hasClass(className)ele.classList.contains(className)检查元素是否包含指定的类名
$ele.addClass(className)ele.classList.add(className)向元素增加一个或多个类名
$ele.removeClass(className)ele.classList.remove(className)从元素中移除一个或多个类
$ele.toggleClass(className)ele.classList.toggle(className)对元素的一个或多个类进行切换

事件方法

绑定事件

jQuery:

$ele.on("click", function (evt) {
    console.log(evt.target);
});

Native:

ele.addEventListener("click", evt => {
    console.log(evt.target);
});

解除绑定

jQuery:

$ele.off("click");

Native:

ele.removeEventListener("click", func);

如果要移除事件,addEventListener 必须使用外部函数,绑定匿名函数的事件是无法移除的。

模拟触发

jQuery:

$ele.trigger("click");

Native:

let event = document.createEvent("MouseEvents");
event.initMouseEvent("click");
ele.dispatchEvent(event);

模拟事件:

  1. 首先通过 document.createEvent 方法创建 Event 对象。
  2. 然后利用 Event 对象的 init 方法对其进行初始化。
  3. 最后使用 dispatchEvent 方法触发 Event 对象。

详见:JavaScript 事件——“模拟事件”的注意要点

Ajax

jQuery

$.ajax({
    url: "http://apis.juhe.cn/ip/ip2addr",
    type: "GET",
    data: {
        "key": "80701ec21437ca36ca466af27bb8e8d3",
        "ip": "220.181.57.216"
    },
    dataType: "json",
    success: function (data) {
        console.log(data);
    }
});

XHR 封装

window.ajax = async function (params, callback) {
    let url = params.url;
    let method = params.method;
    let data = params.data;
    let body = new FormData();
    for (let key in data) {
        if (data.hasOwnProperty(key)) {
            body.append(key, data[key]);
        }
    }
    let xhr = new XMLHttpRequest();
    xhr.timeout = 3000;
    xhr.open(method, url, true);
    xhr.addEventListener("readystatechange", evt => {
        if (xhr.readyState === 4) {
            if (xhr.status === 200) {
                callback(xhr.response);
            } else {
                throw xhr.statusText;
            }
        }
    });
    xhr.send(body);
};

ajax({
        url: "http://apis.juhe.cn/ip/ip2addr",
        method: "GET",
        data: {
            "key": "80701ec21437ca36ca466af27bb8e8d3",
            "ip": "220.181.57.216"
        }
    },function (resp) {
        var json = JSON.parse(resp);
        console.log(json);
    }
)

Fetch API

XMLHttpRequest 并不是专为 Ajax 而设计的. 虽然各种框架对 XHR 的封装已经足够好用, 但更好用的 API 是 fetch 。

/* 构造请求对象 */
let request = new Request(
    "http://apis.juhe.cn/ip/ip2addr",
    {
        method: "GET",
        body: {
            "key": "80701ec21437ca36ca466af27bb8e8d3",
            "ip": "220.181.57.216"
        },
        headers: new Headers()
    }
);
/* 处理响应对象 */
fetch(request)
    .then(response => response.json())
    .then(function (data) {
        console.log(data);
    })
    .catch(function (error) {
        console.log(error);
    });

详见:fetch用法说明

工具

Array

jQueryNative方法说明
$.isArray(array)Array.isArray(array)判断参数是否为一个数组
$.inArray(item, array)array.includes(item)判断值是否在指定数组中
$.makeArray(objlist)Array.from(objlist)将类数组对象转换为数组
$.merge(array1, array2)array1.concat(array2)合并两个数组(有区别)
$.each(array, function (i, item) {}array.forEach((item, i) => {})遍历指定的对象和数组

合并数组时,merge 会改变原数组的内容,而 concat 不会修改原数组,只会返回合并后的数组

Method

jQueryNative方法说明
$.now()Date.now()返回当前时间戳
$.trim(context)context.trim()移除字符串头尾空白
$.type(parameter)typeof parameter检测参数的内部类型
$.parseJSON(jsonstr)JSON.parse(jsonstr)将JSON转换为JS对象
$ele.data("key", "value")ele.dataset.key = "value"在指定的元素上存储数据
$.map(array, function (item, i) {})array.map((item, i) => {})将数组转化为处理后的新数组
查看原文

小团子 赞了文章 · 2018-07-24

团队合作必备的Git操作

编辑器&Mac

1、编辑器的使用vs code

  • 插件
  1. git辅助工具,可查看代码的书写者:Git Blame

2、 Mac工具使用

3、在 macOS 中完美配置文件名大小写敏感(解决git默认对大小写不敏感问题)解决git大小写不敏感

知识篇

一、git使用

  • 一般企业中使用代码管理工具Git开发时都是通过拉分支进行功能细致开发,所以掌握git的分支操作时必要的
  • 使用Git下载指定分支命令为:git clone -b 分支名仓库地址
  • 初始开发git操作流程

    • 本地创建公钥ssh-keygen -t rsa -C "邮箱"并配置
    • 克隆最新主分支项目代码git clone 地址
    • 创建本地分支git branch 分支名
    • 查看本地分支git branch
    • 查看远程分支git branch -a
    • 切换分支git checkout 分支名(一般修改未提交则无法切换,大小写问题经常会有,可强制切换git checkout 分支名 -f非必须慎用)
    • 将本地分支推送到远程分支git push <远程仓库> <本地分支>:<远程分支>

必备知识点

  • 概念:

图片描述

  1. Remote:远程主仓库;
  2. Repository:本地仓库;
  3. Index:Git追踪树,暂存区;
  4. workspace:本地工作区(即你编辑器的代码)
  • 一般操作流程:《工作区》-> git status查看状态 -> git add .将所有修改加入暂存区-> git commit -m "提交描述"将代码提交到本地仓库->git push将本地仓库代码更新到远程仓库

一、git remote

  • 为远程仓库指定别名,以便于管理远程主机,默认只有一个时为origin
  1. 查看主机名:git remote
  2. 查看主机名即网址:git remote -v

    • 默认克隆远程仓库到本地时,远程主机为origin,如需指定别名可使用git clone -o <别名> <远程git地址>
  3. 查看主机的详细信息git remote show <主机名>
  4. 添加远程主机git remote add <主机名> <网址>
  5. 删除远程主机git remote rm <主机名>
  6. 修改远程主机的别名:git remote rename <原主机名> <新主机名>

二、git fetch

  • 将某个远程主机的更新,全部/分支 取回本地(此时之更新了Repository)它取回的代码对你本地的开发代码没有影响,如需彻底更新需合并或使用git pull
  1. 远程主机的更新,全部取回本地git fetch <远程主机名>
  2. 将远程仓库特定分支更新到本地git fetch <远程主机名> <分支名>
  • 如果需要将更新拉取但本地工作代码需要合并到本地某一分支git merge <被合并的远程分支>或者在此基础上创建出新分支并切换git checkout -b <分支名> <在此分支上创建>

三、git pull

  • 拉取远程主机某分支的更新,再与本地的指定分支合并(相当与fetch加上了合并分支功能的操作)
  1. 拉取远程某分支并与本地某一分支合并(没有则默认会创建):git pull <远程主机名> <远程分支名>:<本地分支名>
  2. 如果远程分支是与当前所在分支合并,则冒号后面的部分可以省略:git pull <远程主机名> <远程分支名>
  3. 如果当前分支与远程分支存在追踪关系,则可以省略远程分支名:git pull <远程主机名>
  4. 如果当前分支只有一个追踪分支,则远程主机名都可以省略:git pull

三、git push

  • 将本地分支的更新,推送到远程主机,其命令格式与git pull相似
  1. 将本地分支推送到远程分支:git push <远程主机名> <本地分支名>:<远程分支名>
  2. 如果省略远程分支名,则默认为将本地分支推送到与之关联的远程分支:(一般设置本地分支和与之关联的远程分支同名,防止混淆)git push <远程主机名> <本地分支名>

    • 如果对应的远程分支不存在,则会被创建(m默认与本地分支同名)
  3. 如果省略本地分支名,则表示删除指定的远程分支,这等同于推送一个空的本地分支到对应远程分支:git push origin :<远程分支> 等同于 git push origin --delete <远程分支>
  4. 如果当前分支与远程分支之间存在追踪关系,则本地分支和远程分支都可以省略git push origin
  5. 如果当前分支只有一个追踪分支,那么主机名也可以省略:git push
  6. 如果当前分支与多个主机存在追踪关系(使用场景相对来说较少),可以使用-u指定默认推送主机git push -u origin <主机名>设置时候需推送便可以直接使用git push
  7. 将本地的所有分支都推送到远程主机:git push --all origin
  8. 如果远程主机的版本比本地版本更新,推送时Git会报错,要求先在本地做git pull合并差异,然后再推送到远程主机。如果一定要推送,可以使用--force选项(谨慎使用,除非你非常确认):git push --force origin
  • 注意:分支推送顺序的格式为<来源地>:<目的地>,所以git pull格式:<远程分支>:<本地分支>git push格式为:<本地分支>:<远程分支>

四、分支操作

  1. 创建本地分支:git branch test:(创建名为test的本地分支)
  2. 切换分支:git checkout test:(切换到test分支)
  3. 创建并切换分支git checkout -b test:(相当于以上两条命令的合并)
  4. 查看本地分支:git branch
  5. 查看远程仓库所有分支:git branch -a
  6. 删除本地分支:git branch -d test:(删除本地test分支)
  7. 分支合并:git merge master:(将master分支合并到当前分支)
  8. 本地分支重命名:git branch -m oldName newName
  9. 远程分支重命名:

    1. 重命名远程分支对应的本地分支:git branch -m oldName newName;
    2. 删除远程分支:git push --delete origin oldName;
    3. 上传新命名的本地分支:git push origin newName;
    4. 把修改后的本地分支与远程分支关联:git branch --set-upstream-to origin/newName
  • 分支关联:
  1. 查看当前的本地分支与远程分支的关联关系:git branch -vv

clipboard.png

  1. 把当前本地分支与远程origin的某分支进行关联处理(通过 --set-upstream-to 命令):git branch --set-upstream-to=origin/feature/clear-server-eslint-error_180713

clipboard.png

  • 分支差异查看
  1. 查看本地当前分支与远程某一分支的差异:git diff origin/feature/reserve-3.4
  2. 查看本地特定分支与远程分支的差异:git diff master origin/feature/reserve-3.4 (查看本地master分支与远程feature/reserve-3.4分支的差异,如图)

clipboard.png


五、修改撤销

  1. git checkout -- <文件名>:丢弃工作区的修改,就是让这个文件回到最近一次git commitgit add时的状态。
  2. git reset HEAD <文件名>:把暂存区的修改撤销掉(unstage),重新放回工作区。
  3. git reset --hard commit_id:git版本回退,回退到特定的commit_id版本

    • 流程:
    • git log查看提交历史,以便确定要回退到哪个版本(commit 之后的即为ID);

clipboard.png

  • git reset --hard commit_id:回退到commit_id版本;
  • git reflog查看命令历史,以便确定要回到未来的哪个版本;

    • 更新远程代码到本地
      git fetch origin master(分支)
      git pull // 将fetch下来的代码pull到本地
      git diff master origin/master // 查看本地分支代码和远程仓库的差异
  • 拉取远程分支并创建本地分支
  1. git checkout -b 本地分支名 origin/远程分支名:使用此方式会在本地新建分支,并自动切换到该本地分支;
  2. git fetch origin 远程分支名:本地分支名:使用此方式会在本地新建分支,但是不会自动切换到该本地分支,需要手动checkout。

六、配置

  • git config -l // 陈列出所有的git配置项
  • git config core.ignorecase false //配置git不忽略大小写(默认忽略)参照(git 大小写)

七、暂存

  • git stash 可用来暂存当前正在进行的工作,比如想pull 最新代码又不想commit, 或者另为了修改一个紧急的bug,先stash,使返回到自己上一个commit, 改完bug之后再stash pop, 继续原来的工作;
  • 添加缓存栈:git stash;
  • 查看缓存栈:git stash list;
  • 推出缓存栈:git stash pop;
  • 取出特定缓存内容:git stash apply stash@{1};

clipboard.png

“积跬步、行千里”—— 持续更新中~,喜欢留下个赞哦!
查看原文

赞 302 收藏 251 评论 5

小团子 关注了用户 · 2018-04-23

justjavac @justjavac

会写点 js 代码

关注 14506

小团子 赞了文章 · 2018-03-13

Koa原理学习路径与设计哲学

Koa原理学习路径与设计哲学

本文基于Koa@2.5.0

Koa简介(废话篇)

Koa是基于Node.jsHTTP框架,由Express原班人马打造。是下一代的HTTP框架,更简洁,更高效。

我们来看一下下载量(2018.3.4)

Koa:471,451 downloads in the last month
Express:18,471,701 downloads in the last month

说好的Koa是下一代框架呢,为什么下载量差别有这么大呢,Express一定会说:你大爷还是你大爷!

确实,好多知名项目还是依赖Express的,比如webpack的dev-server就是使用的Express,所以还是看场景啦,如果你喜欢DIY,喜欢绝对的控制一个框架,那么这个框架就应该什么功能都不提供,只提供一个基础的运行环境,所有的功能由开发者自己实现。

正是由于Koa的高性能和简洁,好多知名项目都在基于Koa,比如阿里的eggjs,360奇舞团的thinkjs

所以,虽然从使用范围上来讲,Express对于Koa你大爷还是你大爷!,但是如果Express很好,为什么还要再造一个Koa呢?接下来我们来了解下Koa到底带给我们了什么,Koa到底做了什么。

如何着手分析Koa

先来看两段demo。

下面是Node官方给的一个HTTP的示例。

const http = require('http');

const hostname = '127.0.0.1';
const port = 3000;

const server = http.createServer((req, res) => {
  res.statusCode = 200;
  res.setHeader('Content-Type', 'text/plain');
  res.end('Hello World\n');
});

server.listen(port, hostname, () => {
  console.log(`Server running at http://${hostname}:${port}/`);
});

下面是最简单的一个Koa的官方实例。

const Koa = require('koa');
const app = new Koa();

app.use(async ctx => {
  ctx.body = 'Hello World';
});

app.listen(3000);

Koa是一个基于Node的框架,那么底层一定也是用了一些Node的API。

jQuery很好用,但是jQuery也是基于DOM,逃不过也会用element.appendChild这样的基础API。Koa也是一样,也是用一些Node的基础API,封装成了更好用的HTTP框架。

那么我们是不是应该看看Koahttp.createServer的代码在哪里,然后顺藤摸瓜,了解整个流程。

Koa核心流程分析

Koa的源码有四个文件

  • application.js // 核心逻辑
  • context.js // 上下文,每次请求都会生成一个
  • request.js // 对原生HTTP的req对象进行包装
  • response.js // 对原生HTTP的res对象进行包装

我们主要关心application.js中的内容,直接搜索http.createServer,会搜到

  listen(...args) {
    debug('listen');
    const server = http.createServer(this.callback());
    return server.listen(...args);
  }

刚好和Koa中的这行代码app.listen(3000);关联起来了。

找到源头,现在我们就可以梳理清楚主流程,大家对着源码看我写的这个流程

fn:listen
∨
fn:callback
∨
[fn:compose] // 组合中间件 会生成后面的 fnMiddleware
∨
fn:handleRequest // (@closure in callback)
∨
[fn(req, res):createContext] // 创建上下文 就是中间件中用的ctx
∨
fn(ctx, fnMiddleware):handleRequest // (@koa instance)
∨
code:fnMiddleware(ctx).then(handleResponse).catch(onerror);
∨
fn:handleResponse
∨
fn:respond
∨
code:res.end(body);

从上面可以看到最开始是listen方法,到最后HTTP的res.end方法。

listen可以理解为初始化的方法,每一个请求到来的时候,都会经过从callbackrespond的生命周期。

在每个请求的生命周期中,做了两件比较核心的事情:

  1. 将多个中间件组合
  2. 创建ctx对象

多个中间件组合后,会先后处理ctx对象,ctx对象中既包含的req,也包含了res,也就是每个中间件的对象都可以处理请求和响应。

这样,一次HTTP请求,接连经过各个中间件的处理,再到返回给客户端,就完成了一次完美的请求。

Koa中的ctx

app.use(async ctx => {
  ctx.body = 'Hello World';
});

上面的代码是一个最简单的中间件,每个中间件的第一个参数都是ctx,下面我们说一下这个ctx是什么。

创建ctx的代码:

  createContext(req, res) {
    const context = Object.create(this.context);
    const request = context.request = Object.create(this.request);
    const response = context.response = Object.create(this.response);
    context.app = request.app = response.app = this;
    context.req = request.req = response.req = req;
    context.res = request.res = response.res = res;
    request.ctx = response.ctx = context;
    request.response = response;
    response.request = request;
    context.originalUrl = request.originalUrl = req.url;
    context.cookies = new Cookies(req, res, {
      keys: this.keys,
      secure: request.secure
    });
    request.ip = request.ips[0] || req.socket.remoteAddress || '';
    context.accept = request.accept = accepts(req);
    context.state = {};
    return context;
  }

直接上代码,Koa每次请求都会创建这样一个ctx对象,以提供给每个中间件使用。

参数的req, res是Node原生的对象。

下面解释下这三个的含义:

  • context:Koa封装的带有一些和请求与相应相关的方法和属性
  • request:Koa封装的req对象,比如提了供原生没有的host属性。
  • response:Koa封装的res对象,对返回的bodyhook了getter和setter。

其中有几行一堆 xx = xx = xx,这样的代码。

是为了让ctx、request、response,能够互相引用。

举个例子,在中间件里会有这样的等式

ctx.request.ctx === ctx
ctx.response.ctx === ctx

ctx.request.app === ctx.app
ctx.response.app === ctx.app

ctx.req === ctx.response.req
// ...

为什么会有这么奇怪的写法?其实只是为了互相调用方便而已,其实最常用的就是ctx。

打开context.js,会发现里面写了一堆的delegate

/**
 * Response delegation.
 */

delegate(proto, 'response')
  .method('attachment')
  .method('redirect')
  .method('remove')
  .method('vary')
  .method('set')
  .method('append')
  .method('flushHeaders')
  .access('status')
  .access('message')
  .access('body')
  .access('length')
  .access('type')
  .access('lastModified')
  .access('etag')
  .getter('headerSent')
  .getter('writable');

/**
 * Request delegation.
 */

delegate(proto, 'request')
  .method('acceptsLanguages')
  .method('acceptsEncodings')
  .method('acceptsCharsets')
  .method('accepts')
  .method('get')
  .method('is')
  .access('querystring')
  .access('idempotent')
  .access('socket')
  .access('search')
  .access('method')
  .access('query')
  .access('path')
  .access('url')
  .getter('origin')
  .getter('href')
  .getter('subdomains')
  .getter('protocol')
  .getter('host')
  .getter('hostname')
  .getter('URL')
  .getter('header')
  .getter('headers')
  .getter('secure')
  .getter('stale')
  .getter('fresh')
  .getter('ips')
  .getter('ip');

是为了把大多数的requestresponse中的属性也挂在ctx下,我们为了拿到请求的路径需要ctx.request.path,但是由于代理过path这个属性,ctx.path也是可以的,即ctx.path === ctx.request.path

ctx模块大概就是这样,没有讲的特别细,这块是重点不是难点,大家有兴趣自己看看源码很方便。

一个小tip: 有时候我也会把context.js中最下面的那些delegate当成文档使用,会比直接看文档快一点。

Koa中间件机制

中间件函数的参数解释

  • ctx:上面讲过的在请求进来的时候会创建一个给中间件处理请求和响应的对象,比如读取请求头和设置响应头。
  • next:暂时可以理解为是下一个中间件,实际上是被包装过的下一个中间件。

一个小栗子

我们来看这样的代码:

// 第一个中间件
app.use(async(ctx, next) => {
  console.log('m1.1', ctx.path);
  ctx.body = 'Koa m1';
  ctx.set('m1', 'm1');
  next();
  console.log('m1.2', ctx.path);
});

// 第二个中间件
app.use(async(ctx, next) => {
  console.log('m2.1', ctx.path);
  ctx.body = 'Koa m2';
  ctx.set('m2', 'm2');
  next();
  debugger
  console.log('m2.2', ctx.path);
});

// 第三个中间件
app.use(async(ctx, next) => {
  console.log('m3.1', ctx.path);
  ctx.body = 'Koa m3';
  ctx.set('m3', 'm3');
  next();
  console.log('m3.2', ctx.path);
});

会输出什么呢?来看下面的输出:

m1.1 /
m2.1 /
m3.1 /
m3.2 /
m2.2 /
m1.2 /

来解释一下上面输出的现象,由于将next理解为是下一个中间件,在第一个中间件执行next的时候,第一个中间件就将执行权限给了第二个中间件,所以m1.1后输出的是m2.1,在之后是m3.1

那么为什么m3.1后面输出的是m3.2呢?第三个中间件之后已经没有中间件了,那么第三个中间件里的next又是什么?

我先偷偷告诉你,最后一个中间件的next是一个立刻resolve的Promise,即return Promise.resolve(),一会再告诉你这是为什么。

所以第三个中间件(即最后一个中间件)可以理解成是这样子的:

app.use(async (ctx, next) => {
    console.log('m3.1', ctx.path);
    ctx.body = 'Koa m3';
    ctx.set('m3', 'm3');
    new Promise.resolve(); // 原来是next
    console.log('m3.2', ctx.path);
});

从代码上看,m3.1后面就会输出m3.2

那为什么m3.2之后又会输出m2.2呢?,我们看下面的代码。

let f1 = () => {
  console.log(1.1);
  f2();
  console.log(1.2);
}

let f2 = () => {
  console.log(2.1);
  f3();
  console.log(2.2);
}

let f3 = () => {
  console.log(3.1);
  Promise.resolve();
  console.log(3.2);
}

f1();

/*
  outpout
  1.1
  2.1
  3.1
  3.2
  2.2
  1.2
*/

这段代码就是纯函数调用而已,从这段代码是不是发现,和上面一毛一样,对一毛一样,如果将next理解成是下一个中间件的意思,就是这样。

中间件组合的过程分析

用户使用中间件就是用app.use这个API,我们看看做了什么:

  // 精简后去掉非核心逻辑的代码
  use(fn) {
    this.middleware.push(fn);
    return this;
  }

可以看到,当我们应用中间件的时候,只是把中间件放到一个数组中,然后返回this,返回this是为了能够实现链式调用。

那么Koa对这个数组做了什么呢?看一下核心代码

const fn = compose(this.middleware); // @callback line1
// fn 即 fnMiddleware 
return fnMiddleware(ctx).then(handleResponse).catch(onerror); // @handleRequest line_last

可以看到用compose处理了middleware数组,得到函数fnMiddleware,然后在handleRequest返回的时候运行fnMiddleware,可以看到fnMiddleware是一个Promiseresolve的时候就会处理完请求,能猜到compose将多个中间件组合成了一个返回Promise的函数,这就是奇妙之处,接下来我们看看吧。

精简后的compose源码

// 精简后去掉非核心逻辑的代码
00    function compose (middleware) {
01      return function (context, next) { // fnMiddleware
02        return dispatch(0)
03        function dispatch (i) {
04          let fn = middleware[i] // app.use的middleware
05          if (!fn) return Promise.resolve()
06          return fn(context, function next () {
07            return dispatch(i + 1)
08          })
09        }
10      }
11    }

精简后代码只有十几行,但是我认为这是Koa最难理解、最核心、最优雅、最奇妙的地方。

看着各种function,各种return有点晕是吧,不慌,不慌啊,一行一行来。

compose返回了一个匿名函数,这个匿名函数就是fnMiddleware

刚才我们是有三个中间件,你们准备好啦,请求已经过来啦!

当请求过来的时候,fnMiddleware就运行了,即运行了componse返回的匿名函数,同时就会运行返回的dispatch(0),那我们看看dispatch(0)做了什么,仔细一看其实就是

// dispatch(0)的时候,fn即middleware[0]
return middleware[0](context, function next (){
  return dispatch(1);
})

// 上面的context和next即中间件的两个参数
// 第一个中间件
app.use(async(ctx, next) => {
  console.log('m1.1', ctx.path);
  ctx.body = 'Koa m1';
  ctx.set('m1', 'm1');
  next(); // 这个next就是dispatch(1)
  console.log('m1.2', ctx.path);
});

同理,在第二个中间件里面的next,就是dispatch(2),也就是用上面的方法被包裹一层的第三个中间件。

  • 现在来看第三个中间件里面的next是什么?

可以看到精简过的compose05行有个判断,如果fn不存在,会返回Promise.resolve(),第三个中间件的nextdispatch(3),而一共就有三个中间件,所以middleware[3]是undefined,触发了分支判断条件,就返回了Promise.resolve()

再来复盘一下:

  1. 请求到来的事情,运行fnMiddleware(),即会运行dispatch(0)调起第一个中间件。
  2. 第一个中间件的nextdispatch(1),运行next的时候就调起第二个中间件
  3. 第二个中间件的nextdispatch(2),运行next的时候就调起第三个中间件
  4. 第三个中间件的nextdispatch(3),运行next的时候就调起Promise.resolve()。可以把Promise.resolve()理解成一个空的什么都没有干的中间件。

到此,大概知道了多个中间件是如何被compose成一个大中间件的了吧。

中间件的类型

koa2中,支持三种类型的中间件:

  • common function:普通的函数,需要返回一个promise
  • generator function:需要被co包裹一下,就会返回一个promise
  • async function:直接使用,会直接返回promise

可以看到,无论哪种类型的中间件,只要返回一个promise就好了,因为这行关键代码return fnMiddleware(ctx).then(handleResponse).catch(onerror);,可以看到KoafnMiddleware的返回值认为是promise。如果传入的中间件运行后没有返回promise,那么会导致报错。

结语

Koa的原理就解析到这里啦,欢迎交流讨论。
为了更好地让大家学习Koa,我写了一个mini版本的Koa,大家可以看一下 https://github.com/geeknull/t...

查看原文

赞 54 收藏 79 评论 3

小团子 回答了问题 · 2017-11-04

链表边插入边排序的效率高,还是插完所有再顺序排序效率高?

  1. 边插入边排序时间复杂度为O(n^2),插完再排不是很好,因为在链表上排序无法实现随机访问,许多高效排序算法无法在链表上应用。
  2. 建议先排序再插入,可以选择快速排序归并排序等效率较高的算法。

关注 3 回答 2

小团子 回答了问题 · 2017-11-01

解决字符串object变量解析问题

function getVal(str) {
    const attr = str.split('.');
    let final = a;
    for (let val of attr) {
        final = final[val];
    }
    return final;
}

关注 9 回答 8

小团子 回答了问题 · 2017-10-31

解决TCP协议中,客户端在三次握手过程中最后一次向服务端发送ACK,如果因为链路原因,数据包早于握手包到达会怎么样。

可以确定的是,服务器资源是在完成第二次握手分配的,客户端资源是在完成第三次握手时分配的。另一个问题与”第三次握手失败会怎样“差不多,可以参考这里的回答,What if a TCP handshake segment is lost?

关注 3 回答 2

小团子 回答了问题 · 2017-10-28

IP,域名,服务器三者间的关系?

基本情况就像楼上说的那样。这种问题直接查看相关书籍一看相关概念就懂了的内容,而不是自己哪里空想或猜测。独立查找资料是解决问题最好方式,还没理解的话才再提问。建议题主阅读《计算机网络自顶向下方法》!

关注 4 回答 2

认证与成就

  • 获得 25 次点赞
  • 获得 36 枚徽章 获得 3 枚金徽章, 获得 12 枚银徽章, 获得 21 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2015-04-16
个人主页被 1.2k 人浏览