从浅入深了解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

参考资料

芝麻开门,显示全文!

Koa2从零到脚手架

什么是 Koa2

由 Express 原班人马打造的新生代 Node.js Web 框架,它的代码很简单,没有像 Express 那样,提供路由、静态服务等等,它是为了解决 Node 问题(简化了 Node 中操作)并取代之,它本身是一个简单的中间件框架,需要配合各个中间件才能使用

文档

中文文档 (野生)

最简单的 Koa 服务器

const Koa = require("koa");

const app = new Koa();

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

app.listen(3000, () => {
  console.log("3000端口已启动");
});

洋葱模型

洋葱模型

这是 Koa 的洋葱模型

看看 Express 的中间件是什么样的:

Express的中间件

请求(Request)直接依次贯穿各个中间件,最后通过请求处理函数返回响应(Response)。再来看看 Koa 的中间件是什么样的:

koa的中间件

可以看出,Koa 中间件不像 Express 中间件那样在请求通过了之后就完成自己的使命;相反,中间件的执行清晰地分为两个阶段。我们看看 Koa 中间件具体是什么样的

Koa 中间件的定义

Koa 的中间件是这样一个函数:

async function middleware(ctx, next) {
  // 先做什么
  await next();
  // 后做什么
}

第一个参数是 Koa Context,也就是上图中贯穿中间件和请求处理函数的绿色箭头所传递的内容,里面封装了请求体和响应体(实际上还有其他属性),分别可以通过 ctx.requestctx.response 来获取,以下是一些常用的属性:

ctx.url; // 相当于 ctx.request.url
ctx.body; // 相当于 ctx.response.boby
ctx.status; // 相当于 ctx.response.status

更多 Context 属性请参考 Context API 文档

中间件的第二个参数便是 next 函数:用来把控制权转交给下一个中间件。但它与 Express 的 next 函数本质的区别在于, Koa 的 next 函数返回的是一个 Promise ,在这个 Promise 进入完成状态(Fulfilled)后,就会去执行中间件中第二个阶段的代码。

有哪些常见的中间件

路由中间件——koa-router 或@koa/router

下载 npm 包

npm install koa-router --save

有些教程使用 @koa/router,现如今这两个库由同一个人维护,代码也一致。即 koa-router === @koa/router(写自 2021 年 8 月 23 日)

NPM 包地址:koa-router@koa/router

如何使用

在根目录下创建 controllers 目录,用来存放控制器有关的代码。首先是 HomeController,创建 controllers/home.js,代码如下:

class HomeController {
  static home(ctx) {
    ctx.body = "hello world";
  }
  static async login(ctx) {
    ctx.body = "Login Controller";
  }
  static async register(ctx) {
    ctx.body = "Register Controller";
  }
}

module.exports = HomeController;

实现路由

再创建 routes 文件夹,用于把控制器挂载到对应的路由上面,创建 home.js

const Router = require("koa-router");
const { home, login, register } = require("../controllers/home");

const router = new Router();

router.get("/", home);
router.post("/login", login);
router.post("/register", register);

module.exports = router;

注册路由

在 routes 中创建 index.js,以后所有的路由都放入 routes,我们创建 index.js 的目的是为了让结构更加整齐,index.js 负责所有路由的注册,它的兄弟文件负责各自的路由

const fs = require("fs");
module.exports = (app) => {
  fs.readdirSync(__dirname).forEach((file) => {
    if (file === "index.js") {
      return;
    }
    const route = require(`./${file}`);
    app.use(route.routes()).use(route.allowedMethods());
  });
};

注:allowedMethods 的作用

  1. 响应 option 方法,告诉它所支持的请求方法
  2. 相应地返回 405 (不允许)和 501 (没实现)

注:可以看到 @koa/router 的使用方式基本上与 Express Router 保持一致

引入路由

最后我们需要将 router 注册为中间件,新建 index.js,编写代码如下:

const Koa = require('koa')
const routing = require('./routes')

// 初始化 Koa 应用实例
consr app = new Koa()

// 注册中间件
// 相应用户请求
routing(app)

// 运行服务器
app.listen(3000);

使用 postman 测试一下

测试路由

其他中间件

  • koa-bodyparser ——请求体解析
  • koa-static —— 提供静态资源服务
  • @koa/cors —— 跨域
  • koa-json-error —— 处理错误
  • koa-parameter —— 参数校验
cnpm i koa-bodyparser -S
cnpm i koa-static -S
cnpm i @koa/cors -S
cnpm i koa-json-error -S
cnpm i koa-parameter -S
const path = require("path");
const Koa = require("koa");
const bobyParser = require("koa-bodyparser");
const koaStatic = require("koa-static");
const cors = require("@koa/cors");
const error = require("koa-json-error");
const parameter = require("koa-parameter");
const routing = require("./routes");

const app = new Koa();

app.use(
  error({
    postFormat: (e, { stack, ...rest }) =>
      process.env.NODE_ENV === "production" ? rest : { stack, ...rest },
  })
);
app.use(bobyParser());
app.use(koaStatic(path.join(__dirname, "public")));
app.use(cors());
app.use(parameter(app));
routing(app);

app.listen(3000, () => {
  console.log("3000端口已启动");
});

实现 JWT 鉴权

JSON Web Token(JWT)是一种流行的 RESTful API 鉴权方案

先安装相关的 npm 包

cnpm install koa-jwt jsonwebtoken -S

创建 config/index.js ,用来存放 JWT Secret 常量,代码如下:

const JWT_SECRET = "secret";

module.exports = {
  JWT_SECRET,
};

有些路由我们希望只有已登录的用户才有权查看(受保护路由),而另一些路由则是所有请求都可以访问(不受保护的路由)。在 Koa 的洋葱模型中,我们可以这样实现:

加入JWT后的洋葱模型

可以看出,所有的请求都可以直接访问未受保护的路由,但是受保护的路由都放在 JWT 中间件的后面,我们需要再创建几个文件来做 JWT 的实验

我们知道,所谓的用户(users)是个最常见的需要鉴权的路由,所以我们现在 controllers 中创建 user.js ,写下如下代码:

class UserController {
  static async create(ctx) {
    ctx.status = 200;
    ctx.body = "create";
  }
  static async find(ctx) {
    ctx.status = 200;
    ctx.body = "find";
  }
  static async findById(ctx) {
    ctx.status = 200;
    ctx.body = "findById";
  }
  static async update(ctx) {
    ctx.status = 200;
    ctx.body = "update";
  }
  static async delete(ctx) {
    ctx.status = 200;
    ctx.body = "delete";
  }
}

module.exports = UserController;

注册 JWT 中间件

用户的增删改查都安排上了,语义很明显了,其次我们在 routes 文件中创建 user.js,这里展示与 users 路由相关的代码:

const Router = require("koa-router");
const jwt = require("koa-jwt");
const {
  create,
  find,
  findById,
  update,
  delete: del,
} = require("../controllers/user");

const router = new Router({ prefix: "/users" });
const { JWT_SECRET } = require("../config/");

const auth = jwt({ JWT_SECRET });

router.post("/", create);
router.get("/", find);
router.get("/:id", findById);
router.put("/:id", auth, update);
router.delete("/:id", auth, del);

module.exports = router;

综上代码,routes 文件下的 home.js 都不需要 JWT 中间件的保护,user.js 中的 更新和删除需要 JWT 的保护

测试一下,能看出 JWT 已经起作用了

测试JWT

我们到目前为止,完成了对 JWT 的验证,但是验证的前提是先签发 JWT,怎么签发,你登录的时候我给你一个签好名的 token,要更新/删除时在请求头中带上 token,我就能校验…

这里牵扯到登录,我们先暂停一下,先补充数据库的知识,让项目更加完整

Mongoose 加入战场

如果要做一个完整的项目,数据库是必不可少的,与 Node 匹配的较好的是 NoSql 数据库,其中以 Mongodb 为代表,当然如果我们要使用这一数据库,需要按照相应的库,而这个库就是 mongoose

下载 mongoose

cnpm i mongoose -S

连接及配置

config/index.js 中添加 connectionStr 变量,代表 mongoose 连接的数据库地址

const JWT_SECRET = "secret";
const connectionStr = "mongodb://127.0.0.1:27017/basic";

module.exports = {
  JWT_SECRET,
  connectionStr,
};

创建 db/index.js

const mongoose = require("mongoose");
const { connectionStr } = require("../config/");

module.exports = {
  connect: () => {
    mongoose.connect(connectionStr, {
      useNewUrlParser: true,
      useUnifiedTopology: true,
    });

    mongoose.connection.on("error", (err) => {
      console.log(err);
    });

    mongoose.connection.on("open", () => {
      console.log("Mongoose连接成功");
    });
  },
};

进入主文件 index.js,修改配置并启动

...
const db = require('./db/')
...

db.connect()

启动服务 npm run serve,即 nodemon index.js,能看出 mongoose 已经连接成功了

nodemon

创建数据模型定义

在根目录下创建 models 目录,用来存放数据模型定义文件,在其中创建 User.js,代表用户模型,代码如下:

const mongoose = require("mongoose");

const schema = new mongoose.Schema({
  username: { type: String },
  password: { type: String },
});

module.exports = mongoose.model("User", schema);

具体可以看看 Mongoose 这篇文章,这里我们就看行为,以上代码表示建立了一个数据对象,供操作器来操作数据库

在 Controller 中操作数据库

然后就可以在 Controller 中进行数据的增删改查操作。首先我们打开 constrollers/user.js

const User = require("../models/User");

class UserController {
  static async create(ctx) {
    const { username, password } = ctx.request.body;
    const model = await User.create({ username, password });
    ctx.status = 200;
    ctx.body = model;
  }
  static async find(ctx) {
    const model = await User.find();
    ctx.status = 200;
    ctx.body = model;
  }
  static async findById(ctx) {
    const model = await User.findById(ctx.params.id);
    ctx.status = 200;
    ctx.body = model;
  }
  static async update(ctx) {
    const model = await User.findByIdAndUpdate(ctx.params.id, ctx.request.body);
    ctx.status = 200;
    ctx.body = model;
  }
  static async delete(ctx) {
    await User.findByIdAndDelete(ctx.params.id);
    ctx.status = 204;
  }
}

module.exports = UserController;

以上代码中,

  • User.create({xxx}):在 User 表中创建一个数据
  • User.find():查看所有的 User 表中的数据
  • User.findById(id):查看 User 表中的其中一个
  • User.findByIdAndUpdate(id, body):更新 User 表中的其中一个数据
  • User.findByIdAndDelete(id):删除 User 表中的其中一个数据

以上就是对数据库的增删改查

加盐

这个我们需要对密码进行一下加密,无它,安全。

进数据库一查,就能看到密码,这说明数据对开发人员是公开的,开发人员可以拿着用户的账号密码做任何事,这是不被允许的

数据库中的用户表

下载 npm 包——bcrypt

cnpm i bcrypt --save

我们前往 models/User.js 中,对其进行改造

...
const schema = new mongoose.Schema({
  username: { type: String },
  password: {
    type: String,
    select: false,
    set(val) {
      return require('bcrypt').hashSync(val, 10)
    },
  },
})
...

添加 select:false 不可见,set(val) 对值进行加密,我们来测试一下

创建李四

能看到 password 被加密了,即使在数据库里,也看不出用户的密码,那用户输入的密码难道输入这么一串密码,显然不是,用户要是输入的话,我们要对其进行验证,例如我们的登录

我们进入 constrollers/home 文件中,对其进行改造,

...
class HomeController {
  static async login(ctx) {
    const { username, password } = ctx.request.body
    const user = await User.findOne({ username }).select('+password')
    const isValid = require('bcrypt').compareSync(password, user.password)
    ctx.status = 200
    ctx.body = isValid
  }
  ...
}
  • User.findOne({ username }) 能查到到没有 password 的数据,因为我们人为的把 select 设为 false,如果要看,加上 select(‘+password’) 即可
  • require(‘bcrypt’).compareSync(password, user.password) 将用户输入的明文密码和数据库中的加密密码进行验证,为 true 是正确,false 为密码不正确

回到 JWT

在 Login 中签发 JWT Token

我们需要提供一个 API 端口让用户可以获取到 JWT Token,最合适的当然是登录接口 /login ,打开 controllers/home.js,在 login 控制器中实现签发 JWT Token 的逻辑,代码如下:

const jwt = require('jsonwebtoken')
const User = require('../models/User')

const { JWT_SECRET } = require('../config/')

class HomeController {
  static async login(ctx) {
    const { username, password } = ctx.request.body

    // 1.根据用户名找用户
    const user = await User.findOne({ username }).select('+password')
    if (!user) {
      ctx.status = 422
      ctx.body = { message: '用户名不存在' }
    }
    // 2.校验密码
    const isValid = require('bcrypt').compareSync(password, user.password)
    if (isValid) {
      const token = jwt.sign({ id: user._id }, JWT_SECRET)
      ctx.status = 200
      ctx.body = token
    } else {
      ctx.status = 401
      ctx.body = { message: '密码错误' }
    }
  }
  ...
}

login 中,我们首先根据用户名(请求体中的 name 字段)查询对应的用户,如果该用户不存在,则直接返回 401;存在的话再通过 (bcrypt').compareSync 来验证请求体中的明文密码 password 是否和数据库中存储的加密密码是否一致,如果一致则通过 jwt.sign 签发 Token,如果不一致则还是返回 401。

在 User 控制器中添加访问控制

Token 的中间件和签发都搞定之后,最后一步就是在合适的地方校验用户的 Token,确认其是否有足够的权限。最典型的场景便是,在更新或删除用户时,我们要确保是用户本人在操作。打开 controllers/user.js

const User = require('../models/User')

class UserController {
  ...
  static async update(ctx) {
    const userId = ctx.params.id
    if (userId !== ctx.state.user.id) {
      ctx.status = 403
      ctx.body = {
        message: '无权进行此操作',
      }
      return
    }
    const model = await User.findByIdAndUpdate(ctx.params.id, ctx.request.body)
    ctx.status = 200
    ctx.body = model
  }
  static async delete(ctx) {
    const userId = ctx.params.id

    if (userId !== ctx.state.user.id) {
      ctx.status = 403
      ctx.body = { message: '无权进行此操作' }
      return
    }

    await User.findByIdAndDelete(ctx.params.id)
    ctx.status = 204
  }
}

module.exports = UserController

添加了一些用户并登录,将 Token 添加到请求头中,使用 DELETE 删除用户,能看到 状态码变成 204,删除成功

删除用户操作

断言处理

在做登录时、更新用户信息、删除用户时,我们需要 if else 来判断,这看起来很蠢,如果我们能用断言来处理,代码在看上去会优雅很多,这个时候 http-assert 就出来了

// constrollers/home.js
...
const assert = require('http-assert')


class HomeController {
  static async login(ctx) {
    const { username, password } = ctx.request.body
    // 1.根据用户名找用户
    const user = await User.findOne({ username }).select('+password')
    // if (!user) {
    //   ctx.status = 401
    //   ctx.body = { message: '用户名不存在' }
    // }
    assert(user, 422, '用户不存在')
    // 2.校验密码
    const isValid = require('bcrypt').compareSync(password, user.password)
    assert(isValid, 422, '密码错误')
    const token = jwt.sign({ id: user._id }, JWT_SECRET)
    ctx.body = { token }
  }
   ...
}

同理,处理 controllers/user

...
  static async update(ctx) {
    const userId = ctx.params.id
    assert(userId === ctx.state.user.id, 403, '无权进行此操作')
    const model = await User.findByIdAndUpdate(ctx.params.id, ctx.request.body)
    ctx.status = 200
    ctx.body = model
  }
  static async delete(ctx) {
    const userId = ctx.params.id
    assert(userId === ctx.state.user.id, 403, '无权进行此操作')
    await User.findByIdAndDelete(ctx.params.id)
    ctx.status = 204
  }
...

代码看起来就是整洁清爽

参数校验

之前我们加了一个中间件——koa-parameter,我们当初只是注册了这个中间件,但是未使用,我们在创建用户时需要判断用户名和密码的数据类型为 String 类型且必填,进入 controllers/user.js 添加代码如下:

...
class UserController {
  static async createUser(ctx) {
    ctx.verifyParams({
      username: { type: 'string', required: true },
      password: { type: 'string', required: true },
    })
    const { username, password } = ctx.request.body
    const model = await User.create({ username, password })
    ctx.status = 200
    ctx.body = model
  }
  ...
}

Github 地址:koa-basic

参考资料

一杯茶的时间,上手 Koa2 + MySQL 开发

芝麻开门,显示全文!

GitHub Actions + Github Page

主要用到了 Github Actions 中的别人写的好的动作

#  https://github.com/crazy-max/ghaction-github-pages
	- name: Deploy to GitHub Pages
      uses: JamesIves/github-pages-deploy-action@3.7.1
      with:
          ACCESS_TOKEN: ${{ secrets.ACCESS_TOKEN }}
          # 部署到 gh-pages 分支
          BRANCH: gh-pages
          # 部署目录当前目录
          FOLDER: .

共两步,先配置 main.yml,再配置 ACCESS_TOKEN,就可以 先配置 ACCESS_TOKEN

使用场景

如果我要做 css 效果、做一个 Demo 供别人看,最好是放在网站上,其中 GitHub Page 是最好的选择 做好 Github Actions,就能做到即写即看,而且能一直保留到 GitHub 关门为止

注:其实还可以用 codepen 之类在线编码

在 gh-pages 上加上 CNAME,会将 CNAME 中的 gh-pages 指向 CNAME 中的域名,当然前提是你需要在域名解析那里把你的域名指向 github.com

所以只能放在另一个自己的二级域名上, https://github.azhubaby.com

随便一说,我这域名买了 10 年,2030 年才到期,所以很长一段时间内是有效的

成功案例

我的 demo 集合: https://demo.azhubaby.com

项目 Github 地址: github-actions-github-pages

芝麻开门,显示全文!

GitHub Actions 部署前端项目

我要部署一个项目,我大喊!!

怎么做?

先把代码 npm run build,把生成的 dist 拿来,登录服务器,到指定目录页面,打开 ftp,手动上传 dist 中的文件

其中你要开启你的 nginx 服务

你每一次想部署项目,就要先在本地打包,然后登录服务器,再打开 ftp 服务,再手动上传,这一来一回太麻烦,而且这种情况不利于多人开发,依赖的环境也许受操作系统的影响等等

总之,这不便于开发者以及不科学

我们需要持续集成

在公司中我们可以搭建 GitLab 亦或是 用 Jenkins,但是对个人,独立开发者或者只有五人以下的小公司来说,有什么可以帮我们实现自动化工作流的呢?

Github 推出了 Github Actions

GitHub Actions 是什么

GitHub Actions 是 GitHub 的持续集成服务,于 2018 年 10 月推出

持续集成由很多操作组成,比如抓取代码、运行测试、登录远程服务器,发布到第三方服务等等。GitHub 把这些操作就称为 actions

GitHub 做了一个官方市场,可以搜索到他人提交的 actions。另外,还有一个 awesome actions 的仓库,也可以找到不少 action。

基本概念

GitHub Actions 有一些自己的术语。

(1)workflow (工作流程):持续集成一次运行的过程,就是一个 workflow。

(2)job (任务):一个 workflow 由一个或多个 jobs 构成,含义是一次持续集成的运行,可以完成多个任务。

(3)step(步骤):每个 job 由多个 step 构成,一步步完成。

(4)action (动作):每个 step 可以依次执行一个或多个命令(action)。

具体可以前往 阮一峰的这篇 GitHub Actions 入门教程 了解一二

实操

我们的目的,通过 Github Action 把一个单页面应用部署到 云服务器上

具体可参考 使用 Github Action 部署项目到云服务器 中的操作,我讲讲在使用这位大哥写的 SFTP-Deploy-Action@v1.0 action 时遇到的坑

常见问题:

首先遇到 Load key "../private_key.pem": invalid format 的错误。

解决方案 ssh-keygen -m PEM 生成 新 key

我遇到类似的问题,以为要更新我的私钥,后来发现不是,是因为我的私钥是 openssh,改成 rsa 就没报这个错误了

错误二:Permission denied (publickey,gssapi-keyex,gssapi-with-mic,password).

免登录问题

具体步骤较多,简单来说,就是 .ssh 文件下新建一个 authorized_keys 文件,将公钥放到其中

并去 /etc/ssh/sshd_config 中修改一些配置,实现免登录

这里不细讲,具体可以 Google 查一下

错误三:

Warning: Permanently added '_\*\*' (ECDSA) to the list of known hosts.
sftp> put -r ./build/_ /home/johan/www/react-app
Multiple paths match, but destination "/home/johan/www/react-app" is not a directory

文件路径不对

错误四:

Couldn't canonicalize: No such file or directory
Unable to canonicalize path "/home/johan/www/react-app/static"

找不到 static 文件,新生成的 static 文件不能放入项目中,那就新建一个

去项目目录下 mkdir static

再编译,成功

编译成功

附上 Github 地址:React-Deploy

参考资料

芝麻开门,显示全文!

linux安装node

在 Linux 上安装 node ,方便以后部署 node 服务

一、下载并解压 node

# cd /usr/local/src
# wget https://nodejs.org/dist/v14.16.1/node-v14.16.1-linux-x64.tar.xz
# tar node-v14.16.1-linux-x64.tar.xz
# cd node-v14.16.1-linux-x64/
# ./bin/node -v
v14.16.1

下载地址:https://nodejs.org/en/download/

二、对 node 、npm 设置软连接

# ln -s /usr/local/src/node-v14.16.1-linux-x64/bin/node /usr/bin/node
# node -v
v14.16.1

# ln -s /usr/local/src/node-v14.16.1-linux-x64/bin/npm /usr/bin/npm
# npm -v
6.14.12

三、下载 cnpm、pm2 并设置软连接

# npm install -g cnpm --registry=https://registry.npm.taobao.org
# npm install -g pm2 --registry=https://registry.npm.taobao.org
# ln -s /usr/local/src/node-v14.16.1-linux-x64/bin/cnpm /usr/bin/cnpm
# ln -s /usr/local/src/node-v14.16.1-linux-x64/bin/pm2 /usr/bin/pm2

注意:如果你的软连接设置错了,即报”ln: failed to create symbolic link ‘/usr/local/bin/npm’: File exists“

可以使用 ln -sf 来重新设置

# ln -sf /usr/local/src/node-v14.16.1-linux-x64/bin/npm /usr/bin/npm

芝麻开门,显示全文!

阿里、得物、涂鸦、途虎面试心得(二)

之前在 2021-08-17-阿里、得物、涂鸦、途虎面试心得 里说了面试的考点,这篇主要是讲面试官,个人认为一个好的面试官应该是按照简历展开面试,针对简历上的技能点来展开,这样能从面试者最熟悉的点切入,由浅入深全方位的考量一个面试者

得物一面男

看打扮就明白不是组长级别的,后得知是同事面,问了比较详细,从简历上的种种到部署,可以看出是个面试生手,因为一个熟手不会像他这样一直问一些技术点,生怕我不会一样

得物二面男

是个 30 多岁的说话比较快、发音不标准的男人。和他对话后,我不太喜欢在他底下做事。他会问一些开放型的题目,比如他们要用微前端(我只知道这个技术,但没用过),问我为什么要用微前端,我针对微前端展开,说了几点。他不满意,说微前端最好的好处是方便运营人员,避免他们在两个系统里跳来跳去。惊了,原来是让我跳出程序员的思维,早说,后面就引着这个思路乱说,到现在一个问题印象也没有,因为大多数在临时起意胡说的

涂鸦电话面(女)

我当时有点傻,去了涂鸦上海本地,明明是电话面试,竟然没注意到,去了之后没前台,联系 HR,说是约电话面,ok,我蠢。到楼下看有没有充电宝,电不到 20%,找不到出门找全家,没想到电话来了,让我自我介绍,左右为难下回到大楼坐在沙发上打电话

与她交流,能看出她的实力,她是个很会抓知识点的面试官

开始讲针对项目中登录的问题,简单交流,后续只见她问

她问:“你们怎么做到锁单,就像用户进去下单页后,点击下单后锁单,你们怎么做?“

我说:后端会把这个用户 id 和商品 id 关联锁住,(我知道她想考前端要做什么事情)前端方面会在请求点击后就把按钮置灰并让他不能点击,而且还可以通过防抖、节流优化

她问:“那这个场景是用防抖还是节流?”

我想了想说:“节流”

她说:“那你能手写一个节流吗?“

我无语了,我 TM 都在准备手写节流,让我口喷,怎么喷,吞吞吐吐下说的很差,但说到几个关键字

function debounce(func, wait, flag) {
  let timer = null;
  if (timer && flag) {
    return function (...args) {
      timer = setTimeout(() => {
        func.apply(args);
      }, wait);
    };
  }
  return function (...args) {
    timer = setTimeout(() => {
      func.apply(args);
    }, wait);
  };
}

PS:差不多就是上面的回答,错的,错的,错的

她说:ok,你回答里说到三个点,你能说说三个点是什么吗?

我说:展开运算符,他的用法是能让数组的值展开

她问:除了数字,还能展开哪些数据类型

我开始不知道了,没想到 ES6 还有这么多讲究,我胡扯到 String 类型,Object 类型

她说:String ?你确定吗?

我怂了,这个知识点我不知道

她问你刚才说的 setTimeout 中用箭头函数,你能说说箭头函数与普通函数的区别吗?

这个题在得物二面的时候就问到了,我回答

阿里面试官

与他聊天更像是在与监考老师打交道

阿里:我先问你几个 CSS 方面的问题

我:好…

阿里:我再问你几个 React 方面的问题

我:巴拉巴拉

阿里:最后问你一个 JS 方面的问题

我:

途虎面试官

react 方面

this.setState

异步还是同步?

出题 class C

其中有 promise

链式调用

出题 链式调用

简单化的 链式调用

了解 Fiber 吗

说完 fiber 后,是多了调度器

调度的顺序是如何?

谁的优先级在前,谁的优先级在后

最后一个问题

性能优化怎么做

我说从工程方面讲,webpack 的打包

从 React 方面讲,有 React.memo 、React.useCallback 等代码优化的点

从 网络方面考虑,做 缓存 机制

这里说了下强缓存、协商缓存

又问我图片懒加载怎么做

不知道?

引导

通过图片本身距离顶部的距离,通过监听 scroll …

芝麻开门,显示全文!

阿里、得物、涂鸦、途虎面试心得

五月份搬到安静、相对较大的房间后,决定每天早起来复习,在这之前我是通过下班后待公司里学习和晚上在家复习的方式学习,这种方式不合理,因为经过一天的工作自身精力已经消耗的七七八八了,再去学习,效率已经不高了

经过三个月的努力,于 8 月 8 日奥运结束后,我投了简历,这次共投了 6 家公司,分别为阿里、得物、涂鸦、途虎、拼多多、TapTap。其中 TapTap 是想去的,其他五家各有千秋,属于可以拿到 offer 等待公司

其中阿里、得物、涂鸦、途虎四家给了面试机会。周五下午得物面试,面完后电话面试涂鸦,周六阿里电话面试,周一晚上途虎视频面试,结果是都被刷下去了。问过得物的两个面试官”觉得自己今天的表现如何“,一面面试官(同事面)说觉得一般,简历写的不好,应该把自己项目中有用到的技术都写进技能点里,而不是让面试官去找;二面(组长面)说优点是涉猎面广,但是对原理不够深入

虽然没有被面上,但或许是一个好事,因为这次面试透露出我的一些短板。例如对知识的表达,说到底还是对这块知识不了解

面试考点

JS

  • 防抖与节流

    口喷不出来,之前做过准备,手写可以,但是没喷出来,还是对原理什么的理解的不够深刻

  • 捕获错误怎么做?

    知道 try catch,但是没说来,面试官引导到后面才明白要我说这个

  • 图片懒加载

    没做准备,PASS

  • 类数组与数组的区别

    说不出个所以来

  • 如何释放闭包

    把引用的闭包设为 null

  • 事件循环

    这题高频

  • HTTP 缓存

    就途虎的面试官问了,之前做过这方面的准备

CSS

  • BFC 是什么?

    阿里面试官问的,老掉牙的问题,但是他一直让我说,我说了几个点还不够,要说全,触发 BFC 的元素有哪些,我当时说了四五个,他说还有呢?淦

  • flex: 1 表示什么

    MD,flex-grow 读错了,英语口语还是重要

  • 左边宽度已知、右边自适应方案

    阿里面试官问的,说了四种,问还有吗

React

  • 函数式组件与 class 组件的区别

    这竟然是最常见的问题,说了好几个点,没抓到重要,重点是 函数式组件能捕获每次渲染时的值,这个俗称 Capture Value。React 的主要开发者 Dan 写过一篇文章函数式组件与类组件有何不同?

    周五面试得物、涂鸦,都问到了这个问题

  • 为什么不能在循环中调用 hooks?react 中为什么不能在 for 循环、if 语句里使用 hooks,说下 react hooks 实现原理。

  • 虚拟列表,1000 条数据 插入不卡的那种

  • Fiber 是什么?

    • 多了调度器,有优先级,优先级的顺序是怎么个排序?
  • React.memo、React.useCallback、React.usememo 的作用

  • this.setState 是异步还是同步?为什么?

    • 什么事件可以触发异步,什么会触发同步
  • 如下的代码,它的 a 的值是多少?又 render 了几次
class C extends Component {
   this.state = {
       a: 0;
   }
   componentDidMount() {
       this.setState({ a: 1 })
       setTimeout(() => {
           	this.setState({a: 2})
       }, 0)
       new Promise((resolve) => {
           resolve(this.setState({ a: 3}))
       }).then(() => { this.setState({a:4}) })
   }
}
  • useState 中的值是个对象,改变对象中的值,demo 会渲染吗?如果用 React.memo() 包裹住呢
function demo() {
  const [data, setData] = useState({ foo: { bar: { baz: 1 } } });
  return <div onClick={() => setData(1)}>改变</div>;
}
  • react 从本页面跳转至其他站点页是否会执行 unmount?为什么

    A:不会,但不知道为什么

  • react 中的 错误捕获

  • React.Router 的模式

    手写一个

    三种模式

  • Dva 中与 namespce 同层的参数有哪些

ES6

  • Promise 链式调用

    • 链式调用怎么写
  • 展开运算符的特性

    • 能展开 string、array、object
  • commonJS 与 ES 模块的区别

  • webpack 打包后,commonJS 中的引用会被引用吗?ES6 模块的呢?为什么

移动端

  • 适配方案

  • JsBridge

  • 移动端 ios 输入时 input 被挡住, 安卓键盘回落后留白问题

  • 弹出款滚动问题(没人问,但是我想写一篇)

组件

  • 主题色

TS

  • any 用的多吗?什么时候会用 any?什么时候不用

Webpack

  • webpack 打包后,commonJS 中的引用会被引用吗?ES6 模块的呢?为什么

  • split code 代码分割的原理是什么

CI/CD

  • 你是如何打包项目的

    A:我的情况是,项目在本地打包,然后把 dist 文件提交到 git 上,再登录服务器,手动 git pull

    我当然知道确定这种方式不科学,公司不提供服务器,没资源,组长也不愿意做变化

    我上家公司是把代码放在 gitlab 上,也方便各位 review 代码

目前看,我后续会把个人开发者如何利用 GitHub 来做 CI/CD,可以看看这篇 2021-08-19-GitHubActions-部署前端项目 以及这篇 2021-08-19-GitHubActions+GitHubPage

Docker

  • dockerfile 中的参数有哪些

    FROM、RUN、BUILD…


疑惑

很神奇,五个面试官都没有问原型、闭包、作用域、执行上下文之类的问题,也许是因为现在是高级前端开发面,所以不问这些基础的?

后记

女朋友看到我努力了三个月,每天早上 6 点起来学习,周末也会抽出时间来看一些技术方面的知识,但还是没有面上。

她对我说:你的经历让我看到不是所有的努力都会有回报,所以让我觉得努力没用

我不懂这有什么好伤感的。面试,面不上,很正常,毕竟不是小公司,对人的要求本身就很高,我已经在这次经历中知道自己的不足,这很重要,美团还没面,这也很重要,我也有时间可以复习,再进步,这就足够了…

不是努力有没有用的问题,是成长。能进大厂是结果,学到知识是过程。而且去了大厂我还是会每天早起学习,这是拉开人与人之间距离最好的方式,别人在休息的时候你再学习,你学习时间比别人多,勤能补拙。不要觉得”勤能补拙“这个词有多么不好,我觉得很好。做到勤就比很多人优秀了

但我也不是什么奋斗逼,口号喊得很响亮,但是身体却没力行,面试-不足-克服,就像打游戏一样,打不过 BOOS,先去打大头兵,涨经验,就这么简单

芝麻开门,显示全文!

一道关于解构赋值和参数默认值的编程题

前端俱乐部QQ群中,有朋友发出这样的题目,说最近面试中遇到了,如下所示

function fun( ? ) {
    return {a,b}
}
console.log(fun( )) // {a:1,b:2}
console.log(fun({a:3})) // {a:3,b:456}
console.log(fun({})) // {a:123,b:456}

问,fun 的参数应该填什么?

提示:利用结构赋值和参数默认值

经过我一番测试,其结果如下所示:

function fun({ a = 123, b = 456 } = { a: 1, b: 2 }) {
  return { a, b };
}

解题思路

先看第一个执行: console.log(fun( )) // {a:1,b:2}

fun() 不传参数,直接执行,结果 a 为 1,b 为 2。说明默认值为 a : 1,b : 2。

可以得出

function fun(a = 1, b = 2) {
  return { a, b };
}

再看第二个执行:console.log(fun({a:3})) // {a:3,b:456}

fun({a: 3}) ,参数传入一个对象,对象中 a 为 3,其结果 a 为 3,b 为 456。说明其参数默认值为一个对象,对象中的值又有默认参数 a 与 b。

结合“执行 1”,如果不传参数,默认用 a = 1, b = 2 的选项;如果传入参数;则用对象中的默认参数。即

function fun({ a = XX, b = 456 } = { a: 1, b: 2 }) {
  return { a, b };
}

最后看第三个执行:console.log(fun({})) // {a:123,b:456}

很明显,我们还不知道对象中的默认 a 代表什么。第三个执行告诉我们它为 123

所以最后我们的答案是

function fun({ a = 123, b = 456 } = { a: 1, b: 2 }) {
  return { a, b };
}

难点

在做这道题的时候,我被赋值的 =: 迷惑了。这里做笔记记录

  • : 针对对象赋值

  • = 为默认值

如图所示:

const obj = {
  a: 1,
  b: 2,
  c: 3,
  d: 4,
};
const { a, b } = obj; // 解构赋值 a,b, a为1,b为2
const { a = 11, b = 21, e = 51 } = obj; // 给解构赋值的 设置默认值,a为1,b为2,e为51,得默认值是当你对象中没有值时,赋予该变量的默认值

= 赋予变量默认值

那么 : 何处使用,当解构赋值中的值为一个对象的时,设置对象中的值就用

const obj = {
  a: {
    aa: 11,
    bb: 22,
  },
  b: 2,
  c: 3,
  d: 4,
};
const { a, b } = obj; // a={aa: 11, bb: 22} b=2
const { a = { aa: 111, bb: 222 }, b = 22, e = { aa: 111, bb: 222 } } = obj;
// a={aa: 11, bb: 22},b = 22, e={aa: 111, bb: 222}

这里我们要注意:当解构的值在对象中,即使设置默认值,还是会以值为准;如果解构的值不在对象中,则会以默认值的形式出现在结果值中。

回头看题

function fun( ? ) {
    return {a,b}
}
console.log(fun( )) // {a:1,b:2}
console.log(fun({a:3})) // {a:3,b:456}
console.log(fun({})) // {a:123,b:456}

为什么 a = 123, b = 456 要用等于号=,而不是用冒号:呢,因为它原本是”键“,只能赋予默认值而不能将键重命名

总结

解构赋值时,冒号: 是重命名,等于号= 是赋值默认值

芝麻开门,显示全文!

移动端疑难杂症(持续更新)

写在前面

本来记录在印象笔记中,但放着老是不看,为加深印象以及搜索方便,立一文统一记录移动端的兼容性问题

前人总结

司徒正美-mobileHack

总结几个移动端 H5 软键盘的大坑

input 中输入不要有空格

onChange(replace(/\s+/g, '')

<input onChange={onChange(e.target.value.replace(/\s+/g, ""))} />

input 去除 ios 端输入法首字母大写状态

<input type="text" autocapitalize="off" autocorrect="off" />

css 一行省略

普通版本

overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;

有赞版本

display: -webkit-box;
overflow: hidden;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;

CSS 多行省略

-webkit-line-clamp: 3; // 文本行数
display: -webkit-box;
-webkit-box-orient: vertical;
text-overflow: ellipsis;
word-break: break-all;
overflow: hidden;

ios 使用 border-radius 时失效

原因:ios 端,该元素使用 transform 属性会导致 border-radius 失效

解决方法:在使用动画效果(使用 transform)元素的上一级的 css 上加上以下属性:

-webkit-transform: rotate(0deg);

开发H5营销页面遇到的问题

ios端不能自动播放音乐、视频(但之前看过一个网易云音乐的 H5 能自动播放音乐)

微信里打开的H5不支持点击下载,需要长按保存

ios 端高度超过一屏,input 输入完后不回弹

这个问题老生常谈,但目前我的 iPhone X 手机 ios 15系统未遇到这种情况,而 iPhone 7 手机遇到了这个问题。猜测是老的手机用的 webview 的 bug,解决方法是在监听 input 的 onblur 事件,在失去焦点的时候,拿到它滑动后的高度,使用 window.scrollTo 回到原来的 scrollHeight

如代码:

<input
  type="text"
  onBlur={() => {
    setTimeout(() => {
      if (util.system.ios) {
        const scrollHeight =
          document.documentElement.scrollTop || document.body.scrollTop || 0;
        window.scrollTo(0, Math.max(scrollHeight - 1, 0));
      }
    }, 100);
  }}
/>

看网上答疑:在 IOS12 ,微信版本 v6.7.4 及以上,输入框获取焦点,键盘弹起,页面(webview)整体往上滚动,当键盘收起后,不会到原位,导致键盘原来所在位置是空白的

安卓手机键盘弹出,希望表单跟着向上移动

ios 手机会跟着选中目标后滚动到中间位置,但是安卓系统不会,但你点击下方的表单时,表单不会向上滚动,键盘弹出后会遮住目标表单

安卓中的可视高度=我们看到的页面高度+软键盘的高度,而 IOS 的可视高度与软键盘无关

在 componentDidMount 或者 useEffect 中监听 resize,以 函数式组件为例

const App = () => {
  const originHeight =
    document.documentElement.clientHeight || document.body.clientHeight;
  useEffect(() => {
    window.addEventListener("resize", resizeWindow);
    return () => {
      window.removeEventListener("resize", resizeWindow);
    };
  }, []);
  const resizeWindow = () => {
    if (util.system.android) {
      const resizeHeight =
        document.documentElement.clientHeight || document.body.clientHeight;
      const activeElement = document.activeElement;

      if (resizeHeight < originHeight) {
        if (activeElement && activeElement.tagName === "INPUT") {
          setTimeout(() => {
            activeElement.scrollIntoView({ block: "center" });
          }, 100);
        }
      }
    }
  };
  return <div id="app"></div>;
};

思路如下:

在组件初始化时监听 resize 事件,如果在 android 系统的话,获取文档调整后的高度以及选择的组件,通过对比原先高度,如果文档调整后的高度小于原先的高度,意味着有键盘弹出,我们就使用scrollIntoView,让目标选中组件滚动到页面中间

更多H5软键盘兼容性问题可以看看这这个帖子:可能这些是你想要的H5软键盘兼容方案

如何实现页面刷新后不定位到之前的滚动位置

history.scrollRestoration

if (history.scrollRestoration) {
  history.scrollRestoration = "manual";
}

源自:https://www.zhangxinxu.com/wordpress/2022/05/history-scrollrestoration/

检测 passive 是否支持

var passiveSupported = false;

try {
  var options = Object.defineProperty({}, "passive", {
    get: function () {
      passiveSupported = true;
    },
  });

  window.addEventListener("test", null, options);
} catch (err) {}

https://developer.mozilla.org/zh-CN/docs/Web/API/EventTarget/addEventListener

Input 几个有趣的属性

<input
  autocomplete="false"
  autoCorrect="off"
  autocapitalize="off"
  autofocus="false"
/>

autoComplete="false":自动记录输入的值

浏览器不允许为此字段自动输入或选择一个值

https://developer.mozilla.org/zh-CN/docs/Web/HTML/Attributes/autocomplete

autoCorrect="off":自动纠错

autoCapitalize="off":自动大小写

控制用户输入/编辑文本输入时文本输入是否自动大写,以及如何自动大写

https://developer.mozilla.org/zh-CN/docs/Web/HTML/Global_attributes/autocapitalize

autoFocus=false:自动对焦

自动对焦

微信开发

ios右上角消失

document.addEventListener("WeixinJSBridgeReady", function onBridgeReady() {
  WeixinJSBridge.call("showOptionMenu");
});

https://qydev.weixin.qq.com/wiki/index.php?title=%E5%BE%AE%E4%BF%A1_JS%E6%8E%A5%E5%8F%A3

微信开发

最新版的微信已经不支持通过 debugx5.qq.com 打开 vconsole 了。要调试H5可以通过chrome远程:

① 把手机和电脑用usb连起来 ② 在手机微信中访问 http://[喵喵] debugxweb.qq.com/?inspector=true ③ 在电脑浏览器中访问 chrome://inspect/#devices

隐藏微信网页右上角的按钮

document.addEventListener("WeixinJSBridgeReady", function onBridgeReady() {
  `
// 通过下面这个API隐藏右上角按钮`;
  WeixinJSBridge.call("hideOptionMenu");
});
document.addEventListener("WeixinJSBridgeReady", function onBridgeReady() {
  // 通过下面这个API显示右上角按钮`

  WeixinJSBridge.call("showOptionMenu");
});

https://www.jianshu.com/p/d7f5f5131783

input ios端高度问题

芝麻开门,显示全文!

张潇雨的人生信念笔记

1.无论如何定义[成功],能达到这个状态的人都是极少数

2.一个人是 TA 打交道最多的五个人的平均水平

3.一个人水平的下限由 TA 学习的最差的五个对象决定

4.只向最好的人学习,其他人都不知道自己在干什么,不要理会它们的[建议] 心得:听自己敬佩的人的建议,其他人不用听。父母也好,朋友也罢,不要因为这层关系加重判断,要听,就听成功的人的意见,并放大此人意见的权重

打个比方,一个男生和一个女生,男生小镇青年,家境普通,学历普通,身高长相也普通。女生城市户口,父母公务员,学历比男生高,长相尚可(从小就有人追)。这样一对比,大多数网友的第一反应是不门当户对,这就是网友的意见。多数人靠着提供者提供的信息来分析事物,如果提供信息者隐藏或者没意识到重要信息导致没说出来,那么人给出的意见就不同。 所以微博上的热搜,这类事不用太较真,真真假假,假假真真。公说公有理,婆说婆有理 遇到人生重大事情时该怎么做?这让我想起了《白鹿原》里主角白嘉轩,他每次遇到决定不了的事情时,就会去请教姐夫朱先生,朱先生是私塾老师,是小说中的灵魂人物。姐夫每每点几句,白嘉轩就知道怎么做了 当你遇到一个重要事情时,不妨问问你敬佩的人,客观描述事情,让他分析。不要感情用事,不要代入,不要带有色眼镜

5.在没达到信息手机两的门槛之前,不轻易做判断和决策。多对自己和他人说[我不知道]

6.不是生活中的每一个问题都要解决,和问题共处是人生常态。把精力用在重要的事情上

7.寻找 [非对称回报] 的机会,即那种 [失败了损失很低,单一旦成功回报巨大] 的机会

8.更好的机会是 [失败了有一点线性回报,但成功理由巨大的指数回报],当然这中机会会非常稀少;

9.人境遇的改变往往是非线性的。积累和等待的过程很难熬,这是很多人无法改变的原因之一

10.耐心是非常值得拥有的品质

11.一个人的境遇绝大部分都受运气支配,但运气是可以被影响的

12.人很难靠出卖自己的单位时间获得财富

13.财富来自于对杠杆的使用,常见的杠杆有:互联网、人力、资本、声望、时间等等

14.财富的积累来源自不可代替性。学者让自己很难被替代,就像公司要建立自己的护城河。 心得:成为解决问题的人

15.很多时候最好的竞争优势就是 [别人觉得麻烦而你不觉得]

16.凡是都反过来想:想要投资成功,先弄清什么让人投资失败;想要生活幸福,先看什么事会导致人生不幸福。

17.在混沌、开放、随机的系统内,努力 [减少错误] 要比 [追求正确] 有效得多

18.人是无法用一套思维方式正确认知世界的。广泛地吸收知识,建立多远思维模型是成功的前提

19.当两件事看起来有些矛盾的时候,几乎总有一个更高层面的东西把它们统一起来。不断寻找,你就可以发现游戏的元规则是什么

20.将大部分的时间用在掌握游戏的元规则上

21.详细大数定律。一件事哪怕只有 10% 的把握,连试 20 次之后成功了也有九城

22.大多数的所谓心情问题、状态问题、创造力问题,都是身体健康和精力的问题 心得:大多数人不会主动解决,但身体会给你反馈 我检验精力是否饱满的两个技巧, 一在地铁上坐着看视频,精力指数小于 10,无论看什么都会发困,以至于闭上眼睛就能睡着 二找个安静的地方(图书馆或者半夜)看书,精力指数小于 50 就会打哈气 一般早上的精力比较好,所以早上的效率会比下午,晚上高

23.吃得健康、持续锻炼、保证睡眠,几乎可以待人走出任何困境

24.日常中积累小的信息和正反馈,这样在大的选择面前就会更加从容淡定

25.意志力这种东西几乎不存在。与其努力提高自己的意志力,不如给自己创造更好的环境于试无需调用意志力 心得:鄙人就是,一个好的环境对自己很重要 所以才想和女友搬到一起,这样能省下时间去学习

26.多和 [真、善、美]的东西在一起。它们的珍贵程度叶恒实这个顺序

27.在解决生存问题之后,人的幸福感主要由身边关系 的质量决定

28.爱不是一种情侣之间特有的东西,而是一种生命状态,也是你选择与世界沟通的方式

29.[对自己诚实]试最被低估的品质。一个人如果嫉妒坦诚,TA 就是无坚不催的

30.学会不带评判地自我觉察,这是一切改变的起点

31.人们懂得很多道理但仍然无法改变,是因为 [大脑知道] 和 [身体知道] 完全是两件事 心得:韩寒的“听过很多道理,依然过不好这一生”也是大脑知道。如果通过一些小事让自己觉知很重要

芝麻开门,显示全文!