-
Notifications
You must be signed in to change notification settings - Fork 0
Front end Knowledge
-
混合内容是指 https 页面下有非 https 资源时,浏览器的加载策略。
-
在 Chrome 80 中,如果你的页面开启了 https,同时你在页面中请求了 http 的音频和视频资源,这些资源将将自动升级为 https ,并且默认情况下,如果它们无法通过https 加载,Chrome 将阻止它们。这样就会造成一些未支持 https 协议的资源加载失败。
-
最合理的方案:
- 全站统一https-但是有些时候内容是用户产生的,或者第三方数据;
- 同域-nginx 强制跳转https NGINX配置:server{listen xx.xx.xx.xx; server_name xx.com; rewrite ^(.*)$ https://$host$1 permanent;};
- 跨域的方案:
- 资源有http和https协议,但是url是http的, 兼容低版本浏览器;
<meta http-equiv="Content-Security-Policy" content="upgrade-insecure-requests"> - JS拦截簒改base host, 然后Nginx转发/后端代理;
-
引入MIME sniffing功能的初衷是用来弥补Web服务器响应一个图像请求时有可能返回错误的内容类型Content-Type信息这一缺陷;
-
嗅探攻击解决方案:
- 给返回内容加上对应的 contentType;
- 添加响应头X-Content-Type-Options: nosniff,让浏览器不要尝试去嗅探;
- 嗅探乱码原因:只对HTML内容解析有效,对于css内容中(外部样式表下)的双字节字符(如中文)解析并没有作用,如果浏览器请求回来的css资源的HTTP响应头里的Content-Type未指明"charset=utf-8"的话,浏览器根据自身的嗅探机制来决定采用哪一种编码解析,结果就会概率出现双字节字符乱码的情况;
- 解决方法:
- css资源请求的响应头的Content-Type增加"charset=utf-8"声明;
- 使用 @charset
- 使用 css-unicode-loader
-
登录的原理:
-
问题:通过http方式访问一个https网站时,登录有时会失效(无法登陆、跳转),Cookies中的token没有写成功
-
原因:http和https返回的tokenName相同,由于一个是insecure一个是secure的,两者不能互相覆盖,导致cookie中的sso-token写入失败,所以也就无法登陆了。
-
查看接口,发现:Response Headers中的Set-Cookie是拿到了返回值的,但是后面有个黄色三角感叹号,提示:This Set-Cookie was blocked because it was not sent over a secure connection and would have overwritten a cookie with the Secure attribute.
-
解决方案
- 使用https,Set-Cookie中会有Secure字段;
- 在chrome中打开链接: chrome://flags/#site-isolation-trial-opt-out,搜索samesite上述三个选项禁用(设为disable)后重启chrome,问题解决;
- 删掉https下的token
- 无痕模式(有时也不行,是因为无痕模式打开时也写入了cookie)
- 几种常见的策略
- Same Origin Policy
- Content Security Policy (CSP)
- Referrer-Policy
open -n /Applications/Google\ Chrome.app/ --args --disable-features=SameSiteByDefaultCookies完全关闭Chrome,打开命令行运行上述命令。
- rimraf 包的作用:以包的形式包装rm -rf命令,用来删除文件和文件夹的,不管文件夹是否为空,都可删除
const rimraf = require('rimraf');
rimraf('./test.txt', function (err) { // 删除当前目录下的 test.txt
console.log(err);
});- 作用域链:在 JavaScript 里面,函数、块、模块都可以形成作用域(一个存放变量的独立空间),他们之间可以相互嵌套,作用域之间会形成引用关系,这条链叫做作用域链。
- 查看作用域链:
const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const code = `
function func() {
const guang = 'guang';
function func2() {
const ssh = 'ssh';
{
function func3 () {
const suzhe = 'suzhe';
}
}
}
}
`;
const ast = parser.parse(code);
traverse(ast, {
FunctionDeclaration (path) {
if (path.get('id.name').node === 'func3') {
console.log(path.scope.dump());
}
}
});-
函数和块的作用域内的变量声明会在作用域 (scope) 内创建一个绑定(变量名绑定到具体的值,也就是 binding),然后其余地方可以引用 (refer) 这个 binding,这样就是静态作用域链的变量访问顺序。
-
静态:因为这样的嵌套关系是分析代码就可以得出的,不需要运行,按照这种顺序访问变量的链就是静态作用域链,这种链的好处是可以直观的知道变量之间的引用关系。静态作用域链是可以做静态分析的。
-
JavaScript 除了静态作用域链外,还有一个特点就是函数可以作为返回值!这就会引出闭包。
-
父函数作用域中有很多东西与子函数无关,所以不会因为子函数没结束就一直常驻内存。这样肯定有性能问题,所以还是要销毁。但是销毁了父作用域不能影响子函数,所以要再创建个对象,要把子函数内引用(refer)的父作用域的变量打包里来,给子函数打包带走。
-
闭包的机制:销毁父作用域后,把用到的变量包起来,打包给子函数,放到一个属性上 Scopes。
-
即使子函数没有引用父函数的变量,他也会有一个闭包--闭包最少会包含全局作用域。
-
需要打包的只是环境内没有的,也就是闭包只保存外部引用。然后是在创建函数的时候保存到函数属性上的,创建的函数返回的时候会打包给函数,但是 JS 引擎怎么知道它要用到哪些外部引用呢,需要做 AST 扫描,很多 JS 引擎会做 Lazy Parsing,这时候去 parse 函数,正好也能知道它用到了哪些外部引用,然后把这些外部用打包成 Closure 闭包,加到 scopes 中。
-
所以,闭包是返回函数的时候扫描函数内的标识符引用,把用到的本作用域的变量打成 Closure 包,放到 Scopes 里。调用该函数的时候,JS 引擎 会取出 Scopes 中的打包的 Closure + Global 链,设置成新的作用域链, 这就是函数用到的所有外部环境了,有了外部环境,自然就可以运行了。
-
闭包需要扫描函数内的标识符,做静态分析,那 eval 怎么办,他有可能内容是从网络记载的,从磁盘读取的等等,内容是动态的。
-
eval 确实没法分析外部引用,也就没法打包闭包,这种就特殊处理一下,打包整个作用域就好了。因为没法静态分析动态内容所以全部打包成闭包了,本来闭包就是为了不保存全部的作用域链的内容,结果 eval 导致全部保存了,所以尽量不要用 eval,会导致闭包保存内容过多。
-
但是 JS 引擎只处理了直接调用,也就是说直接调用 eval 才会打包整个作用域,如果不直接调用 eval,就没法分析引用,也就没法形成闭包了。
-
黑魔法,比如 利用「不直接调用 eval 不会生成闭包,会在全局上下文执行」的特性。
-
定义:闭包是在函数创建的时候,让函数打包带走的根据函数内的外部引用来过滤作用域链剩下的链。它是在函数创建的时候生成的作用域链的子集,是打包的外部环境。evel 因为没法分析内容,所以直接调用会把整个作用域打包(所以尽量不要用 eval,容易在闭包保存过多的无用变量),而不直接调用则没有闭包。
-
过滤规则:
-
- 全局作用域不会被过滤掉,一定包含。所以在何处调用函数都能访问到。
-
- 其余作用域会根据是否内部有变量被当前函数所引用而过滤掉一些。不是每个返回的子函数都会生成闭包。
-
- 被引用的作用域也会过滤掉没有被引用的 binding (变量声明)。只把用到的变量打个包。
- JavaScript 引擎会把内存分为函数调用栈、全局作用域和堆,其中堆用于放一些动态的对象,调用栈每一个栈帧放一个函数的执行上下文,里面有一个 local 变量环境用于放内部声明的一些变量,如果是对象,会在堆上分配空间,然后把引用保存在栈帧的 local 环境中。全局作用域也是一样,只不过一般用于放静态的一些东西,有时候也叫静态域。
- 每个栈帧的执行上下文包含函数执行需要访问的所有环境,包括 local 环境、作用域链、this等。
- 如果子函数返回了,也就是父函数执行完了,此时,首先父函数的栈帧会销毁,子函数这个时候其实还没有被调用,所以还是一个堆中的对象,没有对应的栈帧,这时候父函数把作用域链过滤出需要用到的,形成闭包链,设置到子函数的 Scopes 属性上。
- 父函数销毁,栈帧对应的内存马上释放,用到的堆中的对象引用会被 gc 回收,而返回的函数会把作用域链过滤出用到的引用形成闭包链放在堆中。
- 这就导致了一个隐患:本来作用域是随着函数调用的结束而销毁的,因为整个栈帧都会被马上销毁。而形成闭包以后,转移到了堆内存。
- 当运行这个子函数的时候,子函数会创建栈帧,如果这个函数一直在运行,那么它在堆内存中的闭包就一直占用着内存,就会使可用内存减少,严重到一定程度就算是「内存泄漏」了。
- 所以闭包不要乱用,少打包一点东西到堆内存。
- 事件循环既可能是浏览器的主事件循环也可能是被一个 web worker 所驱动的事件循环。
- 原因:JavaScript是单线程的语言!同一个时间只能做一件事情。
- 事件循环的具体流程如下:
- 从宏任务队列中,按照入队顺序,找到第一个执行的宏任务,放入调用栈,开始执行;
- 执行完该宏任务下所有同步任务后,即调用栈清空后,该宏任务被推出宏任务队列,然后微任务队列开始按照入队顺序,依次执行其中的微任务,直至微任务队列清空为止;
- 当微任务队列清空后,一个事件循环结束;
- 接着从宏任务队列中,找到下一个执行的宏任务,开始第二个事件循环,直至宏任务队列清空为止。
- 这里有几个重点:
- 当我们第一次执行的时候,解释器会将整体代码script放入宏任务队列中,因此事件循环是从第一个宏任务开始的;
- 如果在执行微任务的过程中,产生新的微任务添加到微任务队列中,也需要一起清空;微任务队列没清空之前,是不会执行下一个宏任务的。
- 调用栈是解释器(比如浏览器中的 JavaScript 解释器)追踪函数执行流的一种机制。当执行环境中调用了多个函数时,通过这种机制,我们能够追踪到哪个函数正在执行,执行的函数体中又调用了哪个函数。具有LIFO(后进先出,Last in First Out)的结构。调用栈内存放的是代码执行期间的所有执行上下文。
- 每调用一个函数,解释器就会把该函数添加进调用栈并开始执行。
- 正在调用栈中执行的函数还调用了其它函数,那么新函数也将会被添加进调用栈,一旦这个函数被调用,便会立即执行。
- 当前函数执行完毕后,解释器将其清出调用栈,继续执行当前执行环境下的剩余的代码。
- 当分配的调用栈空间被占满时,会引发“堆栈溢出”错误。
- 浏览器里有 JS 引擎做 JS 代码的执行,利用注入的浏览器 API 完成功能,有渲染引擎做页面渲染,两者都比较纯粹,需要一个调度的方式,就是 event loop。
- event loop 实现了 task 和 急事处理机制 microtask,而且每次 loop 结束会 check 是否要渲染,渲染之前会有 requestAnimationFrames 生命周期。
- 帧刷新不能被拖延否则会卡顿甚至掉帧,所以就需要 JS 代码里面不要做过多计算,于是有了 requestIdleCallback 的 api,希望在每次 check 完发现还有时间就执行,没时间就不执行(这个deadline的时间也作为参数让 js 代码自己判断),为了避免一直没时间,还提供了 timeout 参数强制执行。
- 防止计算时间过长导致渲染掉帧是 ui 框架一直关注的问题,就是怎么不阻塞渲染,让逻辑能够拆成帧间隔时间内能够执行完的小块。浏览器提供了 idelcallback 的 api,很多 ui 框架也通过递归改循环然后记录状态等方式实现了计算量的拆分,目的只有一个:loop 内的逻辑执行不能阻塞 check,也就是不能阻塞渲染引擎做帧刷新。所以不管是 JS 代码宏微任务、 requestAnimationCallback、requestIdleCallback 都不能计算时间太长。这个问题是前端开发的持续性阵痛。
- js 引擎包括 parser、解释器、gc 再加一个 JIT 编译器这几部分。
- parser:负责把 javascript 源码转成 AST
- interpreter:解释器, 负责转换 AST 成字节码,并解释执行
- JIT compiler:对执行时的热点函数进行编译,把字节码转成机器码,之后可以直接执行机器码
- gc(garbage collector):垃圾回收器,清理堆内存中不再使用的对象
- 如图,一般的 JS 引擎的编译流水线是 parse 源码成 AST,之后 AST 转为字节码,解释执行字节码。运行时会收集函数执行的频率,对于到达了一定阈值的热点代码,会把对应的字节码转成机器码(JIT),然后直接执行。这就是 js 代码能够生效的流程。
- 有两点关键的区别。
- 首先,每当一个任务存在,事件循环都会检查该任务是否正把控制权交给其他 JavaScript 代码。如若不然,事件循环就会运行微任务队列中的所有微任务。接下来微任务循环会在事件循环的每次迭代中被处理多次,包括处理完事件和其他回调之后。
- 其次,如果一个微任务通过调用 queueMicrotask(), 向队列中加入了更多的微任务,则那些新加入的微任务 会早于下一个任务运行 。这是因为事件循环会持续调用微任务直至队列中没有留存的,即使是在有更多微任务持续被加入的情况下。
- 任务进入任务队列,其实会利用到浏览器的其他线程。虽然说JavaScript是单线程语言,但是浏览器不是单线程的。而不同的线程就会对不同的事件进行处理,当对应事件可以执行的时候,对应线程就会将其放入任务队列。
- 浏览器线程:
- js引擎线程:用于解释执行js代码、用户输入、网络请求等;
- GUI渲染线程:绘制用户界面,与JS主线程互斥(因为js可以操作DOM,进而会影响到GUI的渲染结果);
- http异步网络请求线程:处理用户的get、post等请求,等返回结果后将回调函数推入到任务队列;
- 定时触发器线程:setInterval、setTimeout等待时间结束后,会把执行函数推入任务队列中;
- 浏览器事件处理线程:将click、mouse等UI交互事件发生后,将要执行的回调函数放入到事件队列中。
-
url后面拼接 #page=1&view=FitH,top&toolbar=0 可以把打印下载等功能隐藏
-
设置response的Header使得Chrome浏览器打开PDF而不自动下载
// 其中比较关键的是Content-Disposition是inline而不是attachment,这样提示浏览器来显示文档而不是下载
response.setContentType("application/pdf");
response.setHeader("Content-Disposition", "inline;fileName=XXXX.pdf");- 已知带有content-disposition头部的pdf文件在Chrome下是无法预览的,一种巧妙的思路是,先用fetch下载下来拿到blob对象,然后再用URL.createObjectURL生成临时URL,然后就可以预览啦,代码如下:
// 实际测试貌似依然不可,尤其是针对文件服务器还需要鉴权时
fetch('xxx.pdf').then(resp => resp.blob()).then(blob => {
var url = URL.createObjectURL(blob);
document.querySelector('object').data = url;
});const useWindowWidth = () => {
const [width, setWidth] = useState(window.innerWidth);
useEffect(() => {
const handleResize = () => setWidth(window.innerWidth);
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
};
}, []);
return width;
};
const MyApp = () => {
const width = useWindowWidth();
const [numPages, setNumPages] = useState(null);
const onDocumentLoadSuccess = ({ numPages: page }) => {
Toast.hide();
setNumPages(page);
};
const onDocumentLoadError = (error) => {
Toast.fail('加载失败,请重试', 3);
};
const onLoading = () => {
Toast.loading('努力加载中...');
};
return (
<PageWrapper>
<Header title="详情" />
<PDFWrapper>
<Document
style={{ width: '100%' }}
file={history.location?.state?.pdfurl}
onLoadSuccess={onDocumentLoadSuccess}
onLoadError={onDocumentLoadError}
loading={onLoading}
>
{Array.from(new Array(numPages), (el, index) => (
<Page key={`page_${index + 1}`} pageNumber={index + 1} width={width} />
))}
</Document>
</PDFWrapper>
</PageWrapper>
);
};