From 2beb218d986ec41cef2049edebad4cc5d6bdcf51 Mon Sep 17 00:00:00 2001 From: YoungJeff <1416929275@qq.com> Date: Mon, 16 Oct 2023 16:05:21 +0800 Subject: [PATCH] =?UTF-8?q?feat=EF=BC=9Aadd=20blog?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Nuxt.js\346\216\245\345\205\245Sentry.md" | 12 - ...60\345\274\217\347\274\226\347\250\213.md" | 318 ++++++ ...\347\232\204FP\346\250\241\345\235\227.md" | 78 ++ ...46\344\271\240\346\200\273\347\273\223.md" | 100 ++ ...04\345\273\272\346\241\210\344\276\213.md" | 664 ++++++++++++ ...74\345\256\271\351\227\256\351\242\230.md" | 2 - .../Promise\346\211\213\345\206\231.md" | 974 ++++++++++++++++++ ...344\272\233CSS\345\261\236\346\200\247.md" | 78 -- ...02\346\255\245\347\274\226\347\250\213.md" | 125 +++ ...37\344\270\200\346\226\271\346\241\210.md" | 120 +++ ...60\346\237\257\351\207\214\345\214\226.md" | 266 +++++ ...55\347\202\271\350\260\203\350\257\225.md" | 30 + ...70\345\205\263\351\205\215\347\275\256.md" | 51 + ...76\347\211\207\345\216\213\347\274\251.md" | 13 + sidebars.js | 28 + static/img/blog/function-curry/1.webp | Bin 0 -> 36868 bytes static/img/blog/gulp-auto-build-case/1.webp | Bin 0 -> 16440 bytes static/img/blog/gulp-auto-build-case/2.webp | Bin 0 -> 47534 bytes static/img/blog/gulp-auto-build-case/3.webp | Bin 0 -> 42916 bytes static/img/blog/gulp-auto-build-case/4.webp | Bin 0 -> 52966 bytes static/img/blog/js-async-coding/1.webp | Bin 0 -> 16246 bytes static/img/blog/js-fp-coding/1.webp | Bin 0 -> 6178 bytes static/img/blog/lodash-fp/1.webp | Bin 0 -> 17270 bytes static/img/blog/lodash-fp/2.webp | Bin 0 -> 14948 bytes static/img/blog/ssr-study/1.webp | Bin 0 -> 6964 bytes static/img/blog/ssr-study/2.webp | Bin 0 -> 9118 bytes static/img/blog/ssr-study/3.webp | Bin 0 -> 7242 bytes static/img/blog/ssr-study/4.webp | Bin 0 -> 7624 bytes static/img/blog/ssr-study/5.webp | Bin 0 -> 8826 bytes static/img/blog/ssr-study/6.webp | Bin 0 -> 6216 bytes static/img/docs/tools/1.png | Bin 0 -> 1525766 bytes 31 files changed, 2767 insertions(+), 92 deletions(-) create mode 100644 "blog/program/JavaScript\344\271\213\345\207\275\346\225\260\345\274\217\347\274\226\347\250\213.md" create mode 100644 "blog/program/Lodash\347\232\204FP\346\250\241\345\235\227.md" create mode 100644 "blog/program/SSR\345\255\246\344\271\240\346\200\273\347\273\223.md" create mode 100644 "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" rename "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" => "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" (99%) create mode 100644 "docs/skill/coding/Promise\346\211\213\345\206\231.md" delete mode 100644 "docs/skill/css/\344\270\200\344\272\233CSS\345\261\236\346\200\247.md" create mode 100644 "docs/skill/js/JavaScript\344\271\213\345\274\202\346\255\245\347\274\226\347\250\213.md" create mode 100644 "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" create mode 100644 "docs/skill/js/\345\207\275\346\225\260\346\237\257\351\207\214\345\214\226.md" create mode 100644 "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" create mode 100644 "docs/tools/VScode\347\233\270\345\205\263\351\205\215\347\275\256.md" create mode 100644 "docs/tools/\345\233\276\347\211\207\345\216\213\347\274\251.md" create mode 100644 static/img/blog/function-curry/1.webp create mode 100644 static/img/blog/gulp-auto-build-case/1.webp create mode 100644 static/img/blog/gulp-auto-build-case/2.webp create mode 100644 static/img/blog/gulp-auto-build-case/3.webp create mode 100644 static/img/blog/gulp-auto-build-case/4.webp create mode 100644 static/img/blog/js-async-coding/1.webp create mode 100644 static/img/blog/js-fp-coding/1.webp create mode 100644 static/img/blog/lodash-fp/1.webp create mode 100644 static/img/blog/lodash-fp/2.webp create mode 100644 static/img/blog/ssr-study/1.webp create mode 100644 static/img/blog/ssr-study/2.webp create mode 100644 static/img/blog/ssr-study/3.webp create mode 100644 static/img/blog/ssr-study/4.webp create mode 100644 static/img/blog/ssr-study/5.webp create mode 100644 static/img/blog/ssr-study/6.webp create mode 100644 static/img/docs/tools/1.png 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 #{<&zi4*9Wo;q;^z@1G4+Q7P<)B)a}^Gj2~UGr4m*$uoSh!jO$$BTD}ii{k{N`<_kJ^-HDZqtQK`LlMy&`J zty{${aEj) QRDB>OQDUHsD7c;Rhl8b`0Rtfg57PM0FW(G@c^%QF67ps=$R>VK zrtW7U(tPK2;jty%6S_|*#>tbBRZ8+{M~IDP!A3BJ9d>lF<-RRJkfpe>zlg#YMHWVJ zOoAW_uZRa)wCku1yvXjKyD8wlA0Iy~DjHYtuzR&fH9#5WrIyE(=%9kcJ$g@Gqc0sf zg(Z|V%(X+fC9m%PPD4l4+=eVmx#t|(w?W=f5;Oeu2q@iRJ*5w^@f6W)E%PU?wo8cf zb3d^0c^e3g$o7d=&iK}4+kdZB{f>I`oDakIRXR&Cg4bv9zGz$%|8p)0-8HNhy(=Rz zc)+Qi_9eCE&Qg#@(gGWX4}2Fi4|Oc^i6{lai%n?~ A z-`;JPS{oeK4v !4eEzcGz3mrmB8hB>A(zqc$sO0s zM)8KvasgC(DciUdD=y8!sR89I)};udTXciN!0B8d){oFsj%vbl2xr=2w4F#Pj~0)N zV9N-r1FYT4-lqCA0sFimAU|8^)W%eqesu|V0$$o-_^t!Ddfxd|dA*H^mn9%F; <(R{1qhUR zuHuqcEKyKAvm76y*AH}VKJWYVdNg+>P1pIdV=~x@!jj#oM}0T5v$M#bR`uOD4L8?f z^v~`76Mdr`T?3QNPhd}y=p8Jy3@kW14SQKp- G|gye*yPL9!irH; zCK9d7^qd?eG#i zmMQhz{h3o`>1qS>uCjE6?fOA9%sw!fSct91Ml9T;8Oj|Z{OSZ){f!DUv1% R+r!XdekHLt%87nrL1#1X)Il^YQ9*hZ3 zR{8s6<1u`h0C%v0Hkcb6JGYWEFIu39Zn6o+!}A@w{J?yR=1?utQw%kw7uXP v9AZt8c8@y`_gB2$fUrI=0c-qij zX1ImlS6iQk#zCvS!>lMA REdK z74(1#FCKR5O1P=Z9cYMm4hw-vF3DpOEB2V5)1lvb?c8C$7g8JksOEMn&xH+w-jm4j z0fm>1jBIVd@IXE+v8L0s>Ru0xCfwW+93{G(QDXgk%NfZ{lfqaGickGr#bfiWRpszR zz&!{4StL@e))v9fD`3N%CuruD_G8b= fVa79VnG4ojkrs-()zXc`{uos@w%uO_$?P~~@&ZE^%7ew}OI zX57%#`yGRtuVp{@`d0>rX|}X1JSFJY3a2LRtmtqc11^(HvG;Dewkl?WUCrEY@#sGW z%XXvY2IbLjORoYuqN%KndDVM_nMw1iR*T7Uvh!rZrynbb7!e}dn1iaTuY+W2wMV{@ zvdu#^p{fnqRSMmTJ&CHRCREW%+P@ C3GC|l^?kx8(rhLxkjP#z3tR2?R=qzDQL9UlM%;JoBu0}Id-jqRi4 zDGKW6dXLgddq%FR97{h{_-7GcdU=p?18bDYH&Z^Ochz$+7EAaMPwCS)nh|TL!AbB1 z@D%HN`Dvfq)4J;vk6n*9>_(6qzZMR*w!aBU_V{Q6%ulJ>r^{ys!UXMP7gN_zoGE`K z8M 9?JJMjx_!xA^XT!~{Sz&p&$kHN9t_%M7 zbVcE}GR+rZ-bXmeJnP+zVJ+)%G38gBs#Pp0oV~9vb>yE(RGSpJuID`48vd7P_Y=)d zrV9GPA~L(-br`mvC3BMe6#-l-cEwt~BqQ0O+}4DnayEBe0IRb+$~BiD_0BE*TD7K> z3T}Uj)GPmABq#M#7RBVp=giG=8(H&*S7e2QklSS1 zYSDUAK?$J#{^QN;UvTA&f}TXm(QFZR9E?nR;_lek2PMwb-TZkZsd~6Hl{LW4LgHh? z^w)vpiUWx;jv8|PDY^ykb*)R)QIAgwI$p1BGD<97KwUO`P@e(Mb@Db5*x4NiWOn~X zVvV!yxdPd0->!E3o%1{5gtOv`UWH_R><(lCHh+tCcS(}Od?gyEsWKjff^Uf)El C0+G`&|v zIlt< h7A4KlD`;W5%!?PV|e0>||o?8OKIQ;Sjl> JZMnf$iX85Jfd`Cb)pAUrWZdh|t6TZo`_PEL9|Ipjx_Mp@ zMboaHmXtng(A5U9;YQhi^_(e5z6ICYxo2zG+o+8=n+83V8TY3)7x6lZwS*$4))&0d zK2w|^E=*+yABC`k47-w^MOJazvc~(d8x*vDkwHak5M7M1^kT~u`}(r~j#b{O#*jiM zbZ9j|$l?=ST|0izbD}rR)H1Bxq7+;UXiyw2wL=Y8YXWVXIuTYwSDT{h%!*ekYTJTW $e)dWnxs*zA2x;|eZ3ks! zz6Uyh&C`vY(bUI=v85t8kS9^db8q7*8U(hFMV&P<6;xPO15T}bx#dC4#mMXr=L?dRXe_x7X~}O+?`POO>S3nAr3g9nL7!%t+CqQP zqC96XI)y#S;0t$azDYeelQ$G3f?F#9FTrG2`YhMc=`4rFK{h=(B01g7OXiJ5t~^lk zrk=Y(nhvDFxu?69reJDFR~oL7c<6l|;aRXx@3oNTfT}HzcLG7$5I;=6zM6WX8{M3< zpxLbD6A{F)dhgaMd=erd9jaje1FbpUsRoLQt#Z516X#g~;#ekv<)G+`V|7*%g?&(lZ zY>0}ghQZwim!mCWOb$fNp!)_d-?kMFR)=9sGX|MhWqjOfl8oDZ$8g+-lqS_>F(VYL zOceD~Lvn!qrbE|q-W+5AR Vu%tWVMW8d7%@Dk8f_t0hRpJ(3W+A?TNrU_4bw(lJzX@qST`Pc7u$ zc#b4J?NB}#K;m}XW;-Sr?@>pns5QMSkl`2wx}Wb+Tj$Ei3f+Yg`Z1p_TJrO5Jq=Bv zSj+>e8Sn_sDCtbwAJZFODmjsF{sROFi< `)}EB!l{Yd5y!o$9k5u7q%=_FXd^)maqZgX-`GWo2{(+Rm5zy9?{p zZSRd+dH}Sg1X0PH0<61jI)|O`nbRQ8% +ELoLvF;;kZTkk _ck_c2t#ACm9 39z+ z=i0gg8YpdZv{3kV_
ULhbi6W{7y{0K)-L>XZ9li9b=%+cX?r2s%@KbcJyW+`d zXszLQE-|}fFw`_vFA^dyB@HDJ-L2_-OHgr8uy!Ux7@}&m+ iu6O`AqRg{QWjST*o+tu+bm??TzH9%8ikl zB=)JRW9>k 3qZE4Fcw2ZUxJSjrq2!5GLX@;K>^%H!uH0Ej&4NifmLBFA#uO5)JznFhr2q7y zoq~tR8q*2p&JICJ&L!D%{Cm4?A01pL#g~djF99GVzXl$hKk@!b3>uWKAOGCm<`XBf z^{?-{Hb`Jxq3#YZ#S5&dUSEnIL~{s#(Z;bN7X%H<)W^Lxau#)~zOJR^uzEi-SJPt! zj3(@xG1Lpu=$8Nh3@2c$T>T9No0YN5Sg7g#u9=&RB|R9Wb-x0TO69bKr8#?%B-38{ z??d^%Ma YLt7{fNH$3V|!CQ=X-zwQwlVPl*2$NvC1CyOPLDq9#< za=n)FkhIjSGY8DGgPgJ*tb~Lz^xJ-hcfG-Nt3c1;MBNHT%z%Ou_=fu0qm&yH3 ;zwB= M(Lc9BrQ)b3b@r7TZbKbdGB=xjCNv)x99$X+5-*Vv!l~^j& zd3%)E@NQ-iv}?bX^%ukqwr7*2UeBcHyVMKEfz?hy#U!c)iGGOuHuz?iF-`_PROX__ zGJBMx@MG>%j~bK}qRrf@oK9DmB>5C6*eZ$gqReUooN;C-*XP<(?_8f|Sj4HcT|d)>Gk5an zN||h3Dc53Rek2rL>13p-84|p@`%BK=i6;HxsMiW{rm{#^SKYtstn6uwkkh0Ha<_FL z-k&E!z_J}s1tvuZFuEY5E;!yU;&N-YmKZ}od~{$wc~g4|23y>qJfx)YLORA$D1p}b zMwYR=Ls*%+_jq6hczlx7Y5y8ra!r9^0U2|fTNFa;)%ih-)FY?xRkx=lbhj@Fcf|vG z?>ONNUv9kt-&aFq;}M Q_hsrI$zgx 2!5~FS*mH|_fd7A zQ9E J%V zDubiY6Q!yC=xv7R{W$}qYsE6~?;;Et2)wX?G4Haeu#q95voR_0C6Wbfc9aXo$V7Q& zFG$?&^)rYl*InZ7GdsOh*6vqr UBFqfFuXgmDA%A5~HTfgt3wWTsSQEoRjgb8`-fUp*~h0v$$p}3{m zg@{^(R^N#>tYitlKEbbCGFhm3Vvo{ngIhRiVLe2@65Q;WE7@WknumXYtuYOz%o5j9 zDc|!pfZ-LOym9JZO*b2;YYF>dfJ`2E-#pTwqk#$I+p4u;8htDa%z})4>+6YVs48L1 zimdYIt^fFtGr{rKZ)wS1668%L+!!J&hiqG^KKD#%4WO<(CVGG0U0N8PNRaGRY9}TY z4TICBO%)!1QE9Ah;=*$}^>j79VSQCrrPI~zlX8$ECO=3es*KH!9NJnkx1ovVB+rMp zB5t+ZG=RISI-=H=^jVX-L>C@hA4#CDY3a%*9hP_)gZX08Qof+aFcAjxj yE}!G7zgHZ zCWrYoVEEXZ+rMu<%3iH0{o8hUBT5$MB+184GEV{Q77W6`gWVbo&Jz9gFo~^QSI7>~ z-)>g-NS{f8D)s(!#Qcw^Bq1+dK@bQ`-QX6^>T)9_-0#X~#ATxt=+=`NE*w@THjf&~ z?|ufpD=zdIY51fBj82%7$kFUdaFUaw8Z!Ox=9bSK9C UmCg^*i|=R!$I&p^SpWEdsa8Kgv9gHaGl@j)Q{|$Q5L-NX_6qhi@AOuHO$dP( zBzWQ}KCp*LBjj|R&JhtZx@Or*)Pb1A$O)hjN_}uhgSzGpf%SWg4A+ogxgf%Dsl?LM zLhEN!WbFDYA{Ka?dW#LpIx#UybDk3-D-=Yi!Hcb|j*~Y;T0Cn66iB@SAeBGcbNMrS z?FPS3VCTSiWeA_-#?WQkChk}p%_>fh(U1=NOK~p(3fOilIY;J;(g@V(g%`O^a4(n` zPfl;%fxwPlhQ%1QLsoCQ+2bq!o30pt*NJ*MOL%%M!&X*U6$5E?--`e|1f79!1i~Ds zVvftyk7OPPae__PflN7HuPf-FJUD)^JN#2!d;fX#(d yoGFrtd%Z`1?uBRbsRkhF+WgY zD>!D1*t)9mTu&x$V#b*FYCQAZg|dEN75U}7BoSiR9mH|Ge{}-E8jevp9LVeWYvGUX z3V|*ty-$<#n|{gH UGF7Zo9!e<_Vq}wQa ziC6Q%&@`iLN$n5_V55OoHYri)^U~VNy`A^CZ66&6Eh|s?IX`2Gku-0Wlb*1V*T|k( zJ`vrSlap|!FtgTWvr3%27>P#xhTlcsQlGi+7Kmm;q@ow@;% wTPw7boZ z%WHY@Szan)>-AWi%!P-M_%bvjs%!CvE1;UF-S0wq%J)5;$xfMACW^L`583Wyd4{`Y zS+w2<+gfGj)wc2QDW;ADYs+}9vDqgNB(q1e+_%Sk{USC-mXU3{Ftu-f!72C|etXZH zK!b-oSuAa|Usv!TF%wo<4FU$II$?c^o>ayyPz6w&zG;Bvbv=7^s&9Cr*__3hJBmZ{ z#(;^dJfHIiNoAXPpg!+m%{;OE2zo7{Cj9QemYo432qI7`Xa5Csl7oCyWXNxBk>+Pu zVZyWlY|<(`{5sa~RNpp*?$Gv*J`J;0EY*^`hgDp&E>N*k5w(IxtRPOrMZUbp>n)Bj zD3+G5XoYRM9 0y>ZP;o=m+YClNP#Fw0U!%>DB}xCU3n;5=QjL z+~afuV~6~)!gle+M(*q8{b8;e@9@hV65sZuN(G6vr}-9HueI}nB@X%?hiXl6if)pR z=?>D->-R9uUQYRG-FNx)@@6^dnhGX;xqS?HH@sryYavNN!?m;dXH%JmC{6>g>R)Eb zAfpadV0q eIes-7F+H+Gur$P<7Ib^wX7Dd%08oQjNd zbSwBP<@5(-Ujl^GqOR|(U^k8%Jh6-R0Pd}mcG!*4Patid BoX>S5B{i*8I;Sm8lgiO>|@r|Dp`~CQd*UXylUCg&8fh7 z7@!CFU#t1s4P4=sQg9Zy?o1t3p_F7kuoEWjE%Oo)BjLTfb9=9taA@3q%GsE=LnWVU zbpY1(msVn3o{TE+uB6{<-xwcV7PV-|j@RFh-&;jL{ZjV+@12sf$nB=fpG)H~TjS5A ziuQGG+4DQN!2qrc*dLKTdN=olq;p?9LOSZ+r!eh2Sn50p4f&u4V {nT((FK{IVKqO}$`>Uui3FOv*LMG|zpJSFxvz zXlz&syVW}5pWvAa&Q=-mGO2!A9Ro%$-5#DU5kCaH0I}!I2Sgb)n<6(mF@k$r)zjfi z3l_nY)Svq;Mt~Zry+|<&`pGIFiGKhp&}~dp>8!|hq?D7aLw=R^2e-J=s_KFF|IsSR zXK Y$5Xv&FCaX`y>-x-~4*1sQkSD3dM@;(3*hWG2lo2Z; z=6o@H5AP{dUr?YYY9)T_iWrNclcTtqVy|r+M(XVPd)ovoI#^@)`g?3OeyEVMLU97g zGkxtf0oakDixgbzn@Gl>y0hN&0xe;m!*O*xm>2yAs?U&nL7F7E=lI|f(|jc41qJBa z+U>$vf#P+Xv>;P@pWR>(7hBA2cusgQoe=-6MGhKoeA(zC&5pqdQoMHJy@#%TuZ7`# zE@lsw#TcOuzWhiJbc$^MY-{jH7k#5xclZ-YklRBD+x8>BRt+*0if3zL5 z3+}5VADnf_C$8W@Z-DTw4aK_6Z2xyLds|=wTyhja&f=BSBC6=v8iWRFB_)l;eQ9nd zN^*!K;R#J>TC&?!69&>}{xUo2@4{!ld DX|&AGcnO@tk+X|DA5d7> zqT8x>0c&*;prXv$ThS0SIk9A6J>Eg^;(^~S_C?>4odz2Efuh~Sc%>N)cHP`@*JSMG zgMLC&80Bbxu-(WZrtbI&@W^O&Vi0t#iRR0Q<2%J7Abm1ntt>Keg(QLE16AItu+WxJ zpCSDxq)UlYBsaxTp7ab2{tfKc8~~T@rj8KW2~}WaA17U6^nARym8Rln0HocFoysRa z83wLc%D86hXPhR;@phmN2-9i-pRI201(VLxVg||XSnb>ZMxLE)nGLy5%K@F^n-1C` zXv3d<7O=eG-79ZmG~{!2oo$}Rj((vpVzrw$CW9z35rTX2Nkllq%DeSvOkVYvEwAzD z!)rFhpNL7L7VpLY00000003z+o=BoW-CV74_`Jn$pP}}P*a*me0w0!-qJ^`*E2~8* z$ihP3{8Qf@GIT*S0001A-Uk7Wk+Rd|q32K?M&F8LK^OZcrzs#n+9Vdi6XA$Y{`U)i z+|^3x{O>X56aWAMPK9ytfvxzh l6O5-WBJY%q?}lz*;$CL{BaXzcMn) zM2hnPs8jqZjFyHkOB^*jH4ck?#8(JLR->p?%5s4|y}*YX!Hh-j5z4EMaSa-ly*@M; zL@U-kACnNCU#l?P1GOh@mT|tcuSbhnPHa=QL10Z+$`6Y?uzh61Dr9&HE3BWsYKj(? zhS&;qw)xlBWmE|vQ>Yerq|K;M!W&q3ZA2FF3VAeMTW(h8l#$T_DX{qm0Z_4n%lSDS zv<>NZX`29U(W(S}WM9?Ezr} V$y+&Qb|>tmVv zOGv-3Be;X-OX(luVL*t`r|7jS?e-a=<(y@{FP4q>OBQi^C&%6y2KLY zpO{d_L4m<51lD)UY2%)ZS!rynTv>af&*k!nvGSsGP5D6N_!qQuPHg;X0L8=3-hFKH zKe;#PP^QsKWTfT5Aadpn-oqub5V4^U^Pu?q5fjOSfB*nuyTZg%4K4+|+IUj| _HrEaeaD96+l2*9BBghUA(@~6MlztJJQl3cN1}0O09-{1$^(^&3_{w zdCSf+@b^W{ ^o7I_} #@`gi7#@93-O4fc127vbS!5L}5A`3jQ6M7(qW9^Iz00sy}MBSq= zSq#RQM 6IHYGGc`22)wIXmpHWSuN#eoo*{RI_(-^i38JL@k zr^?e0fV``I8=GX}Yns;Jmu_5})O~rghfQ}k3e*BI=N!XMjjXi>M#RJD=sS7aI|U`u z+WQn@-O^dDO#JB>8iGys-zgP2D0O;e02VV-Mk%0{k@5yA^(WrZ!7f@gghuVuT U<^UHOX0*Obxj5c{ikg_3 zJpKQ@7Nd1y?psK_?G+9}U+0f^^l;m9edRz8w;eUKGv?7dQj9VXM)|e?J7uK;O{NuD z-kG>_?z?A;4(gI8Yu^|jUA3C`98zRbzRyQ_{9A4qAO(|7c%zPA&|yb&O@d5XA*I}U zdUuu8?>aW`V0OpAIY`fxqM8ha`odwr0B`}TN(F~Evy;U|wwz;4MX=}rAdIV IkN7H#RD*eS-12Ni=?r%-w%&V%})Y~FhPCx+G ELzlW?LBG^b zfB)bGNvL( cho z#lY^r)@EU|UgWa36j|ekshHYR&ySxHdF?WX-fvGpG( Rpdim!GEd%X5JtN9fsTLy&L>o>VRnGeMKM@f@s+pG=+P{7+jq0y% zIo^x5x^UkxVX0qYSx|D)*hESwufy4=)6VDU1#ff+RhoR<(WC=XC wAGyKq(ZR|IWZEy;|&r6uZ*qRuUX}M@(@Rny~gG9ZufZLQ33z2*MVlo*G zFZn8y;*z*pumkA6pI2rP=(;Rsy9pmqNKilU3ZftVb7Hmwv^EHCvhAkV&NO~Mhk+49 zm94(!ZY7=XWqSenv>y5*8!tfdGu)6$0%^64+FHIB*7=RI6Wp{1hm-8>h&|1RpTviY zqL`WQXZ%@}=F+J*;8jw#Z5gDiOojJJf4Qx9w-!~am_6!a-^k7=7eM&RD;Vc_NXGq~ z1;QWgaFYYU-$e!^=?|v^9%+xRi@%fWoCcp+C`xY*AsLLRij-V&Ni_!u(#mq178txf zE}I7tm3CtYWm2`$_o)R_i(Q7@b4#%%7>&uy3#n1u5w_U(wY-<|JA}9Uwd*j ajfNM7@g!%#0E>f^Qb13EWFLwUxrJuELl9I5o5 eJvst>VAG zj6V;H#^TLO&7;F%%mHxJF1!q`caql2eo4!Ruww=BS{mq4fE)k-GPD;l8sL-B)%hsK zAjoIb9)GXQ{KY>?%J{f`_k`cevzL83bAJWB0|an?imWk%Iyj%>mDWFPqDN8fX*g@( z{V?noYfXt4C?^4 9pf9>@nwEDpy+PEZPz_8PgNFSlnr7nd8J z$N-9uVVw30=Yz3zE^b?(4w7=4MR?|=Itq*E`u%6Yk4AVX(g^U{<+fZxqHQ+b({)%Zb|yvNW{(aavW8vkT*=jcR2Y6v ztJ<-}+U3B)8p3Z1b;tXuY4{!F;bqdxpw3-b>X`CfQZqkWh1@M&u~p~-id6xQylx8_ zxb$2+30wf*I@fT(7387S9a&fpk&!P^+2;jDKGYNQ_VAo5@o}E$JhPK(_F{e;pmw4f zkUh8U< 2< j zS}d8f66Kf|z~Oi_EZMO54~)>GcRMFsfl|_7^hmKZq=dsObCnJR)8nEY+2X}6SQc)7 z7DB%HCv0JSR#?mh }pB)xOonW5I?|pA@$Vslt{DnsAS7 z9BUgxiYEdX8S<()*3JDYnd+$~GWhDy7{JUOC&f@_*1a9Ng~(nmLL@wCiWfCN)2 z?hPa<@$~J!56eQ@X#?f7IA!QtmG)|8(Hv>$Q8p@?Oc%>mhmJ9cy@i4loWlK%ie7 zwR;pcL;thN7bkkLS~uLyo8VM<|5t&Ld^g|k3{5;4(Hto?bhGh|L1=;9IAE|Z&lrN; z&^)V+oUzc=S-yDBNCfn9NJvP>G~J#c@&Oj=!`s}{nbZu?^y(mq1FX(+x_K#F$;6#1 z;E)i|Xy(*{G(o%BtN2ZUMek|2D13I73+?+G9v#8`uYRr!zxW}<2MP6ZOgNHggUxZ~ zVZVsGY-rcIco9+yYbQ?E+av59NtlXlUyX#rDg#&o61YB61u(GQnLQbubPtGkJ`V+_ zr_Xz?(~Crr?agZUigr{1O}MdNI(rq>S1mt$ &(Gz7x1Zd2WPsK@P 6!ifQ;i~=L$9`Jk@E@H_7#2r00#IF_3Zr=+H#Hmh$~0VIQSVz=%UIA1ldAaCI&j? z b7og9FTSf0sOPm}{jT0gg|Z3NTT2f|ytJy%x)H6U0C_fBO+^rCuoPkQ~Q zL1Bi1+asg9(AL)5pUY`n&0D_vgk5c9+V_nnB($wM#189cLN@bq4Dnfj&Oj?(TGo(s znfE6mwJk7SS$KfW>Dp9N5dNizH|3oNT0r7GVx4+BgWi@(#`*B$dd_w`7G7vK$ w;*@v;ng#l0j=8VP6x&B+ UUrRhuIHse zX&jMJ1@X^he~jbe!8-<)1r0T}23Wtqa*Hc=y i^G?=^RkeT zhqanp!!i|Fh~As9ozVH8O{453tn#R*kUEec)CHq??2^@%b%`B~8+K+g9JK&UFUX$k z_=7i=6UrZxPv)*lZ&?op)QT9j>?kb-t(XN$ r(0 zUwiFN1JXwXDJkJfgiEk$;OOI9<511u>LBw)00000jpv!~kP2^Sp3$FAV;)n8Qc{6C z<#FayCGky{2^BbgyKw*vJcSrAC{QTu0it=WGtILF) U6t3wA0 zC^}d==-Fu$qF(p<*aWAG?G+_g$ZLd9-d|oi-e)oi7Eb8k-Di?0-pU4>)3ZG1sn8l% zB2MEMo6i#E7IiYoZ5L0pP-UT$Lk}79&!hPtsjk)ft2;4x_&&e{xh|1Zq>(wYmRDpC zR#_eWU_=iVYpMf2Z+^83G->uz)OT**5TDPj&5JZeXtYhkU(y-k+tv)D!Tco)T^=o< zQL*@J9EFB@gpP(40fkG!GC@YMyGhH1ot+B KWWu(}+(?hPrE0$u<3(Kjhq$t~J*zvlm>tAur)L z=xq*R{!bhgGHANMa(-vGFijg$e`!1mx%ks}=V}!AkK{%0li}@ykI>7~E#pkUMzT{# zz@CvbY^vx4w_T3H%XzRUsy8FD80}G-eQXa%zQUfL-im)5n?(QsF)VjcOslHe)Y~9G zHJhMijEj8U{-*2KVYt)Xrb7DgRWw2mX< 3ex$pyR5(f000|NO9mLz-pNpGJnjIn6!avwrBU_ax&Mn9ouz5F!98tLkurh1 z*<68PZBukFmX?TZZJETf@aPvMQP_hxDFa~}Mm(W0o{$j6 ME_L*2np;d`ydqJ`(Hm zV+*6B!yBjf%+M%Mn6Y>YDYQQ$2?@|ljV-D;uVGkGKx~DT8^3wwnBsfvza6Ld`n8}| z5W-Nu@i1MHX)2R7!{)jIy;3S56l^=(fLu>*y2&iA+;aVi+zEP~*KD7p>+m-`m0H8F zpKu+igBVwfSC8=x26ZPA0^{4P20!_5xBbFn-v>#?f|gO{aZ9pvH!)PDocR`JPIwv& zkSV=sJmsdT2scF0W=Kwc;|K)(Pe4GF_?i-NM(qFZvc|FekBy4@ib*%8000(ubYo4M z<82;v>J3NHvlDEq*JEeRHu`(5L?-~vo22%nPNK9wJ%H&aVZFJ@s1M`e!#)P)WIG(S zd$-%3BFo458kgL)fEp5*B ?=NQacpC%bHum=G?VR9yctRL>>~Vx`Bjlj>hgi)>e^JjKyUn++mJbu-d_ceMr( z=6tBg!#G> Gzk0%^70x z@Q)Saw@US>88?-n9EU43+0gBEY-F{|GFn(HO`>0tmj)h_JEBd*agK0gpB(;l)rQ+^ z4hfsrI*qcNk0vaE>+x8aaURu(%i@2|_*yWLB7#5&8W? ~J~k!A$CW6-I!!w4`ub&*25_Kx+JiroA-wgS zy3CB6+{wENed~2#C=88=kJPM@(tYZ~*%i_+{8u N!G#D3t(`4nC)!8pB zmFs-DctZuU(mqptRUH`bDAcOWFbW6sZTG^+q3Ew9bpYW8Np=;t^McM^&h*l`u3QuK zx)xBw-**A9%xOEq0%?9SH8(vP9zfsqFVzTuT({IBquuy*=;zi9$`C)k{y}m&`&Y~p z?EQDM(bf&9?pI7k trj0)Af~seyXZXqsWLt!*!=CI%7}M`62H(!x^VRsVXjRx z&dv}(Po<-&i+>t^AXS*uv
1lde}+4RU?<+}&Vj2rG0{-~ z$CdmNg|5CV9XCbQ1j9oKz~Z(`>}D3%Y_o5RoB=0_ht}^|wB}&A5XgAGMDzYl!og7j z1gQmJiJeI%`T? elC=l~6rt`+w6G%H(KSA{%aiSqHnMaX3Jy4j z@A+D>jOk76oiDU7N=OK6Yxh^-s$9zHdP;&n>!5}IXv1F*NZlzu#4c6qKc|a4DiLGd zb=K`(O{!~3CCKU^dEa>@YmQJ&eSYnNh7`EMmIgBVu9R-Df!a+^?tQOtB2+T#%*m|f zhbr`sIt#p2eK7R|kJBC4bO?=wqLsiM0V0sxS7d=erS3J@Ff5OIhb5V%J26l%rFZyJ zoV|sv5D-7Oyat$=t`??SCw7P>-}Ec@dmyt3ONPo(_>btzPoaSVG}i%qJ!$gmf5&D$ z3sWJ 8~3OuWcI_D=4&7JWl1CNqJ-zyen9RuiH1{q2T4>1#U; zCfj*^6bR9LF$ZZn%bvYlaHr306T8E=Cx%q`Ifc5<0003ZvMAoxNjh81O08zqGs9Y2 ziWKFhsDd5%x|hQsA1Zk$>wAUUHPlSeqvsgQ@jt$&d`G*@p1JG4Juwv7we`9lEm0dg z@lYy5sDLE98|I{K0y$kuB_+-vu(A5{h;o|b!L`T`{A1qq!!ksfD&ufV<+;Z|E?KS> zl37I%PH7+p)QnZej0f8OfvSkEE}-@1x<~me&S7BNAq6%xVY4wY`VTqSSu?`pfcC}) z&D%UA>v{JBn%I0gij8z+*6`~5R@K55WeI@gI#byO! k+1E82x37UVDw5O8 zs)+tf>9|6%pK jAOu8R84qtMxyh+A3y*A`k?Tn zoQ4M(CUD*X{&Qfg**mIJo?8tShodmIODse^Mdn#L+(@7R04HVT);0I36;%RwgI-U+ zPCZdU;k>ljz5{YasC_O1XPU{`^o53qhYw_knd3jR^j@^>7wn$J;;`CL&F^=KS_O0a z6&4bA8_3i3wQ?A^csBn| *Qd7h>Lgx;$7x}(l+VDcnHKs{a0000zC_E`AA&NAE z;nwc!;9P()KmY&>Kmnyn1XH*h=}*B!Y*Etz4XA^^_sHpj)y*W;{N#}8kq+TJqS4g# z0LR68hm>Lg4U4olJ;TvUZ1*8S`B;~h!!H%hybElQ+5(32NdJV#p5W{W2wP5KvoGFd z_}ik;g^e=x^qDynNhkzVD=HCutgzFiahu-Ch%$RI7w}HFt3qc%bKDoLs5}i|DAjED ze%JR9@mDm~S~JqL3VPQf yNOBFrzbcJ%V?;OyW4)s|qjCBz zMFXi7R*Iu2_n!&r-08dXj<>a(*9OmIyZ=D>oTX)y;bPgLHL>~Dz9Ctogk6MPvI*6x z^M6yAk{<#GLQ`9`%E=lKlG`U`Xe|431eb3qD-Y)t3Q=z~1_?L`Bx=^8O-%RgH&ZZ8{_SHufrYo32}W_2d%$j`Mdpyp5YO3n-1pw*Juz%0 zZeJ1Ce%KER3%c#`!de4TplT7%adu7_=co51qc2xgHcfx1-0e-ltaaA2TzX{K&Y5=$ zz9Y>cg5!e|Mvt^3gM$vzw3gIWc+hPYW7vf!Y*OTtn$=+19S)TmU%wc+kuUtqTvz-h ztMQZIQ)dANIlpt)`$K5-f~qew_Yuoo`l&`sFo;yPjM1G~#m~VS6znZmWZMp*yK#`^ zWoa!R^^>$m$kKXFBE)KOEKQO6MG@{Mq+kz{P)$yK@&JX3F!4UXwKuMq@-#a0kBa-< zh(%Cz)w@MlhaP%ECWggLVOeIs9ZY$V4c0t%SMNofiot ~NuO@pI5943RhdQ Hs2pvaEryX#rlt#uKwl@_(|C`cm_-%6Yfbl_g$f*gmvc?ByB& zlaTR!L8Db?d7#Qbd_G?y5SIi+?7&hB&@tkyhRAHm5WBD|@m75F)kEv}-$*Z%yuSid z0P9Dm=O-!;x2ib?cs3uFK^mvhStYQwU6w?A*z9*t_VW~k(i~~f>>lZ2!!D)GDqCDf zG^(Tump70h#WZ@0U8N&3(ITbmbK*1(4D8(-;?&3P`&HcPODZ*X%Nw*Cw1QKH!-U0- zqlI#Y^ZMR45UD@7d~TS|NSLzGPaq}6h*uMLw|lvft10peDkj=;x$0j7hAxu-Yru7* z2$VReAM%;vP__yNe >zHT6Xd}~JJUUQJg zuM P2{%3k=VF~gli JaO` zILj-i`!KlTQ}H; e1qq^r17VI32K|BmNF^ljINKn{X;TqnfB|DGxibcg z5@UbQ_=O6y{am)BbVJ7ouiiBYq*4u+)(}Qx)Ls>{`*W({y196Lr^-)H1C_ZfJvop7 zj73~v=889-Y20@EW1bypgF|s6azK^5$@{VU!}bZ8j KfA336OqHsLil6)k+H9-$C(H;=+TqLxd#ul#Ge|O{HNhR#?rmm8($6 zRh(PIGz41y6l$ezEk_RCW+if{_N9tlE(ZVr0J1<3KPZjw^0{omSX13F ai!fWW` z!r>QBqkiR NY$d2*ym;r1HwBJ>zh*g#l8*r9nzl3a&G!4qgB797DCRuN~8hsTv!?u7iu zM{;$ Xc{IxC?P!;f zn=^*}DKiQ5f$Y>b_`RT`6rb@yr@Be}nCq{*`brI}`sDxs6QdQAp3Wk+ra635y`xAi z;%X=#m 2v&7w@VEe-qqbk<*rtx PiMRQ u?HN}j zu0T1iYw$|#BvCoxycp3R!yE}Mgp)ll+s}hUd_ER67&hT#^cE1_8PpcvatAtxZR_Y( zR13#R?I(G0p=LA!{2l=7UAb>gZoaFT+aYGYt7L>P2z;f*dX!c=B-@p8h>2*^W1|Kf z0{oVf36rIdFbG{%C0|F?3m!G&Lf?5%IB| &p4*xo8^QSe&lzqm$)VK zdr !FA_R@O>Pl7##?v&C9JZF~RDY~y}` zff>Hz 8%NJc6c*n<4O0$`W%u}O}nDd5T9QHxtbg*=^8TbY((H-GoteIsA| zF*m*MI%zsTTXK{@$0m6Q4gY@l=I5JWp&DkMl8jg}hg(cHlmi~pbDZ;yg|+m|In!?& z0j9Nrxz8zuF3iw@5YJn)$bo1sLdZ7WZ`H!Jv+ZF-Gr29NttcvyP+ObuW*}aa7XdXw zOT@J)JUqylK7^binU?Ojo^aqr)-@tI@K`~vOV|=txT6?#(tRA_@W2G5T!If0cc@9A zqvX$Vk{V$sB|vF_X70WCb`5iQCfrC(2-dJHm5t7KT?M6$1Q%j@Rv9T#Q&+95ub{bQ z>lW=6=Ljv&%1=|4>XDv;@R^aN$!4k)QP}BcW&nuQmIEEqZ2;CR+6&JNR70C3a85JX z`0xuf2B#6Ls|o_R*Ya0`CqY5q>{tRFn6-CzEgKfr={00sj#aYhliQjpAVCmnzO`#R znRew(AovwW{fMDPb{PzpabNd!O7R=p071(7+%?eshWK3C-zy8cXt$p`J<-huMDc?> zXy64$r<$O8V`8=mE3DLrt~j5aK-^{H41pczMNxKk2)srZZE#)mH-h7S3Cdkn4^~t& zw1~3MJ(SKaLrkAxQ+9O)gBoM=r#EVw)QNovgk0cCsDMgxA#fSAZLfYyPfcJskG7p~ z59 DqO*z7m%Rpa-s!epP%X<3(gG5PNIW;`x$9Be0k&TMBOgIaj#AEmyUZOp1VH z)$b$0Tx_#1?M!QeZX9IIGpU&VTE#|33Ms5hRiz?)aB6WW)x4u$3sYzYEi1HA!0FHC zQmpK=*}k225)d7o?2EsOx7PSpzv@~9vzHg{t 82p+;@-h1G z`ruzw@d23^Zv6K1Tm4}D5;5ufzZR? 9#aKIIyRu9Po BpE!(nRkt{-TNQU} zc$XTH^1Hl3eyqCg?ty(}p3U^@)|sP=f%`lG^(T%;eUV^dWpxLeW_0S{FEaE_$S(;@ zV}-bQSdz-dW6cRmtbE)RUWK~=1_G89q1S!G WR1qi6k-~XoWrn> zzF3BnkBF^$FOt`YjLPlMEe^E~48m*jWoFkajSV3nr|SHh5H&4Z&II~7o%dTg+$R-x z%+T)JUfa24VvEfK{YOG>7VGWuy(#;Es8&_zuoB1F`uh?+!aKnP~ht0fP&3+JAte))=rL7aFauqI?(bP%EJdEJ9OTA z&iUkkqWEjR-Lf0xL6q3;@~sz+VYpA)@M?D_Vm!)cq#N@n -C}u4Ab1T&O^I{mu>weP!!#1E4xAgd1UPSTXGvN1NNR1= zj2h={y_x8b``nE$C^Y!lXSG=h5o-0o#QdWLd{6pjRtmSaow0OP92#j||IO6VB?c+& z`| >T)-zPTX zPeg}Pbw`(Amo712v#b3$cx34m7?Lb*sNzKi>@fA1@6e|eowP4raCglnZ*msPK5p{$ z7z0E#BN0jMfc@)@D{ULheY;0sx1A8)>J{9id>P{A#jk6w)KEq&>;(y38jsxn%D7|I zHCv^DM5&x7)|`%ptQ62Ef9in=OZgDSbpsj|j2KMZp-+{qLvaoK#)T_2+U4PbfN4+f zeS~S=DfwReVzP^qBG#TgHkaY|E$c!%K7$YL&ay#MroZCzavRB5pZT=&`F@I%t*Hy; zFY-dcXX6ZPsnWR=G=L0`YsH1V29ouRt=Xt*Lfx)F`;No91vQ>%o<{#Vi0Igt7Ovyk zM^S85QzNtG?OM2feezlok8Wyd0vnJo2V3O`WEo_;Ejzx#*4O2iYCI@xH3ai31@b>% zZ<8Dkd`hu-%_lSX%;oQmgK;tL()`Wd7==k;e6lKoY?sr?2V 9P8MbPcGO z?NQR?yrwDKa&aSx9t%s4%YCp0L6>;>!)^y<8FJep9ve^V6Ws_4`2HPTz c=0 zHk^H&nP2$Sm9*^$=*#2SPq?fQJjwFztcE`~lzxk?OeVI73W}c$MK@P8Zt~-i7xMu} zYE4ul1&+wLFaNw(TTq_W6a9vit^Z3Fm`Uu)%VQr`bXW5XI4pk36 jB%I*I+|g8ygZfiqq^RkxHdUcb4X$1vFBV`1=i63h3lBZ7vTdGx{&C0}7LF(L5Cu{PeO%aZpnO}X$a>jzt_98M zm`(3|)uD%an>zR^;K?4h?> FOnnAaM>14%$nk`v%}SRcJ)Qw|5laHqZ>&_VJV!4>9S*P&qKZGVMAZqJ0l zF{bmSt$`w3b?+Xk HdT@%BQDtt_r3}oPyWG{< (XEwb{`V?+Lv`5j0E21#7+HqNsTxw(J<~R zUJL#^os4Ja2pf7KVns(Xgwqq_c#gd9j;8#%03t+XAMJRQe-;jC=C5MWgOBg2=ExJ~ zP~?0?Sm4keIC-BcMw}kn6Q7a>RwvrsxSLFhi6irN?PTi@w(pIp7w+2 z^F9`_^%v^IrmC4oEJ-Ff2!$iYw^F`TWMP~w{Kiz`$8fy*$zh>h+Sgx9);#oWojbSS zBL t1kBtqbi_bf~@@&!T9t{SNPI0))X$3-<=)@;5;dr<_KHBTPm zt$n_td=d_J%`LOToq(s~ahpG|`7sNQamI$@CGCDmI*mjMwjX5jx PDW B+NK!wMM3 zz7Hb?0kiadf(rBSUtcltTwpSmsYRPs^wvbZS8OBPXC5}5*h~uZNet7jOR@I@-Hlm{ zwo@X~B(v-UyLFnZrlfDW@S{KIVqX3ZQ(Py ;Rb+y%2b8d zWJS{(kS4rpLVXevQ45eFbQL1T6MHO#HR{5MMk&ayMcb;p_{*8lX)y}Cdj1Pa2T$&{ ziTY`>o QbxpnRVLZd9QVrhuwAY{*M9u#!D$bamnAo+a|8P>Kt5zeft0 zifABrSmn*%>Z x^7K24}Qc6a1#U1Obh<#3{INU}gz1 zyB0TE2jLgFhL&HN{@nn~&!0-k2vEz)el67;3W`t|6q%`(ROK}cH~}u6p8##_Ucw<+ zq6jJ(AlQxQaG-$S)=7J{!S9P+$kyg*z0=g?LHlekHqmCr0O+}!cPLj6FjMrfplUm@ zwXX}b2f!bZT>TI9U)*>Nzpzvh17>*rh&wF?u#6v5xIG8{BEp7_iL?l>`?7kbh}7tw zAQ0I4VGY~hJNTu2>YM+tn3rM?0rTeh?m9Sz2_ewdEgdBb7Tsq(he#?|0MYt{og-0= zLpb(GSd_BjrikwUB5aX1!8HTagAh?soj|s*9qV5y8@SySrW&Ic4dgeU O zVk4f-*191KiWLYE_=Kl?Q6*|CVLlU!Q|#nWM3s_+2OL8V)9V2qh(yja2nICzv&)Y^ z=P?qfug6m-^T$f(rQSTBDBm!q5L7&IvOfJx%maJ;)vp5=S*55)cO96wM=`7Pk2dxu zyHSE)IePU@uJfes8;(hq={;|P!*y_`%6cuiCnWg~xmOn6=ke4sT2s45yC%XPgkTJ> zE!~ 0IS3hx d)6wAAuJeDD|GL WOb!P;PQU44D;O zWC}SX!X+~UyS|)j_Ug8*-%+z81Bd>2OvuqIBI)BNhKA^&9TrbU{61iF3`kZk(>RwU zsjIVkqBiBWrtK6^Jtv~6i@_!{52Kanm=Ux{XLpk`@{+g_%cr==(DcgL XICXFLF6EKZ!Z~kZ$ev6+mhnD1hL5x8H zBxih7)EEmUD4d#opK>#lM&!Z?4(%}x(xvmT=98l&Q1o4Vzi`Hi?D{aRajENM*P4>6 zN0&IrLbxV_OVE4;ztC(CD1mx2fI1B=Y2s5Hwv^n45c^Rz6Bq=111!Llu9zlzc<-5m z%LiXES3$+))>!XMx UV{px;379E4?oK`4Ar?JRc37Gg@#q6qbR#czWe`7u#2-&Zz8kA zthW&M9v`j1Ms`i{xfqcyx80^&J12!40PUFg8<@w0d>W6kj_~>Zl%b3?&&g`}dE9aG z{+)q=sGg33u7JOL>YvtSJT $JX@@og=UH2MJ9y9;|000Ys+$kA(=v`Bd zEH)IFw;x^8+YE@qJb(2FJ_AWO2y8=Z6k+vKVzhXK7oANO2n4AjpaQ2lj7T5s5LwPX z6f3qU1n>joCloq!-`jE)u#y+{v|8r>LemI_dRW0cD^P)=Uh<9M39Mz#u}eHjZKUfv zyE9UB<<>l+I-{Ed#$v*s25RG5t52~)>M(@hhiwzT!!zzo<^b-q#n&<<=IN=oY3LWo zZXiv5GH+7%@Omz358PeZ`Q|gvCjGEpnEnj7%*0#`aG`(0FJs(=I w;~*nO4C!1J`0EF97_LWU%+?=nJ|MFp)%{UGyr?$~bV=%St%wys+-< zDFFS%>i#MTbL9}o$;S8hyMcBs&ZjpxX4#iroc(M3ya{GRldvbsFnJ|Bl#g`@oYroX z_u`n;L4qmgMjG&h0I4F;N>7#fi>LJml;HYKLh1J)ehbn*kWK@XOmkG^(vkZ)2@^rA z8?_t~Zd)l{Tj_HA4G_^jHIifk)eC`qx%q$&M`C1HLst53^DGFjY(hCje-b|7YIHvD zwb6&7(si;Tkj5?N %DHkvQu`QfMC_AlXdI4dpo8e z<6+ZCNG{aIOjDYaBYO(D&M9-e#nHN&NBK0`qf6yV=aM`ZLh_$H%CGIc 0iG#bG9r2?m$})_j zD9SR7qbYCuR8Fiy07l`i879zXM-$GivRcH;V2G6A`h- t041*{Iui;4jZy?{ zp&I@|1IHFCIK^Nnc2|FIex$iI5FP4k?5zANk0H%rrC!WsZFVV${+C=ncD5~=6I&mh zYvL7}I7Qe++aR4 (AfVT%7-qg$z~f08%FaTTJJcjJ#m-T{_uA>mJ`=|CfyTh$lljQ qoN>SxEa{)fAy%x{`wgl J%ZqeJUtzovY~=q1QBey9&ip=8_paLPi(TEK zrJvWSv9o2Ei8}JEZ?}Fc6Ad9khI1hEl|EsC!5ZGPs)`M2@zYG>883>llKVnKmAdRa z4R<>xqn4+DP1&$>q|6wZbl_$=*#+ai-o%p1QensO1n=n&^MoH-Dor<)Sa8Ds;5NKi z8KIxb;6{lPDboF+`v0UpC$k%|`)D83`L7}VNEa7oiR{TC>^Re**gf!^2-O&Nrnh4z zU#2 H$d6LIJD{`k(^Mj(-S~Cr@!3SuKDVKF1{i#-MOa>|%se*m z-K4=|7io@*myBn0lz1*-gR|{f{_ewH%8#JZ!`Aoo{#>?+R!Fr_dr>B&DfxD?+YO9% zl5HQwD12*z8Qwh-)s+^ppm4Wn=DXZ!5X(uIwL%bCeKWzK1D(y#Q{MjLqips&Yq%N?#Ta z5AIY9yep0?HOEvu38nM9TC gu1m7xE<@z271vk?r@2(@;47fEH*)#IDy-20l*Qg|>q*((R zJ5d{h;=j9#do0kG$AJixl*!^;T&S1^daGs1R=^pjz9x2jWef0 2@%4iayxfAaij;{2H2p zToG{`)+^M*Sn&2<@Cu=Dpgt7%Q&lIPzMs!?GrIgy_lvO A_|GOA}#0-^{5HUJeRuqHQt9FRk$+vKk$Ev5N^ S#oKfB zNT}WF_R0Wz2{s|dAK^DPf6QSoRQaz1v6mp~eLM~{M99Wx-L%&a9>f~9klc?&2vT%3 zigCJu-38Dgfa@+Q*nr#nWkVFszA1Heob@ktj~jDz=eNN1x%1l xg1;2I}SvJ54y+!fss8U=@abB`R2U_wL0_f)kQ zSkz2${E=&<5Jp|4<>%ys@m^lPfvQi^j?3(&FJBtPpn8<(sWK#sDw7}clb82#`&wJI zUS9==$?f zn=I%!0$l_HC1^Pb2mP84RQ$gQ{4_Kkqm1V5NS$7+v+l|^(Ke%o6<`Auhp#{-KaR -h{eyn7f)NXZ_gh}p+xO01%%2eC68jXmF?ZJ%=O97cTn zsv^tExIEX5xd}9z_7;@Se^LEITEnOM1H fPrIL4uq0)=rG!g9q8yGWc FlHiy1I*3ElVu@EiaDF%r%($Eqkj{{uHvGwRp)XQ(yH<^Kzf{;$&k zcgl^ySmuZa&xj;wzD`xpohQw8Eslv?dzw%`Gsh~r_{ZtO*pC=}j9^l(XuCDKR+I-E z#wx11$^Yutkk2<6w-Wnet>1iiS~L|!9?YokAw;=Z6tB4N=8v63kh!Z(zz44J+U%_4 zq5uFpvbd$OH#fd?Qv0v~000QS%Jm>p=!l2Sz{SDX6Lv4mLhqkqc+w}N(luTUkfOCo z8rXi(g1M{OZO{DXw*YCPjP_gmAzy%qQqh37TlcfYVLyEsw6p*K03|-`eUyaE17E}! a(^fH%pcTfFHbtla000000000000000>j^*r literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..bc0a00627421f0acc51843e0feaab5da27827d9e GIT binary patch literal 16440 zcmV(~K+nHYNk&F6KmY(&MM6+kP&gnYKmY*n9s!*JDqaC%13r;NolB*oqM@Sm8ep&z z31@EnThw4RQuhc0^a5X= Mz+JQ-6|ut^B|Df84L||2cnj|3&@d_DjuQ%l~lyr~XIS7xMq+f8amBe((Q* z@__D{+x)?O&iildM~lz)zvKN${YU)o{SR;t=wI2r8UI87r~K!WN8vx)|K#@q{)_%2 z_8Z-^`!D@p?SDFbw|!y%$Nks9uk087&t8B3|NXuKe)a$V_iP}Wz2@<=6~b|oM|n*w z^p_I0SlqT&%Kq_brrQbVm!j}DJqv-8^<3aejA3}dE#P6372~41f)x;NV)7}1k)X++ z!f1w3PvJvl&C<|DY;-tySzd3fB3L<`#6Tk}U1N4d=-hYc5W#H#U;Bg~%eiCuFmun) z+S)eEL!P5|dgHlgKDPgizf7_&kz!IqIU`!YUv~P0+s{*ejl-KV%|)K3tg+a+66K9` z$o)md`NQ0n8r<3a6?E+z=E&+gZO@99D6^fva@!uT$;-C@Txu`v&P(ap?ufyach=}O zxQ8|tRi>oj$9TVZ;f-I4!bVRuS ak8Vb+`tz>iPUj*Rg lv z@Eyj3g-;3D^q)^kaq?&tkXmSvh;#^Ej&Y+)aP@28$4reuCf4teo IXUKtr+#J79R BDhXd zwv%5cVc42SXo_uGvdLkwBC5`o;Pe%QzQLVb%&py+mp9lfKK)g65g|09>T=CT@Uh(n zV8*XEgHI+=2DSE)!c>^+&EF5$iU>4(@EKjw&ssH%A$=F% )-}ql4fVl_oWwK6iHi^ny|ELBP3OIM;taD*rp@Dxssdvz%rf) zUT2||`2gm^5BW`RsuK~z0#LLdu#p=RaH3(~nuu96WH< CQzU9SRv8I0zll*tuVbhi9 `~MyEP2KeCoW zLD6Z?8ob}?eco}AUZS8=d~Pw+4CQ?0T-Nai!24iEl*j5}m>*$u&9*zq^i5LFAL8r> zu0tKfr({fo1}PY5G(e7XLOnvmY%?LxHm}8DYO;dF7VmkyaK}t~RIMsJV@NY%fLJzj zKp$n+$+@2np2dXMM>PSar+co3nF^f-1j#-~s Lhki1{KadR?DF$0DDJu-N|BzgWpb6&@=q=D{q@01+O zTOB5g^&gv?=m<*y6^*ayD>al{vl4-^+kF2V?NDm-cf >O^2SSOg9^Zvqk4up8ml@jL#1$j3e;cbDK5QwDK%JaJrIKdQnVqJ2&|r| -`GzPx&!H?zjPTpxS08MrPIK@M*_5ApD}6wz!2(?;&;_1>JXSD_^H4IXc0_ z8zvRwsZL{YTY0>2(C@DXuZ{E_FTI#Lt_xf+m5idg!hE~Iv8R`5)Sm+|BqGs-Uf!aG zFomg@vjQ?s5XMdMHcB<$vGBBbb%eG+QBdAs#kr|Ef)xq2h;)
O~Do6Rfv zQhdU~#{8Gqp5ym{bGFHX$9i_-COvZ>bH%F`ibYd`VY|Y=x&h{%MCntC5aHYnL={o8 zL>m$VJJOJOlFB>nbR_;-=*Yr#&%=M9V#)sYSE?-r-N6f>`SseX15im@V|qoDH8=xU zfB25i>3RaO?V<=oK)b-F*d-FRd{yi3=Ru!Elo&BzFZ9B&!d9E}NK=d!ek 5oJfpL4Ao&DEZFjIt7o?OfX+vB^7c%!N^=bW|GBkcP3DjDYp-Y~|uh~id zm(rap_kiyN(R;@x0)?V{iiy;DEiq3IKVY?J%&a&_g`!J=Njmjr_`AmYfC)zp`j1^5 z@E8@;Zw#$%7wORBcTaZHIFO42=y(K5!;Vi+&iuQm|EnBo9Pi~jI*Qb@j&c~HEP_kr z0fUH1B^xI7G&mG~tEM1rPHM->0o+_H*;bU$z@U6~QTVg@n5KM$mqoVL6@9*Ubue)o zXFV3+RuST$owKmCLb?1cYr=*1sC#!~d=udSl_2hgtol{sBEwLN6M;WZ z;Vmvu#7$YWfxIcI`OF(3uCzcx#_lJT_W;|={##0_#saaemR}QixZ;*VjJ`mrgA}yM zo(1$Clx8R);RA34AV(D6uvJ~sG5*#Bq_bbQ#^EiU=M~ONWx%4K^+9H6%UQqYJkR_? zmx*&Qs1=z!;BMX`Jc$^;E{7#==%#uXn+S3RoXQ^Jb}?L!!#9d^0Wk~q(Y!yIjeV+# z1oayD#e-)RTHG_x}b?gHP?gNm2)~AOlqf;?} z<0D2}N{VZhTg1mX{91gDPrnVYuH*c)Xm^{M=uXK4Dykpvp(!JBG7Q#h)KX~;zkj!3 z!qA!#jC2#-N1dypu^_c?^fHC#vqVtDCof71!=K>ddY&^s?S6XxDoQ5m9NZ?4jXgU_ zfCvyBJC+(}OR(hEWJ?SjWv;>lRt-ic-mM GQ9@)<%091 zjy98lwLp?6@ZC>2|CduGijmi&l^TmX{GsQJiNYh{s9PK-A3por;_YzIp~Fp!E_+f9 zXr^b5PLo)i4+UL##8FRf)Huw}xcmg6J nt_g-@lbiFdDs#9>_)&& zU5sNyy*?1dH#tbBRMO(eaxX;H5YtoV@d!pjYt8cxSh!;Wki&SHGUz#m@kQdG3c7V_ zp%m^rA8wzVq&H^E@l!{DhiLsf=I|Cs?_NA|S66xedy1sPx8Wh^pn#5lhn4feEYK^5 zBRvmgkRoZi=udw0h2%AjCxzsTsUb%Juk e-f}z|_1_6b)5TihbfV|8tsrQVU^=3g%WVUi>DhXCD3JdN-heukeqbuwd zWZGZi)Emt#%dGb1qUB{FQ+RKSq3r mfRMmh&&ja& z_{p8Y1I0^iu5Lg6yy~h0wok<#y|;F#5ao$L!vUe_zvCl9KZI|n3zIY5STEzxSd-)F z?8 odWZhoB+IM%*j6MV5- 0BMgt@k_ zlkn-`;&kC`9HNGdlD)vwNHOivcp> zUmN^>#}g>yqC}|9ekmj3>%I;cv^K4oU%Ln-;h U3{sFKI7At=pQCSB%6xLlDtFLR) zT04Ups@B}XN^aRL=A}h=hJ4~13VJD$k!!(h(J1C$3 EMY$-N0{piiMx7eAOQW$g}`| z)T>HWWrT!q(TAvUq!w265RCObklxPzS+ml?*>?ARdmpCw{B2 6bzKr%(eKPFsu?3!K06Uj?%w0# zx#BQCc{Or#wh}gN`ZM?b+fwFjsbtwk1>~{eXCh5X*wNv@>_KKqk-5`c{iL-JI^FoI*y8B zYS;5-@ZkH=Em-WpKijK|J_!}BP%T|C3Eln603#1xMZ_ c@{x;QCN zv^*MuC8wGav>{Oi4Q`eaI?f@6G14?nN-^zJP6KJ<&mj(mVDdzU171M}YN_87(!ID! z3xoWjy8kO-eLLHzfTmE|%u@ma3o*p7-?wB0v5ZW})FK>;{`XW&(t|2`zQ#DWzwRG~ zeRP8a)t%-9a-k|TI`3_} tKw_W!D4;1w5j=Y4RMAj1`pbt*~3gDj#CBFCHM%`tP z^9q-P+*vsge2hdj(=Pn$$^)(v7-5nB7~t1<@k*jrUW(YsrhPTA5l#B1HE7Ec8OSD* zRKl@u-5J#v n?uaMn z6`Tt%z>uAvr*EOOD@212qOsw-P#pjigW_$K+_tz0fyISx^Q_cO 1Xr9>L$Mq zLWzg5!c?v$r6#b!ag|^lM0U&FAD30F{lD*a5}Bu^o4L=D<(sZjMUYilYH-R+6%bg! zdv{L5WDtoP=qz%-Gy8nTJUQy+j$UO6Ajuxq#a$55yLBWAtN2*0CxAV6ZpEtRunc*e z(4QS96En`@Ux#F+OGc`>;~aq=b;!*geZ&Xh>`w{P+;XbhS~NeoM&CpW=Se#32G3a8 zKe^LyEBTs@ d-E}Te?JVDM?aiBOUzLR mMogIPzHT%Q!>?R900zJ+<9O|1 zZ0aj7@CLVfb2^qREdNs){iagw19A|Gg*rdvYe=Bvb59cLU9(rdZq_>qq_lRT6XpZ= zs2Wgy(zlWK@>r_Tl^FqlWWPrry0V!kE8*AMJg(_ !>r(OR!vitm91VEGrUm+DpIB3e{EFHTA z2VBjm(r*GR8Wo zAW}syDMdlRuM#?5Qi_9tUL !Q;-VPFeoTd-B9bSZjX?cR}#ggPtxUoE&J$ 9r&-x2R zzeFS*nohkp$bd>V_Tn9WRzJ#va^Ohr$YAmLe{r>Fct|WYL2tdS`>FZqB`i)FgKt`m zX)+PhT;@}(^FK{VjSGGavBit6k8>aZJSpsXPYLm4ztL0(Axc3}DZRR~@~?7!a*2~0 z5y}1HIkKkkC_B#2UsiMdm>F+~JeX%Q_%gWFQ!R3t90yGU41o=y@^DuADSdRdeZQm- zpD!fc9l-CyRE;dO@J}Xvj&Tjx=Upc27*%H51ZHRAv-jsU5sZ_ItP55d0Bi2nwe` z-vU_VRW00W!=TI;y|h2^`(dx~#ttNvmQOWg@EZO`n;I{Y2A6NMC)1}D<+)^4Mui19 zi;=HetlE!Vy|`aR41Z`mO&sYgp{9)`&~`@khK^ZTvOQOC?12{+I@M;E&0iPZwIS=X z)KZAr>Wj1$c?VWBm+!OMji8h-)hi01 +OcQWJudr-tx!kL3UW0ag>?M7Q4i=-aHZ{$Wz^dBHGGq$7i_HQ%*k853Bn$@b=q z71fUIiPythu4!jhe^T_vbuLXkrR9k+X_$kdqqnPB$Ub8=tgfv3Ae+efS-U7TuawVm za@~Hy&|N?D{gt;gOmZdv_Vk6>*yrcM`2uj?0?g{cS8G@ 4c_MQZ{{Wqr z`E2GD_YSRp+|G2^n>UIq>w0gEUNPG+|K%~D^iU+KYMVrGpsdKcmSbA=UrXFjjh|Jh z#e!bN{+Sz?f5aAsAnMwg2%_1*9EsA8OwDlV{Z$`Qcn eq&2 98Y&jFS3 z4PC@I_1wJBn95NfrKPTb)SmGb`6_Q{-UmRB1T=U0 p3nRP zpG#x;c+{SDID~YL8~Y(*d2&3d2i977kngF%N2)>tiC91Q#WLCoj@=kF@{#HT>4Ek! z5G{VMpBjley3{BMrXU4K{5L}*UkC%};W`f$U3e#jygoe`iEJ}>#Hd5ktabnuAwCpK zeeb@Fy2~Ht6)y*z69oD~I6C8BxT)D|E- 33oepQKQ`SmwW4?O=qD}1pR-~u+AHo zWJTMMVJ2#~Q>=w0iT_7Q`sGClVWOT{P4hFc!IBQn8#?d1TI(4Eb(uJPTwMPV;K)fA z?0$uY-tA0;heHN{4fU-p)?uBF(Qy#2)2p=pYaDl}Tk;S9@F{*x?)NqDD$ME;V3ADj z6m?+=WPjWU(zQRfHh=G0f1^(b6=S2H`LO#=;`0UWGMN}GHu-|}7q40W^#aZAB5;(& zEvUw!Oy?4M>16ZbTE%-0e|LPf_*;M8H`o5x;xNL2A>LkKA*>cQN?#f1glPypb{DE- z!xumij(#_9w^zr76;z!W98Tv@AK_L^S>=R~#cYUTmHade3FTiVU*ZMa;SLIG+Pcw1 zI&aO3wqH?AET-X7PA>L$kj+Q_J#EvJ?` ^{KUIBRRN4qDw(D5xJnYphj(`g>m>q!`SXI=SeM-=AQw?KHz4Gjg`2j7}k zEgSU&sjOvr*%6qT<0kAQAZRCVGl_<(pGt9Sw#FRKt@YA+x}!z{QOCAEDIBtkctgL} z|9v SNGt?%l_XoI;*wikDN|K^dxx~1UpTaN-8?Wy^{Wx8SQQJAht{jV zIymh;_;3~&;n-f|W6%<+`lj+`hWK9j>#_ Ie#v*Jc5uAynSDU-OAOIp1?1R)4)vF1akkq)>w_&+?*M!y4*& z`ML0LGNeH6Q{^+^E@l)Y(g8Igw+G<>5&JTUewFJ0))F}{_Thzk9}K8Wb@Y0Vy8Dw` zffky=Evt$rd;2sM`XL_#6D2rG*Y7$2(ogZf1{_)l&&8Cm#r_`&P(^6r6$rw#@6~K= z0fv;6DkD+`FHH)q*GWqpc#UNNr1pn`#<9q`X(mk-cATZT%yKy r=9oH3Z$WN zD|F)necJd!i%Sa2wa)dh3IAoF+L7A~l!Fn| ` z@0S%odX~Pk%iD??8W=|{R*md|)A0oppj%Wo(B~H(5VL%KuRX1I1s|b-7|IcF-P7T( zC)1e1N#zf_yRQoQJy0;CmP8*Lu)f!-a3KsS`V_B+6dPw5e!v6~`|j%5&ac#{sapYJ z9;UotQKQ_u ^rf;?oz!LtST~cIETfehj*|MbRb`Ka#u{_2vRr<+$w(f$6oz2lN zo0FAEme(A^Kv_EcH~*RIaDP6Cl~E4wig57?rbDQl=RcSRj$HXUpMtpdSD@|JMW%GZ zz!u84V5?7hhEJMdtI*})!&jlp4BK`oexEC5asW;LNcB6Z!I!OBz(?M_ce%5$H<*}{ zX#XhuSVW!Ct$FpZ>xJi=WFqxHh*cP(ctrj>P>v0!YR=6>kjUy5BsAo&`dk7n3H$E` zP5`X%E!vVeWjRJYv0Bhs$|W%c9uL`Tm84eN>0g45Ko2jCc6NvVAS;*^0B}l0*iBIJ zl6`BBJZKn_oa84^j1cvY!AZcFGc9%fg*=Cr!@DC++diJ3@q}aJ=B@ZXg@oht_<=GD zKeJWB{Ilgo%K3Eoc)l()+Os)E9e=i?8B&Xe5yp9Nm^4wQ_t1KL{M06_S%-pVPe<`s z#{x!_Acg&b&QGI+;YwDSQ1v<{aQU`>grY{Ycva|^7M;Px3`Dld0tL5FJPSp_zU;{? zbHx+ZjmUoccnOR}yBo49DsqW9rh@6mYzg6Oj_LVC;ZBvttD)vczyJLF?cN@4yKkbv z_@DkjHcQiy=p?q}b_}>*sZSd<1}YaF+3}fhbvmqI@uc&O?;bAdr(wDD@eCPP@yOCm z8hD|gR(zJBOPsJ^fYj9u=K(2srzd!e50tspCWCr^I_iR>t4?1>I$>yKeMuJjHS=}2 zpB(T#9)*E_trs3L0*LGBQbqN{Ge~V|ORe`7waf6>i!RuEU=>@L1cN{FVC}F`2!G(W z+rqFh8eP#^qzAfLSgJ$c#>v!g0lnghrYe5uYPqY*`l_6GALr7+{ckYmT2w^j-j9y! z6E61^Z@JK&(>gm}lBwV_jF{^kW_6D%oofYTkaZEP!#NJp1OTXt9#L1pZkQ?i!%E}X zDfn{N_o2lm5!WcgyrInZFpYaD =y%J-rPFOMfB1wPENagHFaY1J4ap1v z9BWHxz`YIt_>x3ng83aEbeJmYg*k2asC=%=?DeY6q@Q>(sW?3tp w6}Nr0Byf$5rJq_a^o>1ReQn*;r72*G5ivd5AwE4qvINM170) z>TxdY_Y~(B$P^IRL9{^LHoB&F%&eBAAW?}-Xgw4Nz{ar`d0c}bVXJB5=F+4M?%%W} z#4el{bXZqKpwPs#+Jk~m`{0INIwVY9Yz`)N0 z0XM*M-nw p2X<1I9c7siU3;T}S-uk7srV@;yYd;w*Bdh@Tg z+&)){c(qn83Z!ccaB2j~{7XM8&Bk{WeQ_S>Ko27z{SIR5^Y@;qK$$0uFcB?NW@ zgW OY;7-n+hk3N?WlEmqR}1D7{3W zv2KRG!G|uikGgP&{hZ-D-YiUmYu*tH`Z%FlIf?Dd(l3aol?fV&+2LJlidf@xOReP0 zyA;ugf1?UYO%YHOFmcRk)(r4MutdZatjM*z07|rZQid5TX-(=1!|<-Etm6m^;z$>5 zb&LxzaYeuNq}`T;wNeDK8nc67wOPAYlZ?{HQ6P-VG4C3+Ke@#_oM)G{`fg#fe6&c= z#h1q379LB&PQ@G{#(0(jYdvO>wTFFq$Rr|S%}W5} `7;AO}4albHUTxG(kfP^R zhYIs{H9>QA-x3Y#NjK4)xtW Yc7%E1f#6Gb H5|>n9-t^g!<9#G!0MdIjI<7SFWoqy~ zYct~losD-#Mg}vLM5APk@kYu4s$c