1 | function getCounter() { |
先看一段代码。
众所周知这道题的答案是
1 | 2 |
然而其实不是!!!
这道题的正确答案是
1 | 2 |
的确,在 getCounter
函数中创建了函数 counter
,而且在函数 counter
中也的的确确访问了 getCounter
中的变量 a
。根据经验,此处一定会形成闭包毋庸置疑,因为 getCounter
的词法作用域被它内部的函数 counter
的词法作用域引用了。
我们也可以根据控制台的打印结果看出来这里确实形成了闭包。
问题在于执行上下文。
从闭包的具体实现上来说,对于函数 counter
而言,闭包对象 Closure (getCounter)
存在于自身的 [[Scopes]]
属性中。也就是说,只要函数体 getCounter
在内存中持久存在,闭包就会持久存在。而如果函数体被回收,闭包对象同样会被回收。
在预解析阶段,函数声明会创建一个函数体,并在代码中持久存在。但是并非所有的函数体都能够持久存在。在上面的示例中,counter
函数是在 getCounter
函数的执行上下文中声明的,当执行上下文执行完毕,执行上下文就会被回收,那么在 getCounter
执行上下文中声明的 counter
函数也会被回收。所以显然由 counter
与 getCounter
产生的闭包也会被回收,我们每次执行 getCounter()()
,实际上创建了不同的闭包对象。
而我们平常见到的闭包版本,如下:
1 | function getCounter() { |
显然对于函数 counter
的引用会一直保存在内存中,所以我们总能访问到 counter
函数。
回到开头的问题,答案就是:闭包对象并非不能被垃圾回收机制回收,具体仍然需要视情况而定。
]]>1 | object.assign() |
不会拷贝对象的继承属性、不会拷贝对象的不可枚举属性、可以拷贝 Symbol 类型的属性
1 | let target = {}; |
还有 concat、slice、拓展运算符均可以实现浅拷贝。
手工实现浅拷贝:
1 | const shallowClone = (target) => { |
乞丐版:JSON.stringify()
存在的问题:(可以结合之前文章的手写 JSON.stringify() 看)
手写深拷贝:
首先要了解一个方法:Object. getOwnPropertyDescriptors()
,这个方法用于获得属性的特性
1 | const person = { |
还有一个是 Reflect.ownKeys(),它返回一个由目标对象自身的属性键组成的数组。返回值等同于 Object.getOwnPropertyNames(target).concat(Object.getOwnPropertySymbols(target))
1 | const isComplexDataType = obj => ((typeof obj === 'object' || typeof obj === 'function') && obj !== null); |
五层:物理、链路、网络、传输、应用
七层:物理、链路、网络、传输、会话、表示、应用
只要能唯一标识资源的就是 URI,在 URI 的基础上给出其资源的访问方式的就是 URL
HTTP 协议由三大部分组成:
header 和 body 之间会有一个空行,header 不能为空,body 可以为空
起始行包括三个字段:请求方法、URL、HTTP 版本号
特性:
缺点:
特性:
缺点:
特性:
缺点:
特性:
缺点:
因为 HTTP/2 使用了多路复用,一般来说同一域名下只需要使用一个 TCP 连接。由于多个数据流使用同一个 TCP 连接,遵守同一个流量状态控制和拥塞控制。只要一个数据流遭遇到拥塞,剩下的数据流就没法发出去,这样就导致了后面的所有数据都会被阻塞。HTTP/2 出现的这个问题是由于其使用 TCP 协议的问题,与它本身的实现其实并没有多大关系。
特性:
缺点:
React 的每一个 element 包含的内容为 type 和 props。
1 | const element = { |
要把上述内容渲染为 dom,我们需要以下代码:
1 | // 创建元素 |
这里默认 dom
代表真实的 dom 元素,而 element
代表 react 元素
现在让我们尝试创造一个自己的 createElement。我们需要做的就是把 JSX 转换为一个 object。
1 | function createElement(type, props, ...children) { |
比方说,
createElement(“div”) 结果是
1 | { |
createElement(“div”, null, a) 结果是
1 | { |
createElement(“div”, null, a, b) 结果是
1 | { |
考虑到 children 其实不一定是 object 类型,我们需要为 children 再创建一个特殊类型 TEXT_ELEMENT。
1 | function createTextElement(text) { |
同时修改 createElement 如下:
1 | function createElement(type, props, ...children) { |
为了更有逼格而且摆脱 React 的束缚,我们要起一个很装逼的名字 ———— Didact。
1 | const Didact = { |
1 | function render(element, container) { |
目前代码其实有一个很大的问题,一旦开始 render,就会不停递归直至渲染完整棵树。如果这棵树非常大的话,他就会长时间占用主线程,导致卡顿。这时候如果浏览器希望做一些更高优先级的事情,比如先去接收用户的输入,将会无法进行,直至渲染完成。
所以我们需要把渲染流程分成多个小单元,在我们渲染完成每一个小单元之后,我们可以让浏览器打断我们的渲染,只要它有别的高优先级任务需要完成。
1 | let nextUnitOfWork = null; // 下一个单元是否需要渲染 |
需要注意的是 React 现在不再使用 requestIdleCallback
了。取而代之的是 scheduler package
,但这对于我们理解原理没有太大影响。
requestIdleCallback 还会给我们一个 deadline 参数,我们可以用它来检查在浏览器需要再次获得主线程的控制权之前,我们还能剩下多少时间。
为了组织 unit 的结构我们需要一个数据结构叫做 fiber tree。
每一个元素会拥有一个 fiber,而每一个 fiber 会成为 work 的一个 unit。
比如说我们要渲染一棵如下的树:
1 | Didact.render( |
在渲染的时候我们会创建 root fiber,并把它设置为 nextUnitOfWork。剩下我们需要在 performUnitOfWork 函数中对 fiber 做以下三件事:
使用 fiber 数据结构的目的就是为了更简单地找到下一个 unit。所以每一个 fiber 和它的第一个孩子、以及紧邻它的兄弟之间都会直接相连。如下所示:
当我们完成了 fiber 上的渲染任务,如果这个 fiber 有 child,这个 child 就会成为下一个 unit。比如说对于上面的例子,div 渲染完成后就轮到 h1 了。
如果当前 fiber 没有 child,就会把紧贴的兄弟节点作为下一个 unit。比如上面例子中的 p,它没有 child 了,所以会把兄弟 a 作为下一个 unit。
如果当前 fiber 没有 child,也没有 sibling,我们就去找它的 uncle,也就是 parent 的兄弟。比如说 a,既没有 child 也没有下一个兄弟了,只能回去找它爹 h1 的兄弟 h2。
同理,如果它爹也没有兄弟,就继续找它爹的爹,如此遍历直到我们到达 root。当到达 root 也就意味着完成了 render。
现在我们将其编写成代码:
1 | function render(element, container) { |
现在我们又遇到了一个新问题,现在我们每个 unit 渲染时都会把一个 dom 挂载到树上,而浏览器可以随时打断我们的渲染。这也就意味着,如果只有部分 unit 完成了渲染,用户将看到不完整的 UI。这不是我们所想要的。
所以我们需要把挂载 dom 的部分从原来的 render 代码中删除。取而代之的持续追踪 fiber 的根,我们将其命名为 wipRoot。
1 | function render(element, container) { |
当我们完成渲染之后,也就是没有 next unit 的时候,我们直接把整棵树挂载到 dom 上。
这一阶段我们叫做 commitRoot。
1 | function commitRoot() { |
目前为止我们的所有操作都是针对于添加节点到 dom 中,那么如果我们要删除或更新节点呢?
这时候我们就需要比对 fiber 中元素和当前元素的情况。
所以我们需要一个变量来存储最新 commit 的 fiber,我们将其称为 currentRoot。
我们还要给每个 fiber 提供一个候选项 alternate,这个是一个直达旧的 fiber 的 link。
我们设置一个函数 reconcileChildren,用来调和旧的 fiber 和新的 react elements。
1 | function reconcileChildren(wipFiber, elements) { |
迭代整个 react elements 的同时,我们也要迭代旧的 fiber 节点,即 wipFiber.alternate。
现在我们要比较 oldFiber 和 element 之间的差异。
比较的步骤如下:
React 会通过属性 key 来优化调和步骤,key 可以用来检查 elements 数组中的子组件是否仅仅只是更换了位置。
因此我们需要一个数组来保存要移除的 dom 节点。
修改 render 函数如下:
1 | function render(element, container) { |
修改 commitWork 函数如下:
1 | function commitWork(fiber) { |
下面我们实现 updateDom 函数。
1 | const isProperty = key => key !== "children"; |
有一种比较特殊的属性值是事件监听,这里假设以 on 开头的就是事件监听。
1 | const isEvent = key => key.startsWith("on"); |
对于事件监听我们需要做以下处理:
1 | // 移除原来的事件 |
1 | // 添加新的事件监听 |
下一步我们要支持函数组件。
1 | /** @jsx Didact.createElement */ |
这个 jsx 语法应该被转换为以下的 js 语法:
1 | function App(props) { |
函数组件与之前的语法有两个不同之处:
1 | function performUnitOfWork(fiber) { |
当 fiber 类型为函数时,我们使用不同的函数来进行更新。在 updateHostComponent 我们按照之前的方法更新。
1 | const isFunctionComponent = fiber.type instanceof Function; |
在函数组件中我们通过执行函数来获得 children。
1 | function updateFunctionComponent(fiber) { |
对于前面的例子
1 | function App(props) { |
fiber.type 就是 App 函数,当执行函数的时候,就会返回 h1 元素。
一旦我们拿到了这个子节点,剩下的调和就跟之前一致,我们不需要修改任何东西了。
接下来修改 commitWork 函数。
1 | function commitWork(fiber) { |
最后一步我们来给函数组件添加 state。我们把示例组件设置为经典的计数器。
1 | /** @jsx Didact.createElement */ |
1 | let wipFiber = null; |
在对应的 fiber 上加上 hooks 数组以支持我们在同一个函数组件中多次调用 useState。然后我们记录当前 hook 的序号。
当函数组件调用 useState,我们查看 fiber 对应的 alternate 字段下的旧 fiber 是否存在旧 hook、以及hook 的序号用以记录是该组件下的第几个 useState。
如果存在旧的 hook,我们将旧的 hook 值拷贝一份到新的 hook。 如果不存在,就将 state 初始化。
然后在 fiber 上添加新 hook,hook 序号会进行自增,然后返回状态。
1 | function useState(initial) { |
useState 还需要返回一个可以更新状态的函数,我们定义 setState,它接收一个 action。(在 Counter 的例子中, action 是自增 state 的函数)
最终完整的 mini-react 代码链接在 https://codesandbox.io/s/didact-8-21ost
]]>1 | <div class="container"> |
1 | body { |
1 | const draggables = document.querySelectorAll(".draggable"); |
1 | sum(1)(2)(3)(4)// 10 |
实现一个函数同时可以求解上述表达式。
先要实现原函数
1 | function sum(a, b, c, d) { |
将其进行柯里化,目标是将它的参数展开,所以柯里化的过程实际上可以理解为一个递归展开参数的过程。展开的便捷就是当前参数个数等于函数需要的参数,不过为了确保完整性把 === 改成了 >= 而已。
1 | function curry(fn, ...args) { |
1 | function curry() { |
1 | function flat(arr, depth = 1) { |
1 | function throttle(fn, delay = 200) { |
1 | function debounce(fn, delay = 200) { |
Fisher-Yates shuffle 算法
每次删除一个数字,并将删除的数字移至数组末尾,即将每个被删除数字与最后一个未删除的数字进行交换。
1 | function shuffle(arr) { |
在一个字符串的二维数组中,有一个隐藏字符串。
1 | I B C A L K A |
可以按照如下步骤找出隐藏消息
比如上面的二维数组的话,隐藏消息就是 IROCLED
1 | /** |
题目链接:
https://leetcode-cn.com/problems/first-bad-version/
二分基础题
1 | /** |
pipe
会传入一个数组,数组中每一项是一个函数。pipe
会依次执行里面的多个函数,最后返回结果。假设每个函数都有且仅有一个参数。如下所示:
1 | pipe([ |
1 | /** |
若 stack2 为空,则直接输入栈顶元素;否则,先把 stack1 的所有元素倒进 stack2 中。
1 | /* you can use this Class which is bundled together with your code |
对同一个函数,当传入相同参数的时候,直接返回上一次的结果而不经过计算。要求允许传入第二个参数决定缓存 key 的生成方式。
1 | /** |
实现自己的 $()
,只需要支持 css(propertyName: string, value: any)
即可。如下面所示:
1 | $('#button') |
1 | function $(elem) { |
要求满足的条件为:
构造函数
1 | const emitter = new Emitter() |
支持事件订阅
1 | const sub1 = emitter.subscribe('event1', callback1) |
emit(eventName, ...args)
可以用来触发 callback
1 | emitter.emit('event1', 1, 2); |
subscribe()
返回一个含有 release()
的对象,可以用来取消订阅。
1 | sub1.release() |
1 | class EventEmitter { |
JavaScript中有 Map,我们可以用任何 data 做 key,包括 DOM 元素。如下所示:
1 | const map = new Map() |
如果运行的 JavaScript 不支持 Map,我们如何能让上述代码工作?
方法是使用对象来模拟一个 map。
1 | class NodeStore { |
给定两个完全一样的 DOM Tree A 和 B,以及 A 中的元素 a,请找到 B 中对应的元素 b。
1 |
|
1 | /** |
1 |
|
1 | function parse(str) { |
运行命令初始化 package.json。
1 | yarn init -y |
安装 webpack 和 webpack-cli。
1 | yarn add webpack webpack-cli -D |
根目录新建文件夹 src,里面新建文件夹 js。
js 文件夹下新建两个文件 math.js 和 foo.js。内容如下:
1 | // math.js |
1 | // foo.js |
src 目录下新建文件 index.js,文件内容如下:
1 | import { sum, mul } from "./js/math"; |
根目录下新建 index.html,内容如下:
1 |
|
运行命令打包文件。
1 | npx webpack |
在 dist/main.js 中,可以看到打包的结果。
在 package.json 中配置 build 运行命令。
1 | "build": "npx webpack" |
src/js 目录下新建文件 title.js,内容如下:
1 | import "../css/title.css"; |
修改 index.js 内容为:
1 | import setTitle from "./js/title"; |
src 目录下新建文件夹 css,css 文件夹下新建文件 title.css,内容如下:
1 | .title { |
在 index.html 中引入 css 文件。
运行命令 yarn build
,报错,原因是缺少 loader。
安装 loader。
1 | yarn add css-loader -D |
根目录下新建文件 webpack.config.js,进行配置。
1 | const path = require("path"); |
但是样式并没有展示,因为现在只是把 css 语法识别为了 js 语法,但是还没有挂载到页面上,我们需要 style-loader 来把内容挂载到 <style>
标签上。
安装 style-loader
1 | yarn add style-loader -D |
修改配置文件:
1 | const path = require("path"); |
首先安装 less。
1 | yarn add less -D |
配置文件修改为下面的内容。
安装 less-loader。
1 | yarn add less-loader -D |
1 | rules: [ |
浏览器使用比例可以查看 caniuse 官网
在 package.json 中新增内容
1 | "browserslist": [ |
运行命令
1 | npx browserslist |
可查看当前兼容的浏览器选项。
postcss 用来处理 css 的兼容性问题。
先安装 postcss 和 postcss-loader
1 | yarn add postcss -D |
css 文件夹下新建文件 test.css,内容如下:
1 | .title { |
然后把文件引入 title.js 中。
修改 webpack.config.js 为:
1 | { |
打包以后发现没有任何效果,原因是 postcss 本身其实不具备修改 css 的功能,还需要额外的插件才行。
安装插件 autoprefixer。
1 | yarn add autoprefixer -D |
修改配置文件为:
1 | { |
安装 postcss-preset-env。
1 | yarn add postcss-preset-env -D |
修改 test.css 内容为:
1 | .title { |
postcss-loader 配置的 plugins 中增加以下内容
1 | require("postcss-preset-env") |
打包以后颜色会以 rgba 形式展示。
为了避免在 css-loader 和 less-loader 中都要进行重复的配置,postcss 还支持我们通过配置文件进行配置。
根目录下新建文件 postcss.config.js,内容为:
1 | module.exports = { |
这样我们就不需要在 webpack.config.js 中书写 postcss-loader 的 plugins 了。
1 | { |
在 src 文件夹下新建文件夹 img,里面塞入一张图片。
在 js 文件夹下新建文件 image.js,内容如下:
1 | import imgSrc from "../img/ai.jpg"; |
修改 index.js,内容如下:
1 | import setImage from "./js/image"; |
打包以后报错缺少 loader。
安装 file-loader。
1 | yarn add file-loader -D |
修改 webpack.config.js,添加如下内容
1 | { |
修改 js/image.js 为以下内容:
1 | import "../css/bg.css"; |
css 文件夹下新建文件 bg.css,内容如下:
1 | .bg-img { |
先删除 dist 目录,再运行打包命令。此时发现 dist 目录下会出现两张图片。
其中一张是正常的,另一张点击打开以后内容如下:
里面的文本内容是一个指向我们需要的图片的导出语句。
这是因为图片是嵌在 css-loader 里面,没有被 file-loader 处理。
css-loader 会把 url 路径处理为 require 语句,而 require 语句使用时需要把 css-loader 的 esModule 属性设置为 false。
1 | { |
处理以后打包问题就解决了。
修改 webpack.config.js 如下
1 | { |
安装 url-loader。
1 | yarn add url-loader -D |
把配置文件的 file-loader 修改为 url-loader,进行打包。
打包后发现页面显示正常,但是 dist 目录下没有出现 img 文件夹。
它会以 base64 形式把图片嵌入代码中。
url-loader VS file-loader
在 img 文件夹下加入一张新图片
修改 image.js 如下:
1 | import "../css/bg.css"; |
修改 webpack.config.js 文件如下:
1 | { |
运行打包命令,会发现只有一张较大的图片,另一张小的图片被以 base64 的形式嵌入文件里了。
webpack5 之后不需要再使用 file-loader 和 url-loader 了。
修改 webpack.config.js 如下:
1 | { |
如果需要设置 limit,则修改如下:
1 | { |
先去 iconfont 官网下载图标。
在 src 下新建文件夹 font,里面留着下面图片中这几个文件即可。
src/js 下新建文件 font.js,内容如下:
1 | function setFont() { |
修改 index.js 内容如下:
1 | import setFont from "./js/font"; |
运行打包命令,报错,这是因为我们没有办法处理 iconfont 里面的路径。
我们直接把字体当成资源文件进行拷贝即可,因此可以使用前面所说的 asset/resource ,给 webpack.config.js 添加如下内容即可:
1 | { |
安装 clean-webpack-plugin
1 | yarn add clean-webpack-plugin -D |
作用是每次打包之前先把 dist 目录删除。
修改 webpack.config.js 内容:
在现版本的 webpack 中,已经不需要再加入此插件了,直接设置 output 的 clean 属性为 true 即可。
安装 html-webpack-plugin
1 | yarn add html-webpack-plugin -D |
引入 HtmlWebpackPlugin 类
1 | const HtmlWebpackPlugin = require("html-webpack-plugin"); |
引入插件:
1 | new HtmlWebpackPlugin({ |
这里打包以后产出的 js 文件是 defer 引入的。但是这样很不灵活,其实我们可以自己书写打印出的模板。
在 src 目录下新建文件夹 public。public 下新建文件 index.html。我们把 vue-cli 的 index.html 拷贝过来。
1 |
|
打包以后报错说找不到 BASE_URL。webpack 可以自定义一些常量,我们在这还没有定义所以会报错。这里用到的是 webpack 内置的一个插件。
1 | const { DefinePlugin } = require("webpack"); |
Babel 的作用:JSX TS ES6+ => 转换为浏览器可以直接使用的语法
将 js/foo.js 内容修改如下:
1 | const foo = () => { |
修改 index.js 如下:
1 | import foo from "./js/foo"; |
打包后,main.js 内容含有箭头函数,可能在某些浏览器无法正常显示。我们需要使用 babel 处理。
安装 @babel/core(核心模块)
1 | yarn add @babel/core -D |
安装 babel-loader
1 | yarn add babel-loader -D |
安装插件 @babel/preset-env -D
1 | yarn add @babel/preset-env -D |
修改配置文件如下:
1 | { |
然后就可以把语法转成 ES5 了。
还可以新建文件 babel.config.js,里面写入一下配置内容:
1 | module.exports = { |
然后就能简写 webpack.config.js 的信息为:
1 | { |
preset-env 并不能把所有的语法都转换,此时我们需要 polyfill。polyfill 即字面意思,填充,意思是填充一些旧版本没有的新语法(比如 Promise)。webpack4 会默认加入 polyfill,所以打包速度很不乐观,在 webpack5 就去掉了。
修改 index.js 内容为:
1 | const p1 = new Promise((resolve, reject) => { |
安装 core-js、regenerator-runtime,注意此处不是开发依赖。
1 | yarn add core-js regenerator-runtime |
然后修改 babel.config.js,内容如下:
1 | module.exports = { |
在 index.js 中引入核心模块。
1 | import "core-js/stable"; |
可以进行一些资源的拷贝,如 favicon 图标。
安装 copy-webpack-plugin。
1 | yarn add copy-webpack-plugin -D |
引入插件:
1 | const CopyWebpackPlugin = require("copy-webpack-plugin"); |
修改 webpack.config.js:
1 | new CopyWebpackPlugin({ |
修改 index.js 内容:
1 | import "./js/font"; |
在 webpack.config.js 中添加 watch: true,如下所示。
当文件修改以后,会自动触发打包。但是这样会影响速度,因为每次保存都要重新打包一次。我们可以在本地开启一个服务器,把文件存储在内存中。
安装 webpack-dev-server。
1 | yarn add webpack-dev-server -D |
在 package.json 中配置命令:
1 | "start": "npx webpack serve" |
运行命令
1 | yarn start |
这样就可以实现开启本地服务器。
HMR 即 hot-module-replacement,模块热替换,也叫热更新,就是可以对页面的局部内容进行替换。
首先修改 index.js 内容为
1 | console.log("HMR"); |
修改 public 文件夹下的 index.html,增加一个输入框,用于验证热替换是否开启。
1 | <body> |
webpack.config.js 中加入内容:
1 | devServer: { |
此时事实上还没有开启热更新。还需要把热更新的选项加入配置中。因为开发中通常是使用框架进行,所以这部分意义不大,省略。
安装 @babel/preset-react,这是一个用于转换 jsx 语法的插件。
1 | yarn add @babel/preset-react -D |
安装 react。
1 | yarn add react react-dom |
src 目录下新建文件 App.jsx,内容如下:
1 | import React, { useState } from "react"; |
修改 index.js,内容如下:
1 | import App from "./App.jsx"; |
修改 babel.config.js,内容如下:
1 | module.exports = { |
此时可以支持 React 语法,为了实现热更新,还需要引入其它插件。
安装 @pmmmwh/react-refresh-webpack-plugin 和 react-refresh
1 | yarn add @pmmmwh/react-refresh-webpack-plugin react-refresh -D |
在 webpack.config.js 中引入插件:
1 | const ReactRefreshWebpackPlugin = require("@pmmmwh/react-refresh-webpack-plugin"); |
1 | new ReactRefreshWebpackPlugin(), |
修改 babel.config.js,内容如下:
1 | module.exports = { |
此时就能实现 React 的热更新了。
1 | devServer: { |
compress 表示是否压缩资源(使用 gzip),open 表示是否自动打开文件,当使用 History API 时,任意的 404 响应会被替代为 index.html。
hot: ‘only’ 和 hot: true 的区别:
如果文件报错了,修改成了对的以后,hot: true 会直接刷新整个页面,而 hot: ‘only’ 不会刷新整个页面。
先开启一个 node 服务。
1 | // 服务端 http://127.0.0.1:8000 |
安装 axios
1 | yarn add axios |
修改 index.js 内容为:
1 | import App from "./App.jsx"; |
此时请求出现跨域,需要设置代理。
1 | devServer: { |
前端更改为:
1 | axios.get("http://127.0.0.1:8000/").then((res) => { |
此时跨域问题得到解决。
devtool 选项控制是否需要 source-map。
source-map 是一种映射方式,可以在调试的时候定位到源代码中的位置。
设置 devtool: 'source-map'
,运行 yarn build
,dist 目录下除了原来的文件外还会多出一个 main.js.map,然后也可以定位到错误的具体位置了。
通过 devtool 还可以进行更多的配置。
src 目录下新建文件 index.ts,内容如下:
1 | const add = (a: number, b: number) => { |
修改 webpack.config.js 文件中的 entry 为 “./src/index.ts”
执行命令
1 | tsc --init |
生成 tsconfig.json 文件。
安装 ts-loader。
1 | yarn add ts-loader -D |
安装 typescript。
1 | yarn add typescript -D |
修改 webpack.config.js 文件内容:
1 | { |
此时 ts 已经可以被正确编译,但是一些新的语法没有被转换为低级的语法,可能会存在兼容性问题,所以还需要 babel-loader 进行进一步的处理。
安装 @babel/preset-typescript
1 | yarn add @babel/preset-typescript -D |
修改 babel.config.js,内容为:
1 | module.exports = { |
ts-loader 如果出现数据类型错误,会在 build 的时候暴露;而 babel-loader 可以进行 polyfill。
我们可以选择使用 babel-loader,然后通过命令进行类型校验。输入命令 tsc
,可以实现数据类型的校验。
也可以选择直接修改打包命令如下:
修改 package.json 中的脚本命令:
在根目录下新建文件夹 config,然后在 config 文件夹下新建 3 个文件 webpack.common.js、webpack.dev.js、webpack.prod.js。
安装 webpack-merge,来进行文件合并。
1 | yarn add webpack-merge -D |
文件内容如下:
1 | // webpack.common.js |
1 | // webpack.dev.js |
1 | // webpack.prod.js |
修改 babel.config.js,区分生产和开发环境:
1 | const isDev = process.env.NODE_ENV !== "production"; |
先初始化项目
1 | yarn init -y |
运行命令初始化 package.json 文件。
安装 webpack 和 webpack-cli。
1 | yarn add webpack webpack-cli -D |
1 | tsc --init |
修改 tsconfig.json,使用 create-react-app 的设置:
1 | { |
在根目录下新建文件夹 config,书写配置信息。
1 | // config/webpack.common.js |
1 | // config/webpack.dev.js |
1 | // config/webpack.prod.js |
1 | yarn add react react-dom |
安装 ts 依赖
1 | yarn add @types/react @types/react-dom -D |
1 | yarn add babel-loader @babel/core @babel/preset-env @babel/plugin-transform-runtime @babel/preset-react @babel/preset-typescript -D |
根目录新建文件 babel.config.js,内容如下:
1 | const isDev = process.env.NODE_ENV !== "production"; |
1 | yarn add sass sass-loader css-loader postcss-loader postcss style-loader babel-loader -D |
1 | yarn add @pmmmwh/react-refresh-webpack-plugin react-refresh copy-webpack-plugin html-webpack-plugin -D |
1 | yarn add postcss-preset-env autoprefixer -D |
新建文件 postcss.config.js:
1 | module.exports = { |
给 package.json 添加 browserslist。
1 | "browserslist": [ |
1 | yarn add webpack-dev-server -D |
1 | yarn add webpack-merge -D |
1 | yarn add cross-env -D |
1 | "scripts": { |
本文主要包括以下内容配置
首先按照这篇文章完成项目创建和基本配置,你也可以使用 webpack 进行基础配置。
把 src 文件夹下的 App.css、index.css、logo.svg 删除。
1 | yarn add styled-components |
如果报错找不到类型文件,就执行以下命令。
1 | yarn add @types/styled-components -D |
在根目录新建文件 globalStyle.ts
,内容如下:
1 | import { createGlobalStyle } from "styled-components"; |
安装 react-router,进行路径配置。(这里以 V6 为准)
1 | yarn add react-router-dom |
在 src 文件夹下新建文件夹 route,创建 index.tsx 文件。内容如下:
1 | import { Routes, Route } from "react-router-dom"; |
在 src 目录下的 index.tsx 中引入 BrowserRouter
1 | import { BrowserRouter } from "react-router-dom"; |
代码修改为
1 | ReactDOM.render( |
修改 src 目录下的 APP.tsx,引入默认样式以及路由配置。
1 | import MyRouter from "routes"; |
在 src 文件夹下新建文件夹 pages,用于存储页面。
文件夹 pages 下新建文件夹 RequestExample、ReduxExample、TopBar、Home,文件内容如下。
1 | // ReduxExample/index.tsx |
1 | // RequestExample/index.tsx |
1 | // Home/index.tsx |
1 | // TopBar/index.tsx |
上面这段代码的 <Outlet />
是为了能够渲染下一级的路由。因为目前 react-router-dom@6 在 ts 环境下不支持重定向,所以要先用 useEffect
强制重定向。
给 TopBar 加上一些简单的样式,方便我们查看。
1 | // TopBar/style.ts |
首先安装依赖,这里处理异步使用的是 redux-thunk。
1 | yarn add redux redux-thunk react-redux immer |
在 src 文件夹下新建文件夹 store,再在 store 文件夹中新建文件 index.ts 和 reducer.ts。内容如下:
1 | // reducer.ts |
1 | // index.ts |
在 App.tsx 中注入 store。
1 | import { Provider } from "react-redux"; |
为了验证 Redux 配置是否正确,我们使用经典的 Counter 来验证。
在 src 下创建文件夹 components,再在 components 文件夹下新建文件夹 Counter。
1 | // Counter/index.tsx |
在 ReduxExample 目录下创建文件夹 store,在 store 文件夹下新建文件 index.ts、store.ts、constants.ts、actions.ts。
1 | // store/constants.ts |
1 | // store/actions.ts |
1 | // store/reducer.ts |
1 | // store/index.ts |
修改 ReduxExample,进行测试。
1 | import Counter from "components/Counter"; |
json-server 是我认为的一种比较不错的 mock 数据方法。
如果还没有安装的话先全局一下安装 json-server。
1 | npm install -g json-server |
在根目录下新建文件夹 __mock__
,然后在文件夹下新建文件 db.json
。文件内容为:
1 | { |
因为 3000 端口已经被我们的页面占用了,所以我们要换一个端口运行命令启动 json-server。为了便捷我们可以在 package.json
中加入新的脚本命令。
配置完成后,运行命令
1 | yarn server |
即可在 http://localhost:3001 开启 json-server。
在浏览器输入路径 http://localhost:3001/subjects ,可以看到 db.json
中的数据。
执行命令安装 axios。
1 | yarn add axios |
在 src 目录下新建文件夹 api,并在 api 文件夹下新建文件 config.ts、request.ts。
1 | // api/config.ts |
1 | import { axiosInstance } from "./config"; |
编写组件 SubjectList 来验证 axios 配置。
在 components 文件夹下新建文件夹 SubjectList。
1 | // SubjectList.tsx |
在 SubjectList 文件夹下新建 store 文件夹。
1 | // store/constants.ts |
1 | // store/reducer.ts |
1 | // store/actions.ts |
1 | // store/index.ts |
然后把 reducer 导入全局:
1 | // src/store/reducer.ts |
修改 RequestExample/index.tsx,内容为:
1 | import SubjectList from "components/SubjectList"; |
完成任务!
]]>Cross-Site Scripting,跨站脚本攻击。指攻击者通过某种方式把恶意脚本注入你写的页面。
document.write(xxx)
、elem.innerHTML = xxx
1 | async function submit(ctx) { |
导致攻击:
比如说一个用户在视频中插入一个 XSS 攻击,然后某一刻脚本被启动了,此时所有的在浏览这个页面的用户都会被脚本攻击,造成信息泄露。
不涉及数据库,而是从 URL 进行攻击
把字段直接生成 HTML 字段,然后被成功攻击
不需要服务器参与,恶意攻击的发起与执行都在浏览器完成。
利用了浏览器渲染 DOM 的特性,对于不同的浏览器执行会有区别。
代码会被渲染为:
又由于 src 属性不符合规范,然后会触发 onerror
事件,也就完成了 XSS 攻击。
Cross-site request forgery,跨站请求伪造
在这个例子中,用户并没有直接请求银行,但是这个请求却被成功执行了,这就是一个经典的 CSRF 攻击。
删库跑路示例:
其余注入:
因为没有过滤导致成功删除跑路
以 Nginx 为例,如果用户可以读取 Nginx 的配置文件,就能把我们的网站转到另一个网站
Denial of Service,服务拒绝。通过某种方式构造特定的请求,导致服务器资源被显著消耗,来不及响应更多的请求,导致请求积压,进而引发雪崩效应。
书写正则的时候是否写 ?
这里的第一行就是贪婪模式
因为贪婪引发回溯
短时间内,收到大量来自僵尸设备的请求流量,服务器不能及时完成全部的请求,导致请求堆积,进而引发雪崩效应,无法响应新的请求。
不搞复杂的,量大就完事儿
攻击者发起大量的 TCP 请求,然后就会产生大量的 SYN,发送给服务器。然后服务器就会产生大量的 ACK 和 SYN 给攻击者。但是,攻击者不会返回第三次 ACK,进而导致三次握手失败,连接无法被释放,于是很快就会到达最大连接次数,所有的新请求就无法被响应。
为什么中间人攻击可以成立?
如果有需求不讲武德,必须动态生成 DOM 呢?
Content Security Policy
第一行:只允许同源;第二行:除了同源之外,还另外允了 domain.com
我们也可以在浏览器端进行设置:
只要我们限制请求的来源,就可以限制伪造请求。可以根据 Origin 或者 Referer 判断。
首先在视觉上,让 button 覆盖住 iframe,然后用户就看不出来了。接着通过 button 的设置导致点击事件穿透了,然后传递给了 iframe,iframe 中的请求没有跨域,因此可以完成攻击。
可以设置 HTTP 响应头 X-Frame-Options: DENY/SAMEORIGIN
(不允许加载 iframe 或只允许加载同源的 iframe)
GET !== GET + POST
一旦被攻击,信息不单止可能泄露,甚至还会被篡改。
限制 cookie 的 domain 属性。我页面的 cookie 只能为我所用,只有同域才能使用这个 cookie,第三方服务的请求不能带上我页面的 cookie。
但是如果服务依赖于第三方的 cookie 怎么办?
比如内嵌了一个 b 站的播放器,需要识别用户的登录状态。
可以设置 SetCookie: SameSite=None; Secure;
即不限制 same site,但是必须确保 cookie 是安全的(只能通过 HTTPS 传输)。
SameSite VS 同源策略:SameSite 主要针对 cookie,同源策略 针对的是请求的资源。
用 Node 做一个中间件防范攻击。
对 SQL 语句做一些 prepare 处理
过滤:
抗量:
使用 HTTPS,其中 HTTP3 内置了 TLS
HTTP Strict Transport Security,HTTO 严格传输安全协议。
HSTS 的作用是强制客户端(如浏览器)使用 HTTPS 与服务器创建连接。服务器开启 HSTS 的方法是,当客户端通过 HTTPS 发出请求时,在服务器返回的超文本传输协议(HTTP)响应头中包含 Strict-Transport-Security 字段。非加密传输时设置的 HSTS 字段无效。
比如,https://example.com/ 的响应头含有 Strict-Transport-Security: max-age=31536000; includeSubDomains。这意味着两点:
Subresource Integrity,子资源完整性。
Web 性能优化中很重要的一点是加快请求完成速度,让可缓存的资源走 CDN 是最通用的做法。CDN 服务提供商通过分布在各地的节点,让用户从最近的节点加载内容,大幅提升速度。但是 CDN 的安全性一直是一个风险点:对于网站来说,让请求从第三方服务器经过,由第三方响应,安全方面肯定不如自己服务器可控。
我们知道 CSP(Content Security Policy) 的外链白名单机制可以在现代浏览器下减小 XSS 风险。但针对 CDN 内容被篡改而导致的 XSS,CSP 并不能防范,因为网站所使用的 CDN 域名,肯定在 CSP 白名单之中。这时候,SRI 就应运而生了。
它通过对比 hash 值,来确保文件的安全性,可以在一定程度上防范 XSS 攻击。
]]>1 | const http = require('http'); |
1 | // 接收请求 |
1 | // 发请求 |
1 | const http = require('http'); |
1 | const http = require('http'); |
请求方法 | 说明 |
---|---|
GET | 请求指定的资源,并返回实体 |
POST | 向指定资源提交数据进行处理请求(例如提交表单或者上传文件)。数据被包含在请求体中。POST 请求可能会导致新的资源的建立和已有资源的修改 |
PUT | 从客户端向服务器传送的数据取代指定的内容 |
DELETE | 请求服务器删除指定的数据 |
OPTIONS | 在采取具体资源的请求之前,决定对该资源采取何种必要措施,或者了解服务器的性能 |
CONNECT | HTTP/1.1 协议中预留于能够将连接改为管道方式 |
HEAD | 类似于 GET 请求,只不过返回的响应中没有请求体 |
状态码概览:
常见状态码总结:
状态码 & 英文描述 | 详细说明 |
---|---|
100 Continue | 服务器收到了请求的一部分,并且希望客户端继续发送其余部分 |
101 Switching Protocols | 切换协议,服务端根据客户端请求的头信息切换协议 |
200 OK | 请求成功,且被服务端成功处理 |
201 Created | 成功请求,且创建了新的资源 |
202 Accepted | 服务器已接受请求,但未处理完成 |
204 No Content | 请求成功处理,但是没有资源可以返回 |
206 Partial Content | 服务器成功处理了部分 GET 请求 |
301 Moved Permanently | 永久重定向。请求的资源被分配了新的 URL,之后应使用更改的URL |
302 Found | 临时重定向。表示请求的资源被分配了新的 URL,希望本次访问使用新的 URL |
304 Not Modified | 缓存相关的状态码。自从上次请求后,请求的资源未被修改过。 服务器返回此响应时,不会返回任何资源。客户端的请求中带有 If-Modified-Since 或者 If-None-Match |
307 Temporary Redirect | 类似 302,但是 307 会遵照浏览器标准,请求方法不会从 POST 变成 GET |
400 Bad Request | 请求报文中存在语法错误 |
401 Unauthorized | 用户未授权 |
403 Forbidden | 服务器拒绝该次访问 |
404 Not Found | 服务器上无法找到请求的资源 |
408 Request Time-out | 服务器等待客户端发送的请求时间过长,超时 |
500 Internal Server Error | 服务器内部错误,无法完成请求 |
502 Bad Gateway | 服务器作为网关或者代理时,为了完成请求访问下一个服务器,但该服务器返回了非法的应答 |
503 Service Unavailable | 服务器超负载或正在进行停机维护,无法处理请求 |
一种 API 设计风格,有以下特点:
CRUD 应该遵循以下语义:
示例:
请求头 | 说明 |
---|---|
Accept | 浏览器可接受的 MIME 类型 |
Content-Type | 资源属于什么 MIME 类型 |
Cache-Control | 用于指定缓存机制。常见的值有 no-cache(不直接使用缓存,要向服务器发起请求确认资源是否更改,也就是我们常说的协商缓存),no-store(不使用任何缓存),max-age=xxx(缓存内容在 xxx 秒后失效) |
If-Modified-Since | 对应于服务端的 Last-Modified,若所请求的内容在指定的日期之后没有修改过,则返回 304 Not Modified,精度达到秒 |
If-None-Match | 对应于服务端的 ETag,若所请求的内容在指定的日期之后没有修改过,则返回 304 Not Modified,精度非常准确 |
Cookie | 有 cookie 而且同域的时候会自动带上 |
User-Agent | 浏览器类型 |
Connection | 若为 Keep-Alive,或者协议是 HTTP/1.1,则开启持久连接 |
Location | 一般用来表示重定向的地址 |
Expires | 缓存过期时间,在此时间内不需要发起请求,可以直接使用缓存 |
Authorization | 授权信息 |
Referer | 说明该页面的来源 URL。用处:防止盗链;避免 CSRF 攻击 |
Origin | 类似于 Referer,把 URI 剥离成 {协议,域名,端口} 的三元组,用于指明当前请求来自于哪个站点。Origin 的出现就是为了实现跨域。 |
响应头 | 说明 |
---|---|
Content-Type | 资源属于什么 MIME 类型 |
Cache-Control | 用于指定缓存机制。常见的值有 no-cache(不直接使用缓存,要向服务器发起请求确认资源是否更改,也就是我们常说的协商缓存),no-store(不使用任何缓存),max-age=xxx(缓存内容在 xxx 秒后失效) |
Last-Modified | 最后修改时间。客户端可以通过 If-Modified-Since 请求头提供一个日期,只有改动时间迟于指定时间,才会返回新的资源,否则返回一个 304 Not Modified |
ETag | 资源特定的标识符。客户端可以通过 If-None-Match 请求头提供一个标识符,如果客户端标识符与服务端不同才会返回新的资源,否则返回一个 304 Not Modified |
Set-Cookie | 设置页面相关的 Cookie |
Access-Control-Allow-Origin | 服务器允许请求的 Origin,如果设置为 * 则表示允许所有的 Origin |
Expires | 缓存过期时间。在此时间内不需要发起请求,可以直接使用缓存 |
Max-age | 本地缓存应该缓存多久,开启了 Cache-Control 后才生效 |
强缓存是利用 http 头中的 Expires 和 Cache-Control 两个字段来控制的。强缓存中,当请求再次发出时,浏览器会根据 Expires 和 Cache-control 判断目标资源是否命中强缓存,若命中则直接从缓存中获取资源,不会再与服务端发生通信。
Expires VS max-age
浏览器需要向服务器去询问缓存的相关信息,进而判断是重新发起请求,还是从本地获取缓存的资源。如果服务端提示缓存资源未改动,资源会被重定向到浏览器缓存(解释了为什么它是 3XX 开头),这种情况下网络请求对应的状态码是 304。
含义:指最后一次修改资源的时间。开启了协商缓存之后,我们的每次请求都会带上 If-Modified-Since 的时间戳字段,它的值正是上一次 response 返回给它的 Last-Modified 值。服务器接收到这个时间戳后,会比对该时间戳和资源在服务器上的最后修改时间是否一致,从而判断资源是否发生了变化。如果发生了变化,就会返回一个完整的响应内容,并在 Response Headers 中添加新的 Last-Modified 值,否则返回 304 Not Modified。
弊端:
Etag 是由服务器为每个资源生成的唯一的标识字符串,这个标识字符串是基于文件内容编码的,只要文件内容不同,它们对应的 Etag 就是不同的;相反,只要文件内容相同,ETag 就是相同的。因此 Etag 能够精准地感知文件的变化。它的作用原理和 If-Modified-Since 类似,都是客户端带上然后去跟服务端进行比较,不同就返回资源以及新的 ETag,相同就返回 304 Not Modified。
弊端:Etag 的生成过程需要服务器额外付出开销,会影响服务端的性能,这是它的弊端。Etag 并不能替代 Last-Modified,它只能作为 Last-Modified 的补充存在。 Etag 在感知文件变化上比 Last-Modified 更加准确,优先级也更高。当 Etag 和 Last-Modified 同时存在时,以 Etag 为准。
示例图:
简单来说就是:
属性 | 说明 |
---|---|
<cookie-name>=<cookie-value> | cookie 的键值对。 |
Path | 指定一个 URL 路径,这个路径必须出现在要请求的资源的路径中才可以发送 cookie。 |
Domain | 指定 cookie 可以送达的主机名。与之前的规范不同的是,域名之前的点号会被忽略。假如指定了域名,那么相当于各个子域名也包含在内了。 |
Sec | 规定必须通过安全的 HTTPS 连接来传输 cookie。 |
HttpOnly | JS 脚本将无法读取到 cookie 信息,这样能有效的防止 XSS 攻击。 |
Expires | 规定 cookie 的最长有效时间。形式为符合 HTTP-Date 规范的时间戳。 |
Max-Age | 在 cookie 失效之前需要经过的秒数。假如 Expires 和 Max-Age 均存在,那么 Max-Age 优先级更高。 |
SameSite=[Strict, Lax] | 允许服务器设定 cookie 不随着跨域请求一起发送,这样可以在一定程度上防范 CSRF 攻击。 |
HTTPS 指的是超文本传输安全协议,HTTPS 是基于 HTTP 协议的,不过它会使用 TLS/SSL 来对数据加密。优点有:
示例图:
静态资源方案:缓存 + CDN + 文件名 hash(确保用户能拿到最新的文件)
Content Delivery Network,内容分发网络,是由分布在不同区域的边缘节点服务器组成的分布式网络。开启 CDN 之后,用户的请求并不是直接发送给源网站,而是发送给 CDN 服务器,由 CND 服务器将请求定位到最近的含有该资源的服务器上去请求。这样有利于提高网站的访问速度,同时也分担了源服务器的访问压力。
是否开启 CDN 的区别
不开启 CDN:
用户输入域名 -> DNS 解析获取 IP -> 向该 IP 对应服务器发送访问请求 -> 返回资源
开启 CDN:
用户输入域名 -> 智能 DNS 解析 -> 获取缓存服务器 IP -> 若缓存有目标资源,返回资源;若没有就向源服务器发起请求,把获取的资源保存到缓存服务器,再把资源返回给用户
在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。这种方式减少了由登录产生的时间消耗,辅助了用户管理。
用户在授权访问后会获得一个凭证,之后访问相关的应用时也会带上这个凭证,所以用户就可以在不需要再次登录的情况下直接登录访问。
常用的跨域解决方案主要有五种:
Ajax 五部曲
1 | // 第一步:创建 xhr 对象 |
无论动画多么简单,始终需要定义两个基本状态,即开始状态和结束状态。没有它们,我们将无法定义插值状态,从而填补两者之间的空白。
插值可能是颜色,也可能是位置等属性,可以理解为是对某个上下文中一个值或多个值的估计。当图形的变化可以由线性方程表示,就是线性插值。
空白的补全方式有以下两种
优点:
缺点:
主要属性:
transform
:可以用于图形的旋转、移动、缩放等transition
:处理开始状态到结束状态的过渡效果animation
:通过 keyframe 定义开始状态、结束状态以及多个中间态,相比于 transition
可以处理更复杂的动画优点:通过矢量元素实现动画,不同的屏幕下均可获得较好的清晰度。可以用于实现一些特殊的效果,如:描字,形变,墨水扩散等。
缺点:使用方式较为复杂,过多使用可能会带来性能问题。
实现 SVG 动画通常有三种方式,SMIL、JS、CSS
兼容性不理想,因此不过多讨论
例一:SVG 文字变形
CSS 属性 filter:可以将模糊或颜色偏移等效果应用于元素,它的属性 url 可以传入一个 svg
1 | elts.text2.style.filter = `blur(${Math.min(8 / fraction - 8, 100)}px)`; |
通过上面的代码控制 blur
和 opacity
不断地变化。每次切换单词的时候,当它的模糊程度快没有,就直接通过透明度把它隐藏掉,造成一种文字溶解的错觉。
例二:JS 画笔
JS 画笔的原理:stroke-dashoffset
、stroke-dasharray
配合使用实现笔画效果。
stroke-dasharray
可控制用来描边的点划线的图案范式。它是一个数列,数与数之间用逗号或者空白隔开,指定短划线和缺口的长度。如果提供了奇数个值,则这个值的数列重复一次,从而变成偶数个值。因此,5,3,2 等同于 5,3,2,5,3,2。stroke-dashoffset
属性指定了 dash 模式到路径开始的距离。(当使用了 stroke-dasharray
,就进入了 dash 模式)1 | <line stroke-dasharray="5, 5" x1="10" y1="10" x2="190" y2="10" /> |
上面第一个表示先走 5 像素实线,再走 5 像素的空白;第二个表示先走 5 像素实线,再走 10 像素空白。
计算 path 的长度:path.getTotalLength()
,然后将 stroke-dashoffset
的值设置为该的长度,就能实现类似画画的效果。
JS 可以通过操作 SVG、CSS、Canvas 等实现动画。
优点:
缺点:
requestAnimationFrame VS setTimeout VS setInterval
JavaScript 动画应该通过 requestAnimationFrame
实现。该内置方法允许设置回调函数以在浏览器准备重绘时运行,因此不容易丢帧,但是 setTimeout
和 setInterval
容易丢帧。通常这很快,确切的时间取决于浏览器。另外,当页面在后台时,根本没有重绘,所以回调不会运行,此时动画将被暂停并且不会消耗资源,这也比 setTimeout
和 setInterval
更优。
1 | function animate({ easing, draw, duration }) { |
这里 r 是距离,v 是速度,t 是时间。动画就是在开始状态和结束状态之间插值,这里的 r 可以简单理解为插值。
另外我们还需要通过比例尺实现物体的缩放,使得动画可以在显示屏中正常显示。
贝塞尔曲线生成网站:cubic-bezier.com
]]>转换式系统:给定输入,求解输出,如编译器,计算器
响应式系统:监听事件,由消息驱动
前端代码并不需要大量的计算,更多的是需要去处理一些事件(比如用户的点击);另外当事件发生时,我们需要进行一些响应(比如改变界面)。这两个特点决定了转换式系统对于前端写起来是很难受的,我们需要一种新的方式。
依据上面的分析,我们对于 React 设计的期望也很容易得出:
状态提升:当多个组件需要共享一个状态的时候,我们需要不断地提升状态,所以我们需要状态管理库。
注意 React 不是双向数据流,是单向的。子组件只是执行了父组件传递过来的函数,而没有把任何的状态传递回去给父组件。(函数在 Js 中是一等公民,所以可以作为一个变量传递)
使用状态管理库的弊端:组件的复用性降低了,一般使用于业务代码
常用的状态管理库有:
什么东西应该放到状态管理库?
如果你觉得某个东西整个 APP 有多处可能用到的,就放进去,比如说用户头像,这样也可以减少我们发送的请求数。
我们可以把 UI 的展现看成一个函数的执行过程。其中,Model 是输入参数,函数的执行结果是 DOM 树,也就是 View。而 React 要保证的,就是每当 Model 发生变化时,函数会重新执行,并且生成新的 DOM 树,然后 React 再把新的 DOM 树以最优的方式更新到浏览器。
所以我们是否真的有必要使用 class 来作为组件呢?
事实上使用 class 是一种很牵强的做法,React 根本没有用到类的两个重要特性:
换句话说,class 并不是最好的组件表现形式,function 才是。
但是当时有一个局限是,函数组件无法存在内部状态,必须是纯函数,而且也无法提供完整的生命周期机制。
因此我们需要一个机制,能够把一个外部的数据绑定到函数的执行。当数据变化时,函数能够自动重新执行。
于是有了 Hooks。
在 React 中,Hooks 就是把某个目标结果钩到某个可能会变化的数据源或者事件源上,那么当被钩到的数据或事件发生变化时,产生这个目标结果的代码会重新执行,产生更新后的结果。
对于函数组件,这个结果是最终的 DOM 树;对于 useCallback、useMemo 这样与缓存相关的组件,则是在依赖项发生变化时去更新缓存。
另外,有一点需要特别注意,Hooks 中被钩的对象,不仅可以是某个独立的数据源,也可以是另一个 Hook 执行的结果,这就带来了 Hooks 的最大好处:实现逻辑的复用。
在之前的 React 使用中,有一点经常被大家诟病,就是非常难以实现逻辑的复用,必须借助于高阶组件等非常复杂的设计模式。但是高阶组件会产生冗余的组件节点,让调试变得困难。不过这些问题可以通过 Hooks 得到了很好的解决。所以如果有人问你 Hooks 有什么好处,那么最关键的答案就是简化了逻辑复用。
在 Hooks 出现之前,高阶组件几乎是 Class 组件中实现代码逻辑复用的唯一方式,其缺点其实比较显然:
但现在我们可以通过 Hooks 的方式对外部数据进行封装,从而将其变成一个可绑定的数据源。
除了逻辑复用之外,Hooks 能够带来的另外一大好处就是有助于关注分离,意思是说 Hooks 能够让针对同一个业务逻辑的代码尽可能聚合在一块儿。这是过去在 Class 组件中很难做到的。因为在 Class 组件中,你不得不把同一个业务逻辑的代码分散在类组件的不同生命周期的方法中。
在过去的 Class 组件中,我们需要在 componentDidMount
中监听事件,在 componentWillUnmount
中解绑事件。而在函数组件中,我们可以把所有逻辑写在一起。
浏览器同源策略是一个安全策略,其中同源指的是 协议 + 域名 + 端口号
三者相同,即使有两个不同的域名指向同一个 IP 地址,也不是同源的。同源策略可以一定程度上防止 XSS、CSRF 攻击。
一个域名的组成包括:
在默认情况下 http 可以省略端口 80, https 可以省略端口 443。也就是说,https://www.baidu.com 和 https://www.baidu.com:443 显然也是同源的,因为它们是一回事。
不符合同源策略导致的后果有:
但是有一些标签是允许跨域加载资源:
<img>
<link>
<script>
值得注意的几个要点有:
1 | // 服务端 http://127.0.0.1:8000 |
1 | <!-- 客户端 http://127.0.0.1:5500/index.html --> |
果不其然报错了:
JSONP,即 JSON with Padding,是一个非官方的跨域解决方案,纯粹凭借程序员的聪明才智开发出来,只支持 get 请求。
JSONP 工作原理:在网页有一些标签天生就具有跨域能力,比如 img
link
script
等。JSONP 就是利用 script
标签的跨域能力来发送请求的。
举个例子,客户端传入 a 和 b,服务端传回 a + b 的结果。
1 | // 服务端 http://127.0.0.1:8000 |
1 | <!-- 客户端 http://127.0.0.1:5500/index.html --> |
缺点:需要前后端配合,只支持 get 请求。
CORS 全称是 Cross-Orgin Resource Sharing,跨域资源共享。CORS 由后端开启,开启后前端就可以跨域访问后端。
服务端设置 Access-Control-Allow-Origin
就可以开启 CORS。该属性表示哪些域名可以访问资源,如果设置为通配符 *,则表示所有网站都可以访问资源。类似的还有 Access-Control-Allow-Methods
,表示允许的请求方法,Access-Control-Allow-Headers
,表示允许的请求头类型。
设置 CORS 本身和前端没什么关系,但是通过这种方式解决跨域问题的话,会在发送请求时出现两种情况,分别为简单请求和复杂请求。
只要同时满足以下条件的就是简单请求:
请求中的任意 XMLHttpRequest 对象均没有注册任何事件监听器;XMLHttpRequest 对象可以使用 XMLHttpRequest.upload 属性访问。
请求中没有使用 ReadableStream 对象。
不是简单请求的请求就是复杂请求。复杂请求必须首先使用 OPTIONS
请求方法发起一个预检请求到服务器,以获知服务器是否允许该实际请求。预检请求的使用,可以避免跨域请求对服务器的用户数据产生预期之外的影响。
代码示例如下:
1 | <!-- 客户端 http://127.0.0.1:5500/index.html --> |
1 | // 服务端 http://127.0.0.1:8000 |
Websocket 属于应用层协议,它基于 TCP 传输协议,并复用 http 的握手通道。
相比于 http 协议,它的优点是:
因为这种方式本质没有使用 http 的响应头, 因此也没有跨域的限制。
那么为什么 WebSocket 可以跨域呢?
因为 WebSocket 根本不属于同源策略,而且它本身就有意被设计成可以跨域的一个手段。由于历史原因,跨域检测一直是由浏览器端来做,但是 WebSocket 出现以后,对于 WebSocket 的跨域检测工作就交给了服务端,浏览器仍然会带上一个 Origin 跨域请求头,服务端则根据这个请求头判断此次跨域 WebSocket 请求是否合法。
依然以 a + b 问题举例:
1 | <!-- 客户端 http://127.0.0.1:5500/index.html --> |
1 | // 服务端 http://127.0.0.1:8000 |
同源策略只在浏览器存在,无法限制后端。也就是说前端与后端之间会受同源策略影响,但是后端与后端之间不会受到限制。所以可以通过 Node 做一层接口代理,先访问已经设置了 CORS 的后端 1,再让后端 1 去访问后端 2,获取数据后传给后端 1,最后再让后端 1 把数据传回给前端。
客户端代码同上,把请求端口改成 8888 即可。
1 | // 后端 1 http://127.0.0.1:8888 |
1 | // 后端 2 http://127.0.0.1:8000 |
实现原理类似于上面提到的 Node 接口代理,需要你搭建一个中转 Nginx 服务器,用于转发请求。使用 Nginx 反向代理实现跨域,是最简单的跨域方式。只需要修改 Nginx 的配置即可解决跨域问题,支持所有浏览器,支持 session,不需要修改任何代码,并且不会影响服务器的性能。
先根据Nginx安装教程进行 Nginx 的安装。
然后修改 conf 目录下的 nginx.conf 文件:
1 | server{ |
输入命令行
1 | nginx -s reload |
此时客户端请求 8888 端口,就不会跨域了。
参考资料:
https://juejin.cn/post/7017614708832206878
https://juejin.cn/post/6844904126246027278
https://juejin.cn/post/6844903767226351623
https://www.jianshu.com/p/9a8d793ec52a
https://developer.mozilla.org/zh-CN/docs/Web/HTTP/CORS
公众号前端点线面
reverse
1 | /** |
1 | /** |
这题就是上面那题的变式
1 | /** |
1 | /** |
1 | /** |
parseInt
1 | /** |
1 | /** |
1 | /** |
1 | /** |
1 | /** |
1 | /** |
1 | /** |
1 | /** |
1 | /** |
1 | /** |
先遍历所有罗马数字进行累加,对于特殊数字的循环,比如:5 + 1 = 6,而实际是 4,相差 2,所以需要在结果上减去 2。
1 | /** |
1 | /** |
1 | /** |
O(N * loglogN)
1 | /** |
循环和递归都很简单,不说了,还有一种骚操作。
在题目给定的 32 位有符号整数的范围内,最大的 3 的幂为 3 ^ 19 = 1162261467。我们只需要判断 n 是否是这个数的约数即可。
1 | /** |
和正常 0 ~ 25 的 26 进制相比,本质上就是每一位多加了 1,所以只要在处理每一位的时候先减 1,就可以按照正常的 26 进制来处理。
1 | /** |
1 | /** |
首先我们需要清楚,快乐数的计算是可能会导致死循环出现的。遇到判断某个可能的死循环是否满足一定的条件,我们可以使用快慢指针,比如链表的经典题目判断链表是否有环。
1 | /** |
1 | /** |
1 | /** |
二分
使用折半计算,每次把 n 缩小一半,通过递归,最终获取 x 的 n 次幂。比如说要计算 x32,推算过程为:
x32 -> x16 -> x8 -> x4 -> x2 -> x
但是具体的操作还要根据数字的奇偶决定,如要计算 x77,推算过程为:
x77 -> x38 -> x19 -> x9 -> x4 -> x2 -> x
当我们要计算 xn 时,我们可以先递归地计算出 y = xn/2,接着:
边界:n = 0,返回 1
1 | /** |
快速幂
若 n = a1 + a2 + a3 + … + am,则
xn = xa1 * xa2 * xa3 * … * xam
依然以 x77,为例,x -> x2 -> x4 -> x9 -> x19 -> x38 -> x77
因此,我们从 x 开始不断平方,得到 x2、x4、x8、x16,… ,如果 n 的第 k 位(从右往左数)二进制为 1,那么我们就将对应的贡献 x2 ^ k 计入答案。
说实话不是很懂为什么快速幂居然会超时,这测试用例有点那啥(
1 | /** |
console.warn
console.error
console.table
:格式化打印数组 / JSON 数据console.dir
:按照树形结构打印 DOM 节点关于 SourceMap 的原理,推荐这篇文章
关于跨域,推荐这篇文章
传说中程序大师随身携带只小黄鸭,在调试代码的时候会在桌上放上这只小黄鸭,然后详细地向鸭子解释每行代码,然后很快就将问题定位修复了。 ——《程序员修炼之道》
]]>