diff --git "a/blog/develop/Nuxt.js\346\216\245\345\205\245Sentry.md" "b/blog/develop/Nuxt.js\346\216\245\345\205\245Sentry.md" index 5beb367..3d9521e 100644 --- "a/blog/develop/Nuxt.js\346\216\245\345\205\245Sentry.md" +++ "b/blog/develop/Nuxt.js\346\216\245\345\205\245Sentry.md" @@ -13,16 +13,12 @@ image: /img/blog/nuxt-import-sentry/1.webp > Sentry 是一个流行的错误监控平台,帮助开发者分析,修复问题,优化代码的性能。可以进行错误捕获,问题追踪,并提供问题详情,适用于多个平台,多种语言。 ---- - ### sentry 后台 1. sentry 默认是纯英文界面,左上角用户 > User settings > Account Details 修改中文,选择 Simplified Chinese 即可;一并把时区修改为东八区;修改后刷新网页即可显示中文 `提示:尽量第一次就把时区更改,否则下次再进行修改有可能一直修改失败(我就是这样)` ![image.png](/img/blog/nuxt-import-sentry/1.webp) 2. 项目 > 右上角创建项目,选择一个平台;官网的 Platforms 选项中是没有 nuxt 的,所以 Platforms 选择 vue,其实配置上是一样的,配置文件不同而已(`vue.config.js/nuxt.config.js`); 3.底部信息按实际填即可,项目名字即为实际项目名, 点击创建后,会自动跳转[接入文档指引](https://docs.sentry.io/platforms/javascript/guides/vue/) ![image.png](/img/blog/nuxt-import-sentry/2.webp) ---- - ### sentry 接入 1. 安装依赖:`npm install --save @sentry/vue @sentry/tracing` @@ -50,8 +46,6 @@ image: /img/blog/nuxt-import-sentry/1.webp 点进去就可看到详细信息 But 压缩混淆之后的代码就导致:即使代码报错了,我们也只能看到错误信息,还是非常难定位到具体是哪行代码出现的错误,如上图;所以我们如果要定位到问题所在还需要上传 sourcemap 文件。 ---- - ### 上传 sourceMap 1. 安装 SentryWebpackPlugin 插件 @@ -128,8 +122,6 @@ auth.token=8f9ca900719b4eed8ea8ed82726ddce006d2c8a105c4268b508069ebc7b1e 还有一点:只需在生产环境(线上环境)上传 sourceMap 开发环境上传 sourceMap 文件过于频繁,sentry 会报错 ---- - ok,忙活了那么久,又到了验证的时候! 1. 同样,写一个 bug; @@ -137,8 +129,6 @@ ok,忙活了那么久,又到了验证的时候! 然后去问题模块,找到错误信息,点进详情;可以看到,已经显示了具体位置; sourcemap 上传到 sentry 后,sentry 会通过反解 sourcemap,通过行列信息映射到源文件上; ![image.png](/img/blog/nuxt-import-sentry/9.webp) ---- - ### Sentry 面板介绍 ![image.png](/img/blog/nuxt-import-sentry/10.webp) ![image.png](/img/blog/nuxt-import-sentry/11.webp) @@ -147,8 +137,6 @@ ok,忙活了那么久,又到了验证的时候! ![image.png](/img/blog/nuxt-import-sentry/13.webp) 分别是错误页面,UA,用户,浏览器,设备等信息; ---- - ### 根据业务自定义错误详情面板 sentry 源码内有以下方法可调用: ![image.png](/img/blog/nuxt-import-sentry/14.webp) diff --git "a/blog/program/JavaScript\344\271\213\345\207\275\346\225\260\345\274\217\347\274\226\347\250\213.md" "b/blog/program/JavaScript\344\271\213\345\207\275\346\225\260\345\274\217\347\274\226\347\250\213.md" new file mode 100644 index 0000000..86e301c --- /dev/null +++ "b/blog/program/JavaScript\344\271\213\345\207\275\346\225\260\345\274\217\347\274\226\347\250\213.md" @@ -0,0 +1,318 @@ +--- +slug: js-fp-coding +title: JavaScript之函数式编程 +date: 2021-06-02 +authors: youngjeff +tags: [js, FP] +keywords: [js, FP] +description: 函数式编程(Functional Programming, FP),是一种编程范式,常用的编程范式还有:面向对象编程,面向过程编程。 +image: /img/blog/js-fp-coding/1.webp +--- + +## 啥是函数式编程? + +函数式编程(Functional Programming, FP),是一种编程范式,常用的编程范式还有:面向对象编程,面向过程编程; + +- 面向对象编程:把现实世界中的事物抽象成程序中的类和对象,通过封装,多态,继承来演示不同事物之间的联系; +- 函数式编程:把现实中的事物和事物的联系抽象到程序中(把运算过程进行抽象) + +**对函数式编程的理解**: + +- 程序本质:根据输入通过运算获得相应输出 +- 函数式编程中的函数不是指程序中的函数 Function,而是数学中的函数(映射关系),例如:y=f(x) +- 相同的输入始终要得到相同的输出(纯函数) + +``` +// 非函数式 +let n1 = 2 +let n2 = 3 +let sum = n1 + n2 +console.log(sum) + +// 函数式 +function add(n1, n2) { + return n1 + n2 +} +let sum = add(2, 3) +console.log(sum) +``` + +## 为啥要学? + +- 前端领域的流行库:react/vue 都在使用 +- 函数式编程可以抛弃 this +- 有很多库可以帮助我们进行函数式开发,比如:[lodash](https://www.lodashjs.com/) + +## 函数式编程的前置知识 + +1. 在 JavaScript 中,函数是一等公民 +2. 高阶函数(用来屏蔽细节,只关心目标),常用的高阶函数有:filter,map,forEach,every 等 + +函数可以存储在变量中,可以当作参数传递,还能当作返回值 + +``` +// 把函数赋值给变量 +let fn = function () { + console.log("hello") +} +fn() + +// 函数作为参数传递,forEach实现 +function forEach (array, fn) { + for (let i = 0; i < array.length; i++) { + fn(array[i]) + } +} +// test +let arr = [1, 2, 3] +forEach(arr, item => { + item = item * 2 + console.log(item) // 2 4 6 +}) + +// 当作返回值返回 +function fn2(){ + let num = 100; + return function(){ + console.log(num) + } +} +// test +const res = fn2() +res() //100 +``` + +3. 闭包(延长作用域链)闭包的概念:内部函数可以访问外部函数的变量和参数闭包的本质:函数在执行的时候会放在一个执行栈上,当函数执行完毕后会从栈移除,但是,堆上的作用域成员因为还被引用着,得不到释放,因为就可以访问到;继续用上面的代码案例 + +``` +function fn2(){ + let num = 100; +} +// 正常情况下,执行完fn2,里面的变量num会释放掉 +function fn2(){ + let num = 100; + return function(){ + console.log(num) + } +} +// 在上面函数中,返回了一个函数,而且在函数中还访问了原来函数内部的成员,就可以称为闭包 + +// test +const res = fn2() +res() +// res为外部函数,当外部函数对内部成员有引用的时候,那么内部的成员num就不能被释放。当调用res时,就可以访问num。 +``` + +## 纯函数是啥? + +概念:相同的输入永远会得到相同的输出,而且没有任何可观察的副作用。类似数学中的函数,y=f(x) + +![图1](/img/blog/js-fp-coding/1.webp) + +``` +let numbers = [1, 2, 3, 4, 5] +// slice方法是纯函数,截取的时候返回截取的函数,不影响原数组 +numbers.slice(0, 3) // => [1, 2, 3] +numbers.slice(0, 3) // => [1, 2, 3] +numbers.slice(0, 3) // => [1, 2, 3] + +// 不纯的函数 +// 对于相同的输入,输出是不一样的 +// splice方法,返回原数组,改变原数组 +numbers.splice(0, 3) // => [1, 2, 3] +numbers.splice(0, 3) // => [4, 5] +numbers.splice(0, 3) // => [] +``` + +纯函数的优点: + +- 可缓存:因为对于相同的输入始终有相同的结果,那么可以把纯函数的结果缓存起来,可以提高性能 +- 可测试:纯函数让测试更加的方便 +- 并行处理 + +``` +// 调用lodash +const _ = require('lodash') +function getArea(r) { + console.log(r) + return Math.PI * r * r +} + +let getAreaWithMemory = _.memoize(getArea) +console.log(getAreaWithMemory(4)) +console.log(getAreaWithMemory(4)) +console.log(getAreaWithMemory(4)) +// 4 +// 50.26548245743669 +// 50.26548245743669 +// 50.26548245743669 + +// 看到输出的4只执行了一次,因为其结果被缓存下来了 +``` + +下面模拟一个记忆函数 + +``` +function memoize (f) { + let cache = {} + return function () { + // arguments是一个伪数组,所以要进行字符串的转化 + let key = JSON.stringify(arguments) + // 如果缓存中有值就把值赋值,没有值就调用f函数并且把参数传递给它 + cache[key] = cache[key] || f.apply(f,arguments) + return cache[key] + } +} + +let getAreaWithMemory1 = memoize(getArea) +console.log(getAreaWithMemory1(4)) +console.log(getAreaWithMemory1(4)) +console.log(getAreaWithMemory1(4)) +// 4 +// 50.26548245743669 +// 50.26548245743669 +// 50.26548245743669 +``` + +## 函数柯里化又是啥? + +详情可参考另一篇拆解柯里化:[函数柯里化](https://www.jianshu.com/p/4a7c3790822f) + +将多变量函数拆解为单变量的多个函数的依次调用;就是利用函数执行,可以形成一个不销毁的私有作用域,把预先处理的内容放到不销毁的作用域里面,返回一个函数供以后调用; + +``` +// 普通的纯函数 +function checkAge (min, age) { + return age >= min +} +console.log(checkAge(18, 20)) //true +console.log(checkAge(18, 24)) //true +// 经常使用18,这段代码是重复的。为了避免重复改造函数: +function checkAge (min) { + return function (age) { + return age >= min + } +} + +let checkAge18 = checkAge(18) + +console.log(checkAge18(20)) //true +console.log(checkAge18(24)) //true +``` + +lodash 中的柯里化-curry + +``` +const _ = require('lodash') + +// 参数是一个的为一元函数,两个的是二元函数 +// 柯里化可以把一个多元函数转化成一元函数 +function getSum (a, b, c) { + return a + b + c +} +// 定义一个柯里化函数 +const curried = _.curry(getSum) + +// 如果输入了全部的参数,则立即返回结果 +console.log(curried(1, 2, 3)) // 6 +//如果传入了部分的参数,此时它会返回当前函数,并且等待接收getSum中的剩余参数 +console.log(curried(1)(2, 3)) // 6 +console.log(curried(1, 2)(3)) // 6 +``` + +**简单实现一个柯里化转换函数** + +分析: + +1. 调用 curry,传递一个纯函数,完成后返回一个柯里化函数 +2. 如果调用 curried 传递的参数和 getSum 参数个数相同,就立即执行并返回结果;如果调用 curried 传递的是部分参数,那么需要返回一个新函数,等待接受 getSum 其他参数 + +``` +function curry(func) { + return function curriedFn(...args) { + // 判断实参和形参的个数 + console.log('看下args', args); + if (args.length < func.length) { + return function () { + // 等待传递的剩余参数 + // 第一部分参数在args里面,第二部分参数在arguments里面 + console.log('看下arguments', arguments); + return curriedFn(...args.concat(Array.from(arguments))); + }; + } + // 如果实参大于等于形参的个数 + // args是剩余参数 + return func(...args); + }; +} +function getSum(a, b, c) { + return a + b + c; +} + +const curriedTest = curry(getSum) + +console.log(curriedTest(1, 2, 3)) // 6 +console.log(curriedTest(1)(2, 3)) // 6 +console.log(curriedTest(1, 2)(3)) // 6 + +``` + +柯里化优点: + +- 参数复用(对函数参数的‘缓存’) +- 让函数粒度更细,变的更灵活 +- 将多元函数比变成一元函数,然后组合函数产生更强大功能 + +## 函数组合 + +纯函数和柯里化很容易写出洋葱代码 h(g(f(x))),函数组合可以避免这种情况; + +``` +a --> fn --> b +a-> f3 -> m -> f2 -> n -> f1 -> b +其实中间m、n、是什么我们也不关心 类似于下面的函数 +``` + +先来看看 Lodash 中的组合函数用法 + +- flow() //从左往右执行 +- flowRight() //从右往左执行 + +``` +// 获取数组的最后一个元素并转化成大写字母 +const _ = require('lodash') + +const reverse = arr => arr.reverse() +const first = arr => arr[0] +const toUpper = s => s.toUpperCase() + +const f = _.flowRight(toUpper, first, reverse) + +console.log(f(['one', 'two', 'three'])) // THREE +``` + +**简单实现一个 flowRight 函数** 分析: 入参不固定,都是函数,出参是一个函数,这个函数要接受一个初始值 + +``` +function compose(...args) { + // args代表调用compose传入的要组合的函数数组 + return function (value) { + // compose返回的函数接受一个初始值value + // 因为要从右往左执行,所以数组反转一下 + // reduce方法接受两个参数:一个迭代函数,一个初始化值; + // 其中的迭代函数的前两个参数:total代表上一次调用fn的返回值,fn指当前正在处理值(此处是函数) + return args.reverse().reduce(function (total, fn) { + return fn(total); + }, value); + }; +} + +//test +const reverse = (arr) => arr.reverse(); +const first = (arr) => arr[0]; +const toUpper = (s) => s.toUpperCase(); + +const fTest = compose(toUpper, first, reverse); +console.log(fTest(['one', 'two', 'three'])); // THREE + +``` diff --git "a/blog/program/Lodash\347\232\204FP\346\250\241\345\235\227.md" "b/blog/program/Lodash\347\232\204FP\346\250\241\345\235\227.md" new file mode 100644 index 0000000..27c7df4 --- /dev/null +++ "b/blog/program/Lodash\347\232\204FP\346\250\241\345\235\227.md" @@ -0,0 +1,78 @@ +--- +slug: lodash-fp +title: Lodash的FP模块 +date: 2021-07-02 +authors: youngjeff +tags: [lodash, FP] +keywords: [lodash, FP] +description: 经常用Lodash的你,是否了解过它提供的FP模块? +image: /img/blog/lodash-fp/1.webp +--- + +> 前言:经常用 Lodash 的你,是否了解过它提供的 FP 模块? FP 是啥 :[FP(Functional Programming):函数式编程](https://www.jianshu.com/p/3fa8d5242659) + +答:[函数组合](https://www.jianshu.com/p/3fa8d5242659)时有很多函数需要频繁[柯里化](https://www.jianshu.com/p/3fa8d5242659),而 Lodash/fp 模块就是解决此问题的; + +FP 模块特性: + +- auto-curried iteratee-first data-last (函数之先,数据之后) +- 自动 curry 化 +- immutable + +Lodash 普通函数使用方法 + +``` +// 数据置先,函数置后 +_.map(['a', 'b', 'c'], _.toUpper) +``` + +FP 模块使用方法 + +``` +// 函数置先,数据置后 +fp.map(fp.toUpper, ['a', 'b', 'c']) +fp.map(fp.toUpper)(['a', 'b', 'c']) +``` + +FP 模块对于组合函数的友好 + +``` +const fp = require('lodash/fp') + +const f = fp.flowRight(fp.join('-'), fp.map(fp.toLower), fp.split(' ')) + +console.log(f('NEVER SAY DIE')) // never-say-die +``` + +使用者可以不用关心具体调用了哪个函数,每个函数可以随意组合调整。 + +--- + +**Lodash 中 map 方法的小问题** + +``` +const _ = require('lodash') +console.log(_.map(['23', '8', '10'], parseInt)) +``` + +期望结果:`[23, 8, 10] ` 实际结果:`[ 23, NaN, 2 ] ` **>为啥呢?** + +![图1](/img/blog/lodash-fp/1.webp) + +![图2](/img/blog/lodash-fp/2.webp) + +**原因:** \_.map 的第二个参数(迭代函数)接受三个参数,第一个是遍历的当前值,第二个是当前索引,第三个是数组本身; parseInt 第二个参数表示进制基数,可选值是 2-36 之间的整数所以运算结果是 + +``` +parseInt('23', 0, array) //0表示默认值10 +parseInt('8', 1, array) +parseInt('10', 2, array) +//[ 23, NaN, 2 ] +``` + +**而使用 fp 模块的 map 方法不存在下面的问题** + +``` +console.log(fp.map(parseInt, ['23', '8', '10'])) +// [ 23, 8, 10 ] +``` diff --git "a/blog/program/SSR\345\255\246\344\271\240\346\200\273\347\273\223.md" "b/blog/program/SSR\345\255\246\344\271\240\346\200\273\347\273\223.md" new file mode 100644 index 0000000..fa0c40d --- /dev/null +++ "b/blog/program/SSR\345\255\246\344\271\240\346\200\273\347\273\223.md" @@ -0,0 +1,100 @@ +--- +slug: ssr-study +title: SSR学习总结 +date: 2022-05-31 +authors: youngjeff +tags: [code, 总结] +keywords: [code, 总结] +description: SSR学习总结。 +image: /img/blog/ssr-study/1.webp +--- + +## 概述 + +随着前端技术栈和工具链的迭代成熟,前端工程化、模块化也已成为了当下的主流技术方案,在这波前端技术浪潮中,涌现了诸如 React、Vue、Angular 等基于客户端渲染的前端框架,这类框架所构建的单页应用(SPA)具有用户体验好、渲染性能好、可维护性高等优点。但也有一些很大的缺陷。 + +其中,主要涉及到以下两点:(1)首屏加载时间过长与传统服务端渲染直接获取服务端渲染好的 HTML 不同,单页应用使用 JavaScript 在客户端生成 HTML 来呈现内容,用户需要等待客户端 JS 解析执行完成才能看到页面,这就使得首屏加载时间变长,从而影响用户体验。(2)不利于 SEO 当搜索引擎爬取网站 HTML 文件时,单页应用的 HTML 没有内容,因为他它需要通过客户端 JavaScript 解析执行才能生成网页内容,而目前的主流的搜索引擎对于这一部分内容的抓取还不是很好。 + +为了解决这两个缺陷,业界借鉴了传统的服务端直出 HTML 方案,提出在服务器端执行前端框架(React/Vue/Angular)代码生成网页内容,然后将渲染好的网页内容返回给客户端,客户端只需要负责展示就可以了 + +![图1](/img/blog/ssr-study/1.webp) + +当然不仅仅如此,为了获得更好的用户体验,同时会在客户端将来自服务端渲染的内容激活为一个 SPA 应用,也就是说之后的页面内容交互都是通过客户端渲染处理。 + +![图2](/img/blog/ssr-study/2.webp) + +这种方式简而言之就是:通过服务端渲染首屏直出,解决首屏渲染慢以及不利于 SEO 问题通过客户端渲染接管页面内容交互得到更好的用户体验这种方式我们通常称之为现代化的服务端渲染,也叫同构渲染,所谓的同构指的就是服务端构建渲染 + 客户端构建渲染。同理,这种方式构建的应用称之为服务端渲染应用或者是同构应用。 + +## 什么是渲染 + +我们这里所说的渲染指的是把(数据 + 模板)拼接到一起的这个事儿。例如对于我们前端开发者来说最常见的一种场景就是:请求后端接口数据,然后将数据通过模板绑定语法绑定到页面中,最终呈现给用户。这个过程就是我们这里所指的渲染。 + +渲染本质其实就是字符串的解析替换,实现方式有很多种;但是我们这里要关注的并不是如何渲染,而是在哪里渲染的问题? + +最早期,Web 页面渲染都是在服务端完成的,即服务端运行过程中将所需的数据结合页面模板渲染为 HTML,响应给客户端浏览器。所以浏览器呈现出来的是直接包含内容的页面。 + +工作流程: + +![图3](/img/blog/ssr-study/3.webp) + +这种方式的代表性技术有:ASP、PHP、JSP,再到后来的一些相对高级一点的服务端框架配合一些模板引擎。 + +在今天看来,这种渲染模式是不合理或者说不先进的。因为在当下这种网页越来越复杂的情况下,这种模式存在很多明显的不足:应用的前后端部分完全耦合在一起,在前后端协同开发方面会有非常大的阻力;前端没有足够的发挥空间,无法充分利用现在前端生态下的一些更优秀的方案;由于内容都是在服务端动态生成的,所以服务端的压力较大;相比目前流行的 SPA 应用来说,用户体验一般; + +但是不得不说,在网页应用并不复杂的情况下,这种方式也是可取的。 + +## 客户端渲染 + +传统的服务端渲染有很多问题,但是这些问题随着客户端 Ajax 技术的普及得到了有效的解决,Ajax 技术可以使得客户端动态获取数据变为可能,也就是说原本服务端渲染这件事儿也可以拿到客户端做了。下面是基于客户端渲染的 SPA 应用的基本工作流程。 + +![图4](/img/blog/ssr-study/4.webp) + +但是这种模式下,也会存在一些明显的不足,其中最主要的就是: + +- 首屏渲染慢:因为 HTML 中没有内容,必须等到 JavaScript 加载并执行完成才能呈现页面内容。 +- SEO 问题:同样因为 HTML 中没有内容,所以对于目前的搜索引擎爬虫来说,页面中没有任何有用的信息,自然无法提取关键词,进行索引了。 + +对于客户端渲染的 SPA 应用的问题有没有解决方案呢?服务端渲染,严格来说是现代化的`服务端渲染`,也叫`同构渲染` + +## 现代化的服务端渲染 + +Nuxt.js 是一个基于 Vue.js 生态开发的一个第三方服务端渲染框架,通过它我们可以轻松构建现代化的服务端渲染应用。 + +isomorphic web apps(同构应用):isomorphic/universal,基于 react、vue 框架,客户端渲染和服务器端渲染的结合,在服务器端执行一次,用于实现服务器端渲染(首屏直出),在客户端再执行一次,用于接管页面交互,核心解决 SEO 和首屏渲染慢的问题。 + +![图5](/img/blog/ssr-study/5.webp) + +1. 客户端发起请求 +2. 服务端渲染首屏内容 + 生成客户端 SPA 相关资源 +3. 服务端将生成的首屏资源发送给客户端 +4. 客户端直接展示服务端渲染好的首屏内容 +5. 首屏中的 SPA 相关资源执行之后会激活客户端 Vue +6. 之后客户端所有的交互都由客户端 SPA 处理 + +## 相关技术 + +React 生态中的 Next.js Vue 生态中的 Nuxt.js Angular 生态中的 Angular Universal 实现原理: + +![图6](/img/blog/ssr-study/6.webp) + +## 服务端渲染的问题: + +- 开发条件所限。浏览器特定的代码,只能在某些生命周期钩子函数 (lifecycle hook) 中使用;一些外部扩展库 (external library) 可能需要特殊处理,才能在服务器渲染应用程序中运行。 +- 涉及构建设置和部署的更多要求。与可以部署在任何静态文件服务器上的完全静态单页面应用程序 (SPA) 不同,服务器渲染应用程序,需要处于 Node.js server 运行环境。 + +- 更多的服务器端负载。在 Node.js 中渲染完整的应用程序,显然会比仅仅提供静态文件的 server +- 更加大量占用 CPU 资源 (CPU-intensive - CPU 密集),因此如果你预料在高流量环境 (high traffic) 下使用,请准备相应的服务器负载,并明智地采用缓存策略。 + +所以,在对你的应用程序使用服务器端渲染 (SSR) 之前,你应该问的第一个问题是,是否真的需要它。这主要取决于内容到达时间 对应用程序的重程度。 + +例如,如果你正在构建一个内部仪表盘,初始加载时的额外几百毫秒并不重要,这种情况下去使用服务器端渲染 (SSR) 将是一个小题大作之举。 + +然而,内容到达时间 要求是绝对关键的指标,在这种情况下,服务器端渲染 (SSR) 可以帮助你实现最佳的初始加载性能。 + +事实上,很多网站是出于效益的考虑才启用服务端渲染,性能倒是在其次。 + +假设 A 网站页面中有一个关键字叫“前端性能优化”,这个关键字是 JS 代码跑过一遍后添加到 HTML 页面中的。那么客户端渲染模式下,我们在搜索引擎搜索这个关键字,是找不到 A 网站的——搜索引擎只会查找现成的内容,不会帮你跑 JS 代码。 + +A 网站的运营方见此情形,感到很头大:搜索引擎搜不出来,用户找不到我们,谁还会用我的网站呢?为了把“现成的内容”拿给搜索引擎看,A 网站不得不启用服务端渲染。 但性能在其次,不代表性能不重要。 + +以上。 diff --git "a/blog/project/gulp\350\207\252\345\212\250\345\214\226\346\236\204\345\273\272\346\241\210\344\276\213.md" "b/blog/project/gulp\350\207\252\345\212\250\345\214\226\346\236\204\345\273\272\346\241\210\344\276\213.md" new file mode 100644 index 0000000..29c44a8 --- /dev/null +++ "b/blog/project/gulp\350\207\252\345\212\250\345\214\226\346\236\204\345\273\272\346\241\210\344\276\213.md" @@ -0,0 +1,664 @@ +--- +slug: gulp-auto-build-case +title: gulp自动化构建案例 +date: 2021-07-22 +authors: youngjeff +tags: [case, code, 总结] +keywords: [case, code, 总结] +description: gulp自动化构建案例。 +image: /img/blog/gulp-auto-build-case/1.webp +--- + +## 官网:[Gulp](https://www.gulpjs.com.cn/docs/api/concepts/) + +> 代码块中的省略号,代表相较于上次代码未改动部分 github 完整项目: [pages-boilerplate](https://github.com/xiaofeng63/pages-boilerplate.git) + +## 准备内容: + +1. 首先初始化项目,目录如下图 +2. 安装 gulp,作为开发时依赖项`npm install --save-dev gulp` +3. 根目录下新增 gulpfile.js 文件,此文件中构建任务 + +![图1](/img/blog/gulp-auto-build-case/1.webp) + +## 构建任务: + +1. 样式文件编译首先安装[gulp-sass](https://www.npmjs.com/package/gulp-sass)到开发依赖 + +``` +//gulpfile.js文件 +const { src, dest } = require("gulp"); +const sass = require("gulp-sass")(require("sass")); + +const style = () => { + return src("src/assets/styles/*.scss", { base: "src" }) + .pipe(sass()) + .pipe(dest("dist")); +}; +module.exports = { + style, +}; +``` + +根目录命令行执行`yarn gulp style `验证 + +2. 脚本编译首先安装 gulp-babel 到开发依赖 + +``` +const { src, dest } = require("gulp"); +const babel = require("gulp-babel"); + +const script = () => { + return src("src/assets/scripts/*.js", { base: "src" }) + .pipe( + babel({ + presets: ["@babel/env"], + }) + ) + .pipe(dest("dist")); +}; +module.exports = { + script, +}; +``` + +3. 页面模版编译首先安装 gulp-swig 到开发依赖 + +``` +const { src, dest } = require("gulp"); +const swig = require("gulp-swig"); + +//假数据 +const data = { + menus: [ + { + name: "Home", + icon: "aperture", + link: "index.html", + }, + { + name: "Features", + link: "features.html", + }, + { + name: "About", + link: "about.html", + }, + ], + pkg: require("./package.json"), + date: new Date(), +}; +const page = () => { + return src("src/*.html", { base: "src" }) + .pipe( + swig({ + data, + }) + ) + .pipe(dest("dist")); +}; +module.exports = { + page, +}; +``` + +根目录命令行执行`yarn gulp compile `验证 + +4. 图片和字体文件转换首先安装 gulp-imagemin 到开发依赖 + +``` +const imgage = () => { + return src("src/assets/images/**", { base: "src" }) + .pipe(imagemin()) + .pipe(dest("dist")); +}; +const font = () => { + return src("src/assets/fonts/**", { base: "src" }) + .pipe(imagemin()) + .pipe(dest("dist")); +}; +``` + +`因为以上任务都是可以异步进行的,所有通过gulp提供的parallel方法,将以上任务组合起来` + +``` +// gulpfile.js文件 +const { src, dest, parallel } = require("gulp"); +const sass = require("gulp-sass")(require("sass")); +const babel = require("gulp-babel"); +const swig = require("gulp-swig"); +const imagemin = require("gulp-imagemin"); + +const data = { + menus: [ + { + name: "Home", + icon: "aperture", + link: "index.html", + }, + { + name: "Features", + link: "features.html", + }, + { + name: "About", + link: "about.html", + }, + ], + pkg: require("./package.json"), + date: new Date(), +}; + +const style = () => { + return src("src/assets/styles/*.scss", { base: "src" }) + .pipe(sass()) + .pipe(dest("dist")); +}; + +const script = () => { + return src("src/assets/scripts/*.js", { base: "src" }) + .pipe( + babel({ + presets: ["@babel/env"], + }) + ) + .pipe(dest("dist")); +}; +const page = () => { + return src("src/*.html", { base: "src" }) + .pipe( + swig({ + data, + }) + ) + .pipe(dest("dist")); +}; +const imgage = () => { + return src("src/assets/images/**", { base: "src" }) + .pipe(imagemin()) + .pipe(dest("dist")); +}; +const font = () => { + return src("src/assets/fonts/**", { base: "src" }) + .pipe(imagemin()) + .pipe(dest("dist")); +}; +const compile = parallel(style, script, page, imgage, font); +module.exports = { + compile, +}; +``` + +以上,src 目录下文件处理完毕;接下来处理 public + +5. public 处理及自动删除 dist 目录首先安装 del 到开发依赖 + +``` +// gulpfile.js文件 +const { src, dest, parallel, series } = require("gulp"); +const sass = require("gulp-sass")(require("sass")); +const babel = require("gulp-babel"); +const swig = require("gulp-swig"); +const imagemin = require("gulp-imagemin"); +const del = require("del"); + +... +const extra = () => { + return src("public/**", { base: "public" }).pipe(dest("dist")); +}; +const compile = parallel(style, script, page, image, font); +//因为要在编译之前,把dist目录清除,所有用series再次组合 +const build = series(clean, parallel(compile, extra)); +module.exports = { + compile, + build, + clean, +}; +``` + +6. 自动加载插件首先安装 gulp-load-plugins 到开发依赖,然后只需要把所有 gulp-开头的插件引用更改为 plugins.+插件不包括 gulp-的内容 `例:(gulp-babel改为plugins.babel)` + +``` +// gulpfile.js +const { src, dest, parallel, series } = require("gulp"); +const sass = require("gulp-sass")(require("sass")); +// 删除即可 +// const plugins.babel = require("gulp-babel"); +// const plugins.swig = require("gulp-swig"); +// const plugins.imagemin = require("gulp-imagemin"); +const del = require("del"); +const plugins = require("gulp-load-plugins")(); + +... +const script = () => { + return src("src/assets/scripts/*.js", { base: "src" }) + .pipe( + plugins.babel({ + presets: ["@babel/env"], + }) + ) + .pipe(dest("dist")); +}; +const page = () => { + return src("src/*.html", { base: "src" }) + .pipe( + plugins.swig({ + data, + }) + ) + .pipe(dest("dist")); +}; +const image = () => { + return src("src/assets/images/**", { base: "src" }) + .pipe(plugins.imagemin()) + .pipe(dest("dist")); +}; +const font = () => { + return src("src/assets/fonts/**", { base: "src" }) + .pipe(plugins.imagemin()) + .pipe(dest("dist")); +}; +... +``` + +7. 开发服务器首先安装[browser-sync](browsersync.cn/docs/gulp/)到开发依赖这里只记录简单用法,具体可参考官网^ + +``` +// 实现这个项目的构建任务 +const { src, dest, parallel, series } = require("gulp"); +... +const browserSync = require("browser-sync").create(); +... +const serve = () => { + browserSync.init({ + files: "dist/**", //files指定文件,监听到变化就自动刷新(注:此配置只会监听dist目录,而src不会,因为src修改后需要重新编译,下面会处理) + server: "dist", + }); +}; +... +module.exports = { + serve, +}; +``` + +8. 监听文件变化以及构建优化 `注意:可能因为swig模版引擎的缓存机制导致页面不会变化,此时需要配置swig中的cache为false` + +- 图片和字体等文件在开发阶段没必要构建,因为这些文件可能只是做了压缩,并不影响页面上的呈现效果,所以为了减小开发阶段的开销,这些文件只在发布上线之前构建 +- 所以对于图片和 public 中的文件,在此直接请求源文件(非 dist) + +``` +//gulpfile.js +const { src, dest, parallel, series, watch } = require("gulp"); +... +const page = () => { + return src("src/*.html", { base: "src" }) + .pipe( + plugins.swig({ + data, + defaults: { cache: false }, //配置成false,防止缓存机制导致页面不会变化 + }) + ) + .pipe(dest("dist")); +}; +... +const serve = () => { + watch("src/assets/styles/*.scss", script); + watch("src/assets/scripts/*.js", script); + watch("src/*.html", page); + // watch("src/assets/images/**", image); + // watch("src/assets/fonts/**", font); + // watch("public/**", extra); + + // 想要监听public或者imgags变化,可以利用browserSync提供的reload方法 + // 该 reload 方法会通知所有的浏览器相关文件被改动,要么导致浏览器刷新,要么注入文件,实时更新改动。 + watch( + ["src/assets/images/**", "src/assets/fonts/**", "public/**"], + browserSync.reload() + ); + browserSync.init({ + files: "dist/**", //files指定文件,监听到变化就自动刷新(注:此配置只会监听dist目录,而src不会,因为src修改后需要重新编译,下面会处理) + server: { + baseDir: ["dist", "src", "public"], //多个基目录,在dist目录下找不到就去src找,否则就去public找,以此类推 + }, + }); +}; +... +// 构建任务优化 +const compile = parallel(style, script, page); +// 上线之前执行的任务 +const build = series(clean, parallel(compile, extra, image, font)); +// 开发阶段执行的任务 +const develop = series(compile, serve); +module.exports = { + build, + clean, + serve, + develop, +}; +``` + +9. useref 文件引用及文件压缩针对 html 文件中,会有一些 node_modules 中的引用文件,在开发阶段,我们可以通过 Browsersync 模块中 server 的 routes 做一个映射来解决 + +``` +// index.html文件 + + + + + ... + + + +... +``` + +``` +// gulpfile.js文件 +... +const serve = () => { + ... + browserSync.init({ + files: "dist/**", //files指定文件,监听到变化就自动刷新(注:此配置只会监听dist目录,而src不会,因为src修改后需要重新编译,下面会处理) + server: { + baseDir: ["dist", "src", "public"], //多个基目录,在dist目录下找不到就去src找,否则就去public找,以此类推 + routes: { + "/node_modules": "node_modules", //通过映射,获取node_modules下的引用 + }, + }, + }); +}; +... +``` + +但是线上环境此方法行不通了,useref 插件便可以解决这个问题(`注:它只负责合并,不负责压缩,配合gulp-if插件可实现压缩`) [gulp-useref](https://www.npmjs.com/package/gulp-useref)这是一款可以将 html 引用的多个 css 和 js 合并起来,减小依赖的文件个数,从而减少浏览器发起的请求次数。gulp-useref 根据注释将 html 中需要合并压缩的区块找出来,对区块内的所有文件进行合并 + +useref 插件会自动处理 html 中的构建注释,构建注释模块由 build:开始,endbulid 结束,中间内容都是引入模块,build:后会跟标记,说明引入的是 js 或 css,最后再指定一个路径,最终注释模块内的引入都是打包到这一个路径中 + +``` +// 构建注释 + + + +``` + +**_使用前后对比_** + +![图2](/img/blog/gulp-auto-build-case/2.webp) + +vs + +![图3](/img/blog/gulp-auto-build-case/3.webp) + +**接下来开始压缩文件** 要压缩的文件有 html css js,所有分别安装`gulp-clean-css` `gulp-htmlmin` `gulp-uglify `到开发依赖,另外我们需要针对不同文件做不同压缩,所以还需安装`gulp-if` + +``` +... +const useref = () => { + // 为啥不是src下的文件呢,因为src下的html是模版,没有意义,必须得是生成后的dist目录才有意义 + return ( + src("dist/*.html", { base: "dist" }) + .pipe(plugins.useref({ searchPath: ["dist", "."] })) + // html js css + .pipe(plugins.if(/\.js$/, plugins.uglify())) + .pipe(plugins.if(/\.css$/, plugins.cleanCss())) + .pipe( + plugins.if( + /\.html$/, + plugins.htmlmin({ + collapseWhitespace: true, //压缩html的空白行 + minifyCSS: true, //压缩html中的css + minifyJS: true, //压缩html中的js + }) + ) + ) + .pipe(dest("release")) //因为放到dist目录,可能导致读写冲突,所以临时写一个目录 + ); +}; +... +``` + +10. 重新规划构建过程原本打包上线的目录应该是 dist 目录,但是因为上述打包过程防止读写冲突,临时把文件放在了 release 目录,这个时候,我们需要上线的应该是 release 目录,而 release 目录又没有图片和字体文件,所以需要重新调整; + +其实,在 useref 之前生成的文件算是一个中间产物,所以应该把 script,page,style 这些任务生成的文件放在一个临时目录,然后 useref 拿到临时目录文件,转换后再放到最终目录 dist;(**_因为 image,font,extra 这三个任务在打包上线之前才会做,不会影响 useref,所以直接放到 dist_**); + +所以修改后代码如下: + +``` +//gulpfile.js文件 +... +const clean = () => { + return del(["dist", "temp"]); +}; + +const style = () => { + return src("src/assets/styles/*.scss", { base: "src" }) + .pipe(sass()) + .pipe(dest("temp")); +}; + +const script = () => { + return src("src/assets/scripts/*.js", { base: "src" }) + .pipe( + plugins.babel({ + presets: ["@babel/env"], + }) + ) + .pipe(dest("temp")); +}; +const page = () => { + return src("src/*.html", { base: "src" }) + .pipe( + plugins.swig({ + data, + defaults: { cache: false }, + }) + ) + .pipe(dest("temp")); +}; +... +const serve = () => { + watch("src/assets/styles/*.scss", script); + watch("src/assets/scripts/*.js", script); + watch("src/*.html", page); + // 想要监听public或者imgags变化,可以利用browserSync提供的reload方法 + // 该 reload 方法会通知所有的浏览器相关文件被改动,要么导致浏览器刷新,要么注入文件,实时更新改动。 + watch( + ["src/assets/images/**", "src/assets/fonts/**", "public/**"], + browserSync.reload() + ); + browserSync.init({ + files: "dist/**", //files指定文件,监听到变化就自动刷新(注:此配置只会监听dist目录,而src不会,因为src修改后需要重新编译,下面会处理) + server: { + baseDir: ["temp", "src", "public"], //多个基目录,在dist目录下找不到就去src找,否则就去public找,以此类推 + routes: { + "/node_modules": "node_modules", //通过映射,获取node_modules下的引用 + }, + }, + }); +}; +const useref = () => { + // 为啥不是src下的文件呢,因为src下的html是模版,没有意义,必须得是生成后的dist目录才有意义 + return ( + src("temp/*.html", { base: "temp" }) + .pipe(plugins.useref({ searchPath: ["temp", "."] })) + // html js css + .pipe(plugins.if(/\.js$/, plugins.uglify())) + .pipe(plugins.if(/\.css$/, plugins.cleanCss())) + .pipe( + plugins.if( + /\.html$/, + plugins.htmlmin({ + collapseWhitespace: true, //压缩html的空白行 + minifyCSS: true, //压缩html中的css + minifyJS: true, //压缩html中的js + }) + ) + ) + .pipe(dest("dist")) //因为放到dist目录,可能导致读写冲突,所以临时写一个目录 + ); +}; +const compile = parallel(style, script, page); +// 上线之前执行的任务 +// 因为useref依赖compile任务,所以两者同步组合,然后和其他任务异步组合 +const build = series( + clean, + parallel(series(compile, useref), extra, image, font) +); +// 开发阶段执行的任务 +const develop = series(compile, serve); +module.exports = { + build, + clean, + serve, + compile, + develop, + useref, +}; +``` + +如下图:命令行构建日志可以验证任务配置没问题 + +![图4](/img/blog/gulp-auto-build-case/4.webp) + +11. 完整版 gulpfile.js 只暴露必要的任务 + +``` +// 实现这个项目的构建任务 +const { src, dest, parallel, series, watch } = require("gulp"); +const sass = require("gulp-sass")(require("sass")); +const del = require("del"); +const plugins = require("gulp-load-plugins")(); +const browserSync = require("browser-sync").create(); + +const data = { + menus: [ + { + name: "Home", + icon: "aperture", + link: "index.html", + }, + { + name: "Features", + link: "features.html", + }, + { + name: "About", + link: "about.html", + }, + ], + pkg: require("./package.json"), + date: new Date(), +}; + +const clean = () => { + return del(["dist", "temp"]); +}; + +const style = () => { + return src("src/assets/styles/*.scss", { base: "src" }) + .pipe(sass()) + .pipe(dest("temp")); +}; + +const script = () => { + return src("src/assets/scripts/*.js", { base: "src" }) + .pipe( + plugins.babel({ + presets: ["@babel/env"], + }) + ) + .pipe(dest("temp")); +}; +const page = () => { + return src("src/*.html", { base: "src" }) + .pipe( + plugins.swig({ + data, + defaults: { cache: false }, + }) + ) + .pipe(dest("temp")); +}; +const image = () => { + return src("src/assets/images/**", { base: "src" }) + .pipe(plugins.imagemin()) + .pipe(dest("dist")); +}; +const font = () => { + return src("src/assets/fonts/**", { base: "src" }) + .pipe(plugins.imagemin()) + .pipe(dest("dist")); +}; +const extra = () => { + return src("public/**", { base: "public" }).pipe(dest("dist")); +}; + +const serve = () => { + watch("src/assets/styles/*.scss", script); + watch("src/assets/scripts/*.js", script); + watch("src/*.html", page); + // 想要监听public或者imgags变化,可以利用browserSync提供的reload方法 + // 该 reload 方法会通知所有的浏览器相关文件被改动,要么导致浏览器刷新,要么注入文件,实时更新改动。 + watch( + ["src/assets/images/**", "src/assets/fonts/**", "public/**"], + browserSync.reload() + ); + browserSync.init({ + files: "dist/**", //files指定文件,监听到变化就自动刷新(注:此配置只会监听dist目录,而src不会,因为src修改后需要重新编译,下面会处理) + server: { + baseDir: ["temp", "src", "public"], //多个基目录,在dist目录下找不到就去src找,否则就去public找,以此类推 + routes: { + "/node_modules": "node_modules", //通过映射,获取node_modules下的引用 + }, + }, + }); +}; +const useref = () => { + // 为啥不是src下的文件呢,因为src下的html是模版,没有意义,必须得是生成后的dist目录才有意义 + return ( + src("temp/*.html", { base: "temp" }) + .pipe(plugins.useref({ searchPath: ["temp", "."] })) + // html js css + .pipe(plugins.if(/\.js$/, plugins.uglify())) + .pipe(plugins.if(/\.css$/, plugins.cleanCss())) + .pipe( + plugins.if( + /\.html$/, + plugins.htmlmin({ + collapseWhitespace: true, //压缩html的空白行 + minifyCSS: true, //压缩html中的css + minifyJS: true, //压缩html中的js + }) + ) + ) + .pipe(dest("dist")) //因为放到dist目录,可能导致读写冲突,所以临时写一个目录 + ); +}; +const compile = parallel(style, script, page); +// 上线之前执行的任务 +// 因为useref依赖compile任务,所以两者同步组合,然后和其他任务异步组合 +const build = series( + clean, + parallel(series(compile, useref), extra, image, font) +); +// 开发阶段执行的任务 +const develop = series(compile, serve); +module.exports = { + build, + clean, + develop, +}; +``` + +也可在 package.json 中配置脚本,方便执行 + +``` +//package.json文件 +{ + "scripts": { + "clean": "gulp clean", + "build": "gulp build", + "develop": "gulp develop" + }, +} +``` diff --git "a/blog/program/\350\256\260\344\270\200\346\254\241es6\345\261\225\345\274\200\350\277\220\347\256\227\347\254\246\357\274\210...\357\274\211\346\265\217\350\247\210\345\231\250\345\205\274\345\256\271\351\227\256\351\242\230.md" "b/blog/project/\350\256\260\344\270\200\346\254\241es6\345\261\225\345\274\200\350\277\220\347\256\227\347\254\246\357\274\210...\357\274\211\346\265\217\350\247\210\345\231\250\345\205\274\345\256\271\351\227\256\351\242\230.md" similarity index 99% rename from "blog/program/\350\256\260\344\270\200\346\254\241es6\345\261\225\345\274\200\350\277\220\347\256\227\347\254\246\357\274\210...\357\274\211\346\265\217\350\247\210\345\231\250\345\205\274\345\256\271\351\227\256\351\242\230.md" rename to "blog/project/\350\256\260\344\270\200\346\254\241es6\345\261\225\345\274\200\350\277\220\347\256\227\347\254\246\357\274\210...\357\274\211\346\265\217\350\247\210\345\231\250\345\205\274\345\256\271\351\227\256\351\242\230.md" index b6673c8..a805d02 100644 --- "a/blog/program/\350\256\260\344\270\200\346\254\241es6\345\261\225\345\274\200\350\277\220\347\256\227\347\254\246\357\274\210...\357\274\211\346\265\217\350\247\210\345\231\250\345\205\274\345\256\271\351\227\256\351\242\230.md" +++ "b/blog/project/\350\256\260\344\270\200\346\254\241es6\345\261\225\345\274\200\350\277\220\347\256\227\347\254\246\357\274\210...\357\274\211\346\265\217\350\247\210\345\231\250\345\205\274\345\256\271\351\227\256\351\242\230.md" @@ -28,8 +28,6 @@ image: /img/blog/es6-expansion-operator-bug/1.webp ![图5](/img/blog/es6-expansion-operator-bug/5.webp) ---- - ## 解决问题 1)首先通过项目根目录下执行`npx browserslist` ,查看筛选后兼容的浏览器(如图 6) ![图6](/img/blog/es6-expansion-operator-bug/6.webp) diff --git "a/docs/skill/coding/Promise\346\211\213\345\206\231.md" "b/docs/skill/coding/Promise\346\211\213\345\206\231.md" new file mode 100644 index 0000000..c1a2426 --- /dev/null +++ "b/docs/skill/coding/Promise\346\211\213\345\206\231.md" @@ -0,0 +1,974 @@ +--- +slug: promise-write +title: Promise手写 +date: 2021-07-12 +authors: youngjeff +tags: [源码实现, 总结] +keywords: [源码实现, 总结] +description: Promise手写 +--- + +> 代码块中的省略号,代表相较于上次代码未改动部分 + +## 1)核心逻辑实现 + +**分析:** + +1. 根据调用方式可知,promise 是一个类,需要传递一个执行器进去,执行器会立即执行 +2. promise 有三种状态,分别为成功-fulfilled 失败-rejected 等待-pending,一旦状态确定就不可改变 pending > fulfilled pending > rejected +3. resolve 和 reject 函数是用来改变状态的,resolve 是成功,reject 是失败; +4. then 接受两个参数,如果状态成功就调用成功回调函数(参数代表成功结果),否则就调用失败回调(参数代表失败原因) + +**分析完毕,开搞:** + +``` +const PENDING = 'pending'; +const FULFILLED = 'fulfilled'; +const REJECTED = 'rejected'; +class MyPromise { + constructor(exector) { + // exector是一个执行器,传入resolve和reject方法,进入会立即执行, + exector(this.resolve, this.reject); + } + // 实例对象上属性,初始状态为等待 + status = PENDING; + // 成功后的值 + value = undefined; + // 失败后的原因 + reason = undefined; + // 使用箭头函数,让this指向当前实例对象 + resolve = (value) => { + // 判断状态不是等待,阻止执行 + if (this.status !== PENDING) return; + // 将状态改为成功,并保存成功值 + this.status = FULFILLED; + this.value = value; + }; + reject = (reason) => { + if (this.status !== PENDING) return; + // 将状态改为失败,并保存失败原因 + this.status = REJECTED; + this.reason = reason; + }; + then(successCallback, failCallback) { + if (this.status === FULFILLED) { + // 调用成功回调,把结果返回 + successCallback(this.value); + } else if (this.status === REJECTED) { + // 调用失败回调,把错误信息返回 + failCallback(this.reason); + } + } +} +``` + +## 2)加入异步处理逻辑 + +``` +const PENDING = 'pending'; +const FULFILLED = 'fulfilled'; +const REJECTED = 'rejected'; +class MyPromise { + constructor(exector) { + // exector是一个执行器,传入resolve和reject方法,进入会立即执行, + exector(this.resolve, this.reject); + } + // 实例对象上属性,初始状态为等待 + status = PENDING; + // 成功后的值 + value = undefined; + // 失败后的原因 + reason = undefined; + // 定义成功回调和失败回调参数 + successCallback = undefined; + failCallback = undefined; + // 使用箭头函数,让this指向当前实例对象 + resolve = (value) => { + // 判断状态不是等待,阻止执行 + if (this.status !== PENDING) return; + // 将状态改为成功,并保存成功值 + this.status = FULFILLED; + this.value = value; + this.successCallback && this.successCallback(this.value); + }; + reject = (reason) => { + if (this.status !== PENDING) return; + // 将状态改为失败,并保存失败原因 + this.status = REJECTED; + this.reason = reason; + this.failCallback && this.failCallback(this.reason); + }; + then(successCallback, failCallback) { + if (this.status === FULFILLED) { + // 调用成功回调,把结果返回 + successCallback(this.value); + } else if (this.status === REJECTED) { + // 调用失败回调,把错误信息返回 + failCallback(this.reason); + } else { + // 等待状态,把成功和失败回调暂存起来 + this.successCallback = successCallback; + this.failCallback = failCallback; + } + } +} +``` + +## 3)then 方法多次调用 + +- promise 的 then 是可以被多次调用的, +- 如下例子,如果三个 then 调用,都是同步调用,则直接返回值即可; +- 如果是异步调用,那么成功回调和失败回调应该是多个不同的; + +``` +let promise = new Promise((resolve, reject) => { + setTimeout(() => { + resolve('success') + }, 2000); + }) + + promise.then(value => { + console.log(1) + console.log('resolve', value) //resolve success + }) + + promise.then(value => { + console.log(2) + console.log('resolve', value) //resolve success +}) + +promise.then(value => { + console.log(3) + console.log('resolve', value) //resolve success +}) +``` + +所以需要改进:把回调放进数组,待状态确定后统一执行 + +``` +const PENDING = 'pending'; +const FULFILLED = 'fulfilled'; +const REJECTED = 'rejected'; +class MyPromise { + constructor(exector) { + // exector是一个执行器,传入resolve和reject方法,进入会立即执行, + exector(this.resolve, this.reject); + } + // 实例对象上属性,初始状态为等待 + status = PENDING; + // 成功后的值 + value = undefined; + // 失败后的原因 + reason = undefined; + // 定义成功回调和失败回调参数,初始化空数组 + successCallback = []; + failCallback = []; + // 使用箭头函数,让this指向当前实例对象 + resolve = (value) => { + // 判断状态不是等待,阻止执行 + if (this.status !== PENDING) return; + // 将状态改为成功,并保存成功值 + this.status = FULFILLED; + this.value = value; + while (this.successCallback.length) { + this.successCallback.shift()(this.value); + } + }; + reject = (reason) => { + if (this.status !== PENDING) return; + // 将状态改为失败,并保存失败原因 + this.status = REJECTED; + this.reason = reason; + while (this.failCallback.length) { + this.failCallback.shift()(this.reason); + } + }; + then(successCallback, failCallback) { + if (this.status === FULFILLED) { + // 调用成功回调,把结果返回 + successCallback(this.value); + } else if (this.status === REJECTED) { + // 调用失败回调,把错误信息返回 + failCallback(this.reason); + } else { + // 等待状态,把成功和失败回调暂存到数组中 + this.successCallback.push(successCallback); + this.failCallback.push(failCallback); + } + } +} +``` + +## 4)then 方法链式调用 + +- then 方法会返回一个新的 Promise 实例。因此可以采用链式写法 +- then 方法可以返回一个普通值或者一个新的 promise 实例 + +返回普通值用法: + +``` +let promise = new Promise((resolve, reject) => { + // 目前这里只处理同步的问题 + resolve('success'); +}); +promise + .then((value) => { + console.log('1', value); // 1 success + return 'hello'; + }) + .then((value) => { + console.log('2', value); // 2 hello + }); + +``` + +返回新的 promise 实例用法: + +``` +let promise = new Promise((resolve, reject) => { + // 目前这里只处理同步的问题 + resolve('success'); +}); +function other() { + return new Promise((resolve, reject) => { + resolve('other'); + }); +} +promise + .then((value) => { + console.log('1', value); // 1 success + return other(); + }) + .then((value) => { + console.log('2', value); // 2 other + }); + +``` + +**实现:** + +``` +const PENDING = 'pending'; +const FULFILLED = 'fulfilled'; +const REJECTED = 'rejected'; +class MyPromise { + constructor(exector) { + exector(this.resolve, this.reject); + } + + status = PENDING; + value = undefined; + reason = undefined; + successCallback = []; + failCallback = []; + + resolve = (value) => { + if (this.status !== PENDING) return; + this.status = FULFILLED; + this.value = value; + while (this.successCallback.length) + this.successCallback.shift()(this.value); + }; + + reject = (reason) => { + if (this.status !== PENDING) return; + this.status = REJECTED; + this.reason = reason; + while (this.failCallback.length) this.failCallback.shift()(this.reason); + }; + + then(successCallback, failCallback) { + // then方法返回第一个promise对象 + let promise2 = new MyPromise((resolve, reject) => { + if (this.status === FULFILLED) { + // x是上一个promise回调函数的返回结果 + // 判断x是普通值还是promise实例 + // 如果是普通值,直接resolve + // 如果是promise实例,待promise状态变为fulfilled,调用resolve或者reject + + // 因为mew MyPromise需要执行完才能拿到promise2,所以通过异步拿到 + setTimeout(() => { + let x = successCallback(this.value); + resolvePromise(promise2, x, resolve, reject); + }, 0); + } else if (this.status === REJECTED) { + failCallback(this.reason); + } else { + this.successCallback.push(successCallback); + this.failCallback.push(failCallback); + } + }); + return promise2; + } +} + +function resolvePromise(promise2, x, resolve, reject) { + // 如果等于了,说明返回了自身,报错 + if (promise2 === x) { + return reject( + new TypeError('Chaining cycle detected for promise #') + ); + } + // 判断x是不是其实例对象 + if (x instanceof MyPromise) { + x.then( + (value) => resolve(value), + (reason) => reject(reason) + ); + } else { + // 普通值 + resolve(x); + } +} +``` + +## 5)捕获错误及优化 + +1. 捕获执行器中的错误 + +``` +constructor(exector) { + // 捕获错误,如果有错误就执行reject + try { + exector(this.resolve, this.reject); + } catch (e) { + this.reject(e); + } + } +``` + +**验证:** + +``` +let promise = new MyPromise((resolve, reject) => { + // resolve('success') + throw new Error('执行器错误') +}) +promise.then(value => { + console.log(1) + console.log('resolve', value) +}, reason => { + console.log(2) + console.log(reason.message) +}) +//2 +//执行器错误 +``` + +2. then 执行的时候报错捕获 + +``` +then(successCallback, failCallback) { + // then方法返回第一个promise对象 + let promise2 = new MyPromise((resolve, reject) => { + if (this.status === FULFILLED) { + setTimeout(() => { + try { + let x = successCallback(this.value); + resolvePromise(promise2, x, resolve, reject); + } catch (e) { + reject(e); + } + }, 0); + } else if (this.status === REJECTED) { + failCallback(this.reason); + } else { + this.successCallback.push(successCallback); + this.failCallback.push(failCallback); + } + }); + return promise2; + } +``` + +**验证:** + +``` +let promise = new MyPromise((resolve, reject) => { + resolve('success'); +}); +// 第一个then方法中的错误要在第二个then方法中捕获到 +promise + .then( + (value) => { + console.log('1', value); + throw new Error('then error'); + }, + (reason) => { + console.log('2', reason.message); + } + ) + .then( + (value) => { + console.log('3', value); + }, + (reason) => { + console.log('4', reason.message); + } + ); +// 1 success +// 4 then error +``` + +3. 错误后的链式调用 + +``` +then(successCallback, failCallback) { + // then方法返回第一个promise对象 + let promise2 = new MyPromise((resolve, reject) => { + if (this.status === FULFILLED) { + setTimeout(() => { + try { + let x = successCallback(this.value); + resolvePromise(promise2, x, resolve, reject); + } catch (e) { + reject(e); + } + }, 0); + } else if (this.status === REJECTED) { + setTimeout(() => { + try { + let x = failCallback(this.reason); + resolvePromise(promise2, x, resolve, reject); + } catch (e) { + reject(e); + } + }, 0); + } else { + this.successCallback.push(successCallback); + this.failCallback.push(failCallback); + } + }); + return promise2; + } +``` + +**验证:** + +``` +let promise = new MyPromise((resolve, reject) => { + throw new Error('执行器错误'); +}); + +// 第一个then方法中的错误要在第二个then方法中捕获到 +promise + .then( + (value) => { + console.log('1', value); + throw new Error('then error'); + }, + (reason) => { + console.log('2', reason.message); + return 200; + } + ) + .then( + (value) => { + console.log('3', value); + }, + (reason) => { + console.log('4', reason.message); + } + ); +// 2 执行器错误 +// 3 200 + +``` + +4. 异步状态下链式调用(then 方法优化) + +``` + ... + resolve = (value) => { + ... + // 调用时,不再需要传值,因为在push回调数组时,已经处理了 + while (this.successCallback.length) this.successCallback.shift()(); + }; + reject = (reason) => { + ... + // 调用时,不再需要传值,因为在push回调数组时,已经处理了 + while (this.failCallback.length) this.failCallback.shift()(); + }; + + then(successCallback, failCallback) { + // then方法返回第一个promise对象 + let promise2 = new MyPromise((resolve, reject) => { + if (this.status === FULFILLED) { + ... + } else if (this.status === REJECTED) { + ... + } else { + // 处理异步情况 + this.successCallback.push(() => { + setTimeout(() => { + try { + let x = successCallback(this.value); + resolvePromise(promise2, x, resolve, reject); + } catch (e) { + reject(e); + } + }, 0); + }); + this.failCallback.push(() => { + setTimeout(() => { + // 如果回调中报错的话就执行reject + try { + let x = failCallback(this.reason); + resolvePromise(promise2, x, resolve, reject); + } catch (e) { + reject(e); + } + }, 0); + }); + } + }); + return promise2; + } +``` + +**验证:** + +``` +let promise = new MyPromise((resolve, reject) => { + setTimeout(() => { + resolve('成功'); + }, 2000); +}); +// 第一个then方法中的错误要在第二个then方法中捕获到 +promise + .then( + (value) => { + console.log('1', value); + return 'hello'; + }, + (reason) => { + console.log('2', reason.message); + return 200; + } + ) + .then( + (value) => { + console.log('3', value); + }, + (reason) => { + console.log('4', reason.message); + } + ); +// 1 成功 +// 3 hello +``` + +## 6)把 then 方法的参数变成可选参数 + +``` +var promise = new Promise((resolve, reject) => { + resolve(100) + }) + promise + .then() + .then() + .then() + .then(value => console.log(value)) +// 在控制台最后一个then中输出了100 + +// 这个相当于 +promise + .then(value => value) + .then(value => value) + .then(value => value) + .then(value => console.log(value)) +``` + +**所以修改 then 方法:** + +``` + then(successCallback, failCallback) { + // 先判断回调函数是否传了,如果没预留就默认一个函数,把参数返回 + successCallback = successCallback ? successCallback : (value) => value; + failCallback = failCallback ? failCallback : (reason) => { throw reason }; + ... + } +``` + +## 7)实现 Promise.all promise.all 方法是解决异步并发问题的 + +``` +// 如果p1是两秒之后执行的,p2是立即执行的,那么根据正常的是p2在p1的前面。 +// 如果我们在all中指定了执行顺序,那么会根据我们传递的顺序进行执行。 +function p1 () { + return new Promise((resolve, reject) => { + setTimeout(() => { + resolve('p1') + }, 2000) + }) +} +function p2 () { + return new Promise((resolve, reject) => { + setTimeout(() => { + resolve('p2') + },0) + }) +} +Promise.all(['a', 'b', p1(), p2(), 'c']).then(result => { + console.log(result) + // ["a", "b", "p1", "p2", "c"] +}) +``` + +**分析:** + +- all 方法接收一个数组,数组中可以是普通值也可以是 promise 对象 +- 数组中值得顺序一定是我们得到的结果的顺序 +- 返回值也是一个 promise 对象,可以调用 then 方法 +- 如果数组中所有值是成功的,那么 then 里面就是成功回调,如果有一个值是失败的,那么 then 里面就是失败的 +- 使用 all 方法是用类直接调用,那么 all 一定是一个静态方法 + +**分析完,开整:** + +``` +class MyPromise { + ... + static all(array) { + // 结果数组 + let result = []; + // 计数器 + let index = 0; + return new MyPromise((resolve, reject) => { + let addData = (key, value) => { + result[key] = value; + index++; + // 当计数器等于参数数组的长度,说明所有参数已经执行完毕 + if (index === array.length) { + resolve(result); + } + }; + // 对传递的数组中遍历 + for (let i = 0; i < array.length; i++) { + let current = array[i]; + if (current instanceof MyPromise) { + current.then( + (value) => addData(i, value), + (reason) => reject(reason) + ); + } else { + addData(i, array[i]); + } + } + }); + } +} +``` + +**验证:** + +``` +function p1() { + return new MyPromise((resolve, reject) => { + setTimeout(() => { + resolve('p1'); + }, 2000); + }); +} + +function p2() { + return new MyPromise((resolve, reject) => { + setTimeout(() => { + resolve('p2'); + }, 0); + }); +} +Promise.all(['a', 'b', p1(), p2(), 'c']).then((result) => { + console.log(result); + // ["a", "b", "p1", "p2", "c"] +}); +``` + +## 8)实现 Promise.resolve + +**分析:** + +- 如果参数是一个 promise 对象,则直接返回;如果是一个值,则生成一个 promise 对象,把值进行返回 +- 肯定是一个静态方法 + +``` +class MyPromise { + ... + // Promise.resolve方法 + static resolve(value) { + if (value instanceof MyPromise) { + return value; + } else { + return new MyPromise((resolve) => resolve(value)); + } + } +} +``` + +**验证:** + +``` +function p1() { + return new MyPromise((resolve, reject) => { + setTimeout(() => { + resolve('p1'); + }, 2000); + }); +} + +Promise.resolve(100).then((value) => console.log(value)); +Promise.resolve(p1()).then((value) => console.log(value)); +// 100 +// 2s 之后输出 p1 +``` + +## 9)实现 finally 方法 + +- 无论最终状态是成功或是失败,finally 都会执行 +- 可以在 finally 之后拿到 then 的结果 +- 这是原型上的方法 + +``` + // finally + // 使用then方法拿到promise的状态,无论成功或失败都返回callback + // then方法返回的就是一个promise,拿到成功回调就把value return,错误回调就把错误信息return + // 如果callback是一个异步promise,还需等待其执行完毕,所以要用到静态方法resolve + finally(callback) { + return this.then( + (value) => { + return MyPromise.resolve(callback()).then(() => value); + }, + (reason) => { + return MyPromise.resolve(callback()).then(() => { + throw reason; + }); + } + ); + } +``` + +**验证:** + +``` +function p1() { + return new MyPromise((resolve, reject) => { + setTimeout(() => { + resolve('p1'); + }, 2000); + }); +} +function p2() { + return new MyPromise((resolve, reject) => { + reject('p2 reject'); + }); +} +p2() + .finally(() => { + console.log('finallyp2'); + return p1(); + }) + .then( + (value) => { + console.log('成功回调', value); + }, + (reason) => { + console.log('失败回调', reason); + } + ); +// finallyp2 +// 两秒之后执行p2 reject +``` + +## 10)实现 catch 方法 + +- 可以捕获全局错误 +- 也是原型对象的方法 + +``` + // 直接调用then方法,然后成功的地方传递undefined,错误的地方传递reason + catch(failCallback) { + return this.then(undefined, failCallback); + } +``` + +--- + +## Promise 全部代码 + +``` +const PENDING = 'pending'; +const FULFILLED = 'fulfilled'; +const REJECTED = 'rejected'; +class MyPromise { + constructor(exector) { + // 捕获错误,如果有错误就执行reject + try { + exector(this.resolve, this.reject); + } catch (e) { + this.reject(e); + } + } + + status = PENDING; + value = undefined; + reason = undefined; + successCallback = []; + failCallback = []; + + resolve = (value) => { + if (this.status !== PENDING) return; + this.status = FULFILLED; + this.value = value; + // 调用时,不再需要传值,因为在push回调数组时,已经处理了 + while (this.successCallback.length) this.successCallback.shift()(); + }; + + reject = (reason) => { + if (this.status !== PENDING) return; + this.status = REJECTED; + this.reason = reason; + // 调用时,不再需要传值,因为在push回调数组时,已经处理了 + while (this.failCallback.length) this.failCallback.shift()(); + }; + + then(successCallback, failCallback) { + // 先判断回调函数是否传了,如果没预留就默认一个函数,把参数返回 + successCallback = successCallback ? successCallback : (value) => value; + failCallback = failCallback + ? failCallback + : (reason) => { + throw reason; + }; + // then方法返回第一个promise对象 + let promise2 = new MyPromise((resolve, reject) => { + if (this.status === FULFILLED) { + // x是上一个promise回调函数的返回结果 + // 判断x是普通值还是promise实例 + // 如果是普通值,直接resolve + // 如果是promise实例,待promise状态变为fulfilled,调用resolve或者reject + + // 因为mew MyPromise需要执行完才能拿到promise2,所以通过异步拿到 + setTimeout(() => { + try { + let x = successCallback(this.value); + resolvePromise(promise2, x, resolve, reject); + } catch (e) { + reject(e); + } + }, 0); + } else if (this.status === REJECTED) { + setTimeout(() => { + try { + let x = failCallback(this.reason); + resolvePromise(promise2, x, resolve, reject); + } catch (e) { + reject(e); + } + }, 0); + } else { + // 处理异步情况 + this.successCallback.push(() => { + setTimeout(() => { + try { + let x = successCallback(this.value); + resolvePromise(promise2, x, resolve, reject); + } catch (e) { + reject(e); + } + }, 0); + }); + this.failCallback.push(() => { + setTimeout(() => { + // 如果回调中报错的话就执行reject + try { + let x = failCallback(this.reason); + resolvePromise(promise2, x, resolve, reject); + } catch (e) { + reject(e); + } + }, 0); + }); + } + }); + return promise2; + } + + static all(array) { + // 结果数组 + let result = []; + // 计数器 + let index = 0; + return new MyPromise((resolve, reject) => { + let addData = (key, value) => { + result[key] = value; + index++; + // 当计数器等于参数数组的长度,说明所有参数已经执行完毕 + if (index === array.length) { + resolve(result); + } + }; + + // 对传递的数组中遍历 + for (let i = 0; i < array.length; i++) { + let current = array[i]; + if (current instanceof MyPromise) { + current.then( + (value) => addData(i, value), + (reason) => reject(reason) + ); + } else { + addData(i, array[i]); + } + } + }); + } + // Promise.resolve方法 + static resolve(value) { + if (value instanceof MyPromise) { + return value; + } else { + return new MyPromise((resolve) => resolve(value)); + } + } + // finally + // 使用then方法拿到promise的状态,无论成功或失败都返回callback + // then方法返回的就是一个promise,拿到成功回调就把value return,错误回调就把错误信息return + // 如果callback是一个异步promise,还需等待其执行完毕,所以要用到静态方法resolve + finally(callback) { + return this.then( + (value) => { + return MyPromise.resolve(callback()).then(() => value); + }, + (reason) => { + return MyPromise.resolve(callback()).then(() => { + throw reason; + }); + } + ); + } + // catch + // 直接调用then方法,然后成功的地方传递undefined,错误的地方传递reason + catch(failCallback) { + return this.then(undefined, failCallback); + } +} + +function resolvePromise(promise2, x, resolve, reject) { + // 如果等于了,说明返回了自身,报错 + if (promise2 === x) { + return reject( + new TypeError('Chaining cycle detected for promise #') + ); + } + // 判断x是不是其实例对象 + if (x instanceof MyPromise) { + x.then( + (value) => resolve(value), + (reason) => reject(reason) + ); + } else { + // 普通值 + resolve(x); + } +} + +``` diff --git "a/docs/skill/css/\344\270\200\344\272\233CSS\345\261\236\346\200\247.md" "b/docs/skill/css/\344\270\200\344\272\233CSS\345\261\236\346\200\247.md" deleted file mode 100644 index 3437283..0000000 --- "a/docs/skill/css/\344\270\200\344\272\233CSS\345\261\236\346\200\247.md" +++ /dev/null @@ -1,78 +0,0 @@ ---- -id: css-properties -slug: /css-properties -title: 一些CSS属性 -date: 2022-08-12 -authors: youngjeff -tags: [css] -keywords: [css] ---- - - - -## [clip-path](https://developer.mozilla.org/zh-CN/docs/Web/CSS/clip-path) - -如果要实现多边形的话,之前的做法通常是使用 border 来实现的,但是用 border 来实现的是比较复杂的,最关键的是不好用。[**`clip-path`**](https://developer.mozilla.org/zh-CN/docs/Web/CSS/clip-path) CSS 属性使用裁剪方式创建元素的可显示区域。可以在这个网站 [Clippy — CSS clip-path 生成器](https://www.html.cn/tool/css-clip-path/) 勾勒出所要的图形,然后将其添加至 css 属性即可。 - -![](https://secure2.wostatic.cn/static/qs1brMUAga5NbQhpbMU5d6/image.png) - -## [linear-gradient](https://developer.mozilla.org/zh-CN/docs/Web/CSS/gradient/linear-gradient) - -线性渐变颜色,也是渐变色用到最多的一个属性,此外还有径向 [`radial-gradient`](https://developer.mozilla.org/zh-CN/docs/Web/CSS/gradient/radial-gradient)与圆锥[conic-gradient](https://developer.mozilla.org/zh-CN/docs/Web/CSS/gradient/conic-gradient) - -```css -/* 渐变轴为45度,从蓝色渐变到红色 */ -linear-gradient(45deg, blue, red); - -/* 从右下到左上、从蓝色渐变到红色 */ -linear-gradient(to left top, blue, red); - -/* 从下到上,从蓝色开始渐变、到高度 40% 位置是绿色渐变开始、最后以红色结束 */ -linear-gradient(0deg, blue, green 40%, red); -``` - -不过这个属性只适用于背景(background)颜色,如果想要在文字,边框,阴影中使用渐变颜色,通常需要先设置渐变背景颜色,然后通过一些 css 属性“裁剪”出相应的部分。 - -这里的“裁剪”主要用到 background-clip 属性,如果想要裁剪出文字可以 `background-clip: text`配合文字`color: transparent`,要裁剪出边框可以 `background-clip: content-box, border-box;`,在给背景颜色添加原背景色。 - -## [backdrop-filter](https://developer.mozilla.org/zh-CN/docs/Web/CSS/backdrop-filter) - -**`backdrop-filter`** [CSS](https://developer.mozilla.org/zh-CN/docs/Web/CSS) 属性可以让你为一个元素后面区域添加图形效果(如模糊或颜色偏移)。因为它适用于元素*背后*的所有元素,为了看到效果,必须使元素或其背景至少部分透明。 - -为背景添加滤镜,比如毛玻璃效果 `backdrop-filter: blur(5px);` 、灰度`backdrop-filter: grayscale(1);`等等。 - -再次之前要实现这类效果还需要使用[filter](https://developer.mozilla.org/zh-CN/docs/Web/CSS/filter)属性(兼容性更好),然后用伪元素双背景的方式来实现,实在过于麻烦。 - -# [-webkit-box-reflect](https://developer.mozilla.org/en-US/docs/Web/CSS/-webkit-box-reflect) - -可以实现类似水下倒影的效果,例如 - -``` --webkit-box-reflect: below 0 linear-gradient(transparent, transparent, rgba(0, 0, 0, 0.4)); -``` - -## [aspect-ratio](https://developer.mozilla.org/zh-CN/docs/Web/CSS/aspect-ratio) - -例如 - -```css -aspect-ratio: 1 / 1; -aspect-ratio: 16 / 9; -aspect-ratio: 4 / 3; -``` - -## [gap](https://developer.mozilla.org/zh-CN/docs/Web/CSS/gap) - -这个属性我经常用到,主要**用于 flex 与 grid 布局中用于设置元素间的间隔**,原本这个属性是只有 grid 布局中才有的,后来在 flex 布局中也可以使用。 - -## [writing-mode](https://developer.mozilla.org/zh-CN/docs/Web/CSS/writing-mode) - -修改文字显示方向,例如竖行显示 `writing-mode: vertical-lr;` - -![img](https://developer.mozilla.org/en-US/docs/Web/CSS/writing-mode/screenshot_2020-02-05_21-04-30.png) - -## 总结 - -此外还有很多特性也在不断了解,每年也会有一些新的特性来帮助开发者更好的使用 css 去美化网站。 - -最直接的体验就是到 [CSS(层叠样式表) | MDN](https://developer.mozilla.org/zh-CN/docs/Web/CSS) ,在 MDN 上能查到关于前端开发技术的文档,可以说是前端的百科全书了。 diff --git "a/docs/skill/js/JavaScript\344\271\213\345\274\202\346\255\245\347\274\226\347\250\213.md" "b/docs/skill/js/JavaScript\344\271\213\345\274\202\346\255\245\347\274\226\347\250\213.md" new file mode 100644 index 0000000..53cb262 --- /dev/null +++ "b/docs/skill/js/JavaScript\344\271\213\345\274\202\346\255\245\347\274\226\347\250\213.md" @@ -0,0 +1,125 @@ +--- +slug: js-async-coding +title: JavaScript之异步编程 +date: 2021-07-07 +authors: youngjeff +tags: [js, EventLoop, 异步编程] +keywords: [js, EventLoop, 异步编程] +description: 最早js语言就是运行在浏览器端的语言,目的是为了实现页面上的动态交互。实现页面交互的核心就是DOM操作,这就决定了它必须使用单线程模型,否则就会出现很复杂的线程同步问题。 +image: /img/blog/js-async-coding/1.webp +--- + +> 最早 js 语言就是运行在浏览器端的语言,目的是为了实现页面上的动态交互。实现页面交互的核心就是 DOM 操作,这就决定了它必须使用单线程模型,否则就会出现很复杂的线程同步问题。 假设在 js 中有多个线程一起工作,其中一个线程修改了这个 DOM 元素,同时另一个线程又删除了这个元素,此时浏览器就无法明确该以哪个工作线程为准。所以为了避免线程同步的问题,从一开始,js 就设计成了单线程的工作模式。 + +**单线程优缺点:** 单线程优点就是更安全,简单;缺点就是如果碰到很耗时的任务(比如 ajax 请求,文件读写),会出现假死情况,用户体验差,所以就出现了同步任务和异步任务来解决这个问题; + +## 同步模式和异步模式 + +- 同步模式:代码按顺序一行一行执行,是典型的请求-相应模式,执行顺序和编写顺序保持一致; + +``` +console.log('global begin') +function bar () { + console.log('bar task') +} +function foo () { + console.log('foo task') + bar() +} +foo() +console.log('global end') + +// global begin +// foo task +// bar task +//global end + +// 使用调用栈的逻辑 +``` + +- 异步模式:任务可以同时执行,不必等待上一个任务结束才继续执行;(比如生活中,你可以同时烧水和煮饭一样) + +``` +console.log('global begin') +// 延时器 +setTimeout(function timer1 () { + console.log('timer1 invoke') +}, 1800) +// 延时器中又嵌套了一个延时器 +setTimeout(function timer2 () { + console.log('timer2 invoke') + setTimeout(function inner () { + console.log('inner invoke') + }, 1000) +}, 1000) +console.log('global end') + +// global begin +// global end +// timer2 invoke +// timer1 invoke +// inner invoke + +//除了调用栈,还用到了消息队列和事件循环 +``` + +js 执行异步代码而不用等待,是因有为有 消息队列和事件循环。 + +- 消息队列:消息队列是一个先进先出的队列,它里面存放着各种消息。 +- 事件循环(EventLoop):事件循环是指主线程重复从消息队列中取消息、执行的过程 + +**事件循环流程:** + +1. 宿主环境(node 服务器或者浏览器)为 js 创建线程时,会创建堆(heap)和栈(stack), 堆内存储 javaScript 对象,栈内存储执行上下文; +2. 栈内执行上下文的同步任务,执行完即退栈;当执行异步任务时,该异步任务进入等待状态(不入栈),同时通知异步进程,执行完该异步进程后的回调放到消息队列中 +3. 当栈内同步任务执行结束后,依次执行消息队列中的任务 `注:js是单线程的,浏览器不是单线程的,有一些API是有单独的线程去做的` + +![图1](/img/blog/js-async-coding/1.webp) + +## 宏任务和微任务 + +- 宏任务(macrotask):每次执行栈执行的代码就是宏任务(包括每次从消息队列中获取一个事件回调并放到执行栈中执行) +- 微任务(microtask):当前宏任务执行结束后立即执行的任务(当前宏任务之后,下一个宏任务之前);所以它的响应速度相比 setTimeout 会更快,因为无需等待渲染,也就是说:在某一个宏任务执行完后,就会将它执行期间产生的所有微任务执行完毕; + +举个例子:我去银行排队办理业务,原本我只想办理取款业务(取款业务当成是宏任务),办理完取款业务后,立即我又想办一个开卡业务(开卡业务当成一个微任务);这个时候,我不会重新去排队,而是在还没离开办理窗口时,立马让柜台人员帮我再次办理这个业务;如果我还有其他业务要办理(更多微任务),都是可以继续办理,只要我不离开窗口(后面排队用户也不应该有任何怨言,因为我有排队并且没有离开柜台)。 + +**宏任务包含:** + +``` +script(整体代码) +setTimeout +setInterval +I/O +UI交互事件 +postMessage +MessageChannel +setImmediate(Node.js 环境) +``` + +**微任务包含:** + +``` +Promise.then +Object.observe +MutaionObserver +process.nextTick(Node.js 环境) +``` + +## 回调函数(异步编程的根基) + +概念:由调用者定义,交给执行者执行的函数缺点:如果异步函数嵌套很深,就会不可避免的产生**_回调地狱_** + +``` +// callback就是回调函数 +function foo(callback) { + setTimeout(function(){ + callback() + }, 3000) +} + +foo(function() { + console.log('这就是一个回调函数') +}) +``` + +[Promise —— 一种更优的异步编程统一方案](https://www.jianshu.com/p/93b63e08f792) diff --git "a/docs/skill/js/Promise \342\200\224\342\200\224\345\274\202\346\255\245\347\274\226\347\250\213\347\273\237\344\270\200\346\226\271\346\241\210.md" "b/docs/skill/js/Promise \342\200\224\342\200\224\345\274\202\346\255\245\347\274\226\347\250\213\347\273\237\344\270\200\346\226\271\346\241\210.md" new file mode 100644 index 0000000..33f11cd --- /dev/null +++ "b/docs/skill/js/Promise \342\200\224\342\200\224\345\274\202\346\255\245\347\274\226\347\250\213\347\273\237\344\270\200\346\226\271\346\241\210.md" @@ -0,0 +1,120 @@ +--- +slug: promise-summary +title: Promise ——异步编程统一方案 +date: 2021-07-07 +authors: youngjeff +tags: [code, 总结] +keywords: [code, 总结] +description: Promise 对象用于表示一个异步操作的最终完成 (或失败)及其结果值。。 +image: /img/blog/promise-summary/1.webp +--- + +虽然回调函数是所有异步编程方案的根基;但是如果我们直接使用传统回调方式去完成复杂的异步流程,就会无法避免大量的回调函数嵌套;导致回调地狱的问题。为了避免这个问题。CommonJS 社区提出了 Promise 的规范,ES6 中称为语言规范。 + +> [MDN:Promise](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Promise) Promise 对象用于表示一个异步操作的最终完成 (或失败)及其结果值。一个 Promise 必然处于以下几种状态之一:待定(pending): 初始状态,既没有被兑现,也没有被拒绝。已兑现(fulfilled): 意味着操作成功完成。 + +- 基本用法已兑现(fulfilled): + +``` +const promise = new Promise((resolve, reject) => { + resolve(1) +}) +promise.then((value) => { + console.log('resolved', value) // resolve 1 +},(error) => { + console.log('rejected', error) +}) +``` + +- 已拒绝(rejected): + +``` +const promise = new Promise((resolve, reject) => { + reject('失败了') +}) +promise.then((value) => { + console.log('resolved', value) +},(error) => { + console.log('rejected', error) // rejected 失败了 +}) +``` + +**即便 promise 中没有任何的异步操作,then 方法的回调函数仍然会进入到事件队列中排队。** **Promise 的本质上也是使用回调函数的方式去定义异步任务结束后所需要执行的任务。这里的回调函数是通过 then 方法传递过去的** + +## 链式调用 + +- promise 对象 then 方法,返回了全新的 promise 对象。可以再继续调用 then 方法,如果 return 的不是 promise 对象,而是一个值,那么这个值会作为 resolve 的值传递,如果没有值,默认是 undefined +- 后面的 then 方法就是在为上一个 then 返回的 Promise 注册回调 +- 前面 then 方法中回调函数的返回值会作为后面 then 方法回调的参数 +- 如果回调中返回的是 Promise,那后面 then 方法的回调会等待它的结束 + +## 异常处理 + +1. then 的第二个参数 onRejected 方法 +2. catch() 两者的区别: **catch 是给整个 promise 链条注册的一个失败回调;而 then 的第二个参数 onRejected 方法,只能捕获第一个 promise 的报错,如果当前 then 的 resolve 函数处理中有报错是捕获不到的。** `使用.catch方法更为常见,因为更加符合链式调用` + +``` +ajax('/api/user.json') + .then(function onFulfilled(res) { + console.log('onFulfilled', res) + }).catch(function onRejected(error) { + console.log('onRejected', error) + }) +``` + +等价于 + +``` +ajax('/api/user.json') + .then(function onFulfilled(res) { + console.log('onFulfilled', res) + }) + .then(undefined, function onRejected(error) { + console.log('onRejected', error) + }) +``` + +## 常用的静态方法 + +- Promise.resolve() +- Promise.reject() +- Promise.all() +- Promise.race() + +## Promise 案例 + +``` +function ajax (url) { + return new Promise((resolve, rejects) => { + // 创建一个XMLHttpRequest对象去发送一个请求 + const xhr = new XMLHttpRequest() + // 先设置一下xhr对象的请求方式是GET,请求的地址就是参数传递的url + xhr.open('GET', url) + // 设置返回的类型是json,是HTML5的新特性 + // 我们在请求之后拿到的是json对象,而不是字符串 + xhr.responseType = 'json' + // html5中提供的新事件,请求完成之后(readyState为4)才会执行 + xhr.onload = () => { + if(this.status === 200) { + // 请求成功将请求结果返回 + resolve(this.response) + } else { + // 请求失败,创建一个错误对象,返回错误文本 + rejects(new Error(this.statusText)) + } + } + // 开始执行异步请求 + xhr.send() + }) +} + +ajax('/api/user.json').then((res) => { + console.log(res) +}, (error) => { + console.log(error) +}) +``` + +--- + +关于 Promise 源码分析可以看另外一篇:[Promise 手写](https://www.jianshu.com/p/62a132eba4ae) diff --git "a/docs/skill/js/\345\207\275\346\225\260\346\237\257\351\207\214\345\214\226.md" "b/docs/skill/js/\345\207\275\346\225\260\346\237\257\351\207\214\345\214\226.md" new file mode 100644 index 0000000..b0756cb --- /dev/null +++ "b/docs/skill/js/\345\207\275\346\225\260\346\237\257\351\207\214\345\214\226.md" @@ -0,0 +1,266 @@ +--- +slug: function-curry +title: 函数柯里化 +date: 2021-09-01 +authors: youngjeff +tags: [code, 总结] +keywords: [code, 总结] +description: 多变量函数拆解为单变量的多个函数的依次调用。 +image: /img/blog/function-curry/1.webp +--- + +### 基础概念 + +当一个函数有多个参数的时候,先传递一部分参数调用他(这部分参数以后永远不变),然后返回一个新的函数接受剩余的参数,返回结果;简言之就是:多变量函数拆解为单变量的多个函数的依次调用; + +--- + +### 可以干嘛呢? + +可以利用它来实现对函数参数的缓存,降低函数粒度,把多元函数转换成一元函数,实现函数的组合,产生更强大的功能 + +--- + +### 核心流程分析 + +就是利用闭包和递归调用,可以形成一个不销毁的私有作用域,把预先处理的内容放到不销毁的作用域里面,返回一个函数供以后调用;举个例子: + +比如我们有一个判断用户年龄是否大于某个值的函数 + +``` +// 普通的纯函数 +function checkAge (min, age) { + return age >= min +} +// 普通调用 +console.log(checkAge(18, 20)) //true +console.log(checkAge(18, 24)) //true +console.log(checkAge(60, 30)) //false +``` + +可能需要经常判断用户是否成年(大于 18 岁),为了减少代码重复,所以改造如下 + +``` +// 柯里化后的函数 +function checkAge (min) { + return function (age) { + return age >= min + } +} +const checkAge18 = checkAge(18) +const checkAge60 = checkAge(60) +console.log(checkAge18(20)) //true +console.log(checkAge18(24)) //true +console.log(checkAge60(30)) //false +``` + +以上就是一个针对 checkAge 函数的柯里化改造,他的自由度很低,因此需要封装一个通用的柯里化函数; + +--- + +### 实现思路 + +首先,我们通过调用 lodash 提供的柯里化函数(curry)来了解一下如何使用,并且分析一下实现思路 + +``` +const _ = require('lodash') +function getSum (a, b, c) { + return a + b + c +} +// 定义一个柯里化函数 +const curried = _.curry(getSum) + +// 如果输入了全部的参数,则立即返回结果 +console.log(curried(1, 2, 3)) // 6 +//如果传入了部分的参数,此时它会返回当前函数,并且等待接收getSum中的剩余参数 +console.log(curried(1)(2, 3)) // 6 +console.log(curried(1, 2)(3)) // 6 +``` + +通过以上可以看出,柯里化函数的运行过程其实是一个参数的收集过程,将每一次传入的参数收集起来,在最后统一处理 + +所以,实现思路: + +- 调用 curry,传递一个函数,然后需要返回一个柯里化函数(curried) +- 如果调用 curried 传递的参数和 getSum 参数个数相同,就立即执行并返回结果;如果调用 curried 传递的是部分参数,那么需要返回一个新函数,等待接受 getSum 其他参数 + +具体实现如下: + +``` +function curry(func) { + return function curriedFn(...args) { + // 若实参的个数小于形参的个数 + if (args.length < func.length) { + return function () { + // 等待传递的剩余参数 + // 第一部分参数在args里面,第二部分参数在arguments里面 + return curriedFn(...args.concat(...arguments)); + }; + } + // 如果实参大于等于形参的个数,立即执行并返回结果 + // args是剩余参数 + return func(...args); + }; +} +``` + +**注意:这里有个细节,就是要柯理化的函数不能有默认值,否则该函数的 length 属性将失真;将造成结果提前返回或者报错** 如下: + +- - ![图1](/img/blog/function-curry/1.webp) + +--- + +## 该技术的优缺点 + +上面费那么大劲封装,到底有什么好处呢? + +优点: + +- 参数复用;参考上面的 checkAge 函数,把 18 这个参数缓存起来,多个地方用到 18 的就可以直接调用 +- 将多元函数比变成一元函数,然后组合函数产生更强大功能 + +- 延迟运行;像经常使用的 bind,就是基于柯里化实现的; + +``` +Function.prototype.bind = function (context) { + var _this = this + var args = Array.prototype.slice.call(arguments, 1) + + return function() { + return _this.apply(context, args) + } +} +``` + +那缺点也显然易见: + +- 使用了大量的闭包,内存得不到释放,容易造成内存泄漏 + +对比传统的函数调用,则不会产生闭包,使用完即可释放 + +其实在大部分应用中,主要的性能瓶颈是在操作 DOM 节点上,这 js 的性能损耗基本是可以忽略不计的,只要注意闭包的内存释放即可放心使用。 + +--- + +### 面试题 + +**一)** + +``` +// 实现一个add方法,使计算结果能够满足如下预期: +add(1,2,3) = 6; +add(1,2)(3) = 6; +add(1)(2)(3) = 6; +``` + +这个题目是想让 add 函数执行后,返回一个能够继续执行的函数,最终计算出所有参数的和,重点在于每次接受的参数可以有一个,也可以有多个(add 接受的参数个数固定); + +**答案如下:** + +``` +function curry(func) { + return function curriedFn(...args) { + // 若实参的个数小于形参的个数 + if (args.length < func.length) { + return function () { + // 等待传递的剩余参数 + // 第一部分参数在args里面,第二部分参数在arguments里面 + return curriedFn(...args.concat(...arguments)); + }; + } + // 如果实参大于等于形参的个数,立即执行并返回结果 + // args是剩余参数 + return func(...args); + }; +} +function add(a,b,c){ + return a+b+c; +} +const newFn = curry(add) +newFn(1)(2)(3) //6 +newFn(1,2)(3) //6 +newFn(1,2,3) //6 +``` + +上述考题是参数固定:也就是 add 已知参数就是 3 个;那参数不固定的,如何解决呢?请看第 2 题 + +**二)** + +``` +// 实现一个add方法,使计算结果能够满足如下预期: +add(1)(2)(3) = 6; +add(1, 2, 3)(4) = 10; +add(1)(2)(3)(4)(5) = 15; +``` + +这个题目相较于第 1 题,它的难点在于 add 的参数不固定;所以要继续优化; + +先来看下面两种解法 + +**解法 1.** + +``` +// 柯里化写法 +function sum(...arr) { + return arr.reduce((per, next) => { + return per + next; + }, 0); +} + +function curry(fn) { + let args = []; + return function curried(...res) { + if (res.length) { + args = [...args, ...res]; + return curried; + } else { + return fn.apply(this, args); + } + }; +} +let add = curry(sum); +console.log(add(1)(2)(3)()); //6 +``` + +**解法 2.** + +``` +//toString 写法 +function curry(a) { + function curried(item) { + a += item; + return curried; + } + curried.toString = function () { + return a; + }; + + return curried; +} +console.log(curry(1)(2)(3).toString()); //6 +``` + +以上两种方式虽然都能实现,但是**解法 1**需要最后再调用一次,而**解法 2**需要多调用一个转换函数;都有点勉强,不太符合考题调用方式; + +那来看最后一种实现方式: + +**解法 3.** + +``` +function add(...args) { + let final = [...args]; + setTimeout(() => { + console.log(final.reduce((sum, cur) => sum + cur)); + }, 0); + const inner = function (...args) { + final = [...final, ...args]; + return inner; + }; + return inner; +} +console.log(add(1)(2)(3)); //6 +``` + +这个方法利用了异步编程,setTimeout 中的内容延迟执行,算是个奇淫技巧,但终归是符合了考题的调用方法; + +具体使用哪种,还要看面试官想考什么?如果是考柯里化知识点,那就选**解法 1**;如果必须按照题目方式调用,那只能选择**解法 3** diff --git "a/docs/skill/web/\346\265\217\350\247\210\345\231\250\346\226\255\347\202\271\350\260\203\350\257\225.md" "b/docs/skill/web/\346\265\217\350\247\210\345\231\250\346\226\255\347\202\271\350\260\203\350\257\225.md" new file mode 100644 index 0000000..f4e876f --- /dev/null +++ "b/docs/skill/web/\346\265\217\350\247\210\345\231\250\346\226\255\347\202\271\350\260\203\350\257\225.md" @@ -0,0 +1,30 @@ +--- +slug: breakpoint-debug +title: 浏览器断点调试 +date: 2021-08-27 +authors: youngjeff +tags: [效率, code, 总结] +keywords: [效率,code, 总结] +--- + +> 以 chrome 为例 + +1.Pause script excution(F8) 单步执行,点击运行到下一个断点,如果没有设置断点会直接运行完代码 + +2.Step over next function call(F10)单步跳过,点击运行到代码的下一行 + +3.Step into next function call(F11)单步进入,会进入函数内部调试,进入后可继续执行 1 和 2 的操作 + +4.Step out of current function(shift+F11) 【单步跳出】: 会跳出当前这个断点的函数,和 3 相反 + +5.step (F9)一步步执行 + +6.Deactivate breakpoints 使所有断点临时失效 + +7.Don’t Pause on exceptions 不要在异常处暂停, + +8.Pause On Caught Exceptions 若抛出异常则需要暂停在那里 + +9.Watch: 监听表达式 不需要一次又一次地输入一个变量名或者表达式,你只需将他们添加到监视列表中就可以时时观察它们的变化: + +9.Call stack:调用栈 diff --git "a/docs/tools/VScode\347\233\270\345\205\263\351\205\215\347\275\256.md" "b/docs/tools/VScode\347\233\270\345\205\263\351\205\215\347\275\256.md" new file mode 100644 index 0000000..ff85120 --- /dev/null +++ "b/docs/tools/VScode\347\233\270\345\205\263\351\205\215\347\275\256.md" @@ -0,0 +1,51 @@ +--- +id: vscode-config +slug: /vscode-config +title: VScode相关配置 +date: 2023-10-16 +authors: youngjeff +tags: [vscode, 开发工具, 配置] +keywords: [vscode, 开发工具, 配置] +--- + +## 插件推荐 + +### GitHub Copilot + +AI 写代码,用过都说好。 + +官网地址 [GitHub Copilot · Your AI pair programmer](https://copilot.github.com/) + +### CodeGeeX + +上面的 GitHub Copilot 是收费的,对于想白嫖的,可以看看这个免费的国产 AI 编程助手,体验也不错。 + +官网地址 [CodeGeeX 智能编程助手](https://codegeex.cn/) + +### Prettier + +如果是 Vue2 用户的话,Vetur 是必装一个插件,不仅能格式化代码,还能提供相对于的提示,如果转型为 Vue3 的话,同样也有插件 Volar 可供选择。 + +### ESLint + +前端工程化代码规范必备,无需多言。 + +### LeetCode + +LeetCode 刷题必备,刷题必备。 + +### 微信读书 + +可以在 vscode 里微信读书 + +### px to rem & rpx & vw + +css 单位转换插件,支持 px、rem、rpx、vw 单位互相转换。 + +### WindiCSS IntelliSense + +[WindiCSS](https://cn.windicss.org/) 插件,提供智能提示。 + +### koroFileHeader + +文件头部注释插件,支持 js、ts、vue、html、css、json、md、go、php、java、c、c diff --git "a/docs/tools/\345\233\276\347\211\207\345\216\213\347\274\251.md" "b/docs/tools/\345\233\276\347\211\207\345\216\213\347\274\251.md" new file mode 100644 index 0000000..8708511 --- /dev/null +++ "b/docs/tools/\345\233\276\347\211\207\345\216\213\347\274\251.md" @@ -0,0 +1,13 @@ +--- +id: image-compress +slug: /image-compress +title: 图片无损压缩 +date: 2023-10-16 +authors: youngjeff +tags: [tools] +keywords: [tools] +--- + +- 通过 [tinypng](https://tinypng.com/) 工具进行图片压缩,经实测,对大多数 图片该工具压缩率可达 70%以上(这一步收益巨大),且画质变化肉眼几乎看不出来 + +![图1](/img/docs/tools/1.png) diff --git a/sidebars.js b/sidebars.js index 416ef1b..c381158 100644 --- a/sidebars.js +++ b/sidebars.js @@ -31,6 +31,19 @@ const sidebars = { }, ], }, + { + label: 'js/ts', + type: 'category', + link: { + type: 'generated-index', + }, + items: [ + { + type: 'autogenerated', + dirName: 'skill/js', + }, + ], + }, { label: 'Css', type: 'category', @@ -44,6 +57,19 @@ const sidebars = { }, ], }, + { + label: '源码实现', + type: 'category', + link: { + type: 'generated-index', + }, + items: [ + { + type: 'autogenerated', + dirName: 'skill/coding', + }, + ], + }, { label: '其他', type: 'category', @@ -60,6 +86,8 @@ const sidebars = { ], tools: [ 'tools/introduction', + 'tools/image-compress', + 'tools/vscode-config' ] } diff --git a/static/img/blog/function-curry/1.webp b/static/img/blog/function-curry/1.webp new file mode 100644 index 0000000..15a98bc Binary files /dev/null and b/static/img/blog/function-curry/1.webp differ diff --git a/static/img/blog/gulp-auto-build-case/1.webp b/static/img/blog/gulp-auto-build-case/1.webp new file mode 100644 index 0000000..bc0a006 Binary files /dev/null and b/static/img/blog/gulp-auto-build-case/1.webp differ diff --git a/static/img/blog/gulp-auto-build-case/2.webp b/static/img/blog/gulp-auto-build-case/2.webp new file mode 100644 index 0000000..0117edd Binary files /dev/null and b/static/img/blog/gulp-auto-build-case/2.webp differ diff --git a/static/img/blog/gulp-auto-build-case/3.webp b/static/img/blog/gulp-auto-build-case/3.webp new file mode 100644 index 0000000..e520e12 Binary files /dev/null and b/static/img/blog/gulp-auto-build-case/3.webp differ diff --git a/static/img/blog/gulp-auto-build-case/4.webp b/static/img/blog/gulp-auto-build-case/4.webp new file mode 100644 index 0000000..b16a005 Binary files /dev/null and b/static/img/blog/gulp-auto-build-case/4.webp differ diff --git a/static/img/blog/js-async-coding/1.webp b/static/img/blog/js-async-coding/1.webp new file mode 100644 index 0000000..8222b54 Binary files /dev/null and b/static/img/blog/js-async-coding/1.webp differ diff --git a/static/img/blog/js-fp-coding/1.webp b/static/img/blog/js-fp-coding/1.webp new file mode 100644 index 0000000..92975e1 Binary files /dev/null and b/static/img/blog/js-fp-coding/1.webp differ diff --git a/static/img/blog/lodash-fp/1.webp b/static/img/blog/lodash-fp/1.webp new file mode 100644 index 0000000..f5fc4d1 Binary files /dev/null and b/static/img/blog/lodash-fp/1.webp differ diff --git a/static/img/blog/lodash-fp/2.webp b/static/img/blog/lodash-fp/2.webp new file mode 100644 index 0000000..c9e199c Binary files /dev/null and b/static/img/blog/lodash-fp/2.webp differ diff --git a/static/img/blog/ssr-study/1.webp b/static/img/blog/ssr-study/1.webp new file mode 100644 index 0000000..dab8945 Binary files /dev/null and b/static/img/blog/ssr-study/1.webp differ diff --git a/static/img/blog/ssr-study/2.webp b/static/img/blog/ssr-study/2.webp new file mode 100644 index 0000000..e1ee74d Binary files /dev/null and b/static/img/blog/ssr-study/2.webp differ diff --git a/static/img/blog/ssr-study/3.webp b/static/img/blog/ssr-study/3.webp new file mode 100644 index 0000000..3bde58b Binary files /dev/null and b/static/img/blog/ssr-study/3.webp differ diff --git a/static/img/blog/ssr-study/4.webp b/static/img/blog/ssr-study/4.webp new file mode 100644 index 0000000..1dae261 Binary files /dev/null and b/static/img/blog/ssr-study/4.webp differ diff --git a/static/img/blog/ssr-study/5.webp b/static/img/blog/ssr-study/5.webp new file mode 100644 index 0000000..b7326c0 Binary files /dev/null and b/static/img/blog/ssr-study/5.webp differ diff --git a/static/img/blog/ssr-study/6.webp b/static/img/blog/ssr-study/6.webp new file mode 100644 index 0000000..4d2050d Binary files /dev/null and b/static/img/blog/ssr-study/6.webp differ diff --git a/static/img/docs/tools/1.png b/static/img/docs/tools/1.png new file mode 100644 index 0000000..9f3cc3d Binary files /dev/null and b/static/img/docs/tools/1.png differ