flex从总结到了解

flex 是一种布局方式,在 CSS3 之后开始有。它主要由父容器和子项组成,父容器有六个属性,分别为:

  • 控制主轴轴向:flex-direction
    • row:横轴(默认)
    • row-reverse:倒过来的横轴
    • column:竖轴
    • column-reverse:倒过来的竖轴
  • 换行方式:flex-wrap
    • nowrap:不换行(默认)
    • wrap:换行
    • wrap-reverse:反着换行
  • 主轴排列:justify-content
  • 交叉轴排列:align-items
  • 轴向与换行组合设置:flex-flow(流向)
    • 一般很少用这个属性,即改变子项的布局顺序,正着来,倒着来

子项也有六个属性,分别为:

  • 弹性扩展:flex-grow
    • 指定容器剩余空间多余时的分配规则
    • 默认值为 0,多余空间不分配
  • 弹性收缩:flex-shrink
    • 指定容器剩余空间不足时的分配规则
    • 默认值为 1,空间不足要分配;如果为 0,表示不分配
  • 基础尺寸:flex-basis
    • 指定 flex 元素在主轴方向上的初始大小(基础尺寸)
    • 默认值为 auto,即项目本身大小
  • 缩写:flex
    • flex-grow、flex-shrink、flex-basis 的缩写
    • 默认值为 0 1 auto
  • 主轴顺序:order
  • 交叉轴对齐方式:align-self

总的来说,父容器控制整体布局,子项控制子项布局

在面试中,常常不会问怎么宽泛,最常见的 flex 面试题为:

  • flex: 0 1 auto 怎么理解?
  • flex: 1具体代表什么,有什么应用场景
  • flex: 0flex: 1flex: noneflex: auto,表示什么意思,并应用在什么场景下使用?

要想回答这些问题,我们必须了解子项中的 flex 属性

flex 语法

flex: none | auto | [< "flex-grow" > < "flex-shrink" >? || < "flex-basis" >];

单管道符 | ,表示排他。也就是这个符号前后的属性值都是支持的,且不能同时出现。因此,下面这些语法都是支持的:

flex: auto;
flex: none;

flex: [< "flex-grow" > < "flex-shrink" >? || < "flex-basis" >];

方括号 [...] 表示范围。支持的属性在这个范围内

其中 ? ,表示 0 个或者 1 个,也就是说 flex-shrink 属性可有可无。因为 flex 属性值也可以是 2 个值

flex: auto;
flex: none;
/* 2个值 */
flex: 1 100px;
/* 3个值 */
flex: 1 1 100px;

双管道 || ,表示”或者“的意思。表示前后可以分开独立使用,也就是 flex: flex-grow flex-shrink?flex-basis 都是合法的。于是我们又多了 2 种合法的写法:

/* 1个值,flex-basis */
flex: 100px;
/* 2个值,flex-grow 和 flex-shrink */
flex: 1 1;

转为文字表述

单值语法:

如果 flex 的属性值只有一个值,有三种情况

  • 一个无单位数,例如例如 flex: 1,表示 flex-shrink: 1,剩余空间扩展。此时,flex-shrinkflex-basis 的值分别是 1 和 0%。注意,这里的 flex-basis 的值是 0%,而不是默认值 auto

    • 只要改变 flex: 数字flex-basis 的值就为 0
  • 一个有效的宽度(width)值,表现形式为长度值,例如 flex: 100px,表示flex-basis: 100px,基础尺寸为 100px。此时,flex-growflex-shrink 的值都是 1,注意,这里的 flex-grow 的值是 1,而不是默认值 0

  • 关键字 noneautoinitial

双值语法:

如果 flex 的属性值有两个值,则第 1 个值一定是 flex-grow,第 2 个根据值的类型不同表示不同的 CSS 属性,具体规则如下:

  • 数值:例如flex: 1 2,则这个 2 表示 flex-shrink,此时 flex-basis 的值为 0%,而非默认值 auto
  • 长度值,例如flex: 1 100px,则这个 100pxflex-basis,此时 flex-shrink 默认值为 0

三值语法:

如果 flex 的属性值有 3 个值,则长度值表示 flex-basis,其余 2 个数值分别表示flex-growflex-shrink。下面两行 CSS 语句的语法都是合法的,且含义也是一样的:

flex: 1 2 50%;
flex: 50% 1 2;

flex 属性值场景应用

flex 默认值为 0 1 auto。除此之外,还有各种其他值

  • flex: none,等同于 flex: 0 0 auto;

  • flex: auto,等同于 flex: 1 1 auto;

  • flex: 1,等同于 flex: 1 1 0%;

  • flex: 0,等同于 flex 0 1 0%;

张鑫旭大神画过一张图:

单值语法等同于备注
flex: initialflex: 0 1 auto初始值,常用
flex: 0flex: 0 1 0%适用场景少
flex: noneflex: 0 0 auto推荐
flex: 1flex: 1 1 0%推荐
flex: autoflex: 1 1 auto适用场景少

默认值 flex: initial

它等同于 flex:0 1 auto,表示 flex 容器有剩余空间时尺寸不增长(flex-grow: 0),flex 容器尺寸不足时尺寸会收缩变小(flex-shrink:1),尺寸自适应于内容(flex-basis:auto)

我的理解:子项总长度小于总容器时,不会去撑满(flex-grow:0),而按实际宽高度存在(flex-basis:auto);当子项总长度大于总容器时,子项会相对于的收缩相对比例(flex-shrink:1)

适用场景

适用于子项总长度小于总容器的场景,例如按钮、标题、小图标等小部件的排版布局

flex: 0 和 flex: none 的区别

flex: 0 等同于设置 flex: 0 1 0%flex:none 等同于 flex: 0 0 auto

flex: 0,因为是一个值且为数值,所以它表示 flex-grow,后续我发现只用设置了flex: 数字,那么 flex-basis 就自动成了 0%,所以,设置flex:0 的元素的最终尺寸表示为最小内容宽度;

注意:

flex: 1 === flex: 1 1 0%

flex: 0 === flex: 0 1 0%

flex 设置为数字后,虽然 flex-basis 为最小宽度,但是前者的 flex-grow 有值,可以把子项扩充满容器,后者为 0,不扩展

flex: none,既不是数值也不是长度值,none 关键字。flex: 0 0 auto 表示元素尺寸不会收缩也不会扩展,再加上 flex-basis: auto 表示固定尺寸由内容决定,由于元素不具有弹性,因为,元素内的元素不会换行,最终尺寸通常表现为最大内容宽度

适用使用 flex: 0 的场景

flex:0的应用场景

无论文字的内容给如何设置,左侧内容的宽度都是图像的宽度

适合使用 flex: none 的场景

当 flex 子项的宽度就是内容的宽度,且内容永远不会换行,则适合使用 flex:none,例如如下的场景,图片和按钮固定长度,内容弹性

flex:none适用场景

flex: 1 和 flex: auto 的区别和适用场景

flex:1 等同于设置 flex: 1 1 0%flex: auto 等同于 flex: 1 1 auto

可以看出两者的 flex-growflex-shrink 都是一样的,意味着它们都可以弹性扩展以及弹性收缩,区别在于 flex: 1flex-basis 为 0,即宽度为 0。flex:auto 中的 flex-basis为 auto,即宽度为自身宽度

表现的样子为:

flex:1

这里需要解释一下,因为我最开始也不理解,其公式为:

每个子项的宽度 = (总宽度 - flex-basis 的宽度)/ 3(以这个例子为例)

因为 flex:1flex-basis 的宽度为 0 ,所以最后它的总宽度扩张或者收缩时每个子项都能等分

适用于 flex: 1 的场景

当希望元素充分利用剩余空间,同时不会侵占其他元素应用的宽度的适用,适合适用 flex:1,例如所有的等分列表

之前适用 flex: none 的例子,同样设置文字部分flex: 1 也能实现类似的效果

flex:1

适用于 flex: auto 的场景

当希望元素充分利用剩余空间,但是各自的尺寸按照各自内容进行分配的时候,适用于 flex: auto

例如导航数量不固定,每个导航文字数量页不固定的导航效果就适合适用 flex: auto

flex-auto

回过头来看之前说的面试题

  1. flex: 0 1 auto 怎么理解?
  2. flex: 1具体代表什么,有什么应用场景
  3. flex: 0flex: 1flex: noneflex: auto,表示什么意思,并应用在什么场景下使用?

第一个问题回答

flex 的默认值为 0 1 auto,表示容器剩余空间有多余的时候不扩展,不足的时候收缩,子项的宽度根据自身的宽度来展示

第二个问题回答

脑子思考 flex 的值如果是一个值且为数字,说明是 flex-grow:1,当它为数字时,flex-basis 会自动变成 0,所以它具体表示为 flex:1 1 0%,表示容器剩余空间有多余的时候扩展,不足的时候收缩,子项的宽度为 0。它一般适用于充分利用剩余空间,又不侵占其他元素的宽度,例如等分布局

第三个问题回答

flex:0,表示 flex: 0 1 0%,表示容器剩余空间有多余的时候不扩展,不足的时候收缩,子项的宽度为 0,适用设置在替换元素的父元素上

flex:1,看第二个回答

flex: none,表示 flex: 0 0 auto,表示容器剩余空间有多余的时候不扩展,不足的时候也不收缩,子项的宽度为自身宽度,适用于不换行的内容或者较少的小控件元素上

flex: auto,表示 flex: 1 1 auto,表示容器剩余空间有多余的时候扩展,不足的时候收缩,子项的宽度为自身宽度,适用于基于内容动态适配的布局(例如导航数量文字长度不固定)

flex:initial,表示 flex: 0 1 auto,表示容器剩余空间有多余的时候不扩展,不足的时候收缩,子项的宽度为自身宽度,适用于小控件元素的分布布局,或者某一项内容动态变化的布局

参考资料

芝麻开门,显示全文!

编程题:为什么最后一个a是1不是5

最近立下的 flag 是每周回答至少三个知乎回答,不限编程,希望能提高自己的书面表达能力。这不,有人邀请我回答一个问题:为什么最后一个 a 是 1 不是 5?

题目如下:

console.log(a);
if (true) {
  a = 1;
  function a() {}
  a = 5;
  console.log(a);
}
console.log(a);

结果

我的第一反应是:undefined,5,5。估计和题主想的一样

分析一波

假设没有 if(true),即如下代码:

console.log(a);
a = 1;
function a() {}
a = 5;
console.log(a);
console.log(a);

那么答案什么?

a()、5、5

这解释了两个特性

  1. 变量、函数提升且函数的权重大于变量
  2. 在 a 没有用 var 声明时,a=XX 默认是用 var 来声明

变量、函数提升方面的知识点在于:

变量会提升,函数也会提升,并且函数提升的优先级大于变量,如下例:

console.log(a);
console.log(a());
var a = 1;
function a() {
  console.log(2);
}
console.log(a);
var a = 3;
a = 4;
console.log(a);
console.log(a());

a()、2、1、4、a is not a function

回过头来看这道题目

console.log(a);
if (true) {
  a = 1;
  function a() {}
  a = 5;
  console.log(a);
}
console.log(a);

if (ture) {} ,形成了作用域,锁住了这片变量,function a(){} 无法逃逸。换句话说,只有 {} 块级标识符在,function a() {} 就被所在块级作用域中,也就说在 if (ture) {} 这片块级作用域下,它不会提升到全局顶层,而是在 if(true){} 下,即代码执行时是这样:

console.log(a);
if (true) {
  +function a() {};
  a = 1;
  -function a() {};
  a = 5;
  console.log(a);
}
console.log(a);

如果你在 a = 1 前打印 a,a 的值就是 function a(){}

所以这道题全局环境下,没有变量提升,写在第一行的 console.log(a) 因为找不到 a,所以值为 undefined

进入 if(true) {} 中,function a(){} 函数提升,且权重最高,所以赋值之前的块级作用域中的 a 为 function a() {}window.aundefined

代码执行到 function a() {} 后,块级作用域中的 a 还是为 1,但是全局变量 a 被赋值为 1

执行到 a = 5,传统赋值,影响的是块级作用域中的 a,而不会影响全局变量 a,所以打印的第二个 console.log(a) 为 5,第三个 console.log(a) 为 1

那么问题来了,为什么一执行 function a(){},全局变量 a 就被赋值为 1?

我陷入的沉思,后来在回答中发现[云补断山]回答了,说是

历史原因,为了兼容之前的 ES5 的语法,所以在规范规定了块级作用域内函数声明的一些行为,各个浏览器实现可能不一样

简单来说,在块级作用域内的函数函数声明,行为类似于 var ,都会在全局作用域声明一个同名变量(也就是 window 上挂一个同名的属性,默认值是 undefined),因为 ES6 遇到块级作用域,会基于块级作用域创建 environment record,存放当前块级作用域内的变量,所以这个函数声明会提升到块级作用域顶部(而非全局作用域顶部)

ECMA262 目录 B

我们学的 JavaScript 是 ECMAScript,但是我们把代码运行在浏览器上时就要按照浏览器的标准,浏览器里会有一些私货在,最经典的是 __proto__ ,倒逼 ECMA 采纳。话说回来,按照这位仁兄的意思

// 因为 function a() 声明过,所以全局有个 window.a
console.log(a);
if (true) {
  // 声明归声明,但是函数提升提升与作用域相关,所以提升至此块级作用域顶部
  a = 1;
  // 块级作用域中的 a 被赋值为 1
  function a() {}
  // 原地爆炸,执行函数后,全局 window.a 被赋值为块级作用域中的 a
  a = 5;
  // 块级作用域中的 a 又被赋值为 5
  console.log(a);
}
console.log(a);

最诡异的是执行 function a() {} 后,全局 window.a 被赋值且为块级作用域中的 a

这个事情没完!!

等等,我就说的玩玩的,如果工作中或面试中真遇到这类问题,我也许还是不会解。

太诡异了,这不是考题范围(块级作用域、函数提升、变量提升)

就这样先吧

芝麻开门,显示全文!

项目实战:弹出广告任意页面展示

最近接到一个需求,产品经理希望能新增弹窗广告,广告可根据后台配置在应用任意页面弹出展示。当后台改变当前页面广告次数、链接或者目标页后,当前页面数据修改,不影响其他页面数据

例如后台设置“首页”出现广告 1 次,“我的”页面广告出现 3 次,用户进去后关闭了“首页”广告 1 次,关闭了“我的”页面广告 2 次。此时退出应用,后台将“首页”广告设置为 2 次,那么该用户“首页”广告重置为 2 次,“我的”页面广告仍为 1 次( 3 - 2)

需求分析

后端返回的数据必然是个数组,每个对象中会有目标页(展示的页面),跳转链接总出现的次数三参数。前端要对数据进行处理:

  • 当本地没有数据时(第一次进入),将总出现次数赋值给一参数 firstTotalTimes(记录原总出现次数)
  • 当本地有数据(非第一次进入)
    1. 将本地存储中的 firstTotalTimes 清除,返回值赋值为 removeLocalTotalTimeList
    2. 将 removeLocalTotalTimeList 与 请求返回的数据 advertisementList 进行对比
      • 相等,说明后台数据没有改变,查看你本地存储中的总出现次数是否大于 0 ,大于则展示广告
      • 不相等,说明后台修改了数据,这里还要分析,只重置修改处页的,未修改的地方不做处理

笔者用的框架是 umi3,其中有 wrappers 概念,即一个配置路由的高阶组件封装,在 umi.conf 中加上后,任何页面都要先经过这一道。关键代码如下:

useEffect(() => {
    dispatch({ type: 'common/fetchGetPopUpAdvertisementList' }).then((resData: any) => {
        if (resData?.resultCode === "S00000") {
            if (!localStorage.advertisementList) {
            const addFirstTotalTimes = resData.advertisementList.map((item: any) => {
                item.firstTotalTimes = item.totalTimes
                return item;
            })
            localStorage.advertisementList = JSON.stringify(addFirstTotalTimes);
        }

        const localAdvertisementList = JSON.parse(localStorage.advertisementList)

        const cloneLocalAdvertisementList = JSON.parse(JSON.stringify(localAdvertisementList))

        const removeLocalTotalTimeList = cloneLocalAdvertisementList.map((item: any) => {
            delete item.firstTotalTimes
            return item
        })
        if (_.isEqual(removeLocalTotalTimeList, resData.advertisementList)) {
            console.log('相等')
            localAdvertisementList.filter((item: any) => {
                if (item.targetUrl.indexOf(history.location.pathname) > -1) {
                    if (item.firstTotalTimes > 0) {
                        setAdItem(item)
                    }
                }
            })
        } else {
            console.log('不相等')
            const cloneList = JSON.parse(JSON.stringify(resData.advertisementList));
            for (let i = 0; i < cloneList.length; i++) {
                for (let j = 0; j < cloneLocalAdvertisementList.length; j++) {
                    if (_.isEqual(cloneList[i].pkId, cloneLocalAdvertisementList[j].pkId)) {
                        if (_.isEqual(cloneList[i], cloneLocalAdvertisementList[j])) {
                            cloneList[i].firstTotalTimes = localAdvertisementList[j].firstTotalTimes
                        } else {
                            cloneList[i].firstTotalTimes = cloneList[i].totalTimes
                        }
                    }
                }
            }
            localStorage.advertisementList = JSON.stringify(cloneList);
            cloneList.filter((item: any) => {
                if (item.targetUrl.indexOf(history.location.pathname) > -1) {
                    if (item.firstTotalTimes > 0) {
                        setAdItem(item)
                        setIsShow(true)
                    }
                }
            })
        }
    }
                                                                     })
}, [])

难点

JS 的数据可变性

第一个坑点在 JS 的数据是可变的,所以要对其数据进行深拷贝,才不会影响到其他数据,这里我用了最简单的深拷贝:JSON.parse(JSON.stringify)

const cloneLocalAdvertisementList = JSON.parse(
  JSON.stringify(localAdvertisementList)
);

判断后台那个数据修改

在之前表述中已经表明,当本地存储和请求过来的数据不一致时要判断,哪要做重置,哪些页面则维持原状。这就要对两个数组进行对比,最简单的方法就是做双循环(On2).

const cloneList = JSON.parse(JSON.stringify(resData.advertisementList));,深拷贝后台返回数据,这样对 cloneList 进行处理时就不会影响到原数据。cloneLocalAdvertisementList 则是本地的存储

if (_.isEqual(cloneList[i].pkId, cloneLocalAdvertisementList[j].pkId)) ,pkId 是广告唯一标识,先识别数组中的每一个对象,这是一一对应的,再判断 if (_.isEqual(cloneList[i], cloneLocalAdvertisementList[j])) ,对比对象中的值,如果是 true,即完全相等,说明后台数据没有变化,那就将本地存储中的 firstTotalTimes 赋值给 cloneList 上的 firstTotalTimes 。如果是 false,说明后台已经修改,就把 firstTotalTimes 重置为本次拉取数据中的 totalTimes

const localAdvertisementList = JSON.parse(localStorage.advertisementList)
const cloneLocalAdvertisementList = JSON.parse(JSON.stringify(localAdvertisementList))
 ...
const cloneList = JSON.parse(JSON.stringify(resData.advertisementList));
for (let i = 0; i < cloneList.length; i++) {
    for (let j = 0; j < cloneLocalAdvertisementList.length; j++) {
        if (_.isEqual(cloneList[i].pkId, cloneLocalAdvertisementList[j].pkId)) {
            if (_.isEqual(cloneList[i], cloneLocalAdvertisementList[j])) {
                    cloneList[i].firstTotalTimes = localAdvertisementList[j].firstTotalTimes
                } else {
                    cloneList[i].firstTotalTimes = cloneList[i].totalTimes
            }
        }
    }
}

以上,就是对这次项目的核心代码,当然,还要考虑到 App 端打开和 微信打开的差异,以及当未登录状态下的去登录后数据的更新等等,但这些可以通过监听登录来判断(useEffect 依赖数据)实现

总结

这次被数据可变性坑了,通过 debugger 来排查

双循环在实际项目中用的次数不多,所以对此做记录

芝麻开门,显示全文!

微信网页授权

微信网页授权步骤差不多有三步,具体文档可查看这里,我画了下流程图:

微信授权流程图

以下为代码实战

第一步:用户同意授权,获取 code

需先调用 /auth 接口,传入必传参数 url 以及 scope(此为参数名)

请求方式:GET

  • url 为回调地址
  • scope 有两个可选参数
    • snsapi_base 只能获取进入页面用户的 openid,用户无感知,叫静默授权
    • snsapi_userinfo 能获取用户的基本信息,但需要用户接受,叫手动授权,如下图

snsapi_userinfo示意图

具体区别可前往 微信文档 查看

第二步:通过 code 换取网页授权 access_token

这里以手动授权为例

获取到微信的 code 后,再请求 /getUserInfo

请求方式: GET

请求参数:code,需请求 /auth 获取到 code 先,如果你在请求 /auth 时传入的 scopesnsapi_userinfo , 那么返回微信个人信息,包括微信名,性别,所在地区,国籍,头像等等,如下

{
  "openid":" OPENID",
  "nickname": NICKNAME,
  "sex":"1",
  "province":"PROVINCE",
  "city":"CITY",
  "country":"COUNTRY",        "headimgurl":"https://thirdwx.qlogo.cn/mmopen/g3MonUZtNHkdmzicIlibx6iaFqAc56vxLSUfpb6n5WKSYVY0ChQKkiaJSgQ1dZuTOgvLLrhJbERQQ4eMsv84eavHiaiceqxibJxCfHe/46",
  "privilege":[ "PRIVILEGE1" "PRIVILEGE2"     ],
  "unionid": "o6_bmasdasdsad6_2sgVt7hMZOPfL"
}

但如果 scopesnsapi_base ,请求成功时只返回用户的 openid

PS: 请求/getOpenId/getUserInfo 成功时会返回 access_token,但此 access_token 和 微信服务端开发中的 access_token 不同,一个是微信与服务器打交道(微信票据服务),另一个是微信网页的 OAuth2.0 服务(网页授权)

第三步:请求 userInfo

拿着 access_token 和 openid,去请求微信官方接口

http:GET(请使用 https 协议) https://api.weixin.qq.com/sns/userinfo?access_token=ACCESS_TOKEN&openid=OPENID&lang=zh_CN

返回 openid、nickname、sex、province、city、country、headimgurl 等信息,拿着 openid 和你想要的数据返回到原来 /auth 参数中的 url 上

实战

先调用 /auth 接口,传入参数 url 和 scope

请求接口:http://192.168.230.209/auth?url=http://192.168.230.209/home&scope=snsapi_userinfo

redis 存 url=http://192.168.230.209/home,即最后授权完成拿到数据后返回的前端地址

判断参数 scope,如果是 snsapi_userinfo,用户点击授权后跳转至 /getWxUserInfo 接口;

如果是 snsapi_base,静默授权后跳转至 getOpenId 接口

这里我们传的 scope 为 snsapi_userinfo,所以请求成功后会有授权页面

授权示意图

点击”同意“会跳转至页面

http://192.168.230.209:8888/api/wechat/getWxUserInfo?code=081UcAFa1s1OAz0o7wGa1wb8vG1UcAFX&state=123

PS:http://192.168.230.209:8888/api/wechat 为该后端服务地址,getWxUserInfo 为路由(即请求接口)

ctx.request.query 中拿到 code,拿着 code 请求 access_token 服务,access_token 服务也是微信官方提供的一个方法

获取code后,请求以下链接获取access_token: https://api.weixin.qq.com/sns/oauth2/access_token?appid=APPID&secret=SECRET&code=CODE&grant_type=authorization_code

请求成功的话,拿着这个返回值中的 access_token 和 openid,请求 userinfo 接口,在上文已经介绍过,这里不做重复

这里要说明的一点是,如果请求 access_token 的返回 code 为 40029,说明 access_token 已经失效,我们需要重新刷新 access_token

拿到 userinfo 的返回值后,在最开始存在 redis 中的 url 上拼接 openid、headimgurl 等即可

这里需要说明一点

需要先配置 OAuth2.0 网页授权的回调页面域名,类似这种

授权回调页面域名

总结

一定要知道一点,微信网页开发和调用微信的 JS-SDK 不一样,也和微信服务端开发不一样

它可以当初拎出来说,坑也比较少,不会遇到像 JS-SDK 那样的各种报错

只要知道,它为为了获取 openid (以及微信个人信息)而弄的一个服务就好了

芝麻开门,显示全文!

实战独立项目「几行字」:从想法到上线全过程

前言

之前尝试过几个小项目,自己也很想做独立的项目,这种自己创造一样东西的感觉很棒,奈何之前太差,虽然现在能力也不是特别强,但好歹有这个心了

我的最终想法是想做个关于”中国美“的项目,但是这个项目太大,能实现是一件特别有成就的事情,但现在还是先做一个独立的项目先

这里记录自己的想法,非礼勿言

想法

最开始是看到 毒鸡汤 的项目(作者自己的域名已经不能访问,当初自己为了学习部署,也搞了一份,网址:),觉得很有趣,简单又有趣

后来看到 今日诗词,这不是差不多吗,无非是提供了 API 调用罢了。

这两则的 star 数都超过了 1000+,这么简单的应用竟然这么受欢迎,有点羡慕嫉妒感

因为好奇,接触了 vite 、tailwindcss 等新技术,就想着用 vite 搭建一个 react 应用,样式用 tailwindcss 定制,于是就想要做个简单的应用,后来脑洞越想越大,就有了后续的规划,直接说规划

规划

这个项目从想法、画原型、写前端、做设计、部署、搞后端、后台一整套,从想法到实现

我最截止到写这篇文章时的规划是:

第一阶段:提出想法、画出原型、做好一个静态页面、部署到线上,即静态独立项目

第二阶段:用 vite + react 开发此项目,并添加功能点,如可选主题色、分享卡片等功能

第三阶段:数据不能裸泳,配置后端功能以及后台编辑功能

第四阶段:将其做成 Flutter 版本

第五阶段:将其做成小程序版本

这五个阶段笔者不会一口气做出来,有些东西只是想法,具体实施时困难肯定比想象中多的很

收集素材

之前混知乎,也关注了几个关于句子的问题,例如 你读过的最有力量的一段文字是什么?有哪些适合摘抄的句子 ,有些句子很喜欢,有些能受启发,与其这样,不如把有些高赞的句子收集起来,也做成像 毒鸡汤、今日诗词这样应用

于是乎,每天去知乎上手动收录素材,加上自己以前的库存,大概收集了 100 多条数据(写于第一阶段),

画原型

主要以简洁为主,能不要的东西统统不要,大致画出了这样

原型

写页面

初始化页面

npm init -y

为什么要弄功能?因为我们要用到 tailwind,它官方支持用这种方式,等 build 的时候会 tree-shake,能减少很多不必要的代码

后续可看 官网安装指南

通过 npm 安装 Tailwind

npm install tailwindcss@latest postcss@latest autoprefixer@latest

作为 PostCSS 插件来添加 Tailwind

// postcss.config.js
module.exports = {
  plugins: {
    tailwindcss: {},
    autoprefixer: {},
  },
};

创建 tailwindcss 配置文件

npx tailwindcss init

这将会在您的工程根目录创建一个最小的 tailwind.config.js 文件。

// tailwind.config.js
module.exports = {
  purge: [],
  darkMode: false, // or 'media' or 'class'
  theme: {
    extend: {},
  },
  variants: {},
  plugins: [],
};

新建 tailwind.css

引入 tailwind 代码

@tailwind base;
@tailwind components;
@tailwind utilities;

新建 index.html

在代码中引入 css、以及编写 html 代码

这里我不细讲,因为花了不少时间,参数太多,大多数是看到符合自己原型的就拿来,然后删删改改

新建 data.js

之前收集了不少素材,将其导入到 data.js 中,并且编写以下逻辑,浏览器中读过的句子保存在本地存储里。句子是随机生成,如果随机生成的句子在本地存储中,那就重新生成。当所有的句子都存在本地存储中的话,就清空所有的本地存储。

因为我的内容有些不是一句话,而是一个数组,所有在插入内容时也需要判断,根据不同的情况做出不同的效果

这里遇到一些问题记录下,太久没有写原生,插入 html 的 api 忘记了,innerHTML 和 appendChild 的区别忘记了

innerHTML :可以插入一段 html,例如

我是 p 标签

appendChild :在内容末插入节点,要先创建标签,在插入

封装成三个方法,即拉取数据,存本地存储,插入网页

做设计

参考了一些别人做 logo 的建议,推荐比较多的是 logo 神器,我按照提示做下来是这样的设计

logo

我表示遗憾,从个人审美上来看,这种设计太傻瓜了,所以自己用 Photoshop 做了一个,

SEO 优化

favicon 处理

在 logo 中扣出 字,然后上传至 https://favicon.io/ 上,导出 favicon,

设置 header 信息

<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>几行字</title>
  <meta
    name="description"
    content="几行字给你温暖、几行字给你激励、几行字给你灵感"
  />
  <meta name="keywords" content="几行字,文案,几行世界" />
  <meta http-equiv="Cache-Control" content="no-siteapp" />
  <meta property="og:title" content="几行字" />
  <meta property="og:image" content="./favicon.ico" />
  <meta property="og:site_name" content="几行字" />
  <meta
    property="og:description"
    content="几行字,几行字给你温暖、几行字给你激励、几行字给你灵感"
  />
  <link rel="alternate icon" type="image/x-icon" href="favicon.ico" />
  <link rel="stylesheet" href="style.css" />
</head>

也写不出什么关键字来,先这样,后期有灵感了再补上

部署

笔者之前写过利用 Github Actions 部署前端 ,也成功部署过 毒鸡汤,大致流程如下

  1. 申请阿里云容器镜像服务
  2. 将代码推到 Github 仓库,触发 Github Actions
    1. Github Actions 中登录 阿里云容器镜像服务,将代码打包成一个镜像,并推到个人镜像站远端
    2. 再登录服务器,执行拉取镜像脚本

主要逻辑是这样,但执行起来很麻烦,还不如直接部署来着算,什么直接部署,就是本地部署到线上,最有用的当属 now,也就是现在的 vercel,笔者之前部署过好几个项目,所以轻车熟路

直接部署上线:https://jihangzi-static.vercel.app/

在阿里云做一下映射:https://jihangzi.azhubaby.com/

第一阶段到此就告一段落

芝麻开门,显示全文!

使用微信wx-open-launch-app标签实现微信网页打开App

前提须知

笔者公司的项目在微信端的功能定位为基础功能交易及服务,通知用户交易提醒、交易流水等,而 APP 为主要的交易功能。之前是在多个页面有引流按钮跳转至 App,功能点比较粗暴,直接 location.href = 应用宝链接。现在产品有需求,说要用微信提供的标签来唤起 App

需求点:

所有跳转至 App 下载页面的部分,改成

需求点

Demo 先行

遇事不决,官网文档。查看后与微信 JS-SDK 功能点很像,这里我不废话,直接跳过。按照官网 demo,把示例写进业务代码中

import React, { useEffect, useRef } from 'react';
import { toDownloadApp, isWechat, getWeixinVersion } from 'utils';

const Download = () => {

    const wxRef = useRef(null)

    useEffect(() => {
        if (wxRef.current) {
            // @ts-ignore
            wxRef.current?.addEventListener('launch', function (e: any) {
                console.log('success');
            });
            // @ts-ignore
            wxRef.current.addEventListener('error', function (e) {
                console.log('fail', e.detail);
                toDownloadApp()
            });
        }
    }, [])

    const onHandleClick = () => {
     	toDownloadApp()
    }

    return (
        <div className="Download" onClick={onHandleClick}>
            {/*  @ts-ignore */}
            <wx-open-launch-app
                ref={wxRef}
                appid="XXXX"
            >
                <script type='text/wxtag-template'>
                    <button>App内查看</button>
                </script>
                {/*  @ts-ignore */}
            </wx-open-launch-app>
        </div>
    )
}

export default React.memo(Download);

测试成功,demo 能跑通

组件试点

现在搞业务,以这个组件(Download)为试点展开,我要点击页面顶部的卡片(多个地方使用,抽离成 Download 组件),让其唤起 App,但是要判断其版本,如果版本过低,让其跳转至应用宝

import React, { useState, useEffect, useRef } from 'react';
import LogoImg from '@/assets/images/logo.png';
import { toDownloadApp, isWechat, getWeixinVersion } from 'utils';

const Download = () => {

    const wxRef = useRef(null)
    const [enableLaunchWeapp, setEnableLaunchWeapp] = useState(false);

    useEffect(() => {
        const wxVersion = isWechat() && getWeixinVersion() || ''
        if (wxVersion) {
            let v = wxVersion.split('.')
            if (Number(v[0]) >= 7) {
                if (Number(v[1]) >= 0) {
                    if (Number(v[2]) >= 12) {
                        setEnableLaunchWeapp(true)
                    }
                }
            }
        }
        if (wxRef.current) {
            // @ts-ignore
            wxRef.current?.addEventListener('launch', function (e: any) {
                console.log('success');
            });
            // @ts-ignore
            wxRef.current.addEventListener('error', function (e) {
                console.log('fail', e.detail);
                toDownloadApp()
            });
        }
    }, [])

    const onHandleClick = () => {
        if (!enableLaunchWeapp) {
            toDownloadApp()
        }
    }

    return (
        <div className="Download" onClick={onHandleClick}>
            <div className="Download__logo">
                <img src={LogoImg} alt="logo" />
            </div>
            <div className="Download__content">
                <div className="Download__content-title">雅美App</div>
                <div className="Download__content-desc">长泽雅美服务专区</div>
            </div>
            {/* <div>1</div> */}
            <div className="Download__btn">立即打开</div>
            {/*  @ts-ignore */}
            <wx-open-launch-app
                ref={wxRef}
                appid="XXXXX"
                style={{ position: 'fixed', top: 0, left: 0, width: '100%', height: '60px', opacity: 0.3, background: 'blue' }}
            >
                <script type='text/wxtag-template'>
                    <div style={{ position: 'fixed', top: 0, left: 0, width: '90%', height: '100%', opacity: 0.3, background: 'red' }} />
                </script>
                {/*  @ts-ignore */}
            </wx-open-launch-app>
        </div>
    )
}

export default React.memo(Download);

效果如下所示:

点击范围

思路逻辑参考:wx-open-launch-weapp 样式问题,我也给它配上颜色,方便后续观察

测试同步,能点击卡片跳转,好,下一步,在所有需要点击跳转页面的地方加入类似这样的代码

<wx-open-launch-app
  ref={wxRef}
  appid="XXXX"
  style={{
    position: "fixed",
    top: 0,
    left: 0,
    width: "100%",
    height: "60px",
    opacity: 0.3,
    background: "blue",
  }}
>
  <script type="text/wxtag-template">
    <div
      style={{
        position: "fixed",
        top: 0,
        left: 0,
        width: "90%",
        height: "100%",
        opacity: 0.3,
        background: "red",
      }}
    />
  </script>
  {/*  @ts-ignore */}
</wx-open-launch-app>

封装组件 WxOpenLaunchApp

如果是这样,就可以将其封装成一个组件了,起个名吧: WxOpenLaunchApp

将唤起 App 的内容包装成一个组件,暴雷 children 和 style 两个 props,代码如下:

import React, { useEffect, useRef, forwardRef } from 'react';
import { toDownloadApp } from 'utils';

export interface WxOpenLaunchAppProps {
    children: React.ReactNode;
    style?: React.CSSProperties;
}

const WxOpenLaunchApp: React.FC<WxOpenLaunchAppProps> = props => {
    const { style, children } = props;

    const wxRef = useRef(null)

    useEffect(() => {
        if (wxRef.current) {
            // @ts-ignore
            wxRef.current?.addEventListener('launch', function (e: any) {
                console.log('success');
            });
            // @ts-ignore
            wxRef.current.addEventListener('error', function (e) {
                console.log('fail', e.detail);
                toDownloadApp()
            });
        }
    }, [])

    return (
        <div className="wx-open-launch-app">
            {/*  @ts-ignore */}
            <wx-open-launch-app
                ref={wxRef}
                appid="XXXX"
                style={style}
            >
                <script type='text/wxtag-template'>
                    {children}
                </script>
                {/*  @ts-ignore */}
            </wx-open-launch-app>
        </div>
    )
}

export default React.memo(WxOpenLaunchApp);

那么 Download 组件也就可以干净很多

...
const Download = () => {
    ...
    return (
    	...
        	<div className="Download__btn">立即打开</div>
            {/*  @ts-ignore */}
            <WxOpenLaunchApp style={{ position: 'fixed', top: 0, left: 0, width: '100%', height: '60px', opacity: 0.3, background: 'blue' }}>
                <div style={{ position: 'fixed', top: 0, left: 0, width: '100%', height: '100%', opacity: 0.3, background: 'red' }} />
            </WxOpenLaunchApp>
		...
    )
}
...

业务组件 OpenAppPopup

回到需求点,每个点击的地方都要弹出弹出框,点击 打开 App ,再唤起 App,这样的话,弹出框 + WxOpenLaunchApp 就可以结合成一个组件,放出来供页面调用,名字就叫 OpenAppPopup ,代码如下:

import React, { FC } from 'react';
import { Popup, WxOpenLaunchApp, Toast } from 'components'; // 此乃公司自研组件库

export interface OpenAppPopupProps {
    show: boolean;
    onCancel: () => void;
    onSubmit: () => void;
}

const OpenAppPopup: FC<OpenAppPopupProps> = (props) => {
    const { show, onCancel, onSubmit } = props;

    return (
        <Popup.Group show={show}>
            <Popup.Confirm
                title="抱歉,此功能需在雅美App中使用"
                btnSubmitText={
                    <div style={{ position: 'relative' }}>
                        打开App
                        <WxOpenLaunchApp style={{ position: 'absolute', top: 0, left: 0, width: '100%', height: '100%', opacity: 0.3, background: 'blue' }}>
                            <div style={{ position: 'absolute', top: 0, left: 0, width: '100%', height: '100%', opacity: 0.6, background: 'red' }} />
                        </WxOpenLaunchApp>
                    </div>
                }
                onCancel={onCancel}
                onSubmit={onSubmit}
            />
        </Popup.Group>
    )
}

export default React.memo(OpenAppPopup);

示意图如下:

OpenAppPopup

接着在所有有跳转 App 的页面上用上这块逻辑即可

封装 HOOK

每个页面点击类似 下载App 按钮时,会弹出 OpenAppPopup,点击 打开App,需要判断你的微信版本,是否达到 7.0.12,如果每个页面都要加上这段

const wxVersion = (isWechat() && getWeixinVersion()) || "";
if (wxVersion) {
  let v = wxVersion.split(".");
  if (Number(v[0]) >= 7) {
    if (Number(v[1]) >= 0) {
      if (Number(v[2]) >= 12) {
        setEnableLaunchWeapp(true);
      }
    }
  }
}

真的太恶心了,果断抽离成 hook。代码如下:

import { useState, useEffect } from "react";
import { isWechat, getWeixinVersion } from "utils";

const useEnableLaunchWeapp = () => {
  const [enableLaunchWeapp, setEnableLaunchWeapp] = useState(false);
  useEffect(() => {
    const wxVersion = (isWechat() && getWeixinVersion()) || "";
    if (wxVersion) {
      let v = wxVersion.split(".");
      if (Number(v[0]) >= 7) {
        if (Number(v[1]) >= 0) {
          if (Number(v[2]) >= 12) {
            setEnableLaunchWeapp(true);
          }
        }
      }
    }
  }, []);
  return enableLaunchWeapp;
};

export default useEnableLaunchWeapp;

逻辑也很简单,在刚加载时判断它是否可以点击,可以点击,就设置 enableLaunchWeapp 为 true。使用方法也很简单

import React, { useState, useEffect } from 'react';
import { Dispatch, History } from 'umi';
import {  OpenAppPopup } from 'components';
+import { useEnableLaunchWeapp } from 'hooks';
import { toDownloadApp } from 'utils';

interface KVProps {
    history: History;
}

const KV: React.FC<KVProps> = (props) => {
    const { history } = props;

    const [isShow, setIsShow] = useState(false);

    +const enableLaunchWeapp = useEnableLaunchWeapp();

    const onHandleClickToBuy = () => {
        setIsShow(true);
    };

    const onHandleClickToSubmit = () => {
        +if (!enableLaunchWeapp) {
        +    toDownloadApp()
        +}
    }

    return (
        <div className="KV" style={{ background: kvBgColor }}>
            <div className="KV__content">
                <img src={img} alt="" />
            </div>
            <OpenAppPopup
                show={isShow}
                onCancel={() => {
                    setIsShow(false);
                }}
                onSubmit={onHandleClickToSubmit}
            />
        </div>
    );
};

export default React.memo(KV);

与 App 交互

需求点里说:要在所在页面跳转至 App 相对页面,文档上写的很明显,可以传参数 extinfo="your-extinfo",随便写了个让客户端同事先测试先

未唤醒 App

我手机是 IOS 的,是可以唤起的,但是安卓同事调试的时候说,后台运行时,可以唤起 App,但是没有切换动作;如果杀掉进程,就无法唤起。而这问题,大概率是 SDK 配置的问题,同事看了半天没解决,扔给他 Android 接入指南 。我又看不懂 Android,只能看他了

如果测试成功,能跳过去,那么就把本页链接当作 extinfo 传过去,他那边接收到 extinfo 后,做个映射表,跳转至自身的页面即可,所以 WxOpenLaunchApp 需要改造,多一个 extinfo 参数。。。

后记

因为我们用的是 flutter,同事说,因为引入的第三方库不支持,所以跳不过去,所以这个功能要后置,等他搞定了我再做更新

错误处理

除了在 WxOpenLaunchApp 组件中加入监听 error,错误就让它跳转至 App 外,还要做当微信或者系统版本不支持微信标签时,需要监听并进行回退兼容,代码如下:

document.addEventListener('WeixinOpenTagsError', function (e: any) {
  console.error(e.detail.errMsg) // 无法使用开放标签的错误原因,需回退兼容。仅无法使用开发标签,JS-SDK其他功能不受影响
  toDownloadApp()
})

总结

又复用就抽离成组件

必须要上生产环境,所以最好是有个预生产环境

参考资料

芝麻开门,显示全文!

张一鸣微博记录

这里记录张一鸣曾经在微博上说过的一些话,不全,仅记录对当前自己有用的东西

  1. 年轻人不要试图追求安全感,特别是年轻的时候,周遭环境从来都不会有绝对的安全感,如果你觉得安全了,很有可能开始暗藏危机。真正的安全感, 来自你对自己的信心,是你每个阶段性目标的实现,而真正的归属感,在于你的内心深处,对自己命运的把控,因为你最大的对手永远是自己。

2.稻盛要辞职离开快倒闭的公司,遭兄长棒喝:“在这样没人干活的公司你都做 不出点成绩来,你还能干什么?2.洛克菲勒感觉再也无法忍受日复一日枯燥的工 作,提出换岗,遭主管冷言“要么好好干、要么另谋出路”。同样的道理,不同 的说法,却像雷一样击中并成就了两个商业巨匠。还是那句,不抱怨、想方法。

3.关于消费:买书、健身、学习都属于资金成本边际成本很低,对于很多人,只要你能真正完成这些消费,资金都不是主要成本而值得大力投入的消费。综上,我非常建议大家买书、买电子书、ipad、智能手机、买健身卡、游泳卡。。。还 有类似的消费吗?

4.听说有人每天能看一本书,问题还不在看书速度,而是在知易行难,实践的速度赶不上所知的要求,欠账很多

5.研究聪明人如何犯错误,回报率很高。聪明人易犯错误包括:1 嫉妒他人成功;2 自命不凡:3 过于相信自己判断;4 停止学习;5 认为世界是静止的,生活在过去荣耀中;6 任何事情都有自己一套言之有据、且深信不疑的说法和理论:忘记了没有调查研究,就没有发言权。你符合吗?

6.坚持原则很多时候是经济的,可以看做是一种短期浮亏的长期受益的投资。

7.加强专注力训练,它是优先级管理的保证,同时持续专注力的一个基础是体力和精力,锻炼修炼。

8.昨天和朋友聊天,总结到:在这个信息流动越来越快越来越透明的社会,从经济的角度来看,做一个表里不一的人成本越来越高,龌龊的人会越来越倒霉,不装不但是一个道德品性优选,而且也是更经济的。很多人还未意识到这点。

9.上午北京大学周其仁教授发言非常精彩。他认为一个持久得到别人信任的人,收入就越高。有比知识、技能更加重要的东西,那就是信任。他们的团队在研究了农民工的收入以后发现,收入最高的人,往往并不是体力最好、技能最好的时候,而是最受信任的人。所以,成为一个受人信任的人,非常重要。

10.最近感想:口碑很重要,人品很重要,信用很重要,越老越重要,原则要坚定。

11.你们读了哪些传记?想起 2 年前朋友说:如果不知道让小孩阅读什么,最适合的就是传记。最近在思考与回忆:关于品格、理想、动机的形成,觉得确实如此。

12.今天手机报上有一段话:“独处是一次心灵按摩” 静坐在斗室里,漫步在小道上, 平躺在沙滩上 … 有意识的面对自己,和内心对话。喜欢独处的人,和别人在一 起时,往往也会处理的更好。交流和独处相辅相成,才能让内心成熟和强大。

13.凡事就怕不认真,不思考。好多问题我应该能知道的,只是之前没有认真看,认真想,想当然(不是没时间)。延迟满足感是一项长期修炼。

14.今天晚上的时候,每个周五晚上下班的时候,我常会和同事说:我明天假期我们再把 xxx 做好。每次突然想这句话矛盾啊,不能这样要求。嗯,生活工作要平衡。不过,别人腐败的时候我们在努力,别人消磨时光的时候我们在学习,那么延迟的满足一定会厚积薄发来到。

15.人不逼一下自己,永远不知道自己潜力有多大。很多事情非不能也,是不为也。

16.《如何阅读一本书》一书在谈在技能之外,更多的是讲学习的态度和沟通的方法。比如赞同和反对作者一章,其实标题亦可写为,关于沟通的赞同和反对

17.好的问题就是一半的的答案

18.乔布斯说 stayhungry,我以为饥渴有三个层次:贪婪、成就动机、好奇心 。三者分别关注:瞬间的结果,持续的过程,和远大的未知。三者也恰好对应了三种人:卑劣的投机者,艰辛的攀登者,与幸福的探索者

19.有不少留言说不理解这段话。研究快乐的专家告诉我们:快乐有三种:pleasure(欢乐),passion(热情),higherpurpose(理想、有意义).其中欢乐是最短暂的,热情其次,而最长久的是理想

20.想学的东西很多,吾生有涯知无涯,以有涯追无涯,怠也。有两种理解,积极的理解是应该有优先级的规划学习。

21.现在年轻人部分流行把三四十岁退休作为理想,我不认同,我觉得理想是一直有机会创造、实现想法,有机会学习,修炼,创造到老。为什么会想退休?想退休 说明你认为现在是在“忍”。我还有很多很多想法想做,希望三四十岁更多条件去实现想法。

22.保证足够睡眠是积极高效的第一步

23.生活中不是缺少美,而是缺少分享

24.很多很好的想法自己都非常认真,现在都被人实现或者通往实现的路上了。真希望自己能分身体几个同时努力,这样人生多精确。但是分身是不可能的,所以只能 1、根据情况排优先级 2、找到志同道合的人

25.应届生应该推崇自信,诚实,努力,相信成功可通过学习和努力获得。别太讨巧,走捷径。事实上面试大多不是因为技能不行,而是人品和性格不行

26.通货膨胀正在洗劫你的钱包,同学问怎么办?三个办法,一是尽可能地提高家庭负债率,当今之世能借到钱的就是英雄;二是配置资源性财产,能够抵抗通涨的 只有三个东西,黄金房产和农产品;三是像傻瓜一样的长期持有,眼前的涨跌都是对耐心的考验。除非天下大乱,否则以上三条应是规律。

27.系统地运动锻炼需要抗身体的惰性,锻炼久了之后不但身体好而且锻炼的积极性 也好容易启动养成习惯,最近觉得读书学习也很类似

28.执行力到底是什么?我认为的执行力是:说到做到,不找借口,完成别人都能完成的事。而更强的人可以做到:完成别人完不成的事。同样的一件事,交给不同的员工,会有不同的结果,完不成的人都会有各种理由来说服自己说服领导,将 一个小困难由点到面扩大化看待。做一个 NB 的人,从此刻开始,不再找借口

29.平庸有重力,需要逃逸速度

30.不怕犯错误,不怕坏方法,甚至不怕坏习惯。只要你会会自我改正。你习惯改正吗?

31.快到 30 岁了,感觉这几年又再重新学习/补习本应在青少年时间学习的东西:如何阅读、如何了解自己、如何与人沟通沟通、如何安排时间、如何正确的看待别人意见、如何激励自己、如何写作、如何坚持锻炼身体、如何耐心

32.互联网让会学习爱学习的人和相反的差距拉的更大,这并不仅限于互联网行业。只不过互联网行业这种趋势先开始而已。现在好多初中生、高中生比大学生、博士生还博学。我见过 2 个中学生,自己用 wiki 整理所学过的,自学的各种知识。我怎么生的这么早

33.延迟满足感 和 坚决告别惰性 是“优秀”的最重要两块基石。

34.当某人开始深入认识自己、研究自己的时候,说明此人开始有了哲学的思考,预示着此人开始迈入一个新的人生阶段。

35.非常同意自控力(也就是反惰性)是优秀的标准。确实马拉松不是高标准,思维意识情绪的自控更难。

36.牛逼的人找方法,傻逼的人找借口

37.看年轻人的潜力,看他周末几点起,周末在干嘛,下班在干嘛。甚至不一定要干嘛,只要看想些什么

38.今天早点睡,明天早上起来看书。本周的学习计划快完不成了。创业过程中不断的学习又能尝试是感觉很好的体验

39.人欲望太强的时候就容易短视,太自我中心的时候就容易盲目

40.以前一直都没觉得找人自信很重要,现在发现越来越重要。惰性、依赖、拖拉、 保守很多也都是是不自信导致的。自信的人自然会和自我高要求联系起来

41.关于勤奋,就我所知,罕有成功者不是工作时间极长的:通用电气的 CEO 每周 工作一百小时,坚持了至少十年。巴菲特为了最早看到次日的华尔街日报,经常在凌晨四点去取报纸。勤奋不是一种形式,而是一种心理状态:享受挑战极限的过程,保持热情和好奇心,坚持不懈

42.哈佛有一个著名的理论:人的差别在于业余时间,而一个人的命运决定于晚上 8 点到 10 点之间。每晚抽出 2 个小时的时间用来阅读、进修、思考或参加有意的 演讲、讨论,你会发现,你的人生正在发生改变,坚持数年之后,成功会向你招手

43.人生的本质是追寻自我的提升。包括思想、能力、意志等等。这些发展好了,一切随之而来。偏偏 大多数人追求的是短期的公司、职位、薪水,运气好的能有所发展,运气差的会迷失方向流于平庸

44.聪明还耐心是有一些矛盾的优点,同时具备两点的人却非常优秀

45.习惯:把要做的事情迅速分配在 calendar 上,会变化没关系,多调整

46.别装,做个坦诚真实的人。团队中都是坦诚真实的人,沟通成本将小很多

47.据多家公司统计,团队淘汰个人的顺序往往如下:第一批,明显缺陷者、众人厌恶的说谎者;第二批,不愿交流者、不合群者;第三批,有能力但慵懒者、妄图 坐享其成者;第四批,居功自傲者,蔑视同僚者。

48.经常看到职位蛮漂亮的人,但细看发现他每次升职都是换工作的时候发生的。这会让我警惕,因为好的人,老板会加薪升职来挽留。如果一个人在同一公司多次 升职,让我会放心很多,因为比我了解他多得多的人看好他给他更多的责任,而 且他一次次胜任。换工作才升职,有可能是外强中干,忽悠了新老板

49.一个身价两百多亿的老板不作秀、不爬山、不吹牛、不打口水仗、不接 受采访、不上电视杂志,以身作则像一个基层员工一样每天脚踏实地测试产品, 无止境地改进产品的体验。这才是腾讯成功的最大原因。而被腾讯打败的 Loser 们始终没有认识到这一点,要么骂它靠抄袭,要么说它靠 QQ 才能成功

50.其实我挺想知道团队成员周末都在干嘛。。。总希望大家把时间充分有效利用了。。当然我说的不是只工作,是指优先利用学习、休息、娱乐、锻炼、交流,思考上。并且可以一起活动。 51.一个公司最强的敌人是什么?韦尔奇说,是“坦率”。深表认同。幸好,坦率是可以培育的

52.【职场】昨晚请多玩优秀员工吃饭,聊了几点职场体会。(1) 把自己当老板看, 象老板一样拼命干活,能力自然就提高了。有了能力,假如多玩不能给你好的回 报,其他公司一定会给。(2) 不是每次付出就一定有回报,但是不断付出就一定 会有回报。@李学凌 补充了一点:象你的老板一样思考,能力会提高得更快

53.有人问我如何突破自己的职业瓶颈,我说:你的瓶颈就在于你的心。你的心更宽, 心态更好,遇到问题将自己拨高一层去看问题,把你心里的那些小纠结小疑惑小算盘小私心,统统打破,你就没有瓶颈 54.最近大家 review 了九九房半年的进展并讨论每个人的总结和设想,对我们的信 心更加增强。和同事曾讨论:创业要经常自省,避免自我强化和催眠。所以要区别信心和“YY”,真正信心源于看到自己的进步和潜力,可以分成两个方面:1、 对事情本身判断的信心 2、对自己和团队的信心

55.这周面了十几个人终于确定一个实习生。最近一个多月可能面试了 50 多人,总共只有 2 个非常有意向的人选,其中失败一个,一个还在谈。每当想放低要求的 时候,我就提醒自己一定不能往低走而要往高走,我们要做的出彩,而不是完成 的事情。而尤其在早期,核心几个人的能力素质态度是最关键的

56.创业就要像生小孩一样:准备好体力,用长劲,快速换气;喊疼和抱怨没用,专注在努力,关键时候坚持再坚持一把!

57.如果你很有才华,在某些方面又有一技之长,请先不要急于露出锋芒,如果你只是以普通身份而不是以领导身份到新单位去的,那就更不能锋芒太露。一个人新到一个单位,就像一粒石子投入一潭平静的池水,往往会引入注目,一举一动, 一言一行,都在别人的视野之中

芝麻开门,显示全文!

用 Node 搭建最小实现脚手架

前言

本文介绍使用 Node 做一个脚手架,便于快速开发项目。我们开发的是脚手架,而非项目,目前本人只有一个脚手架 Koa 脚手架 ,后续写到 React、webpack 时,会搭建属于自己的一套 H5 端的开发模板。本文以实现最小脚手架为出发点展开写作,后续也会在此基础上添砖加瓦

引子

A:大 B 哥,Node 能做什么?

B:搭建 Web 服务噜

A:不仅如此,它还能操作系统

B:怎么说?

A:知道 Webpack 吗?它就是用 Node 写的。还有像 create-react-appvue-cli@tarojs/cli 这些,都是用 Node 写的,这些 cli 被称为脚手架,你只要使用一些命令就能下载模板快速开发

B:(各种羡慕、吹捧后),我也想做一套自己的脚手架

A:我教你啊

一个脚手架的思路

create-react-appvue-cli@tarojs/cli 的各自的仓库,我们能得出一些共同点,例如多套模板、友好的交互、优美的 UI 等等。我们这里以 taro 为例,先用用,看看,再仿着做一个

使用Taro-cli创建项目

它是怎么做到选择不同的模板,能生成不同的文件呢?明明只有一个基础模板啊,选择 scss 就生成 scss 文件,选择 TypeScript 生成 TS 文件,现在还看不懂源码,以后写完 webpack 再来看看,我们这里只先做一个最简单的脚手架

创建工程

mkdir azhu-cli
cd azhu-cli
npm init -y

然后在 package.json 中写点项目信息

需要安装的 npm 包

我们先列个表格,查看一下各个 npm 包是什么,有什么用,后续在写代码时一步步添加进去

包名称说明
commander执行复杂的命令
inquirer问答交互
download-git-repo下载远程模板
chalk让你 console.log 出来的字带颜色,比如成功时的绿色字
oraloading

创建一个命令

先创建 index.js,在代码中写入

#!/usr/bin/env node
console.log("hello world");

在终端中运行 node 程序,输入 node 命令

node index.js

可以正确输出 hello world ,代码顶部的 #!/usr/bin/env node 是告诉终端,这个文件要使用 node 去执行

一般 cli 都有一个特定的命令,例如 tarogit 等,我们设置我们的命令—— azhu。如何让终端识别这个命令呢?很简单,在 package.json 文件中添加一个字段 bin,并且声明一个命令关键字和对应执行的文件:

# package.json
...
"bin": {
    "azhu": "index.js"
}
...

然后我们测试一番,在终端中输入 azhu,会提示:

azhu错误

为什么会这样呢?通常我们在使用 cli 工具时,都需要先安装它,比如 vue-cli,@tarojs/cli,使用前需要全局安装:

npm i vue-cli -g
npm i @tarojs/cli -g

而我们的 azhu-cli 并没有发布到 npm 上,当然也没有安装过,所以终端现在还不认识这个命令。通常我们想本地测试一个 npm 包,可以使用 npm link 这个命令,本地安装这个包,我们执行一下:

npm link

再执行 azhu 命令,就看到 hello world

注:npm unlink 卸载本地包

执行复杂的命令

commander:处理命令行交互

  • 自带了 -V,-h 交互
  • 可以通过 program.command 添加交互
  • program.parse 将命令参数传入 commander 管道中,一般放在最后执行
npm i commander --save

改造 index.js

#!/usr/bin/env node

const program = require("commander");
const package = require("./package.json");
program.version(package.version);
program.parse(process.argv);

运行 azhu -h

commander处理

添加问答操作

inquirer 添加问答操作

npm i inquirer --save

语法很简单,直接看代码:

inquirer
  .prompt([
    { type: "input", message: "请输入项目名称", name: "name" },
    {
      type: "list",
      message: "请选择项目模板",
      name: "template",
      choices: ["koa-basic"],
    },
  ])
  .then((answers) => {
    console.log("answers", answers);
  });

每个选项中的 name 为答案输出的值

inquirer

克隆模板

download-git-repo

  • 下载远程模板
npm i download-git-repo --save

原本使用 bashjs,但是死活下载不下来,只能选择另一个工具

当我们下载写好项目名字,选择好模板后,下一步就要从远程仓库上把模板下载过来

.then((answers) => {
      console.log('正在拷贝项目,请稍等')
      const remote = 'https://github.com:johanazhu/koa-basic#master'
      const tarName = answers.name
      download(remote, tarName, { clone: true }, function (err) {
        if (err) {
          console.log(err)
        } else {
          console.log('成功')
        }
      })
    })

添加 UI 交互

有时候下载远程仓库时会花很多时间,我们必须为了体验,需要加一些 UI 效果优化体验

chalk & ora

npm i chalk ora --save

chalk 是给 console 加颜色

ora 是加 loading 效果的

...
.then((answers) => {
    console.log('正在拷贝项目,请稍等')
    const remote = 'https://github.com:johanazhu/koa-basic#master'
    const tarName = answers.name
    + const spinner = ora('download template......').start()
    download(remote, tarName, { clone: true }, function (err) {
        if (err) {
            + console.log(chalk.red(err))
            spinner.fail()
        } else {
            + console.log(chalk.green('成功'))
            spinner.succeed()
        }
    })
})

效果如下:

chalk&ora

发布 npm

先登录 npm,再发布

npm login
...
npm publish

额外知识点

包管理方式

包管理方式对比

monorepo

  • 将多个项目代码存储在一个仓库里的软件开发策略

  • 把所有的项目相关都放在一个仓库(比如 React,Babel,Umi,Taro)

  • 集中管理

  • 优势

    • 统一工作流
    • 降低基建成本
    • 提高团队协作效率
  • 劣势

    • 体积问题
    • 权限问题
    • 版本控制

multirepo

  • 按模块放在为多个仓库(webpack、rollup)
  • 优势
    • 灵活
    • 安全
  • 劣势
    • 代码复用
    • 版本管理
    • 开发调试
    • 搭建基础架构

大的项目可以使用 monorepo,独立性比较强的可以采用 multirepo

我个人更喜欢 multirepo 的哲学

有人上升到哲学层面,其实俺觉得不同的项目应采用合适自己的管理方式,像 webpack、rollup 之类,项目独立性比较强,就可以用使用 multirepo ,而像 React,Umi,Taro 之类的框架,它首先要拆分功能点,其次每个子库之间需要与主库有所依赖,如果采用 multirepo 方式,关联起来会很麻烦,采用统一管理的方式能节省很多时间

常见问题

一:使用 bashjs 常有报错,暂时解决不了,所以用 download-git-repo 这种方式

fatal: unable to access 'https://github.com/johanazhu/koa-basic/': OpenSSL SSL_read: Connection was reset, errno 10054

解决方案

打开 Git 命令页面,执行 git 命令脚本:修改设置,解除 ssl 验证

git config --global http.sslVerify "false"

注:git config —list 查看你的 config 信息

二:download-git-repo 报错误

'git clone' failed with status 128

解决方案:https://github.com/wuqiong7/Note/issues/17

我将 remote 地址改成:https://github.com:johanazhu/koa-basic#master 就好了

Github 已发布:https://github.com/johanazhu/azhu-cli

参考资料

芝麻开门,显示全文!

一步一步来:手写Koa2

之前讲过Koa2从零到脚手架,以及源码解读

这篇文章讲解如何手写一个 Koa2

Step 1:封装 HTTP 服务和创建 Koa 构造函数

之前阅读 Koa2 的源码得知, Koa 的服务应用是基于 Node 原生的 HTTP 模块,对其进行封装形成的,我们先用原生 Node 实现 HTTP 服务

const http = require("http");

const server = http.createServer((req, res) => {
  res.writeHead(200);
  res.end("hello world");
});

server.listen(3000, () => {
  console.log("监听3000端口");
});

再看看用 Koa2 实现 HTTP 服务

const Koa = require("Koa");
const app = new Koa();

app.use((ctx, next) => {
  ctx.body = "hello world";
});

app.listen(3000, () => {
  console.log("3000请求成功");
});

实现 Koa 的第一步,就是对 原生 HTTP 服务进行封装,我们按照 Koa 源码的结构,新建 lib/application.js 文件,代码如下:

const http = require("http");

class Application {
  constructor() {
    this.callbackFunc;
  }
  listen(port) {
    const server = http.createServer(this.callback());
    server.listen(port);
  }
  use(fn) {
    this.callbackFunc = fn;
  }
  callback() {
    return (req, res) => this.callbackFunc(req, res);
  }
}

module.exports = Application;

我们引入手写的 Koa,并写个 demo

const Koa = require("./lib/application");

const app = new Koa();

app.use((req, res) => {
  res.writeHead(200);
  res.end("hello world");
});

app.listen(3000, () => {
  console.log("3000请求成功");
});

启动服务后,在浏览器中输入 http://localhost:3000,内容显示”Hello,World“

接着我们有两个方向,一是简化 res.writeHead(200)、res.end('Hello world') ;二是做塞入多个中间件。要想做第一个点需要先写 context,response,request 文件。做第二点其实做到后面也需要依赖 context,所以我们先做简化原生 response、request,以及将它集成到 context(ctx)对象上

Step 2:构建 request、response、context 对象

request、response、context 对象分别对应 request.js、response.js、context.js,request.js 处理请求体,response.js 处理响应体,context 集成了 request 和 response

// request
let url = require("url");
module.exports = {
  get query() {
    return url.parse(this.req.url, true).query;
  },
};
// response
module.exporrs = {
  get body() {
    return this._body;
  },
  set body(data) {
    this._body = data;
  },
  get status() {
    return this.res.statusCode;
  },
  set status(statusCode) {
    if (typeof statusCode !== "number") {
      throw new Error("statusCode must be a number");
    }
    this.res.statusCode = statusCode;
  },
};

这里我们在 request 中只做了 query 处理,在 response 中只做了 body、status 的处理。无论是 request 还是 response,我们都使用了 ES6 的 get、set,简单来说,get/set 就是能对一个 key 进行取值和赋值

现在我们已经实现了 request、response,获取了 request、response 对象和它们的封装方法,接下来我们来写 context。我们在源码分析时曾经说过,context 继承了 request 和 response 对象的参数,既有请求体中的方法,又有响应体中的方法,例如既能 ctx.query 查询请求体中 url 上的参数,又能通过 ctx.body 返回数据。

module.exports = {
  get query() {
    return this.request.query;
  },
  get body() {
    return this.response.body;
  },
  set body(data) {
    this.response.body = data;
  },
  get status() {
    return this.response.status;
  },
  set status(statusCode) {
    this.response.status = statusCode;
  },
};

在源码中使用了 delegate,把 context 中的 context.request、context.response 上的方法代理到了 context 上,即 context.request.query === context.query; context.response.body === context.body。而 context.request,context.response 则是在 application 中挂载

总结一下:request.js 负责简化请求体的代码,response.js 负责简化响应体的代码,context.js 把请求体和响应体集成在一个对象上,并且都在 application 上生成,修改 application.js 文件,添加代码如下:

const http = require('http');
const context = require('context')
const request = require('request')
const response = require('response')
class Application {
    constructor() {
        this.callbackFunc
      	this.context = context
    	this.request = request
    	this.response = response
    }
    ...
    createConext(req, res) {
        const ctx = Object.create(this.context)
        ctx.request = Object.create(this.request)
        ctx.response = Object.create(this.response)
        ctx.req = ctx.request.req = req
        ctx.res = ctx.response.res = res
        return ctx
    }
	...
}

因为 context、request、response 在其他方法中要用到,所以我们在构造器中就把他们分别赋值为 this.context、this.request、this.response 。我们实现了上下文 ctx ,现在我们回到之前的问题,简写 res.writeHead(200)、res.end('Hello world')

我们要想把 res.writeHead(200)、res.end('Hello world') 简化为 ctx.body = 'Hello world',该怎么做呢?

res.writeHead(200)、res.end('Hello world') 是原生的, ctx.body = 'Hello world' 是 Koa 的使用方法,我们要对 ctx.body = 'Hello world' 做解析并转换为 res.writeHead(200)、res.end('Hello world') 。好在 ctx 已经通过 createContext 获取,那么再创建一个方法来封装 res.end,用 ctx.body 来表示

  responseBody(ctx) {
    let context = ctx.body
    if (typeof context === 'string') {
      ctx.res.end(context)
    } else if (typeof context === 'object') {
      ctx.res.end(JSON.stringify(context))
    }
  }

最后我们修改 callback 方法

//   callback() {
//     return (req, res) => this.callbackFunc(req, res)
//   }
callback() {
    return (req, res) => {
      // 把原生 req,res 封装为 ctx
      const ctx = this.createContext(req, res)
      // 执行 use 中的函数, ctx.body 赋值
      this.callbackFunc(ctx)
      // 封装 res.end,用 ctx.body 表示
      return this.responseBody(ctx)
    }
}

PS:具体代码:请看仓库中的 Step 2

Step 3:中间件机制和洋葱模型

我们知道, Koa2 中最重要的功能是中间件,它的表现形式是可以用多个 use,每一个 use 方法中的函数就是一个中间件,通过第二个参数 next 来表示传递给下一个中间件,例如

app.use(async (ctx, next) => {
  console.log(1);
  await next();
  console.log(6);
});

app.use(async (ctx, next) => {
  console.log(2);
  await next();
  console.log(5);
});

app.use(async (ctx, next) => {
  console.log(3);
  ctx.body = "hello world";
  console.log(4);
});
// 结果 123456

所以,我们的中间件是个数组,其次,通过 next ,执行和暂停执行。一 next ,就暂停本中间件的执行,去执行下一个中间件。

Koa 的洋葱模型在 Koa1 中是用 generator + co.js 实现的, Koa2 则使用了 async/await + Promise 去实现。这次我们也是用 async/await + Promise 来实现

在源码分析时,我们就说了 Koa2 的中间件合成是独立成一个库,即 koa-compose,它的核心代码如下:

function compose(middleware) {
  return function (context, next) {
    let index = -1;
    return dispatch(0);
    function dispatch(i) {
      if (i <= index)
        return Promise.reject(new Error("next() called multiple times"));
      index = i;
      let fn = middleware[i];
      if (i === middleware.length) fn = next;
      if (!fn) return Promise.resolve();
      try {
        return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
      } catch (err) {
        return Promise.reject(err);
      }
    }
  };
}

具体解读可以去源码分析上查看,这里我们不做探究

这里贴两种解决方案,其实都是递归它

componse() {
    return async (ctx) => {
      function createNext(middleware, oldNext) {
        return async () => {
          await middleware(ctx, oldNext)
        }
      }
      let len = this.middlewares.length
      let next = async () => {
        return Promise.resolve()
      }
      for (let i = len - 1; i >= 0; i--) {
        let currentMiddleware = this.middlewares[i]
        next = createNext(currentMiddleware, next)
      }
      await next()
    }
}

还有一种就是源码,关于 compose 函数,笔者还不能很好的写出个所以然,读者们请自行理解

Step 4:错误捕获与监听机制

中间件中的错误代码如何捕获,因为中间件返回的是 Promise 实例,所以我们只需要 catch 错误处理就好,添加 onerror 方法

onerror(err, ctx) {
    if (err.code === 'ENOENT') {
      ctx.status = 404
    } else {
      ctx.status = 500
    }
    let msg = ctx.message || 'Internal error'
    ctx.res.end(msg)
    this.emit('error', err)
}
callback() {
    return (req, res) => {
      const ctx = this.createContext(req, res)
      const respond = () => this.responseBody(ctx)
      + const onerror = (err) => this.onerror(err, ctx)
      let fn = this.componse()
      + return fn(ctx).then(respond).catch(onerror)
    }
}

我们现在只是对中间件部分做了错误捕获,但是如果其他地方写错了代码,怎么知道以及通知给开发者,Node 提供了一个原生模块——events,我们的 Application 类继承它就能获取到监听功能,这样,当服务器上有错误发生时就能全部捕获

总结

我们先读了 Koa2 的源码,知道后其数据结构及使用方式后,再渐进式手写了一个,这里特别感谢第一名小蝌蚪的 KOA2 框架原理解析和实现,他的这篇文章是我写 Koa2 文章的依据。说回 Koa2,它的功能特别简单,就是对原生 req,res 做了处理,让开发者能更容易地写代码;除此之外,引入中间件概念,这就像插件,引入即可使用,不需要时能减少代码,轻量大概就是 Koa2 的关键字吧

GitHub 地址:https://github.com/johanazhu/jo-koa2

参考资料

芝麻开门,显示全文!

从浅入深了解Koa2源码

在前文我们介绍过什么是 Koa2 的基础

简单回顾下

什么是 koa2

  1. NodeJS 的 web 开发框架
  2. Koa 可被视为 nodejs 的 HTTP 模块的抽象

源码重点

中间件机制

洋葱模型

compose

源码结构

Koa2 的源码地址:https://github.com/koajs/koa

其中 lib 为其源码

koa2源码

可以看出,只有四个文件:application.jscontext.jsrequest.jsresponse.js

application

为入口文件,它继承了 Emitter 模块,Emitter 模块是 NodeJS 原生的模块,简单来说,Emitter 模块能实现事件监听和事件触发能力

application1

删掉注释,从整理看 Application 构造函数

Application构造函数

Application 在其原型上提供了 listen、toJSON、inspect、use、callback、handleRequest、createContext、onerror 等八个方法,其中

  • listen:提供 HTTP 服务
  • use:中间件挂载
  • callback:获取 http server 所需要的 callback 函数
  • handleRequest:处理请求体
  • createContext:构造 ctx,合并 node 的 req、res,构造 Koa 的 参数——ctx
  • onerror:错误处理

其他的先不要在意,我们再来看看 构造器 constructor

Application的构造器

晕,这都啥和啥,我们启动一个最简单的服务,看看实例

const Koa = require("Koa");

const app = new Koa();

app.use((ctx) => {
  ctx.body = "hello world";
});

app.listen(3000, () => {
  console.log("3000请求成功");
});

console.dir(app);

实例

能看出来,我们的实例和构造器一一对应,

打断点看原型

断点

哦了,除去非关键字段,我们只关注重点

Koa 的构造器上的 this.middleware、 this.context、 this.request、this.response

原型上有:listen、use、callback、handleRequest、createContext、onerror

注:以下代码都是删除异常和非关键代码

先看 listen

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

可以看出 listen 就是用 http 模块封装了一个 http 服务,重点是传入的 this.callback()。好,我们现在就去看 callback 方法

callback

  callback() {
    const fn = compose(this.middleware)
    const handleRequest = (req, res) => {
      const ctx = this.createContext(req, res)
      return this.handleRequest(ctx, fn)
    }
    return handleRequest
  }

它包含了中间件的合并,上下文的处理,以及 res 的特殊处理

中间件的合并

使用了 koa-compose 来合并中间件,这也是洋葱模型的关键,koa-compose 的源码地址:https://github.com/koajs/compose。这代码已经三年没动了,稳的一逼

function compose(middleware) {
  return function (context, next) {
    let index = -1;
    return dispatch(0);
    function dispatch(i) {
      if (i <= index)
        return Promise.reject(new Error("next() called multiple times"));
      index = i;
      let fn = middleware[i];
      if (i === middleware.length) fn = next;
      if (!fn) return Promise.resolve();
      try {
        return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
      } catch (err) {
        return Promise.reject(err);
      }
    }
  };
}

一晃眼是看不明白的,我们需要先明白 middleware 是什么,即中间件数组,那它是怎么来的呢,构造器中有 this.middleware,谁使用到了—— use 方法

我们先跳出去先看 use 方法

use

use(fn) {
    this.middleware.push(fn)
    return this
}

除去异常处理,关键是这两步,this.middleware 是一个数组,第一步往 this.middleware 中 push 中间件;第二步返回 this 让其可以链式调用,当初本人被面试如何做 promise 的链式调用,懵逼脸,没想到在这里看到了

回过头来看 koa-compose 源码,设想一下这种场景

...
app.use(async (ctx, next) => {
    console.log(1);
    await next();
    console.log(6);
});
app.use(async (ctx, next) => {
    console.log(2);
    await next();
    console.log(5);
});

app.use(async (ctx, next) => {
    console.log(3);
    ctx.body = "hello world";
    console.log(4);
});
...

我们知道 它的运行是 123456

它的 this.middleware 的构成是

this.middleware = [
  async (ctx, next) => {
    console.log(1);
    await next();
    console.log(6);
  },
  async (ctx, next) => {
    console.log(2);
    await next();
    console.log(5);
  },
  async (ctx, next) => {
    console.log(3);
    ctx.body = "hello world";
    console.log(4);
  },
];

不要感到奇怪,函数也是对象之一,是对象就可以传值

const fn = compose(this.middleware)

我们将其 JavaScript 化,其他不用改,只需要把最后一个函数改成

async (ctx, next) => {
  console.log(3);
  -ctx.body = 'hello world';
  +console.log('hello world');
  console.log(4);
}

测试compose

测试compose2

逐行解析 koa-compose

这一段很重要,面试的时候常考,让你手写一个 compose ,淦它

//1. async (ctx, next) => { console.log(1); await next(); console.log(6); } 中间件
//2. const fn = compose(this.middleware) 合并中间件
//3. fn() 执行中间件

function compose(middleware) {
  return function (context, next) {
    let index = -1;
    return dispatch(0);
    function dispatch(i) {
      if (i <= index)
        return Promise.reject(new Error("next() called multiple times"));
      index = i;
      let fn = middleware[i];
      if (i === middleware.length) fn = next;
      if (!fn) return Promise.resolve();
      try {
        return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
      } catch (err) {
        return Promise.reject(err);
      }
    }
  };
}

执行 const fn = compose(this.middleware),即如下代码

const fn = function (context, next) {
    let index = -1
    return dispatch(0)
    function dispatch (i) {
      if (i <= index) return Promise.reject(new Error('next() called multiple times'))
      index = i
      let fn = middleware[i]
      if (i === middleware.length) fn = next
      if (!fn) return Promise.resolve()
      try {
        return Promise.resolve(fn(context, dispatch.bind(null, i + 1)))
      } catch (err) {
        return Promise.reject(err)
      }
    }
  }
}

执行 fn(),即如下代码:

const fn = function (context, next) {
    let index = -1
    return dispatch(0)
    function dispatch (i) {
      if (i <= index) return Promise.reject(new Error('next() called multiple times'))
      index = i	// index = 0
      let fn = middleware[i] // fn 为第一个中间件
      if (i === middleware.length) fn = next // 当弄到最后一个中间件时,最后一个中间件赋值为 fn
      if (!fn) return Promise.resolve()
      try {
        return Promise.resolve(fn(context, dispatch.bind(null, i + 1)))
          // 返回一个 Promise 实例,执行 递归执行 dispatch(1)
      } catch (err) {
        return Promise.reject(err)
      }
    }
  }
}

也就是第一个中间件,要先等第二个中间件执行完才返回,第二个要等第三个执行完才返回,直到中间件执行执行完毕

Promise.resolve 就是个 Promise 实例,之所以使用 Promise.resolve 是为了解决异步

抛去 Promise.resolve,我们先看一下递归的使用,执行以下代码

const fn = function () {
  return dispatch(0);
  function dispatch(i) {
    if (i > 3) return;
    i++;
    console.log(i);
    return dispatch(i++);
  }
};
fn(); // 1,2,3,4

回过头来再看一次 compose,代码类似于

// 假设 this.middleware = [fn1, fn2, fn3]
function fn(context, next) {
    if (i === middleware.length) fn = next // fn3 没有 next
    if (!fn) return Promise.resolve() // 因为 fn 为空,执行这一行
    function dispatch (0) {
        return Promise.resolve(fn(context, function dispatch(1) {
            return Promise.resolve(fn(context, function dispatch(2) {
                return Promise.resolve()
            }))
        }))
    }
  }
}

这种递归的方式类似执行栈,先进先出

执行栈

这里要多思考一下,递归的使用,对 Promise.resolve 不用太在意

上下文的处理

上下文的处理即调用了 createContext

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.state = {}
    return context
}

传入原生的 request 和 response,返回一个 上下文——context,代码很清晰,不解释

res 的特殊处理

callback 中是先执行 this.createContext,拿到上下文后,再去执行 handleRequest,先看代码:

handleRequest(ctx, fnMiddleware) {
    const res = ctx.res
    res.statusCode = 404
    const onerror = (err) => ctx.onerror(err)
    const handleResponse = () => respond(ctx)
    onFinished(res, onerror)
    return fnMiddleware(ctx).then(handleResponse).catch(onerror)
}

一切都清晰了

const Koa = require("Koa");
const app = new Koa();

console.log("app", app);
app.use((ctx, next) => {
  ctx.body = "hello world";
});
app.listen(3000, () => {
  console.log("3000请求成功");
});

这样一段代码,实例化后,获得了 this.middleware、this.context、this.request、this.response 四大将,你使用 app.use() 时,将其中的函数推到 this.middleware。再使用 app.listen() 时,相当于起了一个 HTTP 服务,它合并了中间件,获取了上下文,并对 res 进行了特殊处理

错误处理

onerror(err) {
    if (!(err instanceof Error))
        throw new TypeError(util.format('non-error thrown: %j', err))

    if (404 == err.status || err.expose) return
    if (this.silent) return

    const msg = err.stack || err.toString()
    console.error()
    console.error(msg.replace(/^/gm, '  '))
    console.error()
}

context.js

映入我眼帘的是两个东西

// 1.
const proto = module.exports = {
	inspect(){...},
    toJSON(){...},
    ...
}
// 2.
delegate(proto, 'response')
  .method('attachment')
  .access('status')
  ...

第一个可以理解为,const proto = { inspect() {…} …},并且 module.exports 导出这个对象

第二个可以这么看,delegate 就是代理,这是为了方便开发者而设计的

// 将内部对象 response 的属性,委托至暴露在外的 proto 上
delegate(proto, 'response')
  .method('redirect')
  .method('vary')
  .access('status')
  .access('body')
  .getter('headerSent')
  .getter('writable');
  ...

而使用 delegate(proto, 'response').access('status')...,就是在 context.js 导出的文件,把 proto.response 上的各个参数都代理到 proto 上,那 proto.response 是什么?就是 context.response,context.response 哪来的?

回顾一下, 在 createContext 中

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.response 有了,就明了了, context.response = this.response,因为 delegate,所以 context.response 上的参数代理到了 context 上了,举个例子

  • ctx.header 是 ctx.request.header 上代理的
  • ctx.body 是 ctx.response.body 上代理的

request.js 和 response.js

一个处理请求对象,一个处理返回对象,基本上是对原生 req、res 的简化处理,大量使用了 ES6 中的 get 和 post 语法

大概就是这样,了解了这么多,怎么手写一个 Koa2 呢,请看下一篇——手写 Koa2

参考资料

芝麻开门,显示全文!