├── FE ├── README.md ├── snakeBorder.html ├── 网络协议.md └── 重绘与回流.md ├── README.md ├── assets ├── README.md └── img │ ├── browerRender.png │ ├── css │ ├── cssvar1.png │ ├── cssvar2.png │ ├── cssvar3.png │ └── cssvar4.png │ ├── event.png │ ├── js.jpg │ ├── prototype.jpg │ ├── react.jpg │ ├── reactLifecycle.png │ ├── react_sc │ ├── FiberEl.png │ ├── FiberTree.jpg │ ├── createEl_babel.png │ └── fiber-scheduler.png │ ├── redux.jpg │ ├── task.jpg │ ├── webpack │ └── cdn.png │ └── xieyi.jpg ├── books ├── 你不知道的JavaScript(上卷).pdf ├── 你不知道的JavaScript(下卷).pdf └── 你不知道的JavaScript(中卷).pdf ├── css └── less-media.md ├── git ├── README.md └── mac配置多个ssh.md ├── js ├── Observer.md ├── README.md ├── call,apply与bind.md ├── eval和with欺骗词法作用域影响性能.md ├── new运算符.md ├── 宏任务与微任务.md ├── 强大的Array.reduce.md ├── 排序算法.md └── 构造函数和原型.md ├── js常用小技巧.md ├── node ├── npm.md └── package.md ├── react ├── 01.react-basic.md ├── 02.react-advance.md ├── 03.Hook.md ├── 04.redux.md ├── 05.redux-saga.md ├── 06.React源码学习-createElment.md ├── 07.React源码学习-Component.md ├── 08.React源码学习-FiberRoot与Fiber.md ├── 09.React源码学习-update.md ├── 10.React源码学习-expirationTime.md ├── 11.React源码学习-任务调度.md ├── 12.React源码学习-beginWork.md ├── 13.React源码学习-commit.md ├── 14.umi2+Antd实现动态主题色切换实践.md └── README.md └── webpack ├── HtmlWebpackPlugin.md └── webpack4.md /FE/README.md: -------------------------------------------------------------------------------- 1 | ### 记录前端知识点 2 | -------------------------------------------------------------------------------- /FE/snakeBorder.html: -------------------------------------------------------------------------------- 1 | 49 |
-------------------------------------------------------------------------------- /FE/网络协议.md: -------------------------------------------------------------------------------- 1 | ## 网络协议 2 | 3 | ### OSI与TCP/IP协议模型 4 | 5 | ![生命周期](../assets/img/xieyi.jpg) -------------------------------------------------------------------------------- /FE/重绘与回流.md: -------------------------------------------------------------------------------- 1 | ### 重绘 2 | 3 | 当节点需要更改外观而不会影响布局的,比如改变 color 就叫称为重绘 4 | 5 | ### 回流 6 | 7 | 将可见DOM节点以及它对应的样式结合起来,可是我们还需要计算它们在设备视口(viewport)内的确切位置和大小,这个计算的阶段就是回流。 8 | 9 | *发生回流的几种情况:* 10 | 11 | - 添加或删除可见的DOM元素 12 | - 元素的位置发生变化 13 | - 元素的尺寸发生变化(包括外边距、内边框、边框大小、高度和宽度等) 14 | - 内容发生变化,比如文本变化或图片被另一个不同尺寸的图片所替代。 15 | - 页面一开始渲染的时候(这肯定避免不了) 16 | - 浏览器的窗口尺寸变化(因为回流是根据视口的大小来计算元素的位置和大小的) 17 | 18 | ##### 回流一定会触发重绘,而重绘不一定会回流 19 | 20 | 这是由于浏览器的渲染过程决定的,其过程如下: 21 | 22 | - 解析HTML,生成DOM树,解析CSS,生成CSSOM树 23 | - 将DOM树和CSSOM树结合,生成渲染树(Render Tree) 24 | - Layout(回流):根据生成的渲染树,进行回流(Layout),得到节点的几何信息(位置,大小) 25 | - Painting(重绘):根据渲染树以及回流得到的几何信息,得到节点的绝对像素 26 | - Display:将像素发送给GPU,展示在页面上。 27 | 28 | ![Event Loop](../assets/img/browerRender.png) 29 | 30 | ### 如何减少重绘和回流 31 | 32 | 减少回流和重绘其实就是需要减少对render tree的操作(合并多次多DOM和样式的修改),并减少对一些样式信息的请求,尽量利用好浏览器的优化策略。 33 | 34 | - 改变样式通过添加class或el.style.cssText,而不是el.style.单个属性 35 | - 尽可能少的修改dom,对于需要频繁操作的dom可以让其暂避重绘检查: 36 | - 使用DocumentFragment进行缓存操作,引发一次回流和重绘 37 | - 使用display:none暂时隐藏当前处理的dom 38 | - 使用cloneNode(true or false) 和 replaceChild 39 | - 对于需要动画的元素使用定位使其脱离文档流 40 | 41 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## 目录 2 | 3 | > 持续更新中,不建议直接fork,欢迎star 👍 4 | 5 | ### ***JS*** 6 | - [js常用小技巧( 推荐:★★★★★ )](/js常用小技巧.md) 7 | - [宏任务与微任务](/js/宏任务与微任务.md) 8 | - [call,apply与bind](/js/call,apply与bind.md) 9 | - [new运算符详解](/js/new运算符.md) 10 | - [es5构造函数和原型](/js/构造函数和原型.md) 11 | - [eval和with欺骗词法作用域影响性能](/js/eval和with欺骗词法作用域影响性能.md) 12 | - [排序算法](/js/排序算法.md) 13 | - [强大的Array.reduce](/js/强大的Array.reduce.md) 14 | - [Observer API(监听resize和无限滚动的新方案)](/js/Observer.md) 15 | 16 | ### node 17 | - [常用 npm 包推荐](/node/npm.md) 18 | - [package.json 笔记](/node/package.md) 19 | 20 | ### ***React*** 21 | - [从0开始使用webpack构建一个React脚手架](https://github.com/TigerHee/react-cli-diy) 22 | - [01.react-基础](/react/01.react-basic.md) 23 | - [02.react-进阶](/react/02.react-advance.md) 24 | - [03.Hook](/react/03.Hook.md) 25 | - [04.redux](/react/04.redux.md) 26 | - [05.redux-saga](/react/05.redux-saga.md) 27 | - [06.React源码学习-createElment](/react/06.React源码学习-createElment.md) 28 | - [07.React源码学习-Component](/react/07.React源码学习-Component.md) 29 | - [08.React源码学习-FiberRoot与Fiber](/react/08.React源码学习-FiberRoot与Fiber.md) 30 | - [09.React源码学习-update](/react/09.React源码学习-update.md) 31 | - [10.React源码学习-expirationTime](/react/10.React源码学习-expirationTime.md) 32 | - [11.React源码学习-任务调度](/react/11.React源码学习-任务调度.md) 33 | - [12.React源码学习-beginWork](/react/12.React源码学习-beginWork.md) 34 | - [13.React源码学习-commit](/react/13.React源码学习-commit.md) 35 | - [14.umi2+Antd实现动态主题色切换实践](/react/14.umi2+Antd实现动态主题色切换实践.md) 36 | 37 | ### ***FE*** 38 | - [重绘与回流](/FE/重绘与回流.md) 39 | - [蛇形边框](/FE/snakeBorder.html) 40 | 41 | ### ***书籍*** 42 | - [你不知道的JavaScript(上卷)](/books/你不知道的JavaScript(上卷).pdf) 43 | - [你不知道的JavaScript(中卷)](/books/你不知道的JavaScript(中卷).pdf) 44 | - [你不知道的JavaScript(下卷)](/books/你不知道的JavaScript(下卷).pdf) 45 | 46 | ### ***webpack*** 47 | - [从0到掌握webpack4](/webpack/webpack4.md) 48 | - [使用HtmlWebpackPlugin向html内插入第三方cdn资源](/webpack/HtmlWebpackPlugin.md) 49 | 50 | ### ***git*** 51 | - [git常用指令](/git/README.md) 52 | - [mac配置多个ssh key](/git/mac配置多个ssh.md) 53 | 54 | ### ***CSS*** 55 | - [less函数实现媒体查询](/css/less-media.md) 56 | -------------------------------------------------------------------------------- /assets/README.md: -------------------------------------------------------------------------------- 1 | ### 此项目所需的静态资源存放目录 -------------------------------------------------------------------------------- /assets/img/browerRender.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TigerHee/shareJS/18d2906d6e26b9db47ff3382e691d3c96fceb7c1/assets/img/browerRender.png -------------------------------------------------------------------------------- /assets/img/css/cssvar1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TigerHee/shareJS/18d2906d6e26b9db47ff3382e691d3c96fceb7c1/assets/img/css/cssvar1.png -------------------------------------------------------------------------------- /assets/img/css/cssvar2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TigerHee/shareJS/18d2906d6e26b9db47ff3382e691d3c96fceb7c1/assets/img/css/cssvar2.png -------------------------------------------------------------------------------- /assets/img/css/cssvar3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TigerHee/shareJS/18d2906d6e26b9db47ff3382e691d3c96fceb7c1/assets/img/css/cssvar3.png -------------------------------------------------------------------------------- /assets/img/css/cssvar4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TigerHee/shareJS/18d2906d6e26b9db47ff3382e691d3c96fceb7c1/assets/img/css/cssvar4.png -------------------------------------------------------------------------------- /assets/img/event.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TigerHee/shareJS/18d2906d6e26b9db47ff3382e691d3c96fceb7c1/assets/img/event.png -------------------------------------------------------------------------------- /assets/img/js.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TigerHee/shareJS/18d2906d6e26b9db47ff3382e691d3c96fceb7c1/assets/img/js.jpg -------------------------------------------------------------------------------- /assets/img/prototype.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TigerHee/shareJS/18d2906d6e26b9db47ff3382e691d3c96fceb7c1/assets/img/prototype.jpg -------------------------------------------------------------------------------- /assets/img/react.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TigerHee/shareJS/18d2906d6e26b9db47ff3382e691d3c96fceb7c1/assets/img/react.jpg -------------------------------------------------------------------------------- /assets/img/reactLifecycle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TigerHee/shareJS/18d2906d6e26b9db47ff3382e691d3c96fceb7c1/assets/img/reactLifecycle.png -------------------------------------------------------------------------------- /assets/img/react_sc/FiberEl.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TigerHee/shareJS/18d2906d6e26b9db47ff3382e691d3c96fceb7c1/assets/img/react_sc/FiberEl.png -------------------------------------------------------------------------------- /assets/img/react_sc/FiberTree.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TigerHee/shareJS/18d2906d6e26b9db47ff3382e691d3c96fceb7c1/assets/img/react_sc/FiberTree.jpg -------------------------------------------------------------------------------- /assets/img/react_sc/createEl_babel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TigerHee/shareJS/18d2906d6e26b9db47ff3382e691d3c96fceb7c1/assets/img/react_sc/createEl_babel.png -------------------------------------------------------------------------------- /assets/img/react_sc/fiber-scheduler.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TigerHee/shareJS/18d2906d6e26b9db47ff3382e691d3c96fceb7c1/assets/img/react_sc/fiber-scheduler.png -------------------------------------------------------------------------------- /assets/img/redux.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TigerHee/shareJS/18d2906d6e26b9db47ff3382e691d3c96fceb7c1/assets/img/redux.jpg -------------------------------------------------------------------------------- /assets/img/task.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TigerHee/shareJS/18d2906d6e26b9db47ff3382e691d3c96fceb7c1/assets/img/task.jpg -------------------------------------------------------------------------------- /assets/img/webpack/cdn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TigerHee/shareJS/18d2906d6e26b9db47ff3382e691d3c96fceb7c1/assets/img/webpack/cdn.png -------------------------------------------------------------------------------- /assets/img/xieyi.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TigerHee/shareJS/18d2906d6e26b9db47ff3382e691d3c96fceb7c1/assets/img/xieyi.jpg -------------------------------------------------------------------------------- /books/你不知道的JavaScript(上卷).pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TigerHee/shareJS/18d2906d6e26b9db47ff3382e691d3c96fceb7c1/books/你不知道的JavaScript(上卷).pdf -------------------------------------------------------------------------------- /books/你不知道的JavaScript(下卷).pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TigerHee/shareJS/18d2906d6e26b9db47ff3382e691d3c96fceb7c1/books/你不知道的JavaScript(下卷).pdf -------------------------------------------------------------------------------- /books/你不知道的JavaScript(中卷).pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TigerHee/shareJS/18d2906d6e26b9db47ff3382e691d3c96fceb7c1/books/你不知道的JavaScript(中卷).pdf -------------------------------------------------------------------------------- /css/less-media.md: -------------------------------------------------------------------------------- 1 | ## less函数实现媒体查询 2 | 3 | - 公用的less函数文件 4 | 5 | 如:mix.less 6 | 7 | ```less 8 | @mq-width-md:768px; 9 | @mq-width-lg:1024px; 10 | 11 | // < 768px 12 | .mediaQuerySm(@style) { 13 | @media screen and (max-width:@mq-width-md) { 14 | @style() 15 | } 16 | } 17 | 18 | // < 1024px 19 | .mediaQueryMd(@style) { 20 | @media screen and (max-width:@mq-width-lg) { 21 | @style() 22 | } 23 | } 24 | ``` 25 | 26 | - 实际组件引入并使用函数 27 | 28 | ```less 29 | @import '~xxx/mix.less'; // 路径自定 30 | 31 | .cls { 32 | padding: 20px; 33 | .mediaQueryMd({ 34 | padding: 12px; 35 | }); 36 | .mediaQuerySm({ 37 | padding: 8px; 38 | }); 39 | } 40 | ``` -------------------------------------------------------------------------------- /git/README.md: -------------------------------------------------------------------------------- 1 | ### 获取本地SSH Key: 2 | ```sh 3 | ls ~/.ssh 4 | 5 | cat ~/.ssh/id_rsa.pub 6 | ``` 7 | 8 | ### 在远程仓库的master分支基础上建立新分支: 9 | ``` 10 | git checkout master -b dev 11 | 12 | git push origin dev 13 | ``` -------------------------------------------------------------------------------- /git/mac配置多个ssh.md: -------------------------------------------------------------------------------- 1 | ## mac配置多个ssh key 2 | 3 | 1. 进入ssh目录: 4 | ``` 5 | cd ~/.ssh 6 | ``` 7 | 8 | 2. 创建新的ssh key: 9 | ``` 10 | ssh-keygen -t rsa -C "tigerhee@qq.com" 11 | ``` 12 | 13 | 后续提示内输入name: id_rsa_tiger 14 | 15 | 3. 创建config文件: 16 | ``` 17 | touch config 18 | ``` 19 | 20 | 配置config: 21 | 22 | ``` 23 | # github 24 | Host git 25 | HostName github.com 26 | PreferredAuthentications publickey 27 | IdentityFile ~/.ssh/id_rsa 28 | 29 | # github tiger 30 | Host git2 31 | HostName github.com 32 | PreferredAuthentications publickey 33 | User tigerhee@qq.com 34 | IdentityFile ~/.ssh/id_rsa_tiger 35 | ``` 36 | 4. 添加密钥: 37 | ``` 38 | ssh-add ~/.ssh/id_rsa_tiger 39 | ``` 40 | 41 | 5. 到要用git2的仓库修改其git配置: 42 | 43 | ``` 44 | git config user.email "tigerhee@qq.com" 45 | git config user.name "tigerHee" 46 | ``` -------------------------------------------------------------------------------- /js/Observer.md: -------------------------------------------------------------------------------- 1 | ## Observer API 2 | 3 | > 友情提示:兼容性还不是很友好 4 | 5 | ### 1. ResizeObserver 6 | 7 | ResizeObserver 接口可以监听到 Element 的内容区域或 SVGElement的边界框改变。内容区域则需要减去内边距padding。 8 | 9 | - #### 构造器: 10 | 11 | - `ResizeObserver()` 12 | 13 | 创建并返回一个ResizeObserver对象。 14 | 15 | - #### 方法: 16 | 17 | - `ResizeObserver.observe()` 18 | 19 | 开始观察指定的 Element或 SVGElement。 20 | 21 | - `ResizeObserver.disconnect()` 22 | 23 | 取消和结束目标对象上所有对 Element或 SVGElement 观察。 24 | 25 | - `ResizeObserver.unobserve()` 26 | 27 | 结束观察指定的Element或 SVGElement。 28 | 29 | - #### 示例: 30 | 31 | 监听body的size变化: 32 | 33 | ```js 34 | const resizeObserver = new ResizeObserver(entries => { 35 | for (let entry of entries) { 36 | console.log(entry) 37 | } 38 | }); 39 | resizeObserver.observe(document.querySelector('body')); 40 | ``` 41 | 42 | ### 2. IntersectionObserver 43 | 44 | Intersection Observer API提供了一种异步观察目标元素与祖先元素或顶级文档viewport的交集中的变化的方法。 45 | 46 | - 可能使用到的场景: 47 | - 当页面滚动时,懒加载图片或其他内容 48 | - 实现“可无限滚动”网站,也就是当用户滚动网页时直接加载更多内容,无需翻页 49 | - 根据用户是否已滚动到相应区域来灵活开始执行任务或动画 50 | 51 | Intersection Observer API 允许你配置一个回调函数,每当目标(target)元素与设备视窗或者其他指定元素发生交集的时候执行。设备视窗或者其他元素我们称它为根元素或根(root)。通常,您需要关注文档最接近的可滚动祖先元素的交集更改,如果元素不是可滚动元素的后代,则默认为设备视窗。如果要观察相对于根(root)元素的交集,请指定根(root)元素为null。 52 | 53 | 目标(target)元素与根(root)元素之间的交叉度是交叉比(intersection ratio)。这是目标(target)元素相对于根(root)的交集百分比的表示,它的取值在0.0和1.0之间。 54 | 55 | - #### 构造器: 56 | 57 | - `IntersectionObserver(callback, options)` 58 | 59 | ##### `callback`接收参数: 60 | 61 | - IntersectionObserverEntry对象 62 | 63 | - 观察者的列表 64 | 65 | ##### `options`对象属性: 66 | 67 | - root: 68 | 69 | 指定根(root)元素,用于检查目标的可见性。必须是目标元素的父级元素。如果未指定或者为null,则默认为浏览器视窗。 70 | 71 | - rootMargin: 72 | 73 | root元素的外边距。该属性值是用作root元素和target发生交集时候的计算交集的区域范围,使用该属性可以控制root元素每一边的收缩或者扩张。默认值为0。 74 | 75 | - threshold: 76 | 77 | 可以是单一的number (eg: 0.5) 或是number数组 (eg: [0, 0.25, 0.5, 0.75, 1]),target元素和root元素相交程度达到该值的时候IntersectionObserver注册的回调函数将会被执行。 78 | 79 | ##### `IntersectionObserverEntry`对象属性: 80 | 81 | - time: 82 | 83 | 可见性发生变化的时间,是一个高精度时间戳 84 | 85 | - target: 86 | 87 | 被观察的目标元素,是一个 DOM 节点对象 88 | 89 | - rootBounds: 90 | 91 | 根元素的矩形区域的信息,getBoundingClientRect()方法的返回值,如果没有根元素(即直接相对于视口滚动),则返回null 92 | 93 | - boundingClientRect: 94 | 95 | 目标元素的矩形区域的信息 96 | 97 | - intersectionRect: 98 | 99 | 目标元素与视口(或根元素)的交叉区域的信息 100 | 101 | - intersectionRatio: 102 | 103 | 目标元素的可见比例,即intersectionRect占boundingClientRect的比例,完全可见时为1,完全不可见时小于等于0 104 | 105 | - #### 示例: 106 | 107 | - 无限滚动: 108 | 109 | 在无限滚动的底部放一个footer,监听footer的可见否来实现无限滚动 110 | 111 | ```js 112 | let Observer = new IntersectionObserver((entries) => { 113 | // 如果footer不可见,就返回 114 | if (entries[0].intersectionRatio <= 0) return; 115 | // 加载更多 116 | loadMore(); 117 | }); 118 | 119 | // 开始观察 120 | Observer.observe( 121 | document.querySelector('.scrollerFooter') 122 | ); 123 | ``` -------------------------------------------------------------------------------- /js/README.md: -------------------------------------------------------------------------------- 1 | ### 记录JS的知识点 -------------------------------------------------------------------------------- /js/call,apply与bind.md: -------------------------------------------------------------------------------- 1 | ## call, apply 与 bind 2 | 3 | #### 1.call() 与 apply() 4 | 5 | call或apply会自动执行对应的函数 6 | 7 | ```js 8 | fun.call(thisNew[, arg1[, arg2[, ...]]]) 9 | ``` 10 | 11 | ```js 12 | fun.apply(thisNew[, argsArray]) 13 | ``` 14 | 15 | thisNew: fun函数运行时指定的this值,可能的值为: 16 | 17 | - 不传,或者传null,undefined, this指向window对象 18 | - 传递另一个函数的函数名fun2,this指向函数fun2的引用 19 | - 值为原始值(数字,字符串,布尔值),this会指向该原始值的自动包装对象,如 String、Number、Boolean 20 | - 传递一个对象,函数中的this指向这个对象 21 | 22 | 用例: 23 | 24 | call: 25 | 26 | ```js 27 | window.name = 'windowName' 28 | var obj = { 29 | name: 'objName' 30 | } 31 | function getName(p1, p2) { 32 | console.log('name === ', this.name) 33 | console.log('p1 === ', p1) 34 | console.log('p2 === ', p2) 35 | } 36 | getName('str1', 'str2') 37 | getName.call(obj, 'str1', 'str2') 38 | ``` 39 | 40 | apply: 41 | 42 | ```js 43 | Math.max.apply(null, [2, 3, 1, 4]) 44 | ``` 45 | 46 | #### 2.bind() 47 | 48 | bind()方法会创建一个新函数,称为绑定函数。bind是ES5新增的一个方法,不会执行对应的函数,而是返回对绑定函数的引用。 49 | 50 | ```js 51 | fun.bind(thisNew[, arg1[, arg2[, ...]]]); 52 | ``` 53 | 54 | 用例: 55 | 56 | ```js 57 | var $ = document.querySelectorAll.bind(document) 58 | ``` 59 | 60 | #### 3.三者异同 61 | 62 | 相同: 63 | 64 | - 改变函数体内 this 的指向 65 | 66 | 不同: 67 | 68 | - call、apply的区别:接受参数的方式不一样 69 | 70 | - bind不立即执行。而apply、call 立即执行 71 | 72 | #### 4.模拟实现 73 | 74 | call: 75 | 76 | ```js 77 | Function.prototype.customCall = function () { 78 | if (typeof this !== 'function') { 79 | throw new TypeError('error!') 80 | } 81 | let context = arguments[0] || window 82 | context.fn = this 83 | let args = [...arguments].slice(1) 84 | let result = context.fn(...args) 85 | delete context.fn 86 | return result 87 | } 88 | ``` 89 | 90 | apply: 91 | 92 | ```js 93 | Function.prototype.customApply = function () { 94 | if (typeof this !== 'function') { 95 | throw new TypeError('error!') 96 | } 97 | let context = arguments[0] || window 98 | context.fn = this 99 | let result = !!arguments[1] ? context.fn(...arguments[1]) : context.fn() 100 | delete context.fn 101 | return result 102 | } 103 | ``` 104 | 105 | bind: 106 | 107 | ```js 108 | Function.prototype.customBind = function () { 109 | if (typeof this !== 'function') { 110 | throw new TypeError('error!') 111 | } 112 | const that = this 113 | let context = arguments[0] || window 114 | const args = [...arguments].slice(1) 115 | return function() { 116 | return that.apply(context, args.concat([...arguments])) 117 | } 118 | } 119 | ``` 120 | -------------------------------------------------------------------------------- /js/eval和with欺骗词法作用域影响性能.md: -------------------------------------------------------------------------------- 1 | ## 欺骗词法作用域影响性能 2 | 3 | 词法作用域就是定义在词法阶段的作用域。换句话说,词法作用域是由你在写 代码时将变量和块作用域写在哪里来决定的,因此当词法分析器处理代码时会保持作用域 不变(大部分情况下是这样的) 4 | 5 | #### JS代码执行过程的重要成员: 6 | 7 | - 引擎: 8 | 9 | 从头到尾负责整个js程序的编译及执行过程 10 | 11 | - 编译器: 12 | 13 | 引擎的好朋友之一,负责语法分析及代码生成等 14 | 15 | - 作用域: 16 | 17 | 引擎的另一好朋友,负责收集并维护由所有声明的标识符(变量)组成的一系列查询,并实施一套非常严格的规则,确定当期执行的代码对这些标识符的访问权限 18 | 19 | #### 对变量的查询分类: 20 | 21 | - LHS查询: 22 | 23 | 查找的目的是对变量进行赋值 24 | 25 | - RHS查询: 26 | 27 | 查找的目的是获取变量的值 28 | 29 | js引擎首先会在代码执行前对其进行编译,在这个过程中,像var a=2这样的声明会被分解成两个独立的步骤: 30 | 31 | 1.首先,var a在其作用域中声明变量,这会在最开始的阶段,也就是代码执行前进行; 32 | 33 | 2.接下来,a=2会查询(LHS查询)变量a并对其进行赋值; 34 | 35 | #### 欺骗词法作用域会导致性能下降: 36 | 37 | eval和with会在运行时修改或创建新的作用域,以此来欺骗其他在书写时定义的词法作用域。 38 | 39 | js引擎会在编译阶段进行数项的性能优化,其中有些优化项依赖于能够根据代码的词法进行静态分析,并预先确定所有变量和函数的定义位置,才能在执行过程中快速找到标识符。 40 | 但如果引擎在代码中发现了eval和with,它只能简单地假设关于标识符位置的判断都是无效的,因为无法在词法分析阶段明确的知道eval会接收到什么代码,这些代码会如何对作用域进行修改,也无法知道传递给with用来创建新词法作用域的对象内容到底是什么, 41 | 42 | 最悲观的情况是如果出现了 eval(..) 或 with,所有的优化可能都是无意义的,因此最简 单的做法就是完全不做任何优化。 43 | 44 | 如果代码中大量使用 eval(..) 或 with,那么运行起来一定会变得非常慢。无论引擎多聪 明,试图将这些悲观情况的副作用限制在最小范围内,也无法避免如果没有这些优化,代 码会运行得更慢这个事实。 -------------------------------------------------------------------------------- /js/new运算符.md: -------------------------------------------------------------------------------- 1 | #### new 运算符创建一个用户定义的对象类型的实例或具有构造函数的内置对象的实例。 2 | 3 | --- 4 | new进行了如下操作: 5 | - 创建一个新对象obj 6 | - 把obj的__proto__指向构造函数的prototype对象 实现继承 7 | - 将步骤1新创建的对象obj作为this的上下文 8 | - 返回创建的对象obj(如果该函数没有返回对象,则返回this) 9 | --- 10 | 11 | ##### *自己实现一个new:* 12 | 13 | ```js 14 | function _new(...args) { 15 | const [constructor, ...otherArgs] = args; 16 | 17 | if(typeof constructor !== 'function') { 18 | throw new TypeError('constructor is not a function'); 19 | }; 20 | 21 | // 1~2 步骤简单写法 const obj = Object.create(constructor.prototype); 22 | 23 | // 1.创建一个空的简单JavaScript对象(即{}); 24 | const obj = {}; 25 | 26 | // 2.链接该对象(即设置该对象的构造函数)到另一个对象 ; 27 | Object.setPrototypeOf(obj, constructor.prototype) 28 | 29 | // 3.将步骤1新创建的对象作为this的上下文 30 | const result = constructor.apply(obj, otherArgs); 31 | 32 | // 4.如果该函数没有返回对象,则返回this。 33 | return isPrimitive(result) ? obj : result 34 | } 35 | 36 | function isPrimitive(value) { 37 | return value == null || ['string', 'number', 'boolean', 'symbol'].includes(typeof(value)) 38 | } 39 | 40 | function Test(x, y) { 41 | this.x = x; 42 | this.y = y; 43 | } 44 | 45 | var testObj = _new(Test, 1, 2) 46 | ``` 47 | -------------------------------------------------------------------------------- /js/宏任务与微任务.md: -------------------------------------------------------------------------------- 1 | ### 事件循环 2 | 3 | JavaScript 语言的一大特点就是单线程,也就是说,同一个时间只能做一件事。为了协调事件、用户交互、脚本、UI 渲染和网络处理等行为,防止主线程的不阻塞,Event Loop 的方案应用而生。Event Loop 包含两类:一类是基于 [Browsing Context][1],一种是基于 [Worker][2]。二者的运行是独立的,也就是说,每一个 JavaScript 运行的"线程环境"都有一个独立的 Event Loop,每一个 Web Worker 也有一个独立的 Event Loop。 4 | 5 | > 本文所涉及到的事件循环是基于 Browsing Context。 6 | 7 | ### 任务队列 8 | 9 | 根据规范,事件循环是通过任务队列的机制来进行协调的。一个 Event Loop 中,可以有一个或者多个任务队列(task queue),一个任务队列便是一系列有序任务(task)的集合;每个任务都有一个任务源(task source),源自同一个任务源的 task 必须放到同一个任务队列,从不同源来的则被添加到不同队列。setTimeout/Promise 等API便是任务源,而进入任务队列的是他们指定的具体执行任务。 10 | 11 | 在事件循环中,每进行一次循环操作称为 tick,每一次 tick 的任务处理模型是比较复杂的,但关键步骤如下: 12 | 13 | - 在此次 tick 中选择最先进入队列的任务(oldest task),如果有则执行(一次) 14 | - 检查是否存在 Microtasks,如果存在则不停地执行,直至清空 Microtasks Queue 15 | - 更新 render 16 | - 主线程重复执行上述步骤 17 | 18 | 在上诉tick的基础上需要了解几点: 19 | 20 | - JS分为同步任务和异步任务 21 | - 同步任务都在主线程上执行,形成一个执行栈 22 | - 主线程之外,事件触发线程管理着一个任务队列,只要异步任务有了运行结果,就在任务队列之中放置一个事件。 23 | - 一旦执行栈中的所有同步任务执行完毕(此时JS引擎空闲),系统就会读取任务队列,将可运行的异步任务添加到可执行栈中,开始执行。 24 | 25 | ![Event Loop](../assets/img/event.png) 26 | 27 | ### 宏任务 28 | 29 | (macro)task,可以理解是每次执行栈执行的代码就是一个宏任务(包括每次从事件队列中获取一个事件回调并放到执行栈中执行)。 30 | 31 | 浏览器为了能够使得JS内部(macro)task与DOM任务能够有序的执行,会在一个(macro)task执行结束后,在下一个(macro)task 执行开始前,对页面进行重新渲染,流程如下: 32 | 33 | ``` 34 | (macro)task->渲染->(macro)task->... 35 | ``` 36 | 37 | ##### 宏任务包含: 38 | ``` 39 | script(整体代码) 40 | setTimeout 41 | setInterval 42 | I/O 43 | UI交互事件 44 | postMessage 45 | MessageChannel 46 | setImmediate(Node.js 环境) 47 | ``` 48 | 49 | ### 微任务 50 | 51 | microtask,可以理解是在当前 task 执行结束后立即执行的任务。也就是说,在当前task任务后,下一个task之前,在渲染之前。 52 | 53 | 所以它的响应速度相比setTimeout(setTimeout是task)会更快,因为无需等渲染。也就是说,在某一个macrotask执行完后,就会将在它执行期间产生的所有microtask都执行完毕(在渲染前)。 54 | 55 | ##### 微任务包含: 56 | ``` 57 | Promise.then 58 | Object.observe 59 | MutaionObserver 60 | process.nextTick(Node.js 环境) 61 | ``` 62 | 63 | ### 运行机制 64 | 65 | 在事件循环中,每进行一次循环操作称为 tick,每一次 tick 的任务处理模型是比较复杂的,但关键步骤如下: 66 | 67 | - 执行一个宏任务(栈中没有就从事件队列中获取) 68 | - 执行过程中如果遇到微任务,就将它添加到微任务的任务队列中 69 | - 宏任务执行完毕后,立即执行当前微任务队列中的所有微任务(依次执行) 70 | - 当前宏任务执行完毕,开始检查渲染,然后GUI线程接管渲染 71 | - 渲染完毕后,JS线程继续接管,开始下一个宏任务(从事件队列中获取) 72 | 73 | 如图: 74 | 75 | ![运行机制](../assets/img/task.jpg) 76 | 77 | [1]: https://html.spec.whatwg.org/multipage/browsers.html#browsing-context 78 | [2]: https://www.w3.org/TR/workers/#worker -------------------------------------------------------------------------------- /js/强大的Array.reduce.md: -------------------------------------------------------------------------------- 1 | ## 强大的 Array.reduce 2 | 3 | 它可以返回任意值,它的功能就是将一个数组的内容聚合成单个值 4 | 5 | #### 语法: 6 | 7 | ```js 8 | arr.reduce(callback(accumulator, currentValue[, index[, array]])[, initialValue]) 9 | ``` 10 | 11 | #### 参数说明: 12 | 13 | - callback - 执行数组中每个值 (如果没有提供 initialValue 则第一个值除外)的函数 14 | - accumulator - 累计器累计回调的返回值; 它是上一次调用回调时返回的累积值,或 initialValue 15 | - currentValue - 数组中正在处理的元素 16 | - index - 数组中正在处理的当前元素的索引。 如果提供了 initialValue,则起始索引号为 0,否则从索引 1 起始 17 | - array - 调用 reduce()的数组 18 | - initialValue - 作为第一次调用 callback 函数时的第一个参数的值。 如果没有提供初始值,则将使用数组中的第一个元素。 在没有初始值的空数组上调用 reduce 将报错 19 | 20 | #### 注意: 21 | 22 | 如果数组为空且没有提供 initialValue,会抛出 TypeError 。如果数组仅有一个元素(无论位置如何)并且没有提供 initialValue, 或者有提供 initialValue 但是数组为空,那么此唯一值将被返回并且 callback 不会被执行。 23 | 24 | #### 使用场景: 25 | 26 | ##### 数组求和 27 | 28 | ```js 29 | [ 1, 2, 3 ].reduce(( acc, cur ) => acc + cur, 0) 30 | ``` 31 | 32 | ##### 累加对象数组里的值 33 | 34 | ```js 35 | [{x: 1}, {x:2}, {x:3}].reduce(function (acc, cur) { 36 | return acc + cur.x; 37 | }, 0) 38 | ``` 39 | 40 | ##### 按序执行 Promise 41 | 42 | 在不想用 async await 的时,同样可以处理 promise 回调链的问题 43 | 44 | ```js 45 | function p1(a) { 46 | return new Promise((resolve, reject) => { 47 | resolve(a * 2); 48 | }); 49 | } 50 | function p2(a) { 51 | return new Promise((resolve, reject) => { 52 | resolve(a * 3); 53 | }); 54 | } 55 | function p3(a) { 56 | return new Promise((resolve, reject) => { 57 | resolve(a * 4); 58 | }); 59 | } 60 | let initVal = 1; 61 | [p1, p2, p3].reduce( 62 | (promiseChain, currentFunction) => promiseChain.then(currentFunction), 63 | Promise.resolve(initVal) 64 | ).then(res=>console.log(res)) 65 | ``` 66 | 67 | ##### 二维数组转一维数组 68 | 69 | ```js 70 | [[0, 1], [2, 3], [4, 5]].reduce(( acc, cur ) => acc.concat(cur), []) 71 | ``` 72 | 73 | ##### 统计数组中每个元素出现的次数 74 | 75 | ```js 76 | ['aa', 'bb', 'aa', 'cc', 'aa', 'bb'].reduce((allNames, name) => { 77 | if (name in allNames) { 78 | allNames[name]++; 79 | } 80 | else { 81 | allNames[name] = 1; 82 | } 83 | return allNames; 84 | }, {}) 85 | ``` 86 | 87 | ##### 数组去重 88 | 89 | ```js 90 | [1, 2, 3, 4, 4, 1].reduce((acc, cur) => { 91 | if (acc.includes(cur)) { 92 | return acc 93 | } else { 94 | return [...acc, cur] 95 | } 96 | }, []) 97 | ``` 98 | -------------------------------------------------------------------------------- /js/排序算法.md: -------------------------------------------------------------------------------- 1 | ## 排序算法 2 | 3 | #### 冒泡排序 4 | 5 | ```js 6 | var arr = [1, 5, 9, 3, 2, 6, 8, 4] 7 | var length = arr.length 8 | for (var i = 0; i < length; i++) { 9 | for (var j = i + 1; j < length; j++) { 10 | if (arr[i] > arr[j]) { 11 | ;[arr[i], arr[j]] = [arr[j], arr[i]] 12 | } 13 | } 14 | } 15 | console.log('arr === ', arr) 16 | ``` 17 | 18 | #### 快速排序 19 | 20 | 找到一个基准点,然后将集合分成两部分,左边的小于基准点,右边的大于基准点 21 | 22 | ```js 23 | let testArr = [1, 5, 9, 3, 2, 6, 8, 4] 24 | function sortFn(arr) { 25 | let length = arr.length 26 | if (length <= 1) { 27 | return arr 28 | } 29 | let sortIndex = parseInt(length / 2) 30 | let sortItem = arr.splice(sortIndex, 1)[0] 31 | let leftArr = [] 32 | let rightArr = [] 33 | arr.forEach(item => { 34 | if (item < sortItem) { 35 | leftArr.push(item) 36 | } else { 37 | rightArr.push(item) 38 | } 39 | }) 40 | return [...sortFn(leftArr), ...sortFn([sortItem, ...rightArr])] 41 | } 42 | 43 | console.log('arr === ', sortFn(testArr)) 44 | ``` 45 | 46 | #### 选择排序 47 | 48 | 寻找最小的数,将最小数的索引保存 49 | 50 | ```js 51 | var arr = [1, 5, 9, 3, 2, 6, 8, 4] 52 | var length = arr.length 53 | var minIndex 54 | for (var i = 0; i < length; i++) { 55 | minIndex = i 56 | for (var j = i + 1; j < length; j++) { 57 | if (arr[j] < arr[minIndex]) { 58 | minIndex = j 59 | } 60 | } 61 | ;[arr[i], arr[minIndex]] = [arr[minIndex], arr[i]] 62 | } 63 | console.log('arr === ', arr) 64 | ``` 65 | 66 | #### 插入排序 67 | 68 | 类似摸扑克牌 69 | 70 | ```js 71 | var arr = [1, 5, 9, 3, 2, 6, 8, 4] 72 | var length = arr.length 73 | var preIndex, current 74 | for (var i = 1; i < length; i++) { 75 | preIndex = i - 1 76 | current = arr[i] 77 | while (preIndex >= 0 && arr[preIndex] > current) { 78 | arr[preIndex + 1] = arr[preIndex] 79 | preIndex-- 80 | } 81 | arr[preIndex + 1] = current 82 | } 83 | console.log('arr === ', arr) 84 | ``` 85 | #### 二分插入排序 86 | #### 希尔排序 87 | #### 归并排序 88 | #### 堆排序 89 | -------------------------------------------------------------------------------- /js/构造函数和原型.md: -------------------------------------------------------------------------------- 1 | ## 构造函数和原型 2 | 3 | #### 构造函数 4 | 5 | 构造函数通过原型分配的函数是所有对象所`共享的` 6 | 7 | JS规定,每一个构造函数都有一个 `prototype` 属性,指向另一个对象,注意这个prototype就是一个对象,这个对象的所有属性和方法都会被构造函数所拥有。 8 | 9 | 我们把那些不变的方法直接定义在prototype对象上,这样所有对象的实例就可以共享这些方法。 10 | 11 | - 原型是什么? 12 | 13 | *一个对象,我们也称prototype为原型对象,每个构造函数都有* 14 | 15 | - 原型的作用是什么? 16 | 17 | *共享方法* 18 | 19 | 一般情况下,我们的公共属性定义在构造函数里面,公共的方法放到原型对象上。 20 | 21 | ```js 22 | function Star(name, age) { 23 | this.name = name; 24 | this.age = age; 25 | } 26 | 27 | Star.prototype.sing = function() { 28 | console.log('我会唱歌'); 29 | } 30 | 31 | var ldh = new Star('刘德华', 18); 32 | var zxy = new Star('张学友', 19); 33 | console.log(ldh.sing === zxy.sing); // true 34 | ldh.sing(); 35 | console.log(ldl.__proto__ === Star.prototype); // true 36 | ``` 37 | #### 实例对象 38 | 39 | 构造函数生成的实例对象有一个属性 `__proto__` , 指向其构造函数的原型对象 `prototype` 40 | 41 | 所以实例对象的 `__proto__` 和构造函数的 `prototype` 是等价的。 42 | 43 | 实例对象方法和属性查找规则: 先在对象自己身上找,没有再去构造函数的prototype上及原型链上找。 44 | 45 | #### constructor 46 | 47 | 对象原型(__proto__)和构造函数的原型对象(prototype)里面都有一个属性`constructor`属性, constructor我们称为构造函数,因为它指回构造函数本身。 48 | 49 | constructor主要用于记录该对象引用于哪个构造函数,它可以让原型对象重新指向原来的构造函数。 50 | ```js 51 | Star.prototype = { 52 | constructor: Star, 53 | sing: function() { 54 | console.log('唱歌') 55 | }, 56 | dance: funciton() { 57 | console.log('跳舞') 58 | } 59 | } 60 | ``` 61 | 62 | #### 构造函数,原型对象以及实力对象的关系 63 | 64 | ![关系](../assets/img/prototype.jpg) 65 | 66 | #### this指向 67 | 68 | 1. 在构造函数中,this指向的是实例对象。 69 | 70 | 2. 原型对象prototype的函数中,this指向实例对象。 71 | 72 | #### 继承 73 | 74 | 借用父构造函数继承属性,利用原型对象继承方法 75 | ```js 76 | function Father(name, age) { 77 | this.name = name; 78 | this.age = age; 79 | } 80 | function Son(name, age){ 81 | Father.call(this, name, age); 82 | } 83 | Father.prototype.money = funciton() { 84 | console.log('money'); 85 | } 86 | Son.prototype = new Father(); 87 | Son.prototype.constructor = Son; 88 | Son.prototype.other = function() { 89 | console.log('other'); 90 | } 91 | var son = new Son('姓名', 18); 92 | console.log(son); 93 | ``` 94 | -------------------------------------------------------------------------------- /js常用小技巧.md: -------------------------------------------------------------------------------- 1 | - 小数取整: 2 | 3 | ```js 4 | 1.234 | 0 5 | ``` 6 | 7 | ```js 8 | ~~1.234 9 | ``` 10 | 11 | ```js 12 | 1.234 >> 0 13 | ``` 14 | 15 | - 妙用隐式转换: 16 | 17 | - 字符串转number: 18 | 19 | ```js 20 | +'123' 21 | ``` 22 | 23 | - new Date转时间戳: 24 | 25 | ```js 26 | +new Date() 27 | ``` 28 | 29 | - 数组/多维数组转为逗号分隔字符串(可用于多维数组转一维): 30 | 31 | ```js 32 | ""+[1, 2 , 3, 3, [2, 3, 4]] 33 | ``` 34 | 35 | - 解构: 36 | 37 | - 交换a,b的值: 38 | 39 | ```js 40 | var a=1; 41 | var b=2; 42 | [a, b] = [b, a]; 43 | console.log(a, b); 44 | ``` 45 | 46 | - 扩展运算符: 47 | 48 | - 取数组最大值/最小值: 49 | 50 | ```js 51 | Math.max(...[1,2,3]) 52 | Math.min(...[1,2,3]) 53 | ``` 54 | 55 | - 生成时间: 56 | 57 | ```js 58 | new Date(...[2018,6,4]) 59 | ``` 60 | 61 | - 字符串转数组: 62 | 63 | ```js 64 | method 1: 65 | [...'string'] 66 | 67 | method 2: 68 | Array.from('string') 69 | ``` 70 | 71 | - 合并对象: 72 | 73 | ```js 74 | let obj1 = {a:1, b:2}; 75 | let obj2 = {b:3, c:4}; 76 | 77 | {...obj1, ...obj2} 78 | 等同于 79 | Object.assign(obj1, obj2) 80 | ``` 81 | 82 | - 利用URL API获取url的query值 83 | 84 | ```js 85 | var url = new URL('http://localhost:8000/news?a=1&b=2&c=3'); 86 | var searchParams = url.searchParams; 87 | for (let key of searchParams.keys()){ 88 | console.log('===='); 89 | console.log('key === ', key); 90 | console.log('value === ', searchParams.get(key)) 91 | } 92 | ``` 93 | 94 | - 常用方法: 95 | 96 | - 数字前补0: 97 | 98 | ```js 99 | function preFixNum(num, length) { 100 | return (Array(length).join('0') + num).slice(-length); 101 | } 102 | ``` 103 | 104 | - 数组元素为对象的去重: 105 | 106 | ```js 107 | [...new Set(arr.map(v => JSON.stringify(v)))].map(v => JSON.parse(v)) 108 | ``` 109 | 110 | - 数组求和: 111 | 112 | ```js 113 | var arr = [1,2,3,4,5]; 114 | 115 | method 1: 116 | var sum = eval(arr.join('+')); 117 | 118 | method 2: 119 | var sum = arr.reduce((prev,cur) => prev + cur); 120 | ``` 121 | 122 | - 金钱格式化(千分): 123 | 124 | ```js 125 | let money = 11111; 126 | 127 | method 1: 128 | money.toLocaleString('en-US'); 129 | 130 | method 2: 131 | Intl.NumberFormat().format(money); 132 | 133 | method 3: 134 | String(money).replace(/\B(?=(\d{3})+(?!\d))/g, ','); 135 | ``` 136 | 137 | - 短路逻辑代替if: 138 | 139 | ```js 140 | isTrue && console.log(1); 141 | ``` 142 | 143 | - RGB to Hex: 144 | 145 | ```js 146 | function RGBtoHEX(rgb){ 147 | return ((1<<24) + (rgb.r<<16) + (rgb.g<<8) + rgb.b).toString(16).substr(1); 148 | } 149 | ``` 150 | 151 | - 延时函数: 152 | 153 | ```js 154 | const delay = ms => new Promise(resolve => setTimeout(resolve, ms)) 155 | ``` 156 | 157 | - 生成指定长度数组: 158 | 159 | ```js 160 | Array.from(new Array(10).keys()); 161 | ``` 162 | 163 | - 快速创建a标签: 164 | 165 | ```js 166 | let a = '超链接'.link('https://github.com/TigerHee/shareJS'); 167 | console.log('a === ', a) 168 | ``` 169 | 170 | - 正则进阶: 171 | 172 | - 捕获括号: 173 | 174 | ```js 175 | 匹配 'tigerHee' 并且记住匹配项 176 | /(tigerHee)/ 177 | ``` 178 | 179 | - 非捕获括号: 180 | 181 | ```js 182 | 匹配 'tigerHee' 但是不记住匹配项 183 | /(?:tigerHee)/ 184 | ``` 185 | 186 | - 先行断言: 187 | 188 | ```js 189 | 匹配'tiger'仅仅当'tiger'后面跟着'Hee' 190 | /tiger(?=Hee)/ 191 | ``` 192 | 193 | - 后行断言: 194 | 195 | ```js 196 | 匹配'Hee'仅仅当'Hee'前面是'tiger' 197 | /(?<=tiger)Hee/ 198 | ``` 199 | 200 | - 正向否定查找: 201 | 202 | ```js 203 | 匹配'tiger'仅仅当'tiger'后面不跟着'java' 204 | /tiger(?!java)/ 205 | ``` 206 | 207 | 208 | 209 | -------------------------------------------------------------------------------- /node/npm.md: -------------------------------------------------------------------------------- 1 | ## 常用 npm 包 2 | 3 | - ### `decimal.js` 4 | 5 | > 高精度计算库 6 | 7 | - ### `WOW.js` 8 | 9 | > 页面滚动时元素出现的动画库 10 | 11 | [demo](https://www.delac.io/wow/) 12 | 13 | - ### `object-path` 14 | 15 | > js 可选链式调用之前的替代品,解决 `TypeError: Cannot read property ‘x’ of undefined`的问题 16 | 17 | ##### 用例: 18 | 19 | ```js 20 | import objectPath from "object-path"; 21 | 22 | objectPath.get(obj, ["a.c.b"], "DEFAULT"); 23 | ``` 24 | 25 | - ### `water-wave` 26 | 27 | > 一个创建点击后产生水波纹效果的 React 组件 28 | 29 | - ### `clsx` 30 | 31 | > clsx 是一个小型工具集,用于有条件地从一个对象中构造 className 字符串,此对象的键是类字符串(class strings),而值是布尔值(booleans) 32 | 33 | ##### 用例: 34 | 35 | ```js 36 | // 使用前: 37 |
; 38 | 39 | // 使用后 40 |
; 46 | ``` 47 | 48 | ## package.json 49 | 50 | - ### 锁死依赖包的依赖: 51 | 52 | ```js 53 | { 54 | "dependencies": {}, 55 | "resolutions": { 56 | "包名": "版本号" 57 | } 58 | } 59 | ``` 60 | -------------------------------------------------------------------------------- /node/package.md: -------------------------------------------------------------------------------- 1 | ## package.json 笔记 2 | 3 | #### resolutions 4 | 5 | 锁死依赖包内部依赖的版本 6 | 7 | ```js 8 | // eg: 9 | 10 | "resolutions": { 11 | "@react-dnd/asap": "4.0.0" 12 | }, 13 | ``` -------------------------------------------------------------------------------- /react/01.react-basic.md: -------------------------------------------------------------------------------- 1 | ## react 基础 2 | 3 | ### JSX 4 | 5 | - JSX是一个 JavaScript 的语法扩展,可以很好地描述 UI 应该呈现出它应有交互的本质形式。 6 | - React DOM 在渲染所有输入内容之前,默认会进行转义。它可以确保在你的应用中,永远不会注入那些并非自己明确编写的内容。所有的内容在渲染之前都被转换成了字符串。 7 | - JSX 里的 class 变成了 className 8 | 9 | ###### 深入了解: 10 | 11 | JSX 仅仅只是 React.createElement(component, props, ...children) 函数的语法糖。 12 | 13 | 如下JSX代码: 14 | ``` 15 | 16 | Click Me 17 | 18 | ``` 19 | 会编译为: 20 | ``` 21 | React.createElement( 22 | MyButton, 23 | {color: 'blue', shadowSize: 2}, 24 | 'Click Me' 25 | ) 26 | ``` 27 | 28 | ### 元素 29 | 30 | - 元素是构成 React 应用的最小砖块。 31 | - React 元素是创建开销极小的普通对象。React DOM 会负责更新 DOM 来与 React 元素保持一致。 32 | - 组件是由元素构成的。 33 | - `false`, `null`, `undefined`, `true` 是合法的子元素。但它们并不会被渲染。 34 | 35 | 以下的 JSX 表达式渲染结果相同: 36 | ``` 37 |
38 | 39 |
40 | 41 |
{false}
42 | 43 |
{null}
44 | 45 |
{undefined}
46 | 47 |
{true}
48 | ``` 49 | 50 | ### 组件 51 | 52 | 组件名称必须以大写字母开头(React 会将以小写字母开头的组件视为原生 DOM 标签) 53 | 54 | - 函数组件(以前称之为无状态组件,但`Hook`出来之后叫为函数组件): 55 | ``` 56 | function Welcome(props) { 57 | return

Hello, {props.name}

; 58 | } 59 | ``` 60 | 61 | - class组件 62 | ``` 63 | class Welcome extends React.Component { 64 | render() { 65 | return

Hello, {this.props.name}

; 66 | } 67 | } 68 | ``` 69 | 70 | ### props 71 | 72 | 当 React 元素为用户自定义组件时,它会将 JSX 所接收的属性(attributes)转换为单个对象传递给组件,这个对象被称之为 “props”。组件无论是使用函数声明还是通过 class 声明,都决不能修改自身的 props 73 | ``` 74 | // 这段代码会在页面上渲染 “Hello, Sara” 75 | function Welcome(props) { 76 | return

Hello, {props.name}

; 77 | } 78 | 79 | const element = ; 80 | ReactDOM.render( 81 | element, 82 | document.getElementById('root') 83 | ); 84 | 85 | //上段代码渲染时发生了什么: 86 | 1. 我们调用 ReactDOM.render() 函数,并传入 作为参数。 87 | 2. React 调用 Welcome 组件,并将 {name: 'Sara'} 作为 props 传入。 88 | 3. Welcome 组件将

Hello, Sara

元素作为返回值。 89 | 4. React DOM 将 DOM 高效地更新为

Hello, Sara

。 90 | ``` 91 | 有多种方式可以在 JSX 中指定 props。 92 | - JavaScript 表达式作为 Props 93 | - 字符串字面量 94 | - props 默认值为`true` 95 | 96 | ### State 97 | 98 | - 使用`this.setState()`设置state的值 99 | - `this.setState()`可能是异步的 100 | - 调用`this.setState()`的时候,React 会把你提供的对象合并到当前的 state。 101 | 102 | ### 数据流 103 | 104 | `react`是单向数据流,任何的 state 总是所属于特定的组件,而且从该 state 派生的任何数据或 UI 只能影响树中“低于”它们的组件。 105 | 106 | ### 生命周期 107 | 108 | 在 V16 版本中引入了 Fiber 机制。这个机制一定程度上的影响了部分生命周期的调用,并且也引入了新的 2 个 API 来解决问题。(Fiber 本质上是一个虚拟的堆栈帧,新的调度器会按照优先级自由调度这些帧,从而将之前的同步渲染改成了异步渲染,在不影响体验的情况下去分段计算更新。在之前的版本中,如果你拥有一个很复杂的复合组件,然后改动了最上层组件的 state,那么调用栈可能会很长,调用栈过长,再加上中间进行了复杂的操作,就可能导致长时间阻塞主线程,带来不好的用户体验。Fiber 就是为了解决该问题而生。) 109 | ``` 110 | class ExampleComponent extends React.Component { 111 | // 用于初始化 state 112 | constructor(props) { 113 | super(props) 114 | this.state = { hasError: false }; 115 | } 116 | 117 | // 用于替换 `componentWillReceiveProps` ,该函数会在初始化和 `update` 时被调用 118 | // 因为该函数是静态函数,所以取不到 `this`, 如果需要对比 `prevProps` 需要单独在 `state` 中维护 119 | // 它应返回一个对象来更新 state 120 | static getDerivedStateFromProps(nextProps, prevState) {} 121 | 122 | // 判断是否需要更新组件,多用于组件性能优化 123 | shouldComponentUpdate(nextProps, nextState) {} 124 | 125 | // 组件挂载后调用 126 | // 可以在该函数中进行请求或者订阅 127 | componentDidMount() {} 128 | 129 | // 用于替换 componentWillUpdate ,该函数会在 update 后 DOM 更新前被调用 130 | // 用于读取最新的 DOM 数据。 131 | getSnapshotBeforeUpdate() {} 132 | 133 | // 组件即将销毁 134 | // 可以在此处移除订阅,定时器等等 135 | componentWillUnmount() {} 136 | 137 | // 组件销毁后调用 138 | componentDidUnMount() {} 139 | 140 | // 组件更新后调用 141 | componentDidUpdate() {} 142 | 143 | // 错误边界 - 渲染备用 UI 144 | // 更新 state 使下一次渲染能够显示降级后的 UI 145 | // 注意错误边界仅可以捕获其子组件的错误,它无法捕获其自身的错误 146 | static getDerivedStateFromError(error) { 147 | return { hasError: true }; 148 | } 149 | 150 | // 错误边界 - 打印错误信息 151 | // 你同样可以将错误日志上报给服务器 152 | // 注意错误边界仅可以捕获其子组件的错误,它无法捕获其自身的错误 153 | componentDidCatch(error, info) { 154 | console.log(error, info); 155 | } 156 | 157 | // 渲染组件函数 158 | render() {} 159 | 160 | // 以下函数不建议使用 161 | UNSAFE_componentWillMount() {} 162 | UNSAFE_componentWillUpdate(nextProps, nextState) {} 163 | // 接收到新的props时调用 164 | UNSAFE_componentWillReceiveProps(nextProps) {} 165 | } 166 | ``` 167 | 对于异步渲染,现在渲染有两个阶段:reconciliation 和 commit 。前者过程是可以打断的,后者不能暂停,会一直更新界面直到完成。 168 | - Reconciliation 阶段: 169 | - componentWillMount 170 | - componentWillUpdate 171 | - componentWillReceiveProps 172 | - shouldComponentUpdate 173 | - Commit 阶段: 174 | - componentDidMount 175 | - componentDidUpdate 176 | - componentWillUnmount 177 | 178 | 因为 reconciliation 阶段是可以被打断的,所以 reconciliation 阶段会执行的生命周期函数就可能会出现调用多次的情况,从而引起 Bug。所以对于 reconciliation 阶段调用的几个函数,除了 shouldComponentUpdate 以外,其他都应该避免去使用。 179 | 180 | #### V16.4以后生命周期图解(不包含官方不建议使用的) 181 | ![生命周期](../assets/img/reactLifecycle.png) 182 | 183 | ### 事件处理 184 | 185 | - React 事件的命名采用小驼峰式(camelCase),而不是纯小写。 186 | - 使用 JSX 语法时你需要传入一个函数作为事件处理函数,而不是一个字符串。 187 | 188 | 为JSX内时间绑定this的几种方式: 189 | 190 | - constructor内处理: 191 | ``` 192 | constructor() { 193 | this.handleClick = this.handleClick.bind(this); 194 | } 195 | ``` 196 | - JSX内使用bind: 197 | ``` 198 | 199 | ``` 200 | - 箭头函数: 201 | ``` 202 | 203 | ``` 204 | 205 | ### key 206 | 207 | key 帮助 React 识别哪些元素改变了,比如被添加或删除。因此你应当给数组中的每一个元素赋予一个确定的标识。 208 | key 只是在兄弟节点之间必须唯一 209 | 210 | ### 受控组件 211 | 212 | 使 React 的 state 成为“唯一数据源”。渲染表单的 React 组件还控制着用户输入过程中表单发生的操作。被 React 以这种方式控制取值的表单输入元素就叫做`受控组件`。 213 | 214 | ### 非受控组件 215 | 216 | 表单数据将交由 DOM 节点来处理。使用非受控组件时如果想赋予组件一个初始值,但是不去控制后续的更新。 在这种情况下, 你可以指定一个 `defaultValue` 属性,而不是 value。 217 | 218 | `` 始终是一个非受控组件 219 | 220 | ### 进阶 221 | [redux-adcanve](https://github.com/TigerHee/shareJS/blob/master/react/02.react-advance.md) -------------------------------------------------------------------------------- /react/02.react-advance.md: -------------------------------------------------------------------------------- 1 | ## react 进阶 2 | 3 | ### 懒加载 4 | 5 | `React.lazy`函数能让你像渲染常规组件一样处理动态引入(的组件)。 6 | `Suspense`加载指示器为组件做优雅降级。 7 | `fallback`属性接受任何在组件加载过程中你想展示的 React 元素。 8 | 9 | ``` 10 | const OtherComponent = React.lazy(() => import('./OtherComponent')); 11 | 12 | function MyComponent() { 13 | return ( 14 |
15 | Loading...
}> 16 | 17 | 18 |
19 | ); 20 | } 21 | ``` 22 | 23 | ### Context 24 | 25 | `Context`提供了一个无需为每层组件手动添加 props,就能在组件树间进行数据传递的方法,设计目的是为了共享那些对于一个组件树而言是“全局”的数据。 26 | ``` 27 | // Context 可以让我们无须明确地传遍每一个组件,就能将值深入传递进组件树。 28 | // 为当前的 theme 创建一个 context(“light”为默认值)。 29 | const ThemeContext = React.createContext('light'); 30 | 31 | class App extends React.Component { 32 | render() { 33 | // 使用一个 Provider 来将当前的 theme 传递给以下的组件树。 34 | // 无论多深,任何组件都能读取这个值。 35 | // 在这个例子中,我们将 “dark” 作为当前的值传递下去。 36 | return ( 37 | 38 | 39 | 40 | ); 41 | } 42 | } 43 | 44 | // 中间的组件再也不必指明往下传递 theme 了。 45 | function Toolbar(props) { 46 | return ( 47 |
48 | 49 |
50 | ); 51 | } 52 | 53 | class ThemedButton extends React.Component { 54 | // 指定 contextType 读取当前的 theme context。 55 | // React 会往上找到最近的 theme Provider,然后使用它的值。 56 | // 在这个例子中,当前的 theme 值为 “dark”。 57 | static contextType = ThemeContext; 58 | render() { 59 | return 119 | )); 120 | 121 | // 你可以直接获取 DOM button 的 ref: 122 | const ref = React.createRef(); 123 | Click me!; 124 | ``` 125 | 上例中,`FancyButton` 使用 `React.forwardRef` 来获取传递给它的 `ref`,然后转发到它渲染的 DOM `button`。这样,使用 `FancyButton` 的组件可以获取底层 DOM 节点 `button` 的 `ref` ,并在必要时访问,就像其直接使用 DOM `button` 一样。 126 | 127 | ### Fragments 128 | 129 | Fragments 允许你将子列表分组,而无需向 DOM 添加额外节点。key 是唯一可以传递给 Fragment 的属性 130 | ``` 131 | render() { 132 | return ( 133 | 134 | 135 | 136 | 137 | 138 | ); 139 | } 140 | ``` 141 | ``可简写为`<>` 142 | 143 | ### 高阶组件(HOC) 144 | 145 | HOC是参数为组件,返回值为新组件的函数。 146 | 147 | HOC 不会修改传入的组件,也不会使用继承来复制其行为。相反,HOC 通过将组件包装在容器组件中来组成新组件。HOC 是纯函数,没有副作用。 148 | 149 | 示例: 150 | ``` 151 | // 此函数接收一个组件... 152 | function withSubscription(WrappedComponent, selectData) { 153 | // ...并返回另一个组件... 154 | return class extends React.Component { 155 | constructor(props) { 156 | super(props); 157 | this.handleChange = this.handleChange.bind(this); 158 | this.state = { 159 | data: selectData(DataSource, props) 160 | }; 161 | } 162 | 163 | componentDidMount() { 164 | // ...负责订阅相关的操作... 165 | DataSource.addChangeListener(this.handleChange); 166 | } 167 | 168 | componentWillUnmount() { 169 | DataSource.removeChangeListener(this.handleChange); 170 | } 171 | 172 | handleChange() { 173 | this.setState({ 174 | data: selectData(DataSource, this.props) 175 | }); 176 | } 177 | 178 | render() { 179 | // ... 并使用新数据渲染被包装的组件! 180 | // 请注意,我们可能还会传递其他属性 181 | return ; 182 | } 183 | }; 184 | } 185 | ``` 186 | 上例中class组件为HOC的容器组件,容器组件担任分离将高层和低层关注的责任,由容器管理订阅和状态,并将 prop 传递给处理渲染 UI。HOC 使用容器作为其实现的一部分,你可以将 HOC 视为参数化容器组件。 187 | 188 | *注意事项:* 189 | 190 | - 不要在 render 方法中使用 HOC。 191 | 192 | 在render中使用会导致diff 算法在对比组件变化时每次检测都不一样,每次渲染都会进行卸载,和重新挂载的操作,这不仅仅是性能问题 - 重新挂载组件会导致该组件及其所有子组件的状态丢失。 193 | 194 | - 务必复制静态方法到容器组件上。 195 | 196 | 可以使用`hoist-non-react-statics`自动拷贝所有非 React 静态方法 197 | ``` 198 | import hoistNonReactStatic from 'hoist-non-react-statics'; 199 | function enhance(WrappedComponent) { 200 | class Enhance extends React.Component {/*...*/} 201 | hoistNonReactStatic(Enhance, WrappedComponent); 202 | return Enhance; 203 | } 204 | ``` 205 | 206 | - Refs 不会被传递。 207 | 208 | 可用过Refs 转发解决 209 | 210 | ##### 常见的HOC: 211 | 212 | redux的 `connect` 213 | 214 | ### React.PureComponent 215 | 216 | 大部分情况下,你可以使用 React.PureComponent 来代替手写 shouldComponentUpdate。只有当检测数据是数组或对象时,由于浅拷贝的问题会导致比较出现偏差不能使用,此时使用深拷贝仍可继续使用。 217 | 218 | 如以下代码: 219 | ``` 220 | class CounterButton extends React.Component { 221 | constructor(props) { 222 | super(props); 223 | this.state = {count: 1}; 224 | } 225 | 226 | shouldComponentUpdate(nextProps, nextState) { 227 | if (this.props.color !== nextProps.color) { 228 | return true; 229 | } 230 | if (this.state.count !== nextState.count) { 231 | return true; 232 | } 233 | return false; 234 | } 235 | 236 | render() { 237 | return ( 238 | 243 | ); 244 | } 245 | } 246 | ``` 247 | 可替换为: 248 | ``` 249 | class CounterButton extends React.PureComponent { 250 | constructor(props) { 251 | super(props); 252 | this.state = {count: 1}; 253 | } 254 | 255 | render() { 256 | return ( 257 | 262 | ); 263 | } 264 | } 265 | ``` 266 | 267 | ### Portals 268 | 269 | Portal 提供了一种将子节点渲染到存在于父组件以外的 DOM 节点的优秀的方案。 270 | 271 | 一个 portal 的典型用例是当父组件有 overflow: hidden 或 z-index 样式时,但你需要子组件能够在视觉上“跳出”其容器。例如,对话框、悬浮卡以及提示框: 272 | 273 | ``` 274 | render() { 275 | // React 并*没有*创建一个新的 div。它只是把子元素渲染到 `domNode` 中。 276 | // `domNode` 是一个可以在任何位置的有效 DOM 节点。 277 | return ReactDOM.createPortal( 278 | this.props.children, 279 | domNode 280 | ); 281 | } 282 | ``` 283 | 284 | ### React.StrictMode 285 | 286 | StrictMode 不会渲染任何可见的 UI。它为其后代元素触发额外的检查和警告。严格模式检查仅在开发模式下运行;它们不会影响生产构建。 287 | 288 | ###### 作用: 289 | - 识别不安全的生命周期 290 | - 关于使用过时字符串 ref API 的警告 291 | - 关于使用废弃的 findDOMNode 方法的警告 292 | - 检测意外的副作用 293 | - 检测过时的 context API 294 | 295 | ### React.memo 296 | 297 | `React.memo` 为高阶组件。它与 React.PureComponent 非常相似,但它适用于函数组件,但不适用于 class 组件。 298 | 299 | 如果你的函数组件在给定相同 props 的情况下渲染相同的结果,那么你可以通过将其包装在 `React.memo` 中调用,以此通过记忆组件渲染结果的方式来提高组件的性能表现。这意味着在这种情况下,React 将跳过渲染组件的操作并直接复用最近一次渲染的结果。 300 | 301 | 默认情况下其只会对复杂对象做浅层对比,如果你想要控制对比过程,那么请将自定义的比较函数通过第二个参数传入来实现。 302 | 303 | ``` 304 | const MyComponent = React.memo(function MyComponent(props) { 305 | /* 使用 props 渲染 */ 306 | }); 307 | ``` -------------------------------------------------------------------------------- /react/03.Hook.md: -------------------------------------------------------------------------------- 1 | ## Hook 2 | 3 | Hook 是 `React 16.8.0` 的新增特性。 4 | 5 | Hook 使你在非 class 的情况下可以使用更多的 React 特性。Hook 不能在 class 组件中使用。 6 | 7 | ### 使用规则: 8 | 9 | - 只能在函数最外层调用 Hook。不要在循环、条件判断或者子函数中调用。 10 | - 只能在 React 的函数组件中调用 Hook。不要在其他 JavaScript 函数中调用。 11 | 12 | ### State Hook 13 | 14 | > ##### useState 15 | 16 | 使用 useState 可以不通过 class 组件而在函数组件内使用 state,可通过多次调用声明多个 state 17 | 18 | - 参数: 19 | 20 | _useState() 方法里面唯一的参数就是初始 state。_ 21 | 22 | - 返回值: 23 | 24 | _当前 state 以及更新 state 的函数。_ 25 | 26 | **_函数式更新:_** 27 | 28 | 如果新的 state 需要通过使用先前的 state 计算得出,那么可以将函数传递给 setState。该函数将接收先前的 state,并返回一个更新后的值。 29 | 30 | ```js 31 | function Counter({ initialCount }) { 32 | const [count, setCount] = useState(initialCount) 33 | return ( 34 | <> 35 | Count: {count} 36 | 37 | 38 | 39 | 40 | ) 41 | } 42 | ``` 43 | 44 | ### Effect Hook 45 | 46 | Effect Hook 可以让你在函数组件中执行副作用操作(在 React 组件中执行过数据获取、订阅或者手动修改过 DOM。我们统一把这些操作称为“副作用”,或者简称为“作用”。) 47 | 48 | > ##### useEffect 49 | 50 | 可以把 useEffect Hook 看做 componentDidMount,componentDidUpdate 和 componentWillUnmount 这三个函数的组合。 51 | 52 | useEffect 会在每次渲染后(第一次渲染之后和每次更新之后)都执行,如果你的 effect 返回一个函数,React 将会在组件卸载的时候执行清除操作时调用它。 53 | 54 | useEffect 在组件内可多次调用,Hook 允许我们按照代码的用途分离他们,React 将按照 effect 声明的顺序依次调用组件中的每一个 effect。 55 | 56 | **_使用位置:_** 57 | 58 | _组件内部调用 useEffect。 将 useEffect 放在组件内部让我们可以在 effect 中直接访问 count state 变量(或其他 props)。_ 59 | 60 | **_性能优化:_** 61 | 62 | _useEffect 的第二个可选参数可以实现如果某些特定值在两次重渲染之间没有发生变化,你可以通知 React 跳过对 effect 的调用。请确保数组中包含了所有外部作用域中会随时间变化并且在 effect 中使用的变量_ 63 | 64 | ```js 65 | // 仅在 count 更改时更新 66 | useEffect(() => { 67 | document.title = `You clicked ${count} times` 68 | }, [count]) 69 | 70 | // 仅在组件初次渲染和组件销毁时执行 71 | useEffect(() => { 72 | console.log("1") 73 | return () => { 74 | console.log("2") 75 | } 76 | }, []) 77 | ``` 78 | 79 | ### 示例代码详解 `useState` 与 `useEffect` : 80 | 81 | ```js 82 | // 引入 React 中的 useState Hook。它让我们在函数组件中存储内部 state 83 | // 引入 useEffect 84 | import React, { useState, useEffect } from "react" 85 | 86 | function Example(props) { 87 | // 声明了一个叫 count 的 state 变量,然后把它设为 0 88 | const [count, setCount] = useState(0) 89 | // 声明第2个state 90 | const [isOnline, setIsOnline] = useState(null) 91 | 92 | // 无需清除的 effect 93 | useEffect(() => { 94 | // 将 document 的 title 设置为包含了点击次数的消息。 95 | document.title = `You clicked ${count} times` 96 | }) 97 | 98 | // 需要清除的 effect 99 | useEffect(() => { 100 | function handleFn(val) { 101 | setIsOnline(val) 102 | } 103 | // 注册监听 104 | XXAPI.subscribe(handleFn) 105 | // 清除监听 106 | return () => { 107 | XXAPI.unsubscribe(handleFn) 108 | } 109 | }) 110 | 111 | return ( 112 |
113 | // 读取 State: 我们可以直接用 count 114 |

You clicked {count} times

115 | // 更新 State: 可以通过调用 setCount 来更新当前的 count 116 | 117 |
118 | ) 119 | } 120 | ``` 121 | 122 | ### useLayoutEffect 123 | 124 | 其函数签名与 useEffect 相同,但它会在所有的 DOM 变更之后同步调用 effect。可以使用它来读取 DOM 布局并同步触发重渲染。在浏览器执行绘制之前,useLayoutEffect 内部的更新计划将被同步刷新。尽可能使用标准的 useEffect 以避免阻塞视觉更新。 125 | 126 | 与 componentDidMount 或 componentDidUpdate 不同,使用 useEffect 调度的 effect 不会阻塞浏览器更新屏幕,这让你的应用看起来响应更快。大多数情况下,effect 不需要同步地执行。在个别情况下(例如测量布局),这时需要用到 useLayoutEffect 127 | 128 | ### useRef 129 | 130 | useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数(initialValue)。返回的 ref 对象在组件的整个生命周期内保持不变。 131 | 132 | ```js 133 | function TextInputWithFocusButton() { 134 | const inputEl = useRef(null) 135 | const onButtonClick = () => { 136 | // `current` 指向已挂载到 DOM 上的文本输入元素 137 | inputEl.current.focus() 138 | } 139 | return ( 140 | <> 141 | 142 | 143 | 144 | ) 145 | } 146 | ``` 147 | 148 | useRef() 比 ref 属性更有用。它可以很方便地保存任何可变值,其类似于在 class 中使用实例字段的方式。当 ref 对象内容发生变化时,useRef 并不会通知你。变更 .current 属性不会引发组件重新渲染。例如: 149 | 150 | ```js 151 | function Timer() { 152 | const intervalRef = useRef() 153 | 154 | useEffect(() => { 155 | intervalRef.current = setInterval(() => { 156 | // ... 157 | }) 158 | return () => { 159 | clearInterval(intervalRef.current) 160 | } 161 | }) 162 | // ... 163 | } 164 | ``` 165 | 166 | ### 其它 Hook 167 | 168 | > ##### useReducer 169 | 170 | `useReducer`是`useState`的替代方案,它接收一个形如 (state, action) => newState 的 reducer,并返回当前的 state 以及与其配套的 dispatch 方法。 171 | 172 | 在某些场景下,useReducer 会比 useState 更适用,例如 state 逻辑较复杂且包含多个子值,或者下一个 state 依赖于之前的 state 等。并且,使用 useReducer 还能给那些会触发深更新的组件做性能优化,因为你可以向子组件传递 dispatch 而不是回调函数 。 173 | 174 | 使用示例: 175 | 176 | ```js 177 | const initialState = { count: 0 } 178 | 179 | function reducer(state, action) { 180 | switch (action.type) { 181 | case "increment": 182 | return { count: state.count + 1 } 183 | case "decrement": 184 | return { count: state.count - 1 } 185 | default: 186 | throw new Error() 187 | } 188 | } 189 | 190 | function Counter() { 191 | const [state, dispatch] = useReducer(reducer, initialState) 192 | return ( 193 | <> 194 | Count: {state.count} 195 | 196 | 197 | 198 | ) 199 | } 200 | ``` 201 | 202 | 通过阅读 React 源码 `ReactFiberHooks.js` 发现 `useState` 就是对 `useReducer` 的封装: 203 | 204 | ```js 205 | // useState 206 | export function useState(initialState: (() => S) | S): [S, Dispatch>] { 207 | return useReducer( 208 | basicStateReducer, 209 | // useReducer has a special case to support lazy useState initializers 210 | (initialState: any) 211 | ) 212 | } 213 | 214 | // useReducer 215 | export function useReducer( 216 | reducer: (S, A) => S, 217 | initialState: S, 218 | initialAction: A | void | null 219 | ): [S, Dispatch] { 220 | // ... 221 | // ... 222 | // ... 223 | } 224 | ``` 225 | 226 | 利用 useReducer 实现 forceUpdate: 227 | 228 | ```js 229 | const [ignored, forceUpdate] = useReducer((x) => x + 1, 0) 230 | 231 | function handleClick() { 232 | forceUpdate() 233 | } 234 | ``` 235 | 236 | > ##### useMemo 与 useCallback 237 | 238 | 可用于给子组件传递参数及回调函数时的优化项 239 | 240 | ```js 241 | import React, { useState, useMemo, useCallback } from "react" 242 | 243 | function fnComponent() { 244 | const [count, setCount] = useState(0) 245 | const [name, setName] = useState("name") 246 | 247 | // 不会在每次fnComponent有更新都重新渲染Child 248 | const config = useMemo(() => { 249 | text: `count is ${count}` 250 | }, [count]) 251 | const handleButtonClk = useCallback(() => { 252 | setCount((c) => c + 1) 253 | }, []) 254 | 255 | return ( 256 |
257 |
{name}
258 |
{count}
259 | { 262 | setName(e.target.value) 263 | }} 264 | /> 265 | 266 |
267 | ) 268 | } 269 | 270 | function Child({ config, handleButtonClk }) { 271 | console.log("child render") 272 | return 273 | } 274 | ``` 275 | 276 | > ##### useImperativeHandle 277 | 278 | 解决父组件调用子组件函数的问题,在使用 ref 时自定义暴露给父组件的实例值, `useImperativeHandle` 应当与 `forwardRef` 一起使用 279 | 280 | ```js 281 | function FancyInput(props, ref) { 282 | const inputRef = useRef(); 283 | useImperativeHandle(ref, () => ({ 284 | focus: () => { 285 | inputRef.current.focus(); 286 | } 287 | })); 288 | return ; 289 | } 290 | FancyInput = forwardRef(FancyInput); 291 | // 渲染 的父组件可以调用 inputRef.current.focus() 292 | ``` 293 | 294 | ### 自定义 Hook 295 | 296 | 自定义 Hook 是一个函数,其名称以 `use` 开头(必须以 `use` 开头),函数内部可以调用其他的 Hook。自定义 Hook 用于提取多组件之间的共享逻辑,可用于替代 `render props` 和 `HOC`。 297 | 298 | 在需要共享逻辑的组件内调用很简单,只需要引入定义好的自定义 Hook,并传入自己想要的参数拿到你想要的返回值作用于当前组件。 299 | 300 | ##### 如下例: 301 | 302 | 1. 提取自定义 Hook: 303 | 304 | ```js 305 | import React, { useState, useEffect } from "react" 306 | 307 | function useFriendStatus(friendID) { 308 | const [isOnline, setIsOnline] = useState(null) 309 | 310 | useEffect(() => { 311 | function handleStatusChange(status) { 312 | setIsOnline(status.isOnline) 313 | } 314 | 315 | XXXAPI.subscribeToFriendStatus(friendID, handleStatusChange) 316 | return () => { 317 | XXXAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange) 318 | } 319 | }) 320 | 321 | return isOnline 322 | } 323 | ``` 324 | 325 | 2. 使用自定义 Hook: 326 | 327 | ```js 328 | function FriendListItem(props) { 329 | const isOnline = useFriendStatus(props.friend.id) 330 | 331 | return
  • {props.friend.name}
  • 332 | } 333 | ``` 334 | 335 | #### 自定义 hook 实际应用: 336 | 337 | - 获取上一轮的 props 或 state 338 | 339 | ```js 340 | function Counter() { 341 | const [count, setCount] = useState(0) 342 | const prevCount = usePrevious(count) 343 | return ( 344 |

    345 | Now: {count}, before: {prevCount} 346 |

    347 | ) 348 | } 349 | 350 | function usePrevious(value) { 351 | const ref = useRef() 352 | useEffect(() => { 353 | ref.current = value 354 | }) 355 | return ref.current 356 | } 357 | ``` 358 | 359 | - 封装axios 360 | 361 | ```js 362 | import React, { useEffect, useState } from "react"; 363 | import Axios from "axios"; 364 | 365 | export const useFetch = (url, type, config) => { 366 | const [refresh, setRefresh] = useState(null); 367 | const [data, setData] = useState(null); 368 | const [fetching, setFetching] = useState(false); 369 | const [error, setError] = useState(null); 370 | 371 | useEffect(() => { 372 | let isMounted = true; 373 | 374 | const refresh = async () => { 375 | if (!url) { 376 | return; 377 | } 378 | if (isMounted) { 379 | setFetching(true); 380 | } 381 | const fetchFunction = type === "post" ? Axios.post : Axios.get; 382 | try { 383 | const result = await fetchFunction(url, config); 384 | // If fetching succeeds. 385 | if (result.status === 200 || result.status === 202) { 386 | if (isMounted) { 387 | setData(result.data); 388 | } 389 | } else { 390 | // If fetching fails 391 | if (isMounted) { 392 | setError(new Error(result.statusText)); 393 | } 394 | } 395 | } catch (e) { 396 | if (isMounted) { 397 | setError(e); 398 | } 399 | } finally { 400 | if (isMounted) { 401 | setFetching(false); 402 | } 403 | } 404 | }; 405 | refresh(); 406 | setRefresh(() => refresh); 407 | return () => { 408 | isMounted = false; 409 | }; 410 | }, [url, type, config]); 411 | 412 | return { 413 | url, 414 | type, 415 | config, 416 | refresh, 417 | data, 418 | fetching, 419 | error, 420 | }; 421 | }; 422 | 423 | ``` 424 | -------------------------------------------------------------------------------- /react/04.redux.md: -------------------------------------------------------------------------------- 1 | ## redux 2 | 3 | > ### 基础 4 | 5 | ![redux工作流](../assets/img/redux.jpg) 6 | 7 | #### ***1.数据流:*** 8 | **严格的单向数据流是 Redux 架构的设计核心。** 9 | **Redux 应用中数据的生命周期遵循下面 4 个步骤:** 10 | 1. 调用 store.dispatch(action)。 11 | - Action 就是一个描述“发生了什么”的普通对象。 12 | 2. Redux store 调用传入的 reducer 函数。 13 | - Store 会把两个参数传入 reducer: 当前的 state 树和 action。 14 | 3. 根 reducer 应该把多个子 reducer 输出合并成一个单一的 state 树。 15 | - 根 reducer 的结构完全由你决定。Redux 原生提供combineReducers()辅助函数,来把根 reducer 拆分成多个函数,用于分别处理 state 树的一个分支。 16 | 4. Redux store 保存了根 reducer 返回的完整 state 树。 17 | - 这个新的树就是应用的下一个 state!所有订阅 store.subscribe(listener) 的监听器都将被调用;监听器里可以调用 store.getState() 获得当前 state。 18 | 19 | ---- 20 | #### ***2.Action:*** 21 | **Action是把数据从应用传到 store 的有效载荷。它是 store 数据的唯一来源。** 22 | **一般来说你会通过 store.dispatch() 将 action 传到 store。** 23 | **可以把 action 理解成新闻的摘要。如 “玛丽喜欢42号文章。” 或者 “任务列表里添加了'学习 Redux 文档'”。** 24 | **你可以在任何地方调用 store.dispatch(action),包括组件中、XHR 回调中、甚至定时器中。** 25 | 26 | ---- 27 | #### ***3.Reducer:*** 28 | **Reducers 指定了应用状态的变化如何响应 actions 并发送到 store 的,** 29 | **Reducers 就是一个纯函数,接收旧的 state 和 action,返回新的 state。(previousState, action) => newState** 30 | **永远不要在 reducer 里做这些操作:** 31 | 1. 修改传入参数; 32 | 1. 执行有副作用的操作,如 API 请求和路由跳转; 33 | 1. 调用非纯函数,如 Date.now() 或 Math.random()。 34 | 35 | ---- 36 | #### ***4.Store:*** 37 | **Store 就是用来维持应用所有的 state 树 的一个对象。 改变 store 内 state 的惟一途径是对它 dispatch 一个 action。** 38 | **Store 不是类。它只是有几个方法的对象。 要创建它,只需要把根部的 reducing 函数 传递给 createStore。** 39 | **Store 就是把Action和Reducer联系到一起的对象。** 40 | - ##### Store的方法: 41 | - ###### getState() 42 | - ###### *作用:* 43 | 返回应用当前的 state 树。它与 store 的最后一个 reducer 返回值相同。 44 | - ###### *返回值:* 45 | (any): 应用当前的 state 树。 46 | - ###### dispatch(action) 47 | - ###### *作用:* 48 | 分发 action。这是触发 state 变化的惟一途径。会使用当前 getState() 的结果和传入的 action 以同步方式的调用 store 的 reduce 函数。返回值会被作为下一个 state。从现在开始,这就成为了 getState() 的返回值,同时变化监听器(change listener)会被触发。 49 | - ###### *参数:* 50 | action (Object) 51 | - ###### *返回值:* 52 | (Object): 要 dispatch 的 action。 53 | - ###### *注意:* 54 | 使用 createStore 创建的 “纯正” store 只支持普通对象类型的 action,而且会立即传到 reducer 来执行。 55 | 但是,如果你用 applyMiddleware 来套住 createStore 时,middleware 可以修改 action 的执行,并支持执行 dispatch intent(意图)。Intent 一般是异步操作如 Promise、Observable 或者 Thunk。 56 | - ###### subscribe(listener) 57 | - ###### *作用:* 58 | 添加一个变化监听器。每当 dispatch action 的时候就会执行,state 树中的一部分可能已经变化。你可以在回调函数里调用 getState() 来拿到当前 state。如果需要解绑这个变化监听器,执行 subscribe 返回的函数即可。 59 | - ###### *参数:* 60 | listener (Function): 每当 dispatch action 的时候都会执行的回调。state 树中的一部分可能已经变化。你可以在回调函数里调用 getState() 来拿到当前 state。store 的 reducer 应该是纯函数,因此你可能需要对 state 树中的引用做深度比较来确定它的值是否有变化。 61 | - ###### *返回值:* 62 | (Function): 一个可以解绑变化监听器的函数。 63 | - ###### replaceReducer(nextReducer) 64 | - ###### *作用:* 65 | 替换 store 当前用来计算 state 的 reducer。这是一个高级 API。只有在你需要实现代码分隔,而且需要立即加载一些 reducer 的时候才可能会用到它。在实现 Redux 热加载机制的时候也可能会用到。 66 | - ###### *参数:* 67 | reducer (Function) store 会使用的下一个 reducer。 68 | > ### API 69 | - #### ***createStore(reducer, [preloadedState], enhancer)*** 70 | - ###### *作用:* 71 | 创建一个 Redux store 来以存放应用中所有的 state。应用中应有且仅有一个 store。 72 | - ###### *参数:* 73 | - reducer (Function): 74 | 75 | 接收两个参数,分别是当前的 state 树和要处理的 action,返回新的state 树。 76 | - [preloadedState] (any): 77 | 78 | 初始时的 state。 在同构应用中,你可以决定是否把服务端传来的 state 水合(hydrate)后传给它,或者从之前保存的用户会话中恢复一个传给它。如果你使用 combineReducers 创建 reducer,它必须是一个普通对象,与传入的 keys 保持同样的结构。否则,你可以自由传入任何 reducer 可理解的内容。 79 | - enhancer (Function): 80 | 81 | Store enhancer 是一个组合 store creator 的高阶函数,返回一个新的强化过的 store creator。这与 middleware 相似,它也允许你通过复合函数改变 store 接口。 82 | - ###### *返回值:* 83 | (Store): 保存了应用所有 state 的对象。改变 state 的惟一方法是 dispatch action。你也可以 subscribe 监听 state 的变化,然后更新 UI。 84 | 85 | ---- 86 | - #### ***combineReducers()*** 87 | - ###### *作用:* 88 | combineReducers() 所做的只是生成一个函数,这个函数来调用你的一系列 reducer,每个 reducer 根据它们的 key 来筛选出 state 中的一部分数据并处理,然后这个生成的函数再将所有 reducer 的结果合并成一个大的对象。没有任何魔法。正如其他 reducers,如果 combineReducers() 中包含的所有 reducers 都没有更改 state,那么也就不会创建一个新的对象。 89 | 90 | ---- 91 | - #### ***applyMiddleware(...middlewares)*** 92 | - ###### *作用:* 93 | 使用包含自定义功能的 middleware 来扩展 Redux 是一种推荐的方式。Middleware 可以让你包装 store 的 dispatch 方法来达到你想要的目的。同时, middleware 还拥有“可组合”这一关键特性。多个 middleware 可以被组合到一起使用,形成 middleware 链。其中,每个 middleware 都不需要关心链中它前后的 middleware 的任何信息。 94 | Middleware 最常见的使用场景是无需引用大量代码或依赖类似 Rx 的第三方库实现异步 actions。这种方式可以让你像 dispatch 一般的 actions 那样 dispatch 异步 actions。 95 | 96 | ---- 97 | - #### ***bindActionCreators(actionCreators, dispatch)*** 98 | - ###### *作用:* 99 | 把一个 value 为不同 action creator 的对象,转成拥有同名 key 的对象。同时使用 dispatch 对每个 action creator 进行包装,以便可以直接调用它们。 100 | 一般情况下你可以直接在 Store 实例上调用 dispatch。如果你在 React 中使用 Redux,react-redux 会提供 dispatch 函数让你直接调用它 。 101 | 惟一会使用到 bindActionCreators 的场景是当你需要把 action creator 往下传到一个组件上,却不想让这个组件觉察到 Redux 的存在,而且不希望把 dispatch 或 Redux store 传给它。 102 | 为方便起见,你也可以传入一个函数作为第一个参数,它会返回一个函数。 103 | 另一替代 bindActionCreators 的做法是直接把 dispatch 函数当作 prop 传递给子组件,但这时你的子组件需要引入 action creator 并且感知它们 104 | 105 | ---- 106 | - #### ***compose(...functions)*** 107 | - ###### *作用:* 108 | 从右到左来组合多个函数。 109 | 这是函数式编程中的方法,为了方便,被放到了 Redux 里。当需要把多个 store 增强器 依次执行的时候,需要用到它。 110 | 111 | > ### react-redux 112 | 113 | Redux 官方提供的 React 绑定库,具有高效且灵活的特性。此库并不是 Redux 内置,需要单独安装。 114 | 115 | - #### react-redux API: 116 | 117 | - ##### ***``*** 118 | - ###### *作用:* 119 | `` 使组件层级中的 connect() 方法都能够获得 Redux store。正常情况下,你的根组件应该嵌套在 `` 中才能使用 connect() 方法。 120 | - ###### *属性:* 121 | - store (Redux Store): 应用程序中唯一的 Redux store 对象 122 | - children (ReactElement) 组件层级的根组件。 123 | 124 | - ##### ***`connect([mapStateToProps], [mapDispatchToProps], [mergeProps], [options])`*** 125 | - ###### *作用:* 126 | 连接 React 组件与 Redux store。连接操作不会改变原来的组件类。反而返回一个新的已与 Redux store 连接的组件类。 127 | - ###### *参数:* 128 | - [mapStateToProps(state, [ownProps]): stateProps] (Function): 129 | 130 | 如果定义该参数,组件将会监听 Redux store 的变化。任何时候,只要 Redux store 发生改变,mapStateToProps 函数就会被调用。该回调函数必须返回一个纯对象,这个对象会与组件的 props 合并。如果你省略了这个参数,你的组件将不会监听 Redux store。如果指定了该回调函数中的第二个参数 ownProps,则该参数的值为传递到组件的 props,而且只要组件接收到新的 props,mapStateToProps 也会被调用(例如,当 props 接收到来自父组件一个小小的改动,那么你所使用的 ownProps 参数,mapStateToProps 都会被重新计算)。 131 | 132 | - [mapDispatchToProps(dispatch, [ownProps]): dispatchProps] (Object or Function): 133 | 134 | 如果传递的是一个函数,该函数将接收一个 dispatch 函数,然后由你来决定如何返回一个对象,这个对象通过 dispatch 函数与 action creator 以某种方式绑定在一起(提示:你也许会用到 Redux 的辅助函数 bindActionCreators()。如果你省略这个 mapDispatchToProps 参数,默认情况下,dispatch 会注入到你的组件 props 中。如果指定了该回调函数中第二个参数 ownProps,该参数的值为传递到组件的 props,而且只要组件接收到新 props,mapDispatchToProps 也会被调用。 135 | 136 | - [mergeProps(stateProps, dispatchProps, ownProps): props] (Function): 137 | 138 | 如果指定了这个参数,mapStateToProps() 与 mapDispatchToProps() 的执行结果和组件自身的 props 将传入到这个回调函数中。该回调函数返回的对象将作为 props 传递到被包装的组件中。你也许可以用这个回调函数,根据组件的 props 来筛选部分的 state 数据,或者把 props 中的某个特定变量与 action creator 绑定在一起。如果你省略这个参数,默认情况下返回 Object.assign({}, ownProps, stateProps, dispatchProps) 的结果。 139 | 140 | - [options] (Object): 141 | 142 | 如果指定这个参数,可以定制 connector 的行为。 143 | 144 | `[pure = true]` (Boolean):如果为 true,connector 将执行 shouldComponentUpdate 并且浅对比 mergeProps 的结果,避免不必要的更新,前提是当前组件是一个“纯”组件,它不依赖于任何的输入或 state 而只依赖于 props 和 Redux store 的 state。默认值为 true。 145 | 146 | `[withRef = false]` (Boolean): 如果为 true,connector 会保存一个对被被包含的组件实例的引用,该引用通过 getWrappedInstance() 方法获得。默认值为 false。 147 | 148 | 149 | > ### redux v7.1.0 150 | [redux的hooks API](https://react-redux.js.org/api/hooks#using-hooks-in-a-react-redux-app) 151 | 152 | > ### 异步Ation 153 | [redux-saga](https://github.com/TigerHee/shareJS/blob/master/react/05.redux-saga.md) 154 | 155 | > ### 示例项目: 156 | 💯[tiger-react-cli](https://github.com/TigerHee/tiger-react-cli) 157 | -------------------------------------------------------------------------------- /react/05.redux-saga.md: -------------------------------------------------------------------------------- 1 | ## redux-saga 2 | 3 | ### 介绍: 4 | 在 redux-saga 的世界里,所有的任务都通用 yield Effects 来完成(Effect 可以看作是 redux-saga 的任务单元) 5 | 6 | ---- 7 | ### 名词释义: 8 | #### Effect: 9 | 一个 effect 就是一个 Plain Object JavaScript 对象,包含一些将被 saga middleware 执行的指令。使用 redux-saga 提供的工厂函数来创建 effect。一个 Saga 所做的实际上是组合那些所有的 Effect,共同实现所需的控制流。 最简单的例子是直接把 yield 一个接一个地放置来对序列化 yield Effect。你也可以使用熟悉的控制流操作符(if, while, for) 来实现更复杂的控制流。 10 | 11 | ---- 12 | ### 核心API: 13 | - Saga 辅助函数: 14 | - takeEvery 15 | - 允许多个实例同时启动 16 | - takeLatest 17 | - 在任何时刻 takeLatest 只允许执行一个任务,并且这个任务是最后被启动的那个,如果之前已经有一个任务在执行,那之前的这个任务会自动被取消 18 | - Effect Creators 19 | - take(pattern) 20 | - take函数可以理解为监听未来的action,它创建了一个命令对象,告诉middleware等待一个特定的action, Generator会暂停,直到一个与pattern匹配的action被发起,才会继续执行下面的语句,也就是说,take是一个阻塞的 effect。take 让我们通过全面控制 action 观察进程来构建复杂的控制流成为可能。 21 | - put(action) 22 | - put函数是用来发送action的 effect,你可以简单的把它理解成为redux框架中的dispatch函数,当put一个action后,reducer中就会计算新的state并返回,注意: put 也是阻塞 effect 23 | - call(fn, …args) 24 | - call函数你可以把它简单的理解为就是可以调用其他函数的函数,它命令 middleware 来调用fn 函数, args为函数的参数,注意: fn 函数可以是一个 Generator 函数,也可以是一个返回 Promise 的普通函数,call 函数也是阻塞 effect 25 | - select(selector, …args) 26 | - select 函数是用来指示 middleware调用提供的选择器获取Store上的state数据,你也可以简单的把它理解为redux框架中获取store上的 state数据一样的功能 :store.getState() 27 | - fork(fn, …args) 28 | - fork 函数和 call 函数很像,都是用来调用其他函数的,但是fork函数是非阻塞函数,也就是说,程序执行完 yield fork(fn, args) 这一行代码后,会立即接着执行下一行代码语句,而不会等待fn函数返回结果后,在执行下面的语句 29 | - cancel(task) 30 | - 一旦任务被 fork,可以使用 yield cancel(task) 来中止任务执行,使用 yield cancelled() 来检查 Generator 是否已经被取消。取消正在运行的任务。取消接口请求(race也可以实现类似取消功能) 31 | - createSagaMiddleware() 32 | - createSagaMiddleware 函数是用来创建一个 Redux 中间件,将 Sagas 与 Redux Store 链接起来。sagas 中的每个函数都必须返回一个 Generator 对象,middleware 会迭代这个 Generator 并执行所有 yield 后的 Effect(Effect 可以看作是 redux-saga 的任务单元) 33 | - middleware.run(sagas, …args) 34 | - 动态执行sagas,用于applyMiddleware阶段之后执行sagas 35 | 36 | ---- 37 | ### 错误处理: 38 | 我们可以使用熟悉的 try/catch 语法在 Saga 中捕获错误。 39 | 40 | ---- 41 | ### 示例项目: 42 | * 💯[tiger-react-cli](https://github.com/TigerHee/tiger-react-cli) -------------------------------------------------------------------------------- /react/06.React源码学习-createElment.md: -------------------------------------------------------------------------------- 1 | ## createElement 2 | 3 | 平时我们在React组件内书写的JSX代码在编译时会被处理成如下: 4 | 5 | ![Event Loop](../assets/img/react_sc/createEl_babel.png) 6 | 7 | 此效果在 [Babel](https://babeljs.io/repl/) 上实现。 8 | 9 | React.js内是如何引用createElement: 10 | ``` 11 | //首先引入 12 | import { 13 | createElement, 14 | createFactory, 15 | cloneElement, 16 | isValidElement, 17 | } from './ReactElement'; 18 | 19 | //应用 20 | const React = { 21 | // ...other props 22 | createElement: __DEV__ ? createElementWithValidation : createElement, 23 | cloneElement: __DEV__ ? cloneElementWithValidation : cloneElement, 24 | createFactory: __DEV__ ? createFactoryWithValidation : createFactory, 25 | }; 26 | export default React; 27 | ``` 28 | 29 | createElement代码在同级目录下 ReactElement.js 内: 30 | ``` 31 | export function createElement(type, config, children) { 32 | let propName; 33 | // Reserved names are extracted 34 | const props = {}; 35 | let key = null; 36 | let ref = null; 37 | let self = null; 38 | let source = null; 39 | // 处理props 40 | if (config != null) { 41 | if (hasValidRef(config)) { 42 | ref = config.ref; 43 | } 44 | if (hasValidKey(config)) { 45 | key = '' + config.key; 46 | } 47 | self = config.__self === undefined ? null : config.__self; 48 | source = config.__source === undefined ? null : config.__source; 49 | // Remaining properties are added to a new props object 50 | for (propName in config) { 51 | if ( 52 | hasOwnProperty.call(config, propName) && 53 | !RESERVED_PROPS.hasOwnProperty(propName) 54 | ) { 55 | props[propName] = config[propName]; 56 | } 57 | } 58 | } 59 | // 处理多个children 60 | const childrenLength = arguments.length - 2; 61 | if (childrenLength === 1) { 62 | props.children = children; 63 | } else if (childrenLength > 1) { 64 | const childArray = Array(childrenLength); 65 | for (let i = 0; i < childrenLength; i++) { 66 | childArray[i] = arguments[i + 2]; 67 | } 68 | if (__DEV__) { 69 | if (Object.freeze) { 70 | Object.freeze(childArray); 71 | } 72 | } 73 | props.children = childArray; 74 | } 75 | // defaultProps的处理 76 | if (type && type.defaultProps) { 77 | const defaultProps = type.defaultProps; 78 | for (propName in defaultProps) { 79 | if (props[propName] === undefined) { 80 | props[propName] = defaultProps[propName]; 81 | } 82 | } 83 | } 84 | if (__DEV__) { 85 | if (key || ref) { 86 | const displayName = typeof type === 'function' ? type.displayName || type.name || 'Unknown' : type; 87 | if (key) { defineKeyPropWarningGetter(props, displayName); } 88 | if (ref) { defineRefPropWarningGetter(props, displayName); } 89 | } 90 | } 91 | return ReactElement( 92 | type, 93 | key, 94 | ref, 95 | self, 96 | source, 97 | ReactCurrentOwner.current, 98 | props, 99 | ); 100 | } 101 | ``` 102 | ReactElement通过createElement创建,调用该方法需要传入三个参数: 103 | 104 | - type (指代这个ReactElement的类型) 105 | - config (ReactElement的属性) 106 | - children (ReactElement的内容) 107 | 108 | 从如下代码: 109 | ``` 110 | if ( 111 | hasOwnProperty.call(config, propName) && 112 | !RESERVED_PROPS.hasOwnProperty(propName) 113 | ) { 114 | props[propName] = config[propName]; 115 | } 116 | ``` 117 | ``` 118 | //内嵌props 119 | const RESERVED_PROPS = { 120 | key: true, 121 | ref: true, 122 | __self: true, 123 | __source: true, 124 | }; 125 | ``` 126 | 可以看出`key`,`ref`,`__self`,`__source`不会被复到元素的props上去 127 | 128 | 129 | createElement最后返回了一个ReactElement,ReactElement会返回一个对象代码如下: 130 | 131 | ``` 132 | const ReactElement = function(type, key, ref, self, source, owner, props) { 133 | const element = { 134 | // This tag allows us to uniquely identify this as a React Element 135 | $$typeof: REACT_ELEMENT_TYPE, 136 | // Built-in properties that belong on the element 137 | type: type, 138 | key: key, 139 | ref: ref, 140 | props: props, 141 | // Record the component responsible for creating this element. 142 | _owner: owner, 143 | }; 144 | 145 | if (__DEV__) { 146 | element._store = {}; 147 | Object.defineProperty(element._store, 'validated', { 148 | configurable: false, 149 | enumerable: false, 150 | writable: true, 151 | value: false, 152 | }); 153 | Object.defineProperty(element, '_self', { 154 | configurable: false, 155 | enumerable: false, 156 | writable: false, 157 | value: self, 158 | }); 159 | Object.defineProperty(element, '_source', { 160 | configurable: false, 161 | enumerable: false, 162 | writable: false, 163 | value: source, 164 | }); 165 | if (Object.freeze) { 166 | Object.freeze(element.props); 167 | Object.freeze(element); 168 | } 169 | } 170 | return element; 171 | }; 172 | ``` 173 | ReactElement只是一个用来承载信息的容器,他会告诉后续的操作这个节点的以下信息: 174 | - `type`类型,用于判断如何创建节点 175 | - `key`和`ref`这些特殊信息 176 | - `props`新的属性内容 177 | - `$$typeof` 用来标识element是什么类型,用于确定是否属于ReactElement 178 | -------------------------------------------------------------------------------- /react/07.React源码学习-Component.md: -------------------------------------------------------------------------------- 1 | ## Component 2 | 3 | React.js内是如何引用createElement: 4 | ``` 5 | //首先引入 6 | import {Component, PureComponent} from './ReactBaseClasses'; 7 | 8 | //应用 9 | const React = { 10 | // ...other props 11 | Component, 12 | PureComponent, 13 | }; 14 | export default React; 15 | ``` 16 | 17 | Component代码在同级目录下 ReactBaseClasses.js 内: 18 | 19 | ``` 20 | function Component(props, context, updater) { 21 | this.props = props; 22 | this.context = context; 23 | // If a component has string refs, we will assign a different object later. 24 | this.refs = emptyObject; 25 | // We initialize the default updater but the real one gets injected by the 26 | // renderer. 27 | this.updater = updater || ReactNoopUpdateQueue; 28 | } 29 | 30 | Component.prototype.forceUpdate = function(callback) { 31 | this.updater.enqueueForceUpdate(this, callback, 'forceUpdate'); 32 | }; 33 | 34 | ``` 35 | 36 | ##### 发现一个不常见的api: 37 | 38 | `forceUpdate` :强制刷新组件 39 | 40 | 另`PureComponent`就比`Component`多了一个`isPureReactComponent`属性: 41 | 42 | ``` 43 | /** 44 | * Convenience component with default shallow equality check for sCU. 45 | */ 46 | function PureComponent(props, context, updater) { 47 | this.props = props; 48 | this.context = context; 49 | // If a component has string refs, we will assign a different object later. 50 | this.refs = emptyObject; 51 | this.updater = updater || ReactNoopUpdateQueue; 52 | } 53 | 54 | const pureComponentPrototype = (PureComponent.prototype = new ComponentDummy()); 55 | pureComponentPrototype.constructor = PureComponent; 56 | // Avoid an extra prototype jump for these methods. 57 | Object.assign(pureComponentPrototype, Component.prototype); 58 | pureComponentPrototype.isPureReactComponent = true; 59 | 60 | export {Component, PureComponent}; 61 | 62 | ``` 63 | -------------------------------------------------------------------------------- /react/08.React源码学习-FiberRoot与Fiber.md: -------------------------------------------------------------------------------- 1 | ## *React中的数据结构* 2 | 3 | ### FiberRoot 4 | 5 | 什么是 FiberRoot? 6 | 7 | - 整个应用的起点 8 | - 包含应用挂载的目标节点 9 | - 记录整个应用更新过程的各种信息 10 | 11 | 结构如下: 12 | 13 | ``` 14 | type BaseFiberRootProperties = {| 15 | // root节点,render方法接收的第二个参数 16 | containerInfo: any, 17 | // 只有在持久更新中会用到,也就是不支持增量更新的平台,react-dom不会用到 18 | pendingChildren: any, 19 | // 当前应用对应的Fiber对象,是Root Fiber 20 | current: Fiber, 21 | // 一下的优先级是用来区分 22 | // 1) 没有提交(committed)的任务 23 | // 2) 没有提交的挂起任务 24 | // 3) 没有提交的可能被挂起的任务 25 | // 我们选择不追踪每个单独的阻塞登记,为了兼顾性能 26 | // The earliest and latest priority levels that are suspended from committing. 27 | // 最老和新的在提交的时候被挂起的任务 28 | earliestSuspendedTime: ExpirationTime, 29 | latestSuspendedTime: ExpirationTime, 30 | // The earliest and latest priority levels that are not known to be suspended. 31 | // 最老和最新的不确定是否会挂起的优先级(所有任务进来一开始都是这个状态) 32 | earliestPendingTime: ExpirationTime, 33 | latestPendingTime: ExpirationTime, 34 | // The latest priority level that was pinged by a resolved promise and can 35 | // be retried. 36 | // 最新的通过一个promise被reslove并且可以重新尝试的优先级 37 | latestPingedTime: ExpirationTime, 38 | // 如果有错误被抛出并且没有更多的更新存在,我们尝试在处理错误前同步重新从头渲染 39 | // 在`renderRoot`出现无法处理的错误时会被设置为`true` 40 | didError: boolean, 41 | // 正在等待提交的任务的`expirationTime` 42 | pendingCommitExpirationTime: ExpirationTime, 43 | // 已经完成的任务的FiberRoot对象,如果你只有一个Root,那他永远只可能是这个Root对应的Fiber,或者是null 44 | // 在commit阶段只会处理这个值对应的任务 45 | finishedWork: Fiber | null, 46 | // 在任务被挂起的时候通过setTimeout设置的返回内容,用来下一次如果有新的任务挂起时清理还没触发的timeout 47 | timeoutHandle: TimeoutHandle | NoTimeout, 48 | // 顶层context对象,只有主动调用`renderSubtreeIntoContainer`时才会有用 49 | context: Object | null, 50 | pendingContext: Object | null, 51 | // 用来确定第一次渲染的时候是否需要融合 52 | +hydrate: boolean, 53 | // 当前root上剩余的过期时间 54 | // TODO: 提到renderer里面区处理 55 | nextExpirationTimeToWorkOn: ExpirationTime, 56 | // 当前更新对应的过期时间 57 | expirationTime: ExpirationTime, 58 | // List of top-level batches. This list indicates whether a commit should be 59 | // deferred. Also contains completion callbacks. 60 | // TODO: Lift this into the renderer 61 | // 顶层批次(批处理任务?)这个变量指明一个commit是否应该被推迟 62 | // 同时包括完成之后的回调 63 | // 貌似用在测试的时候? 64 | firstBatch: Batch | null, 65 | // root之间关联的链表结构 66 | nextScheduledRoot: FiberRoot | null, 67 | |}; 68 | ``` 69 | 70 | ### Fiber 71 | 72 | 什么是Fiber? 73 | 74 | - 每一个ReactElement对应一个Fiber对象 75 | - 记录节点的各种状态 76 | - 串联整个应用形成树结构 77 | 78 | 结构如下: 79 | ``` 80 | // Fiber对应一个组件需要被处理或者已经处理了,一个组件可以有一个或者多个Fiber 81 | type Fiber = {| 82 | // 标记不同的组件类型 83 | tag: WorkTag, 84 | // ReactElement里面的key 85 | key: null | string, 86 | // ReactElement.type,也就是我们调用`createElement`的第一个参数 87 | elementType: any, 88 | // The resolved function/class/ associated with this fiber. 89 | // 异步组件resolved之后返回的内容,一般是`function`或者`class` 90 | type: any, 91 | // The local state associated with this fiber. 92 | // 跟当前Fiber相关本地状态(比如浏览器环境就是DOM节点) 93 | stateNode: any, 94 | // 指向他在Fiber节点树中的`parent`,用来在处理完这个节点之后向上返回 95 | return: Fiber | null, 96 | // 单链表树结构 97 | // 指向自己的第一个子节点 98 | child: Fiber | null, 99 | // 指向自己的兄弟结构 100 | // 兄弟节点的return指向同一个父节点 101 | sibling: Fiber | null, 102 | index: number, 103 | // ref属性 104 | ref: null | (((handle: mixed) => void) & {_stringRef: ?string}) | RefObject, 105 | // 新的变动带来的新的props 106 | pendingProps: any, 107 | // 上一次渲染完成之后的props 108 | memoizedProps: any, 109 | // 该Fiber对应的组件产生的Update会存放在这个队列里面 110 | updateQueue: UpdateQueue | null, 111 | // 上一次渲染的时候的state 112 | memoizedState: any, 113 | // 一个列表,存放这个Fiber依赖的context 114 | firstContextDependency: ContextDependency | null, 115 | // 用来描述当前Fiber和他子树的`Bitfield` 116 | // 共存的模式表示这个子树是否默认是异步渲染的 117 | // Fiber被创建的时候他会继承父Fiber 118 | // 其他的标识也可以在创建的时候被设置 119 | // 但是在创建之后不应该再被修改,特别是他的子Fiber创建之前 120 | mode: TypeOfMode, 121 | // Effect 122 | // 用来记录Side Effect 123 | effectTag: SideEffectTag, 124 | // 单链表用来快速查找下一个side effect 125 | nextEffect: Fiber | null, 126 | // 子树中第一个side effect 127 | firstEffect: Fiber | null, 128 | // 子树中最后一个side effect 129 | lastEffect: Fiber | null, 130 | // 代表任务在未来的哪个时间点应该被完成 131 | // 不包括他的子树产生的任务 132 | expirationTime: ExpirationTime, 133 | // 快速确定子树中是否有不在等待的变化 134 | childExpirationTime: ExpirationTime, 135 | // 在Fiber树更新的过程中,每个Fiber都会有一个跟其对应的Fiber 136 | // 我们称他为`current <==> workInProgress` 137 | // 在渲染完成之后他们会交换位置 138 | alternate: Fiber | null, 139 | // 下面是调试相关的,收集每个Fiber和子树渲染时间的 140 | actualDuration?: number, 141 | // If the Fiber is currently active in the "render" phase, 142 | // This marks the time at which the work began. 143 | // This field is only set when the enableProfilerTimer flag is enabled. 144 | actualStartTime?: number, 145 | // Duration of the most recent render time for this Fiber. 146 | // This value is not updated when we bailout for memoization purposes. 147 | // This field is only set when the enableProfilerTimer flag is enabled. 148 | selfBaseDuration?: number, 149 | // Sum of base times for all descedents of this Fiber. 150 | // This value bubbles up during the "complete" phase. 151 | // This field is only set when the enableProfilerTimer flag is enabled. 152 | treeBaseDuration?: number, 153 | // Conceptual aliases 154 | // workInProgress : Fiber -> alternate The alternate used for reuse happens 155 | // to be the same as work in progress. 156 | // __DEV__ only 157 | _debugID?: number, 158 | _debugSource?: Source | null, 159 | _debugOwner?: Fiber | null, 160 | _debugIsCurrentlyTiming?: boolean, 161 | |}; 162 | ``` 163 | 164 | ##### 来看看Fiber是如何串联节点的: 165 | 166 | 有如下节点结构: 167 | 168 | ![Event Loop](../assets/img/react_sc/FiberEl.png) 169 | 170 | 被Fiber处理成能高效遍历的树形结构后: 171 | 172 | ![Event Loop](../assets/img/react_sc/FiberTree.jpg) -------------------------------------------------------------------------------- /react/09.React源码学习-update.md: -------------------------------------------------------------------------------- 1 | ## update 2 | 3 | ### 什么是update 4 | 5 | - 用于记录组件状态的改变 6 | - 存放于updateQueue中 7 | - 多个update可以同时存在 8 | 9 | ### 如何创建update 10 | 11 | 源码在 react-reconciler 下的 ReactUpdateQueue.js 内: 12 | 13 | 创建update: 14 | 15 | ``` 16 | export function createUpdate(expirationTime: ExpirationTime): Update<*> { 17 | return { 18 | // 更新的过期时间 19 | expirationTime: expirationTime, 20 | // export const UpdateState = 0; 21 | // export const ReplaceState = 1; 22 | // export const ForceUpdate = 2; 23 | // export const CaptureUpdate = 3; 24 | // 指定更新的类型,值为以上几种 25 | tag: UpdateState, 26 | // 更新内容,比如`setState`接收的第一个参数 27 | payload: null, 28 | // 对应的回调,`setState`,`render`都有 29 | callback: null, 30 | // 指向下一个更新 31 | next: null, 32 | // 指向下一个`side effect` 33 | nextEffect: null, 34 | }; 35 | } 36 | ``` 37 | 38 | 创建updateQueue: 39 | 40 | ``` 41 | export function createUpdateQueue(baseState: State): UpdateQueue { 42 | const queue: UpdateQueue = { 43 | // 每次操作完更新之后的`state` 44 | baseState, 45 | // 队列中的第一个和最后一个`Update` 46 | firstUpdate: null, 47 | lastUpdate: null, 48 | // 第一个和最后一个捕获类型的`Update` 49 | firstCapturedUpdate: null, 50 | lastCapturedUpdate: null, 51 | // 第一个和最后一个`side effect` 52 | firstEffect: null, 53 | lastEffect: null, 54 | // 第一个和最后一个捕获产生的`side effect` 55 | firstCapturedEffect: null, 56 | lastCapturedEffect: null, 57 | }; 58 | return queue; 59 | } 60 | ``` -------------------------------------------------------------------------------- /react/10.React源码学习-expirationTime.md: -------------------------------------------------------------------------------- 1 | ## expirationTime 2 | 3 | React 中有两种类型的ExpirationTime,一个是Interactive的,另一种是普通的异步。Interactive的比如说是由事件触发的,那么他的响应优先级会比较高因为涉及到交互。 4 | 5 | 源码在 react-reconciler 下的 ReactFiberExpirationTime.js 内: 6 | 7 | ``` 8 | const UNIT_SIZE = 10 9 | const MAGIC_NUMBER_OFFSET = 2 10 | 11 | // 1 unit of expiration time represents 10ms. 12 | function msToExpirationTime(ms) { 13 | // Always add an offset so that we don't clash with the magic number for NoWork. 14 | return ((ms / UNIT_SIZE) | 0) + MAGIC_NUMBER_OFFSET 15 | } 16 | 17 | function expirationTimeToMs(expirationTime) { 18 | return (expirationTime - MAGIC_NUMBER_OFFSET) * UNIT_SIZE 19 | } 20 | 21 | function ceiling(num, precision) { 22 | return (((num / precision) | 0) + 1) * precision 23 | } 24 | 25 | function computeExpirationBucket(currentTime, expirationInMs, bucketSizeMs) { 26 | return ( 27 | MAGIC_NUMBER_OFFSET + 28 | ceiling( 29 | currentTime - MAGIC_NUMBER_OFFSET + expirationInMs / UNIT_SIZE, 30 | bucketSizeMs / UNIT_SIZE, 31 | ) 32 | ) 33 | } 34 | 35 | const LOW_PRIORITY_EXPIRATION = 5000 36 | const LOW_PRIORITY_BATCH_SIZE = 250 37 | 38 | function computeAsyncExpiration(currentTime) { 39 | return computeExpirationBucket( 40 | currentTime, 41 | LOW_PRIORITY_EXPIRATION, 42 | LOW_PRIORITY_BATCH_SIZE, 43 | ) 44 | } 45 | 46 | // const HIGH_PRIORITY_EXPIRATION = __DEV__ ? 500 : 150; 47 | const HIGH_PRIORITY_EXPIRATION = 500 48 | const HIGH_PRIORITY_BATCH_SIZE = 100 49 | 50 | function computeInteractiveExpiration(currentTime) { 51 | return computeExpirationBucket( 52 | currentTime, 53 | HIGH_PRIORITY_EXPIRATION, 54 | HIGH_PRIORITY_BATCH_SIZE, 55 | ) 56 | } 57 | ``` 58 | 59 | ceiling函数的`(((num / precision) | 0) + 1) * precision`类似上取整的操作,以及调用ceiling的参数来源是`LOW_PRIORITY_BATCH_SIZE` `HIGH_PRIORITY_BATCH_SIZE`,这么做也许是为了让非常相近的两次更新得到相同的expirationTime,然后在一次更新中完成,相当于一个自动的batchedUpdates。 -------------------------------------------------------------------------------- /react/11.React源码学习-任务调度.md: -------------------------------------------------------------------------------- 1 | ## 任务调度 2 | 3 | 任务调度图解: 4 | 5 | ![Event Loop](../assets/img/react_sc/fiber-scheduler.png) 6 | 7 | 源码在 react-reconciler 下的 ReactFiberScheduler.js 内: 8 | 9 | ##### scheduleWork: 10 | ``` 11 | function scheduleWork(fiber: Fiber, expirationTime: ExpirationTime) { 12 | // 更新Fiber及所有子树的expirationTime, 13 | // 返回FiberRoot 14 | const root = scheduleWorkToRoot(fiber, expirationTime); 15 | if (root === null) { 16 | // 去掉__DEV__代码 17 | return; 18 | } 19 | if ( 20 | !isWorking && 21 | nextRenderExpirationTime !== NoWork && 22 | expirationTime > nextRenderExpirationTime 23 | ) { 24 | // This is an interruption. (Used for performance tracking.) 25 | interruptedBy = fiber; 26 | // 优先执行高优先级的任务 27 | resetStack(); 28 | } 29 | markPendingPriorityLevel(root, expirationTime); 30 | if ( 31 | // If we're in the render phase, we don't need to schedule this root 32 | // for an update, because we'll do it before we exit... 33 | !isWorking || 34 | isCommitting || 35 | // ...unless this is a different root than the one we're rendering. 36 | nextRoot !== root 37 | ) { 38 | const rootExpirationTime = root.expirationTime; 39 | requestWork(root, rootExpirationTime); 40 | } 41 | if (nestedUpdateCount > NESTED_UPDATE_LIMIT) { 42 | // Reset this back to zero so subsequent updates don't throw. 43 | // 防止更新中修改state,无限循环进入更新 44 | nestedUpdateCount = 0; 45 | invariant( 46 | false, 47 | 'Maximum update depth exceeded. This can happen when a ' + 48 | 'component repeatedly calls setState inside ' + 49 | 'componentWillUpdate or componentDidUpdate. React limits ' + 50 | 'the number of nested updates to prevent infinite loops.', 51 | ); 52 | } 53 | } 54 | ``` 55 | 56 | ##### requestWork: 57 | 58 | - 加入到root调度队列 59 | - 判断是否批量更新 60 | - 根据expirationTime判断调度类型 61 | 62 | ``` 63 | function requestWork(root: FiberRoot, expirationTime: ExpirationTime) { 64 | // 处理firstScheduledRoot,lastScheduledRoot,root的expirationTime 65 | addRootToSchedule(root, expirationTime); 66 | if (isRendering) { 67 | // 已经开始 68 | return; 69 | } 70 | if (isBatchingUpdates) { 71 | // Flush work at the end of the batch. 72 | if (isUnbatchingUpdates) { 73 | // ...unless we're inside unbatchedUpdates, in which case we should 74 | // flush it now. 75 | nextFlushedRoot = root; 76 | nextFlushedExpirationTime = Sync; 77 | performWorkOnRoot(root, Sync, false); 78 | } 79 | return; 80 | } 81 | // TODO: Get rid of Sync and use current time? 82 | if (expirationTime === Sync) { 83 | performSyncWork(); 84 | } else { 85 | scheduleCallbackWithExpirationTime(root, expirationTime); 86 | } 87 | } 88 | ``` 89 | 90 | setState 是同步的还是异步的? 91 | 92 | setState本身的方法调用是同步的,但是调用setState并不标志着React的state立马就更新了,这个更新是要根据我们当前的执行环境的上文来判断的如果处于`isBatchingUpdates`环境下不会同步更新的,另还有异步更新调度也不会同步更新。 93 | 94 | ##### scheduler包(被提到与react-reconciler同级的目录): 95 | 96 | - 维护时间片 97 | - 模拟requestIdleCallback(等浏览器把要做的事做完后来调取回调) 98 | - 调度列表和超时判断 99 | 100 | 主要用到的方法: 101 | > 1 102 | 103 | ``` 104 | function scheduleCallbackWithExpirationTime(root: FiberRoot, expirationTime: ExpirationTime,){} 105 | ``` 106 | 107 | 异步进行root任务调度就是通过这个方法来做的,这里最主要的就是调用了scheduler的scheduleDeferredCallback方法(在scheduler包中是scheduleWork) 108 | 109 | 传入的的是回调函数performAsyncWork,以及一个包含timeout超时事件的对象 110 | 111 | > 2 112 | 113 | ``` 114 | function unstable_scheduleCallback(callback, deprecated_options){} 115 | ``` 116 | 117 | 创建一个调度节点newNode,并按照timoutAt的顺序加入到CallbackNode链表,调用ensureHostCallbackIsScheduled 118 | 119 | 这里面的expirationTime是调用时传入的timeoutAt加上当前时间形成的过期时间。 120 | 121 | > 3 122 | 123 | ``` 124 | function ensureHostCallbackIsScheduled(){} 125 | ``` 126 | 127 | 如果已经在调用回调了,就 return,因为本来就会继续调用下去,isExecutingCallback在flushWork的时候会被修改为true 128 | 129 | 如果isHostCallbackScheduled为false,也就是还没开始调度,那么设为true,如果已经开始了,就直接取消,因为顺序可能变了。 130 | 131 | 调用requestHostCallback开始调度 132 | 133 | > 4 134 | 135 | ``` 136 | requestHostCallback = function(callback, absoluteTimeout){} 137 | ``` 138 | 139 | 开始进入调度,设置调度的内容,用scheduledHostCallback和timeoutTime这两个全局变量记录回调函数和对应的过期时间 140 | 141 | 调用requestAnimationFrameWithTimeout,其实就是调用requestAnimationFrame在加上设置了一个100ms的定时器,防止requestAnimationFrame太久不触发。 142 | 143 | 调用回调animtionTick并设置isAnimationFrameScheduled全局变量为true 144 | 145 | > 5 146 | 147 | ``` 148 | var animationTick = function(rafTime) {} 149 | ``` 150 | 151 | 只要scheduledHostCallback还在就继续调要requestAnimationFrameWithTimeout因为这一帧渲染完了可能队列还没情况,本身也是要进入再次调用的,这边就省去了requestHostCallback在次调用的必要性 152 | 153 | 接下去一段代码是用来计算相隔的requestAnimationFrame的时差的,这个时差如果连续两次都小鱼当前的activeFrameTime,说明平台的帧率是很高的,这种情况下会动态得缩小帧时间。 154 | 155 | 最后更新frameDeadline,然后如果没有触发idleTick则发送消息 156 | 157 | > 6 158 | 159 | ``` 160 | window.addEventListener('message', idleTick, false) 161 | 162 | var idleTick = function(event) {} 163 | ``` 164 | 165 | 首先判断postMessage是不是自己的,不是直接返回 166 | 167 | 清空scheduledHostCallback和timeoutTime 168 | 169 | 获取当前时间,对比frameDeadline,查看是否已经超时了,如果超时了,判断一下任务callback的过期时间有没有到,如果没有到,则重新对这个callback进行一次调度,然后返回。如果到了,则设置didTimeout为true 170 | 171 | 接下去就是调用callback了,这里设置isFlushingHostCallback全局变量为true代表正在执行。并且调用callback也就是flushWork并传入didTimeout 172 | 173 | > 7 174 | 175 | ``` 176 | function flushWork(didTimeout) {} 177 | ``` 178 | 179 | 先设置isExecutingCallback为true,代表正在调用callback 180 | 181 | 设置deadlineObject.didTimeout,在 React 业务中可以用来判断任务是否超时 182 | 183 | 如果didTimeout,会一次从firstCallbackNode向后一直执行,知道第一个没过期的任务 184 | 185 | 如果没有超时,则依此执行第一个callback,知道帧时间结束为止 186 | 187 | 最后清理变量,如果任务没有执行完,则再次调用ensureHostCallbackIsScheduled进入调度 188 | 189 | 顺便把Immedia优先级的任务都调用一遍。 190 | 191 | > 8 192 | 193 | ``` 194 | function flushFirstCallback() {} 195 | ``` 196 | 197 | 如果当前队列中只有一个回调,清空队列 198 | 199 | 调用回调并传入deadline对象,里面有timeRemaining方法通过frameDeadline - now()来判断是否帧时间已经到了 200 | 201 | 如果回调有返回内容,把这个返回加入到回调队列 202 | 203 | ##### performWork: 204 | 205 | `performWork`通过两种方式调用: 206 | 207 | - performAsyncWork 异步方式 208 | 209 | 异步情况给performWork设置的minExpirationTime是NoWork,并且会判断dl.didTimeout,这个值是指任务的expirationTime是否已经超时,如果超时了,则直接设置newExpirationTimeToWorkOn为当前时间,表示这个任务直接执行就行了,不需要判断是否超过了帧时间 210 | 211 | - performSyncWork 同步方式 212 | 213 | 同步方式久比较简单了,设置minExpirationTime为Sync也就是1 214 | 215 | ##### performWorkOnRoot 216 | 217 | 这里也分为同步和异步两种情况,但是这两种情况的区别其实非常小。 218 | 219 | 首先是一个参数的区别,isYieldy在同步的情况下是false,而在异步情况下是true。这个参数顾名思义就是是否可以中断,那么这个区别也就很好理解了。 220 | 221 | 第二个区别就是在renderRoot之后判断一下shouldYeild,如果时间片已经用完,则不直接completeRoot,而是等到一下次requestIdleCallback之后再执行。 222 | 223 | `renderRoot` 和 `completeRoot` 分别对应两个阶段: 224 | 225 | - 渲染阶段 226 | - 提交阶段 227 | 228 | 渲染阶段可以被打断,而提交阶段不能 229 | 230 | ##### findHighestPriorityRoot 231 | 232 | 一般情况下我们的 React 应用只会有一个root,所以这里的大部分逻辑其实都不是常见情况。 233 | 234 | 循环`firstScheduledRoot => lastScheduledRoot`,`remainingExpirationTime`是`root.expirationTime`,也就是最早的过期时间。 235 | 236 | 如果他是`NoWork`说明他已经没有任务了,从链表中删除。 237 | 238 | 从剩下的中找到`expirationTime`最小的也就是优先级最高的`root`然后把他赋值给`nextFlushedRoot`并把他的`expirationTime`赋值给`nextFlushedExpirationTime`这两个公共变量。 239 | 240 | 一般来说会直接执行下面这个逻辑 241 | 242 | ``` 243 | if (root === root.nextScheduledRoot) { 244 | // This is the only root in the list. 245 | root.nextScheduledRoot = null; 246 | firstScheduledRoot = lastScheduledRoot = null; 247 | break; 248 | } 249 | ``` 250 | 251 | ##### renderRoot 252 | 253 | 首先是一个判断是否需要初始化变量的判断 254 | ``` 255 | if ( 256 | expirationTime !== nextRenderExpirationTime || 257 | root !== nextRoot || 258 | nextUnitOfWork === null 259 | ) { 260 | // Reset the stack and start working from the root. 261 | resetStack() 262 | nextRoot = root 263 | nextRenderExpirationTime = expirationTime 264 | nextUnitOfWork = createWorkInProgress( 265 | nextRoot.current, 266 | null, 267 | nextRenderExpirationTime, 268 | ) 269 | root.pendingCommitExpirationTime = NoWork 270 | } 271 | ``` 272 | 他判断的情况是是否有新的更新进来了。假设这种情况:上一个任务因为时间片用完了而中断了,这个时候nextUnitOfWork是有工作的,这时候如果下一个requestIdleCallback进来了,中途没有新的任务进来,那么这些全局变量都没有变过,root的nextExpirationTimeToWorkOn肯定也没有变化,那么代表是继续上一次的任务。而如果有新的更新进来,则势必nextExpirationTimeToWorkOn或者root会变化,那么肯定需要重置变量 273 | 274 | `resetStack`如果是被中断的情况,会推出`context`栈 275 | 276 | 然后就进入整体,调用workLoop 277 | 278 | ``` 279 | function workLoop(isYieldy) { 280 | if (!isYieldy) { 281 | // Flush work without yielding 282 | while (nextUnitOfWork !== null) { 283 | nextUnitOfWork = performUnitOfWork(nextUnitOfWork) 284 | } 285 | } else { 286 | // Flush asynchronous work until the deadline runs out of time. 287 | while (nextUnitOfWork !== null && !shouldYield()) { 288 | nextUnitOfWork = performUnitOfWork(nextUnitOfWork) 289 | } 290 | } 291 | } 292 | ``` 293 | workLoop逻辑很简单的,只是判断是否需要继续调用performUnitOfWork 294 | 295 | 在workLoop执行完之后,就进入收尾阶段了。 296 | 297 | 首先如果didFatal为true,代表有一个无法处理的错误,直接调用onFatal,不commit 298 | ``` 299 | function onFatal(root) { 300 | root.finishedWork = null 301 | } 302 | ``` 303 | 304 | 如果nextUnitOfWork !== null,代表任务没有执行完,是yield了,执行onYield 305 | 306 | ``` 307 | function onYield(root) { 308 | root.finishedWork = null 309 | } 310 | ``` 311 | 312 | 如果以上都没有,说明已经complete整棵树了,如果nextRenderDidError代表有捕获到可处理的错误 313 | 314 | 这时候先判断是否有优先级更低的任务,有的话把当前的渲染时间设置进suspendTime,同时调用onSuspend 315 | 316 | 如果不符合再判断是否帧时间超时,如果没有超时并且没有root.didError,并且把root.expirationTime设置为Sync,然后调用onSuspend。 317 | 318 | 需要注意的是,他们调用onSuspend最后一个参数传递的都是-1,看onSuspend的逻辑可以发现其实什么都不做。什么都不做代表着,他们不会设置root.finishedWork,那么返回到上一层的performWorkOnRoot的时候 319 | 320 | ``` 321 | finishedWork = root.finishedWork 322 | if (finishedWork !== null) { 323 | if (!shouldYield()) { 324 | // Still time left. Commit the root. 325 | completeRoot(root, finishedWork, expirationTime) 326 | } else { 327 | root.finishedWork = finishedWork 328 | } 329 | } 330 | ``` 331 | 并不会执行completeRoot也就不会commit,会再返回到performWork找下一个root 332 | ``` 333 | function onSuspend( 334 | root: FiberRoot, 335 | finishedWork: Fiber, 336 | suspendedExpirationTime: ExpirationTime, 337 | rootExpirationTime: ExpirationTime, 338 | msUntilTimeout: number, 339 | ): void { 340 | root.expirationTime = rootExpirationTime 341 | if (enableSuspense && msUntilTimeout === 0 && !shouldYield()) { 342 | // Don't wait an additional tick. Commit the tree immediately. 343 | root.pendingCommitExpirationTime = suspendedExpirationTime 344 | root.finishedWork = finishedWork 345 | } else if (msUntilTimeout > 0) { 346 | // Wait `msUntilTimeout` milliseconds before committing. 347 | root.timeoutHandle = scheduleTimeout( 348 | onTimeout.bind(null, root, finishedWork, suspendedExpirationTime), 349 | msUntilTimeout, 350 | ) 351 | } 352 | } 353 | 354 | function onTimeout(root, finishedWork, suspendedExpirationTime) { 355 | if (enableSuspense) { 356 | // The root timed out. Commit it. 357 | root.pendingCommitExpirationTime = suspendedExpirationTime 358 | root.finishedWork = finishedWork 359 | // Read the current time before entering the commit phase. We can be 360 | // certain this won't cause tearing related to batching of event updates 361 | // because we're at the top of a timer event. 362 | recomputeCurrentRendererTime() 363 | currentSchedulerTime = currentRendererTime 364 | 365 | if (enableSchedulerTracing) { 366 | // Don't update pending interaction counts for suspense timeouts, 367 | // Because we know we still need to do more work in this case. 368 | suspenseDidTimeout = true 369 | flushRoot(root, suspendedExpirationTime) 370 | suspenseDidTimeout = false 371 | } else { 372 | flushRoot(root, suspendedExpirationTime) 373 | } 374 | } 375 | } 376 | ``` 377 | 其中scheduleTimeout是不同平台的setTimout 378 | 379 | 最后一个判断就是真正的挂起任务了,也就是suquense的情况,其实做的事情跟上面两个差不多,唯一的区别是调用onSuspend的时候最后一个参数肯定是大于等于零的。代表着他是立刻就要commit还是在一个timeout之后再commit。因为我们可以看到onTimeout最后是flushRoot,就是以Sync的方式调用performWork 380 | 381 | 如果以上逻辑都没有,那么直接调用onComplete 382 | ``` 383 | function onComplete( 384 | root: FiberRoot, 385 | finishedWork: Fiber, 386 | expirationTime: ExpirationTime, 387 | ) { 388 | root.pendingCommitExpirationTime = expirationTime 389 | root.finishedWork = finishedWork 390 | } 391 | ``` -------------------------------------------------------------------------------- /react/12.React源码学习-beginWork.md: -------------------------------------------------------------------------------- 1 | ## beginWork 2 | 3 | 执行对整棵树的每一个节点进行更新的操作(performUnitOfWork内调用beginWork) 4 | 5 | 源码在 react-reconciler 下的 ReactFiberBeginWork.js 内: 6 | 7 | `beginWork()`方法里通过 `switch (workInProgress.tag)`对不同的组件做不同的更新处理: 8 | 9 | 下面看看不同组件的更新: 10 | 11 | 出现频率很高的一个判断 `current === null` 用于判断组件是否是第一次渲染。 12 | 13 | ### 1.FunctionComponent 14 | 15 | ``` 16 | function updateFunctionComponent( 17 | current, 18 | workInProgress, 19 | Component, 20 | nextProps: any, 21 | renderExpirationTime, 22 | ) { 23 | // 此处去掉__DEV__代码 24 | const unmaskedContext = getUnmaskedContext(workInProgress, Component, true); 25 | const context = getMaskedContext(workInProgress, unmaskedContext); 26 | let nextChildren; 27 | prepareToReadContext(workInProgress, renderExpirationTime); 28 | prepareToUseHooks(current, workInProgress, renderExpirationTime); 29 | if (__DEV__) { 30 | // 此处去掉__DEV__代码 31 | } else { 32 | nextChildren = Component(nextProps, context); 33 | } 34 | nextChildren = finishHooks(Component, nextProps, nextChildren, context); 35 | // React DevTools reads this flag. 36 | workInProgress.effectTag |= PerformedWork; 37 | reconcileChildren( 38 | current, 39 | workInProgress, 40 | nextChildren, 41 | renderExpirationTime, 42 | ); 43 | return workInProgress.child; 44 | } 45 | ``` 46 | 调用`Component`时传入了两个值`nextProps`和`context`, 那么函数组件可以直接通过第二个参数拿到`context`,官方文档没有说明过这点。 47 | 48 | ### 2.ClassComponent 49 | 50 | ``` 51 | function updateClassComponent( 52 | current: Fiber | null, 53 | workInProgress: Fiber, 54 | Component: any, 55 | nextProps, 56 | renderExpirationTime: ExpirationTime, 57 | ) { 58 | if (__DEV__) { 59 | // 去掉__DEV__代码 60 | } 61 | // Push context providers early to prevent context stack mismatches. 62 | // During mounting we don't know the child context yet as the instance doesn't exist. 63 | // We will invalidate the child context in finishClassComponent() right after rendering. 64 | let hasContext; 65 | if (isLegacyContextProvider(Component)) { 66 | hasContext = true; 67 | pushLegacyContextProvider(workInProgress); 68 | } else { 69 | hasContext = false; 70 | } 71 | prepareToReadContext(workInProgress, renderExpirationTime); 72 | 73 | const instance = workInProgress.stateNode; 74 | let shouldUpdate; 75 | if (instance === null) { 76 | if (current !== null) { 77 | // An class component without an instance only mounts if it suspended 78 | // inside a non- concurrent tree, in an inconsistent state. We want to 79 | // tree it like a new mount, even though an empty version of it already 80 | // committed. Disconnect the alternate pointers. 81 | current.alternate = null; 82 | workInProgress.alternate = null; 83 | // Since this is conceptually a new fiber, schedule a Placement effect 84 | workInProgress.effectTag |= Placement; 85 | } 86 | // In the initial pass we might need to construct the instance. 87 | constructClassInstance( 88 | workInProgress, 89 | Component, 90 | nextProps, 91 | renderExpirationTime, 92 | ); 93 | mountClassInstance( 94 | workInProgress, 95 | Component, 96 | nextProps, 97 | renderExpirationTime, 98 | ); 99 | shouldUpdate = true; 100 | } else if (current === null) { 101 | // In a resume, we'll already have an instance we can reuse. 102 | shouldUpdate = resumeMountClassInstance( 103 | workInProgress, 104 | Component, 105 | nextProps, 106 | renderExpirationTime, 107 | ); 108 | } else { 109 | shouldUpdate = updateClassInstance( 110 | current, 111 | workInProgress, 112 | Component, 113 | nextProps, 114 | renderExpirationTime, 115 | ); 116 | } 117 | const nextUnitOfWork = finishClassComponent( 118 | current, 119 | workInProgress, 120 | Component, 121 | shouldUpdate, 122 | hasContext, 123 | renderExpirationTime, 124 | ); 125 | if (__DEV__) { 126 | // 去掉__DEV__代码 127 | } 128 | return nextUnitOfWork; 129 | } 130 | ``` 131 | 132 | 开始是对context的操作,因为ClassComponent可以成为context provider。 133 | 134 | current === null只会出现在第一次渲染的时候,因为会先创建workInProcess,在渲染结束之后才会把workInProcess拷贝成current,代表着第一次渲染结束。而后面也会出现根据current === null来判断是否需要调用componentDidMount的代码 135 | 136 | 在这里如果current === null就行要进行实例的构建工作,如果不是,直接updateClassInstance 137 | 138 | 如果是还要判断实例是否已经创建workInProgress.stateNode === null,如果是的话要创建这个实例,通过constructClassInstance(代码在同级的ReactFiberClassComponent.js内,这个方法调用adoptClassInstance给 ClassComponent 的实例挂载一个updater对象,里面包含我们常用的方法:forceUpdate, replaceState, setState),并且挂载实例mountClassInstance(初始化 props、state 等实例属性,如果有updateQueue就更新之,一般来说第一次渲染是没有的。) 139 | 140 | 如果已经有current则调用updateClassInstance 141 | 142 | 最后调用finishClassComponent 143 | 144 | ### 3.IndeterminateComponent 145 | 146 | FunctionalComponent在含有render方法时会被当做ClassComponent来处理 147 | 148 | ``` 149 | export default ()=>{ 150 | return { 151 | componentDidMount() { 152 | console.log('Indeterminate') 153 | }, 154 | render() { 155 | return
    Indeterminate
    156 | }, 157 | } 158 | } 159 | ``` 160 | ### 4.HostComponent 161 | 162 | 原生的html标签(completeWork阶段就是从HostComponent开始逆着Fiber输往回return Fiber,并在HostComponent上进行虚拟DOM的diff判断比较props) 163 | 164 | ### reconcileChildren 165 | 166 | *不同类型的组件更新最后都会调用此方法* 167 | 168 | - 根据props.children生成Fiber子树 169 | - 判断Fiber对象是否可以复用 170 | - 列表根据key优化 171 | 172 | ``` 173 | export function reconcileChildren( 174 | current: Fiber | null, 175 | workInProgress: Fiber, 176 | nextChildren: any, 177 | renderExpirationTime: ExpirationTime, 178 | ) { 179 | if (current === null) { 180 | workInProgress.child = mountChildFibers( 181 | workInProgress, 182 | null, 183 | nextChildren, 184 | renderExpirationTime, 185 | ); 186 | } else { 187 | workInProgress.child = reconcileChildFibers( 188 | workInProgress, 189 | current.child, 190 | nextChildren, 191 | renderExpirationTime, 192 | ); 193 | } 194 | } 195 | ``` 196 | 查看上面两种不同调用的方法引入: 197 | ``` 198 | import { 199 | mountChildFibers, 200 | reconcileChildFibers, 201 | cloneChildFibers, 202 | } from './ReactChildFiber'; 203 | ``` 204 | 然后去ReactChildFiber.js内查看方法定义: 205 | mountChildFibers和reconcileChildFibers方法是一样的,唯一的区别是生成这个方法的时候的一个参数不同 206 | ``` 207 | export const reconcileChildFibers = ChildReconciler(true); 208 | export const mountChildFibers = ChildReconciler(false); 209 | ``` 210 | 这个参数叫shouldTrackSideEffects,他的作用是判断是否要增加一些effectTag,主要是用来优化初次渲染的 211 | ``` 212 | if (shouldTrackSideEffects && newFiber.alternate === null) { 213 | newFiber.effectTag = Placement 214 | } 215 | 216 | ``` 217 | ChildReconciler最终调用的是reconcileChildFibers: 218 | ``` 219 | function ChildReconciler(shouldTrackSideEffects) { 220 | // 省略其他代码 221 | function reconcileChildFibers( 222 | returnFiber: Fiber, 223 | currentFirstChild: Fiber | null, 224 | newChild: any, 225 | expirationTime: ExpirationTime, 226 | ): Fiber | null {} 227 | return reconcileChildFibers; 228 | } 229 | ``` 230 | reconcileChildFibers会根据newChild的不同类型进行对应的处理,最终的返回是当前节点的第一个孩子节点,会在performUnitWork中 return 并赋值给nextUnitOfWork。 231 | 232 | children的合法类型: 233 | 234 | - ReactElement,通过createElement和ReactDOM.createPortal创建,$$typeof不同 235 | - string或者number,
    abc
    中div的children就是"abc" 236 | - [// renderAble]数组,每一项都可以是其他合法类型,不能嵌套 237 | - Iterator,跟数组类似,只是遍历方式不同 238 | - React.ConcurrentMode这些内置组件,最终会转换成ReactElement,不同的是ReactElement.type 239 | - reconcileSingleElement & reconcileSinglePortal & reconcileSingleTextNode 240 | - reconcileChildrenArray & reconcileChildrenArray -------------------------------------------------------------------------------- /react/13.React源码学习-commit.md: -------------------------------------------------------------------------------- 1 | ## commit 2 | 3 | 首先要标记优先级,因为有一部分优先级的任务已经被提交了,所以需要清楚一些相关的优先级。被提交的任务应该是: 4 | 5 | - 子树中优先级最高的任务 6 | - 或者外部指定的优先级(flushSync或者retry) 7 | 8 | 如果 `RootFiber` 本身也有副作用(一般只有第一次),那么他本身也要加到 `effect` 链上,放在最后。接下去是三个提交操作,分别是: 9 | 10 | - 提交Snapshot 11 | - 提交HostComponent的 side effect` 12 | - 提交所有组件的生命周期 13 | 14 | ``` 15 | function commitRoot(root: FiberRoot, finishedWork: Fiber): void { 16 | isWorking = true 17 | isCommitting = true 18 | startCommitTimer() 19 | 20 | invariant( 21 | root.current !== finishedWork, 22 | 'Cannot commit the same tree as before. This is probably a bug ' + 23 | 'related to the return field. This error is likely caused by a bug ' + 24 | 'in React. Please file an issue.', 25 | ) 26 | const committedExpirationTime = root.pendingCommitExpirationTime 27 | invariant( 28 | committedExpirationTime !== NoWork, 29 | 'Cannot commit an incomplete root. This error is likely caused by a ' + 30 | 'bug in React. Please file an issue.', 31 | ) 32 | root.pendingCommitExpirationTime = NoWork 33 | 34 | const updateExpirationTimeBeforeCommit = finishedWork.expirationTime 35 | const childExpirationTimeBeforeCommit = finishedWork.childExpirationTime 36 | const earliestRemainingTimeBeforeCommit = 37 | updateExpirationTimeBeforeCommit === NoWork || 38 | (childExpirationTimeBeforeCommit !== NoWork && 39 | childExpirationTimeBeforeCommit < updateExpirationTimeBeforeCommit) 40 | ? childExpirationTimeBeforeCommit 41 | : updateExpirationTimeBeforeCommit 42 | markCommittedPriorityLevels(root, earliestRemainingTimeBeforeCommit) 43 | 44 | let prevInteractions: Set = (null: any) 45 | 46 | // Reset this to null before calling lifecycles 47 | ReactCurrentOwner.current = null 48 | 49 | let firstEffect 50 | if (finishedWork.effectTag > PerformedWork) { 51 | // A fiber's effect list consists only of its children, not itself. So if 52 | // the root has an effect, we need to add it to the end of the list. The 53 | // resulting list is the set that would belong to the root's parent, if 54 | // it had one; that is, all the effects in the tree including the root. 55 | if (finishedWork.lastEffect !== null) { 56 | finishedWork.lastEffect.nextEffect = finishedWork 57 | firstEffect = finishedWork.firstEffect 58 | } else { 59 | firstEffect = finishedWork 60 | } 61 | } else { 62 | // There is no effect on the root. 63 | firstEffect = finishedWork.firstEffect 64 | } 65 | 66 | prepareForCommit(root.containerInfo) 67 | 68 | // Invoke instances of getSnapshotBeforeUpdate before mutation. 69 | nextEffect = firstEffect 70 | startCommitSnapshotEffectsTimer() 71 | while (nextEffect !== null) { 72 | let didError = false 73 | let error 74 | if (__DEV__) { 75 | invokeGuardedCallback(null, commitBeforeMutationLifecycles, null) 76 | if (hasCaughtError()) { 77 | didError = true 78 | error = clearCaughtError() 79 | } 80 | } else { 81 | try { 82 | commitBeforeMutationLifecycles() 83 | } catch (e) { 84 | didError = true 85 | error = e 86 | } 87 | } 88 | if (didError) { 89 | invariant( 90 | nextEffect !== null, 91 | 'Should have next effect. This error is likely caused by a bug ' + 92 | 'in React. Please file an issue.', 93 | ) 94 | captureCommitPhaseError(nextEffect, error) 95 | // Clean-up 96 | if (nextEffect !== null) { 97 | nextEffect = nextEffect.nextEffect 98 | } 99 | } 100 | } 101 | stopCommitSnapshotEffectsTimer() 102 | 103 | nextEffect = firstEffect 104 | startCommitHostEffectsTimer() 105 | while (nextEffect !== null) { 106 | let didError = false 107 | let error 108 | if (__DEV__) { 109 | invokeGuardedCallback(null, commitAllHostEffects, null) 110 | if (hasCaughtError()) { 111 | didError = true 112 | error = clearCaughtError() 113 | } 114 | } else { 115 | try { 116 | commitAllHostEffects() 117 | } catch (e) { 118 | didError = true 119 | error = e 120 | } 121 | } 122 | if (didError) { 123 | invariant( 124 | nextEffect !== null, 125 | 'Should have next effect. This error is likely caused by a bug ' + 126 | 'in React. Please file an issue.', 127 | ) 128 | captureCommitPhaseError(nextEffect, error) 129 | // Clean-up 130 | if (nextEffect !== null) { 131 | nextEffect = nextEffect.nextEffect 132 | } 133 | } 134 | } 135 | stopCommitHostEffectsTimer() 136 | 137 | resetAfterCommit(root.containerInfo) 138 | 139 | root.current = finishedWork 140 | 141 | nextEffect = firstEffect 142 | startCommitLifeCyclesTimer() 143 | while (nextEffect !== null) { 144 | let didError = false 145 | let error 146 | if (__DEV__) { 147 | invokeGuardedCallback( 148 | null, 149 | commitAllLifeCycles, 150 | null, 151 | root, 152 | committedExpirationTime, 153 | ) 154 | if (hasCaughtError()) { 155 | didError = true 156 | error = clearCaughtError() 157 | } 158 | } else { 159 | try { 160 | commitAllLifeCycles(root, committedExpirationTime) 161 | } catch (e) { 162 | didError = true 163 | error = e 164 | } 165 | } 166 | if (didError) { 167 | invariant( 168 | nextEffect !== null, 169 | 'Should have next effect. This error is likely caused by a bug ' + 170 | 'in React. Please file an issue.', 171 | ) 172 | captureCommitPhaseError(nextEffect, error) 173 | if (nextEffect !== null) { 174 | nextEffect = nextEffect.nextEffect 175 | } 176 | } 177 | } 178 | 179 | isCommitting = false 180 | isWorking = false 181 | stopCommitLifeCyclesTimer() 182 | stopCommitTimer() 183 | onCommitRoot(finishedWork.stateNode) 184 | 185 | const updateExpirationTimeAfterCommit = finishedWork.expirationTime 186 | const childExpirationTimeAfterCommit = finishedWork.childExpirationTime 187 | const earliestRemainingTimeAfterCommit = 188 | updateExpirationTimeAfterCommit === NoWork || 189 | (childExpirationTimeAfterCommit !== NoWork && 190 | childExpirationTimeAfterCommit < updateExpirationTimeAfterCommit) 191 | ? childExpirationTimeAfterCommit 192 | : updateExpirationTimeAfterCommit 193 | if (earliestRemainingTimeAfterCommit === NoWork) { 194 | // If there's no remaining work, we can clear the set of already failed 195 | // error boundaries. 196 | legacyErrorBoundariesThatAlreadyFailed = null 197 | } 198 | onCommit(root, earliestRemainingTimeAfterCommit) 199 | 200 | // profiler 相关 201 | } 202 | ``` -------------------------------------------------------------------------------- /react/14.umi2+Antd实现动态主题色切换实践.md: -------------------------------------------------------------------------------- 1 | 前言: 2 | --- 3 | 4 | > 新项目设计稿评审后准备开工,老大说:考虑一下需要支持主题切换,我说:好的 5 | 6 | 开始: 7 | --- 8 | 9 | 项目框架选型:umi + Antd 10 | 11 | 算是比较成熟的套餐了,主题切换应该是小case的需求,于是: 12 | 13 | 1. 去antd Design官网看,只能定制主题,没介绍怎么支持动态切换 14 | 2. 去umi文档查看,只有设置theme参数(看起来和定制主题差不多的意思) 15 | 3. 不死心去gayhub看isuues,于是: 16 | 17 | ![](../assets/img/css/cssvar1.png) 18 | ![](../assets/img/css/cssvar2.png) 19 | ![](../assets/img/css/cssvar3.png) 20 | 21 | 开始在gayhub乱逛,终于发现好东西: 22 | 23 | [umi-plugin-antd-theme](https://link.zhihu.com/?target=https%3A//github.com/chenshuai2144/umi-plugin-antd-theme) 24 | 25 | 虽然插件应该是为[Ant Design Pro](https://link.zhihu.com/?target=https%3A//pro.ant.design/docs/dynamic-theme-cn)打造的但是Antd是一家,拿来开搞按照文档里说明的配置项目: 26 | 27 | ```text 28 | yarn add umi-plugin-antd-theme -D 29 | ``` 30 | 31 | 修改.umirc.js: 32 | 33 | ```js 34 | plugins: [ 35 | ...other配置, 36 | [ 37 | 'umi-plugin-antd-theme', 38 | { 39 | theme: [ 40 | { 41 | key: 'normal', 42 | fileName: 'normal.css', 43 | modifyVars: { 44 | '@primary-color': '#0FCD8C', 45 | '@btn-primary-color': '#01081E', 46 | }, 47 | }, 48 | { 49 | key: 'dark', 50 | fileName: 'dark.css', 51 | modifyVars: { 52 | '@primary-color': '#ff6100', 53 | '@btn-primary-color': '#ff6100', 54 | }, 55 | }, 56 | ], 57 | }, 58 | ], 59 | ], 60 | alias:{ 61 | theme: `${__dirname}/src/theme/`, 62 | } 63 | 64 | ``` 65 | 66 | src目录下面新增theme目录: 67 | 68 | 1.config.less( 并在global.less中引入 ): 69 | 70 | ```css 71 | .body-wrap-theme-normal { 72 | // theme1下的全局变量在此定义 73 | --font-color: #000000; 74 | --bg-color: #011313; 75 | } 76 | 77 | .body-wrap-theme-dark { 78 | // theme2下的全局变量在此定义 79 | --font-color: #ffffff; 80 | --bg-color: #ffffff; 81 | } 82 | ``` 83 | 84 | 2.index.less( react组件的样式文件内引入此变量文件即可使用less变量 ): 85 | 86 | ```less 87 | @color: var(--font-color); 88 | @background: var(--bg-color); 89 | ``` 90 | 91 | 3.设置切换主题的方法: 92 | 93 | ```js 94 | export const setTheme = (theme = 'normal') => { 95 | console.log('== setTheme ==', theme) 96 | let styleLink = document.getElementById('theme-style'); 97 | let body = document.querySelector('body'); 98 | let hrefSrc = '/theme/normal.css'; 99 | let bodyClsName = 'body-wrap-theme-normal'; 100 | 101 | if (theme === 'normal') { 102 | hrefSrc = '/theme/normal.css'; 103 | bodyClsName = 'body-wrap-theme-normal'; 104 | } else if (theme === 'dark') { 105 | hrefSrc = '/theme/dark.css'; 106 | bodyClsName = 'body-wrap-theme-dark'; 107 | } 108 | 109 | if (styleLink) { 110 | styleLink.href = hrefSrc; 111 | body.className = bodyClsName; 112 | } else { 113 | styleLink = document.createElement('link'); 114 | styleLink.type = 'text/css'; 115 | styleLink.rel = 'stylesheet'; 116 | styleLink.id = 'theme-style'; 117 | styleLink.href = hrefSrc; 118 | body.className = bodyClsName; 119 | document.body.append(styleLink); 120 | } 121 | } 122 | 123 | ``` 124 | 125 | 然后把项目跑起来在chrome里调试调用setTheme, 嗯~~达到了想要的效果, 但是要兼容ie10,去查查css变量的兼容性,一首凉凉: 126 | 127 | ![](../assets/img/css/cssvar4.png) 128 | 129 | _还能怎么办,处理兼容咯,2333333333333_ 130 | 131 | **找找大佬们的插件:** 132 | 133 | 1. [postcss-custom-properties](https://link.zhihu.com/?target=https%3A//github.com/postcss/postcss-custom-properties) 134 | 135 | 尝试后发现只能处理设置在 :root{} 下面的css变量并且不能动态变更,告辞! 136 | 137 | 2.[postcss-css-variables](https://link.zhihu.com/?target=https%3A//github.com/MadLittleMods/postcss-css-variables) 138 | 139 | 看到作者在README里面diss了插件1只能处理:root,燃起信心拿来试试,结果此插件不能做到智能地处理DOM结构嵌套的场景. 140 | 141 | _只能处理:_ 142 | 143 | ```less 144 | .a{ 145 | --color: #fff; 146 | .b{ 147 | color: var(--color) 148 | } 149 | } 150 | ``` 151 | 152 | _而不能处理:_ 153 | 154 | ```less 155 | .a{ 156 | --color: #fff; 157 | } 158 | .b{ 159 | color: var(--color) 160 | } 161 | ``` 162 | 163 | 3.最后找到救星[css-vars-ponyfill](https://link.zhihu.com/?target=https%3A//github.com/jhildenbiddle/css-vars-ponyfill)支持动态处理: 164 | 165 | 在上面theme目录下新增index.js, 并且删除config.less及其引用: 166 | 167 | ```js 168 | import cssVars from 'css-vars-ponyfill'; 169 | 170 | export const THEME = { 171 | normal: { 172 | '--font-color': '#000', 173 | '--bg-color': '#000', 174 | }, 175 | dark: { 176 | '--font-color': '#fff', 177 | '--bg-color': '#fff', 178 | }, 179 | }; 180 | 181 | export const setTheme = themeKey => { 182 | const themeKeys = Object.keys(THEME); 183 | let KEY = null; 184 | if (themeKeys.includes(themeKey)) { 185 | KEY = themeKey; 186 | } else { 187 | KEY = localStorage.theme || 'normal'; 188 | } 189 | localStorage.theme = KEY; 190 | 191 | let styleLink = document.getElementById('theme-style'); 192 | let hrefSrc = '/theme/normal.css'; 193 | if (KEY === 'normal') { 194 | hrefSrc = '/theme/normal.css'; 195 | } else if (KEY === 'dark') { 196 | hrefSrc = '/theme/dark.css'; 197 | } 198 | 199 | if (styleLink) { 200 | styleLink.href = hrefSrc; 201 | } else { 202 | styleLink = document.createElement('link'); 203 | styleLink.type = 'text/css'; 204 | styleLink.rel = 'stylesheet'; 205 | styleLink.id = 'theme-style'; 206 | styleLink.href = hrefSrc; 207 | document.body.append(styleLink); 208 | } 209 | cssVars({ 210 | onlyLegacy: false, 211 | variables: THEME[KEY], 212 | }); 213 | }; 214 | 215 | ``` 216 | 217 | 并在合适的地方调用setTheme(), 完美!!! 218 | 219 | [demo项目](https://github.com/TigerHee/umi-switchTheme) 220 | 221 | 写文章的时候是用的umi2,demo内umi已升级到3 -------------------------------------------------------------------------------- /react/README.md: -------------------------------------------------------------------------------- 1 | ### React 学习过程中的一些笔记, 包含: 2 | 3 | - React 知识点 4 | - Redux 5 | - Redux-saga 6 | - React源码学习( version: 16.6.1 ) -------------------------------------------------------------------------------- /webpack/HtmlWebpackPlugin.md: -------------------------------------------------------------------------------- 1 | ## 使用HtmlWebpackPlugin向html内插入第三方cdn资源 2 | 3 | 以往我们需要向页面内插入csn资源通常是自己写函数,然后在index.js内调用如: 4 | 5 | ```js 6 | export const insertJs = () => { 7 | const scriptEl = document.createElement('script'); 8 | scriptEl.type = 'text/javascript'; 9 | scriptEl.async = true; 10 | scriptEl.src = 'xx.xxx.js'; 11 | scriptEl.onload = () => {}; 12 | 13 | document.body.appendChild(scriptEl); 14 | }; 15 | // 然后在入口js文件内调用 insertJs 16 | ``` 17 | 18 | 而使用 HtmlWebpackPlugin 则会使资源插入更加优雅: 19 | 20 | 本文使用create-react-app与@craco/craco实现 21 | 22 | ### 第一步:修改webpack配置 23 | 24 | craco.config.js: 25 | 26 | ```js 27 | const { getPlugin, pluginByName } = require('@craco/craco'); 28 | 29 | module.exports = { 30 | webpack: { 31 | configure: (webpackConfig, { paths }) => { 32 | const { isFound: _isFound, match: _match } = getPlugin( 33 | webpackConfig, 34 | pluginByName('HtmlWebpackPlugin'), 35 | ); 36 | 37 | if (_isFound) { 38 | _match.userOptions.cdnCss = [ 39 | 'https://cdn.bootcdn.net/ajax/libs/animate.css/4.1.1/animate.css', 40 | 'https://cdn.bootcdn.net/ajax/libs/basscss/8.1.0/css/basscss.min.css', 41 | ]; 42 | _match.userOptions.cdnJs = [ 43 | 'https://cdn.bootcdn.net/ajax/libs/lodash.js/4.17.21/lodash.js', 44 | 'https://cdn.bootcdn.net/ajax/libs/echarts/5.4.0/echarts.js', 45 | ]; 46 | } 47 | 48 | return webpackConfig; 49 | }, 50 | }, 51 | }; 52 | 53 | ``` 54 | 55 | ### 第二步:修改html模版 56 | 57 | index.html 58 | ```html 59 | 60 | 61 | 62 | <% htmlWebpackPlugin.options.cdnCss.forEach(path=> { %> 63 | 64 | <% }) %> 65 | 66 | 67 | <% htmlWebpackPlugin.options.cdnJs.forEach(cdnURL=> { %> 68 | 69 | <% }) %> 70 | 71 | 72 | ``` 73 | 74 | ### 效果: 75 | ![效果](../assets/img/webpack/cdn.png) 76 | 77 | - Date: 2022/12/08 -------------------------------------------------------------------------------- /webpack/webpack4.md: -------------------------------------------------------------------------------- 1 | 2 | - [安装前先npm初始化](#%E5%AE%89%E8%A3%85%E5%89%8D%E5%85%88npm%E5%88%9D%E5%A7%8B%E5%8C%96) 3 | - [本地服务](#%E6%9C%AC%E5%9C%B0%E6%9C%8D%E5%8A%A1) 4 | - [复制html](#%E5%A4%8D%E5%88%B6html) 5 | - [处理css](#%E5%A4%84%E7%90%86css) 6 | - [处理less](#%E5%A4%84%E7%90%86less) 7 | - [抽离css文件,通过link引入](#%E6%8A%BD%E7%A6%BBcss%E6%96%87%E4%BB%B6%E9%80%9A%E8%BF%87link%E5%BC%95%E5%85%A5) 8 | - [压缩css和js](#%E5%8E%8B%E7%BC%A9css%E5%92%8Cjs) 9 | - [给css加上兼容浏览器的前缀](#%E7%BB%99css%E5%8A%A0%E4%B8%8A%E5%85%BC%E5%AE%B9%E6%B5%8F%E8%A7%88%E5%99%A8%E7%9A%84%E5%89%8D%E7%BC%80) 10 | - [es6 转 es5](#es6-%E8%BD%AC-es5) 11 | - [es 7的语法](#es-7%E7%9A%84%E8%AF%AD%E6%B3%95) 12 | - [全局变量引入](#%E5%85%A8%E5%B1%80%E5%8F%98%E9%87%8F%E5%BC%95%E5%85%A5) 13 | - [webpack图片打包](#webpack%E5%9B%BE%E7%89%87%E6%89%93%E5%8C%85) 14 | - [当图片小于多少,用base64](#%E5%BD%93%E5%9B%BE%E7%89%87%E5%B0%8F%E4%BA%8E%E5%A4%9A%E5%B0%91%E7%94%A8base64) 15 | - [打包文件分类](#%E6%89%93%E5%8C%85%E6%96%87%E4%BB%B6%E5%88%86%E7%B1%BB) 16 | - [希望输出的时候,给这些`css\img`加上前缀,传到服务器也能访问](#%E5%B8%8C%E6%9C%9B%E8%BE%93%E5%87%BA%E7%9A%84%E6%97%B6%E5%80%99%E7%Bimg%E5%8A%A0%E4%B8%8A%E5%89%8D%E7%BC%80%E4%BC%A0%E5%88%B0%E6%9C%8D%E5%8A%A1%E5%99%A8%E4%B9%9F%E8%83%BD%E8%AE%BF%E9%97%AE) 17 | - [如果只希望处理图片](#%E5%A6%82%E6%9E%9C%E5%8F%AA%E5%B8%8C%E6%9C%9B%E5%A4%84%E7%90%86%E5%9B%BE%E7%89%87) 18 | - [打包多页应用](#%E6%89%93%E5%8C%85%E5%A4%9A%E9%A1%B5%E5%BA%94%E7%94%A8) 19 | - [配置`source-map`](#%E9%85%8D%E7%BD%AEsource-map) 20 | - [`watch` 改完代表重新打包实体](#watch-%E6%94%B9%E5%AE%8C%E4%BB%A3%E8%A1%A8%E9%87%8D%E6%96%B0%E6%89%93%E5%8C%85%E5%AE%9E%E4%BD%93) 21 | - [`webpack`的其他三个小插件](#webpack%E7%9A%84%E5%85%B6%E4%BB%96%E4%B8%89%E4%B8%AA%E5%B0%8F%E6%8F%92%E4%BB%B6) 22 | - [`webpack` 跨域](#webpack-%E8%B7%A8%E5%9F%9F) 23 | - [如果后端给的请求没有API 「跨域」](#%E5%A6%82%E6%9E%9C%E5%90%8E%E7%AB%AF%E7%BB%99%E7%9A%84%E8%AF%B7%E6%B1%82%E6%B2%A1%E6%9C%89api-%E8%B7%A8%E5%9F%9F) 24 | - [前端只想单纯mock数据 「跨域」](#%E5%89%8D%E7%AB%AF%E5%8F%AA%E6%83%B3%E5%8D%95%E7%BA%AFmock%E6%95%B0%E6%8D%AE-%E8%B7%A8%E5%9F%9F) 25 | - [有服务端,不用代理, 服务端启动webpack 「跨域」](#%E6%9C%89%E6%9C%8D%E5%8A%A1%E7%AB%AF%E4%B8%8D%E7%94%A8%E4%BB%A3%E7%90%86-%E6%9C%8F%E5%90%AF%E5%8A%A8webpack-%E8%B7%A8%E5%9F%9F) 26 | - [webpack解析resolve](#webpack%E8%A7%A3%E6%9E%90resolve) 27 | - [但是每次引入都很长,如何优雅引入](#%E4%BD%86%E6%98%AF%E6%AF%8F%E6%AC%A1%E5%BC%95%E5%85%A5%E9%83%BD%E5%BE%88%E9%95%BF%E5%A6%82%E4%B%9B%85%E5%BC%95%E5%85%A5) 28 | - [省略扩展名](#%E7%9C%81%E7%95%A5%E6%89%A9%E5%B1%95%E5%90%8D) 29 | - [定义环境变量](#%E5%AE%9A%E4%B9%89%E7%8E%AF%E5%A2%83%E5%8F%98%E9%87%8F) 30 | - [区分两个不同的环境](#%E5%8C%BA%E5%88%86%E4%B8%A4%E4%B8%AA%E4%B8%8D%E5%90%8C%E7%9A%84%E7%8E%AF%E5%A2%83) 31 | - [webpack 优化](#webpack-%E4%BC%98%E5%8C%96) 32 | - [优化:当某些包是独立的个体没有依赖](#%E4%BC%98%E5%8C%96%E5%BD%93%E6%9F%90%E4%BA%9B%E5%8C%85%E6%98%AF%E7%8B%AC%E7%AB%8B%E7%9A%84%E4%B8%AA%E4%BD%93%E6%B2%A1%E6%9C%89%E4%BE%9D%E8%B5%96) 33 | - [优化:规则匹配设置范围](#%E4%BC%98%E5%8C%96%E8%A7%84%E5%88%99%E5%8C%B9%E9%85%8D%E8%AE%BE%E7%BD%AE%E8%8C%83%E5%9B%B4) 34 | - [优化:忽略依赖中不必要的语言包](#%E4%BC%98%E5%8C%96%E5%BF%BD%E7%95%A5%E4%BE%9D%E8%B5%96%E4%B8%AD%E4%B8%8D%E5%BF%85%E8%A6%81%E7%9A%A8%80%E5%8C%85) 35 | - [动态链接库](#%E5%8A%A8%E6%80%81%E9%93%BE%E6%8E%A5%E5%BA%93) 36 | - [多线程打包happypack](#%E5%A4%9A%E7%BA%BF%E7%A8%8B%E6%89%93%E5%8C%85happypack) 37 | - [webpack 自带的优化](#webpack-%E8%87%AA%E5%B8%A6%E7%9A%84%E4%BC%98%E5%8C%96) 38 | - [抽取公共代码](#%E6%8A%BD%E5%8F%96%E5%85%AC%E5%85%B1%E4%BB%A3%E7%A0%81) 39 | - [懒加载(延迟加载)](#%E6%87%92%E5%8A%A0%E8%BD%BD%E5%BB%B6%E8%BF%9F%E5%8A%A0%E8%BD%BD) 40 | - [热更新(当页面改变只更新改变的部分,不重新打包)](#%E7%83%AD%E6%9B%B4%E6%96%B0%E5%BD%93%E9%A1%B5%E9%9D%A2%E6%94%B9%E5%8F%98%E5%8F%AA%E6%9B%B4%E6%96%B0%E6%94%B9%E5%8F%98%E7%9A%84%E9%83%A8%E5%88%86%E4%B8%8D%E9%87%8D%E6%96%B0%E6%89%93%E5%8C%85) 41 | - [tapable介绍 - SyncHook](#tapable%E4%BB%8B%E7%BB%8D---synchook) 42 | - [tapable介绍 - SyncBailHook](#tapable%E4%BB%8B%E7%BB%8D---syncbailhook) 43 | - [tapable介绍 - SyncWaterfallHook](#tapable%E4%BB%8B%E7%BB%8D---syncwaterfallhook) 44 | - [tapable介绍 - SyncLoopHook](#tapable%E4%BB%8B%E7%BB%8D---syncloophook) 45 | - [`AsyncParallelHook` 与 `AsyncParallelBailHook`](#asyncparallelhook-%E4%B8%8E-asyncparallelbailhook) 46 | + [AsyncParallelHook](#asyncparallelhook) 47 | + [AsyncParallelBailHook](#asyncparallelbailhook) 48 | - [异步串行 —— AsyncSeriesHook](#%E5%BC%82%E6%AD%A5%E4%B8%B2%E8%A1%8C--asyncserieshook) 49 | - [异步串行 —— AsyncSeriesWaterfallHook](#%E5%BC%82%E6%AD%A5%E4%B8%B2%E8%A1%8C--asyncserieswaterfallhook) 50 | - [手写webpack](#%E6%89%8B%E5%86%99webpack) 51 | - [webpack分析及处理](#webpack%E5%88%86%E6%9E%90%E5%8F%8A%E5%A4%84%E7%90%86) 52 | - [创建依赖关系](#%E5%88%9B%E5%BB%BA%E4%BE%9D%E8%B5%96%E5%85%B3%E7%B3%BB) 53 | - [ast递归解析](#ast%E9%80%92%E5%BD%92%E8%A7%A3%E6%9E%90) 54 | - [生成打包工具](#%E7%94%9F%E6%88%90%E6%89%93%E5%8C%85%E5%B7%A5%E5%85%B7) 55 | - [增加loader](#%E5%A2%9E%E5%8A%A0loader) 56 | - [增加plugins](#%E5%A2%9E%E5%8A%A0plugins) 57 | - [loader](#loader) 58 | - [配置多个loader](#%E9%85%8D%E7%BD%AE%E5%A4%9A%E4%B8%AAloader) 59 | - [`babel-loader`实现](#babel-loader%E5%AE%9E%E7%8E%B0) 60 | - [`banner-loader`实现(自创)](#banner-loader%E5%AE%9E%E7%8E%B0%E8%87%AA%E5%88%9B) 61 | - [实现`file-loader`和`url-loader`](#%E5%AE%9E%E7%8E%B0file-loader%E5%92%8Curl-loader) 62 | - [`less-loader`和`css-loader`](#less-loader%E5%92%8Ccss-loader) 63 | - [`css-loader`](#css-loader) 64 | - [webpack 中的插件](#webpack-%E4%B8%AD%E7%9A%84%E6%8F%92%E4%BB%B6) 65 | - [文件列表插件](#%E6%96%87%E4%BB%B6%E5%88%97%E8%A1%A8%E6%8F%92%E4%BB%B6) 66 | - [内联的`webpack`插件](#%E5%86%85%E8%81%94%E7%9A%84webpack%E6%8F%92%E4%BB%B6) 67 | - [打包后自动发布](#%E6%89%93%E5%8C%85%E5%90%8E%E8%87%AA%E5%8A%A8%E5%8F%91%E5%B8%83) 68 | 69 | ## 安装前先npm初始化 70 | 71 | ``` 72 | npm init -y 73 | npm i webpack webpack-cli -D 74 | ``` 75 | 76 | 77 | ```js 78 | let path = require('path') // 相对路径变绝对路径 79 | 80 | module.exports = { 81 | mode: 'production', // 模式 默认 production development 82 | entry: './src/index', // 入口 83 | output: { 84 | filename: 'bundle.[hash:8].js', // hash: 8只显示8位 85 | path: path.resolve(__dirname, 'dist'), 86 | publicPath: '' // // 给所有打包文件引入时加前缀,包括css,js,img,如果只想处理图片可以单独在url-loader配置中加publicPath 87 | } 88 | } 89 | ``` 90 | ## 本地服务 91 | 92 | `npm i webpack-dev-server -D` 93 | 94 | ``` 95 | devServer: { 96 | port: 3000, 97 | progress: true // 滚动条 98 | contentBase: './build' // 起服务的地址 99 | open: true // 自动打开浏览器 100 | compress: true // gzip压缩 101 | } 102 | ``` 103 | 104 | 105 | ## 复制html 106 | 107 | `npm i html-webpack-plugin -D` 108 | 109 | ``` 110 | let HtmlWebpackPlugin = require('html-webpack-plugin') 111 | plugins: [ // 放着所有webpack插件 112 | new HtmlWebpackPlugin({ // 用于使用模板打包时生成index.html文件,并且在run dev时会将模板文件也打包到内存中 113 | template: './index.html', // 模板文件 114 | filename: 'index.html', // 打包后生成文件 115 | hash: true, // 添加hash值解决缓存问题 116 | minify: { // 对打包的html模板进行压缩 117 | removeAttributeQuotes: true, // 删除属性双引号 118 | collapseWhitespace: true // 折叠空行变成一行 119 | } 120 | }) 121 | ] 122 | 123 | ``` 124 | 125 | [html-webpack-plugin#options](https://github.com/jantimon/html-webpack-plugin#options) 126 | 127 | 128 | ## 处理css 129 | 130 | `npm i css-loader style-loader -D` 131 | 132 | ``` 133 | // css-loader 作用:用来解析@import这种语法 134 | // style-loader 作用:把 css 插入到head标签中 135 | // loader的执行顺序: 默认是从右向左(从下向上) 136 | module: { // 模块 137 | rules: [ // 规则 138 | // style-loader 把css插入head标签中 139 | // loader 功能单一 140 | // 多个loader 需要 [] 141 | // 顺便默认从右到左 142 | // 也可以写成对象方式 143 | { 144 | test: /\.css$/, // css 处理 145 | // use: 'css-loader' 146 | // use: ['style-loader', 'css-loader'], 147 | use: [ 148 | // { 149 | // loader: 'style-loader', 150 | // options: { 151 | // insertAt: 'top' // 将css标签插入最顶头 这样可以自定义style不被覆盖 152 | // } 153 | // }, 154 | MiniCssExtractPlugin.loader, 155 | 'css-loader', // css-loader 用来解析@import这种语法, 156 | 'postcss-loader' 157 | ] 158 | } 159 | ] 160 | } 161 | ``` 162 | 163 | 164 | ## 处理less 165 | 166 | `npm i less-loader` 167 | 168 | ``` 169 | { 170 | test: /\.less$/, // less 处理 171 | // use: 'css-loader' 172 | // use: ['style-loader', 'css-loader'], 173 | use: [ 174 | // { 175 | // loader: 'style-loader', 176 | // options: { 177 | // insertAt: 'top' // 将css标签插入最顶头 这样可以自定义style不被覆盖 178 | // } 179 | // }, 180 | MiniCssExtractPlugin.loader, // 这样相当于抽离成一个css文件, 如果希望抽离成分别不同的css, 需要再引入MiniCssExtractPlugin,再配置 181 | 'css-loader', // css-loader 用来解析@import这种语法 182 | 'postcss-loader', 183 | 'less-loader' // less-loader less -> css 184 | // sass node-sass sass-loader 185 | // stylus stylus-loader 186 | ] 187 | } 188 | ``` 189 | 190 | [less-loader](https://webpack.js.org/loaders/less-loader/#src/components/Sidebar/Sidbar.jsx) 191 | 192 | ## 抽离css文件,通过link引入 193 | 194 | `yarn add mini-css-extract-plugin -D` 195 | 196 | [mini-css-extract-plugin](https://github.com/webpack-contrib/mini-css-extract-plugin) 197 | 198 | ``` 199 | let MiniCssExtractPlugin = require('mini-css-extract-plugin') 200 | 201 | // 压缩css 202 | 203 | plugins: [ 204 | new MiniCssExtractPlugin({ 205 | filename: 'css/main.css' 206 | }) 207 | ] 208 | 209 | { 210 | test: /\.css$/, // css 处理 211 | // use: 'css-loader' 212 | // use: ['style-loader', 'css-loader'], 213 | use: [ 214 | // { 215 | // loader: 'style-loader', 216 | // options: { 217 | // insertAt: 'top' // 将css标签插入最顶头 这样可以自定义style不被覆盖 218 | // } 219 | // }, 220 | // 此时不需要style-loader 221 | MiniCssExtractPlugin.loader, // 抽离 222 | 'css-loader', // css-loader 用来解析@import这种语法, 223 | 'postcss-loader' 224 | ] 225 | } 226 | 227 | ``` 228 | 229 | 抽离css插件文件时可使用`optimize-css-assets-webpack-plugin`优化压缩css以及js文件 230 | 231 | ## 压缩css和js 232 | 233 | ``` 234 | // 用了`mini-css-extract-plugin`抽离css为link需使用`optimize-css-assets-webpack-plugin`进行压缩css,使用此方法压缩了css需要`uglifyjs-webpack-plugin`压缩js 235 | const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin") 236 | const UglifyJsPlugin = require("uglifyjs-webpack-plugin") 237 | 238 | module.exports = { 239 | optimization: { // 优化项 240 | minimizer: [ 241 | new UglifyJsPlugin({ // 优化js 242 | cache: true, // 是否缓存 243 | parallel: true, // 是否并发打包 244 | // sourceMap: true // 源码映射 set to true if you want JS source maps 245 | }), 246 | new OptimizeCSSAssetsPlugin({}) // css 的优化 247 | ] 248 | }, 249 | mode: 'production', 250 | entry: '', 251 | output: {}, 252 | } 253 | 254 | ``` 255 | 256 | ## 给css加上兼容浏览器的前缀 257 | 258 | `yarn add postcss-loader autoprefixer -D` 259 | 260 | ``` 261 | // css 262 | { 263 | test: /\.css$/, // css 处理 264 | // use: 'css-loader' 265 | // use: ['style-loader', 'css-loader'], 266 | use: [ 267 | // { 268 | // loader: 'style-loader', 269 | // options: { 270 | // insertAt: 'top' // 将css标签插入最顶头 这样可以自定义style不被覆盖 271 | // } 272 | // }, 273 | MiniCssExtractPlugin.loader, 274 | 'css-loader', // css-loader 用来解析@import这种语法, 275 | 'postcss-loader' 276 | ] 277 | } 278 | // less 279 | { 280 | test: /\.less$/, // less 处理 281 | // use: 'css-loader' 282 | // use: ['style-loader', 'css-loader'], 283 | use: [ 284 | // { 285 | // loader: 'style-loader', 286 | // options: { 287 | // insertAt: 'top' // 将css标签插入最顶头 这样可以自定义style不被覆盖 288 | // } 289 | // }, 290 | MiniCssExtractPlugin.loader, // 这样相当于抽离成一个css文件, 如果希望抽离成分别不同的css, 需要再引入MiniCssExtractPlugin,再配置 291 | 'css-loader', // css-loader 用来解析@import这种语法 292 | 'postcss-loader', 293 | 'less-loader' // less-loader less -> css 294 | // sass node-sass sass-loader 295 | // stylus stylus-loader 296 | ] 297 | }, 298 | ``` 299 | 300 | postcss 需要配置文档 `postcss.config1.js` 301 | 302 | [postcss-loader](https://github.com/postcss/postcss-loader) 303 | 304 | ``` 305 | module.exports = { 306 | plugins: [ 307 | require('autoprefixer') 308 | ] 309 | } 310 | ``` 311 | 312 | ## es6 转 es5 313 | 314 | `npm i babel-loader @babel/core @babel/preset-env -D` 315 | 316 | ``` 317 | module.exports = { 318 | module: { 319 | rules: [ 320 | { 321 | test: /\.js$/, 322 | use: { 323 | loader: 'babel-loader', 324 | options: { 325 | presets: [ //预设 326 | '@babel/preset-env' 327 | ], 328 | plugins:[ 329 | // 转es7的语法 330 | ["@babel/plugin-proposal-decorators", { "legacy": true }], 331 | ["@babel/plugin-proposal-class-properties", { "loose" : true }] 332 | ] 333 | } 334 | }, 335 | exclude: /node_modules/ 336 | } 337 | ] 338 | } 339 | } 340 | 341 | ``` 342 | 343 | 344 | ## 转es7的语法 345 | 346 | ``` 347 | // 转class 348 | npm i @babel/plugin-proposal-class-properties -D 349 | 350 | // 转装饰器 351 | npm i @babel/plugin-proposal-decorators -D 352 | ``` 353 | 354 | 配置如上 355 | 356 | ### 其他不兼容的高级语法 357 | 358 | ``` 359 | 使用 @babel/polyfill 360 | ``` 361 | 362 | ## 语法检查 eslint 363 | 364 | `npm i eslint eslint-loader -S` 365 | 366 | 根目录添加 `.eslintrc.json` 配置文件 367 | 368 | ``` 369 | module.exports = { 370 | module: { 371 | rules: [ 372 | { 373 | test: /\.js$/, 374 | use: { 375 | loader: 'eslint-loader', 376 | options: { 377 | enforce: 'pre' // previous优先执行 post-普通loader之后执行 378 | } 379 | } 380 | }, 381 | { 382 | test: /\.js$/, // mormal 普通的loader 383 | use: { 384 | loader: 'babel-loader', 385 | options: { 386 | presets: [ //预设 387 | '@babel/preset-env' 388 | ] 389 | } 390 | }, 391 | exclude: /node_modules/ 392 | } 393 | ] 394 | } 395 | } 396 | 397 | ``` 398 | 399 | ## 全局变量引入 400 | 401 | jquery的引入 402 | 403 | ``` 404 | npm i jquery -S 405 | ``` 406 | 407 | ``` 408 | let webpack = require('webpack') 409 | 410 | new webpack.ProvidePlugin({ 411 | $: 'jquery' 412 | }) 413 | ``` 414 | 415 | 其他情况 416 | 417 | 1. 暴露全局 418 | 419 | `npm i expose-loader -D` 暴露全局的`loader` 420 | 421 | #### 法1: 422 | 423 | 可以在js中 `import $ from 'expose-loader?$!jquery'` // 全局暴露jquery为$符号 424 | 425 | 可以调用`window.$` 426 | 427 | #### 法2: 428 | 429 | 也可在`webpack.config.js` 中配置 `rules` 430 | 431 | ``` 432 | module.exports = { 433 | module: { 434 | rules: [ 435 | { 436 | test: require.resolve('jquery'), 437 | use: 'expose-loader?$' 438 | } 439 | ] 440 | } 441 | } 442 | 443 | ``` 444 | 以后在`.js`文件中引入 445 | 446 | ``` 447 | import $ from 'jquery' 448 | ``` 449 | 450 | #### 法3. 如何在每个模块中注入: 451 | 452 | ``` 453 | let webpack = require('webpack') 454 | 455 | module.exports = { 456 | plugins: [ 457 | new webpack.ProvidePlugin({ 458 | $: 'jquery' 459 | }) 460 | ] 461 | } 462 | 463 | 之后代码内直接使用 $ 464 | ``` 465 | #### 法4: 466 | 467 | 在`index.html`中通过`script`标签引入`jquery`, 但是在`js`中,用`import`会重新打包`jquery`,如何避免 468 | 469 | 从输出的bundle 中排除依赖 470 | 471 | ``` 472 | module.exports = { 473 | externals: { // 告知webpack是外部引入的,不需要打包 474 | jquery: 'jQuery' 475 | } 476 | } 477 | 478 | ``` 479 | 480 | 此时在index.js上 481 | 482 | ``` 483 | import $ from 'jquery' 484 | 485 | console.log($) 486 | ``` 487 | 488 | ## webpack图片打包 489 | 490 | 1. js中创建 491 | 2. css中引入 492 | 3. `` 493 | 494 | `yarn add file-loader -D` 495 | 496 | 适合一二情况 497 | 498 | ``` 499 | module.export={ 500 | module: { 501 | rules: [ 502 | { 503 | test: /\.(png|jpg|gif)$/, 504 | use: 'file-loader' 505 | } 506 | ] 507 | } 508 | } 509 | 510 | ``` 511 | 512 | 默认会内部生成一张图片到build,生成图片的路径返回回来 513 | 514 | 第一种情况: 图片地址要`import`引入,直接写图片的地址,会默认为字符串 515 | 516 | ``` 517 | import logo from './logo.png' 518 | 519 | let image = new Image() 520 | image.src = logo 521 | document.body.appendChild(image) 522 | ``` 523 | 524 | 第二种情况: `css-loader`会将`css`里面的图片转为`require`的格式 525 | 526 | ``` 527 | div { 528 | background: url("./logo.png"); 529 | } 530 | ``` 531 | 532 | 第三种情况: 解析`html`中的`image` 533 | 534 | `yarn add html-withimg-loader -D` 535 | 536 | ``` 537 | { 538 | test: /\.html$/, 539 | use: 'html-withimg-loader' 540 | } 541 | ``` 542 | 543 | ## 当图片小于多少,用base64 544 | 545 | `yarn add url-loader -D` 546 | 547 | 如果过大,才用`file-loader` 548 | 549 | ``` 550 | { 551 | test: /\.(png|jpg|gif)$/, 552 | // 当图片小于多少,用base64,否则用file-loader产生真实的图片 553 | use: { 554 | loader: 'url-loader', 555 | options: { 556 | limit: 200 * 1024, // 小于200k变成base64 557 | // outputPath: '/img/', // 打包后输出地址 558 | // publicPath: '' // 给资源加上域名路径 559 | } 560 | } 561 | } 562 | ``` 563 | 564 | ## 打包文件分类 565 | 566 | 1.图片: 567 | 568 | ``` 569 | { 570 | test: /\.(png|jpg|gif)$/, 571 | // 当图片小于多少,用base64,否则用file-loader产生真实的图片 572 | use: { 573 | loader: 'url-loader', 574 | options: { 575 | limit: 1, // 200k 200 * 1024 576 | outputPath: 'img/' // 打包后输出地址 在dist/img 577 | } 578 | } 579 | }, 580 | ``` 581 | 582 | 2.css: 583 | 584 | ``` 585 | plugins: [ 586 | new MiniCssExtractPlugin({ 587 | filename: 'css/main.css' 588 | }), 589 | ] 590 | ``` 591 | 592 | ## 希望输出的时候,给这些`css\img`加上前缀,传到服务器也能访问 593 | 594 | ``` 595 | output: { 596 | filename: 'bundle.[hash:8].js', // hash: 8只显示8位 597 | path: path.resolve(__dirname, 'dist'), 598 | publicPath: 'http://www.mayufo.cn' // 给静态资源统一加 599 | }, 600 | ``` 601 | 602 | 603 | ## 如果只希望处理图片 604 | 605 | ``` 606 | { 607 | test: /\.(png|jpg|gif)$/, 608 | // 当图片小于多少,用base64,否则用file-loader产生真实的图片 609 | use: { 610 | loader: 'url-loader', 611 | options: { 612 | limit: 1, // 200k 200 * 1024 613 | outputPath: '/img/', // 打包后输出地址 614 | publicPath: 'http://www.mayufo.cn' 615 | } 616 | } 617 | } 618 | ``` 619 | 620 | ## 打包多页应用 621 | 622 | ``` 623 | // 多入口 624 | let path = require('path') 625 | let HtmlWebpackPlugin = require('html-webpack-plugin') 626 | 627 | module.exports = { 628 | mode: 'development', 629 | entry: { 630 | home: './src/index.js', 631 | other: './src/other.js' 632 | }, 633 | output: { 634 | filename: "[name].js", 635 | path: path.resolve(__dirname, 'dist2') 636 | }, 637 | plugins: [ 638 | new HtmlWebpackPlugin({ 639 | template: './index.html', 640 | filename: 'home.html', 641 | chunks: ['home'] 642 | }), 643 | new HtmlWebpackPlugin({ 644 | template: './index.html', 645 | filename: 'other.html', 646 | chunks: ['other', 'home'] // other.html 里面有 other.js & home.js 647 | }), 648 | ] 649 | } 650 | 651 | ``` 652 | 653 | ## 配置`source-map` 654 | 655 | `yarn add @babel/core @babel/preset-env babel-loader webpack-dev-server -D` 656 | 657 | ``` 658 | module.exports = { 659 | devtool: 'source-map' // 增加映射文件调试源代码 660 | } 661 | ``` 662 | 1. 源码映射 会标识错误的代码 打包后生成独立的文件 大而全 「source-map」 663 | 2. 不会陈胜单独的文件 但是可以显示行和列 「eval-source-map」 664 | 3. 不会产生列有行,产生单独的映射文件 「cheap-module-source-map」 665 | 4. 不会产生文件 集成在打包后的文件中 不会产生列有行 「cheap-module-eval-source-map」 666 | 667 | 668 | ## `watch` 改完代表重新打包实体 669 | 670 | ``` 671 | module.exports = { 672 | watch: true, 673 | watchOptions: { 674 | poll: 1000, // 每秒监听1000次 675 | aggregateTimeout: 300, // 防抖,当第一个文件更改,会在重新构建前增加延迟 676 | ignored: /node_modules/ // 对于某些系统,监听大量文件系统会导致大量的 CPU 或内存占用。这个选项可以排除一些巨大的文件夹, 677 | }, 678 | } 679 | ``` 680 | 681 | 682 | ## `webpack`的其他三个小插件 683 | 684 | 1. `cleanWebpackPlugin` 685 | 686 | 每次打包之前删掉dist目录 687 | `yarn add clean-webpack-plugin -D` 688 | 689 | [clean-webpack-plugin](https://github.com/johnagan/clean-webpack-plugin) 690 | 691 | ``` 692 | const CleanWebpackPlugin = require('clean-webpack-plugin'); 693 | 694 | module.exports = { 695 | output: { 696 | path: path.resolve(process.cwd(), 'dist'), 697 | }, 698 | plugins: [ 699 | new CleanWebpackPlugin('./dist') 700 | ] 701 | } 702 | ``` 703 | 704 | 2. `copyWebpackPlugin` 705 | 706 | 一些静态资源也希望拷贝的dist中 707 | 708 | `yarn add copy-webpack-plugin -D` 709 | 710 | ``` 711 | const CopyWebpackPlugin = require('copy-webpack-plugin') 712 | 713 | module.exports = { 714 | plugins: [ 715 | new CopyWebpackPlugin([ 716 | {from: 'doc', to: './dist'} 717 | ]) 718 | ] 719 | } 720 | ``` 721 | 722 | 3. `bannerPlugin`内置模块 723 | 724 | 版权声明 725 | 726 | ``` 727 | const webpack = require('webpack'); 728 | 729 | new webpack.BannerPlugin('hello world') 730 | // or 731 | new webpack.BannerPlugin({ banner: 'hello world'}) 732 | 733 | ``` 734 | 735 | ## `webpack` 跨域 736 | 737 | 设置一个服务,由于`webpack-dev-server`内含`express` 738 | 739 | [express](https://expressjs.com/zh-cn/starter/hello-world.html) 740 | 741 | `server.js` 742 | 743 | ``` 744 | // express 745 | 746 | let express = require('express') 747 | 748 | let app = express(); 749 | 750 | app.get('/api/user', (res) => { 751 | res.json({name: 'mayufo'}) 752 | }) 753 | 754 | app.listen(3000) // 服务端口在3000 755 | ``` 756 | 757 | 写完后记得`node server.js` 758 | 759 | 访问 `http://localhost:3000/api/user` 可见内容 760 | 761 | 762 | `index.js` 763 | 764 | ``` 765 | // 发送一个请求 766 | let xhr = new XMLHttpRequest(); 767 | 768 | // 默认访问 http://localhost:8080 webpack-dev-server 的服务 再转发给3000 769 | xhr.open('GET', '/api/user', true); 770 | 771 | xhr.onload = function () { 772 | console.log(xhr.response) 773 | } 774 | 775 | xhr.send(); 776 | 777 | ``` 778 | 779 | 780 | `webpack.config.js` 781 | 782 | ``` 783 | module.exports = { 784 | devServer: { 785 | proxy: { 786 | '/api': 'http://localhost:3000' 787 | } 788 | }, 789 | } 790 | ``` 791 | 792 | ## 1.如果后端给的请求没有API 「跨域」 793 | 794 | ``` 795 | // express 796 | 797 | let express = require('express') 798 | 799 | let app = express(); 800 | 801 | 802 | app.get('/user', (res) => { 803 | res.json({name: 'mayufo'}) 804 | }) 805 | 806 | app.listen(3000) // 服务端口在3000 807 | ``` 808 | 809 | 810 | 请求已api开头, 转发的时候再删掉api 811 | 812 | ``` 813 | devServer: { 814 | proxy: { 815 | '/api': { 816 | target: 'http://localhost:3000', 817 | pathRewrite: {'^/api': ''} 818 | } 819 | } 820 | } 821 | ``` 822 | 823 | ## 2.前端只想单纯mock数据 「跨域」 824 | 825 | ``` 826 | devServer: { 827 | // proxy: { 828 | // '/api': 'http://localhost:3000' // 配置一个代理 829 | // } 830 | // proxy: { // 重写方式 把请求代理到express 上 831 | // '/api': { 832 | // target: 'http://localhost:3000', 833 | // pathRewrite: {'^/api': ''} 834 | // } 835 | // } 836 | before: function (app) { // 勾子 837 | app.get('/api/user', (req, res) => { 838 | res.json({name: 'tigerHee'}) 839 | }) 840 | } 841 | }, 842 | ``` 843 | 844 | ## 3.有服务端,不用代理, 服务端启动webpack 「跨域」 845 | 846 | `server.js`中启动`webpack` 847 | 848 | `yarn add webpack-dev-middleware -D` 849 | 850 | `server.js` 851 | 852 | ``` 853 | // express 854 | 855 | let express = require('express') 856 | let webpack = require('webpack') 857 | let app = express(); 858 | 859 | 860 | // 中间件 861 | let middle = require('webpack-dev-middleware') 862 | 863 | let config = require('./webpack.config') 864 | 865 | 866 | let compiler = webpack(config) 867 | 868 | 869 | app.use(middle(compiler)) 870 | 871 | app.get('/user', (req, res) => { 872 | res.json({name: 'mayufo'}) 873 | }) 874 | 875 | 876 | app.listen(3000) 877 | 878 | ``` 879 | 880 | ## webpack解析resolve 881 | 882 | 以`bootstrap`为例 883 | 884 | ``` 885 | npm install bootstrap -D 886 | ``` 887 | 888 | `index.js` 889 | 890 | ``` 891 | import 'bootstrap/dist/css/bootstrap.css' 892 | ``` 893 | 894 | 报错 895 | ``` 896 | ERROR in ./node_modules/bootstrap/dist/css/bootstrap.css 7:0 897 | Module parse failed: Unexpected token (7:0) 898 | You may need an appropriate loader to handle this file type. 899 | | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 900 | | */ 901 | > :root { 902 | | --blue: #007bff; 903 | | --indigo: #6610f2; 904 | @ ./src/index.js 22:0-42 905 | @ multi (webpack)-dev-server/client?http://localhost:8081 ./src/index.js 906 | 907 | ``` 908 | 909 | 这是因为`bootstrap` 4.0的css引入了新的特性,CSS Variables 910 | 911 | 安装 912 | `npm install postcss-custom-properties --save-dev` 913 | 914 | 915 | 配置`webpack.config.js` 916 | ``` 917 | { 918 | test: /\.css$/, 919 | use: ['style-loader', 'css-loader', { 920 | loader: 'postcss-loader', 921 | options: { 922 | plugins: (loader) => [ 923 | require("postcss-custom-properties") 924 | ] 925 | } 926 | }] 927 | } 928 | ``` 929 | 930 | ## 但是每次引入都很长,如何优雅引入 931 | 932 | ``` 933 | resolve: { 934 | // 在当前目录查找 935 | modules: [path.resolve('node_modules')], 936 | alias: { 937 | 'bootstrapCss': 'bootstrap/dist/css/bootstrap.css' 938 | } 939 | }, 940 | ``` 941 | 942 | ``` 943 | import 'bootstrapCss' // 在node_modules查找 944 | ``` 945 | 946 | ## 省略扩展名 947 | 948 | extensions: 949 | 950 | ``` 951 | resolve: { 952 | // 在当前目录查找 953 | modules: [path.resolve('node_modules')], 954 | // alias: { 955 | // 'bootstrapCss': 'bootstrap/dist/css/bootstrap.css' 956 | // }, 957 | mainFields: ['style', 'main'], // 先用bootstrap中在package中的style,没有在用main 958 | // mainFiles: [] // 入口文件的名字 默认index 959 | extensions: ['.js', '.css', '.json'] // 当没有拓展命的时候,先默认js、次之css、再次之json 960 | }, 961 | ``` 962 | 963 | ## 定义环境变量 964 | 965 | `DefinePlugin` 允许创建一个在编译时可以配置的全局常量。这可能会对开发模式和生产模式的构建允许不同的行为非常有用。 966 | 967 | ``` 968 | let url = '' 969 | if (DEV === 'dev') { 970 | // 开发环境 971 | url = 'http://localhost:3000' 972 | } else { 973 | // 生成环境 974 | url = 'http://www.mayufo.cn' 975 | } 976 | ``` 977 | 978 | `webpack.config.js` 979 | 980 | ``` 981 | new webpack.DefinePlugin({ 982 | // DEV: '"production"', 983 | DEV: JSON.stringify('production'), 984 | FLAG: 'true', // 布尔 985 | EXPRESSION: '1 + 1' // 字符串 如果希望是字符串 JSON.stringify('1 + 1') 986 | }) 987 | ``` 988 | 989 | ## 区分两个不同的环境 990 | 991 | 分别配置不同的环境 992 | 993 | - `webpack.base4.js` 基础配置 994 | - `webpack.dev4.js` 开发环境 995 | - `webpack.prod4.js` 生产环境 996 | 997 | `yarn add webpack-merge -D` 998 | 999 | 1000 | `npm run build -- -- config webpack.dev4.js` 1001 | `npm run build -- -- config webpack.build.js` 1002 | 1003 | [官方文档](https://webpack.docschina.org/guides/production/) 1004 | 1005 | 1006 | `webpack.base4.js` 1007 | 1008 | ``` 1009 | let path = require('path') 1010 | let HtmlWebpackPlugin = require('html-webpack-plugin') 1011 | let CleanWebpackPlugin = require('clean-webpack-plugin') 1012 | 1013 | module.exports = { 1014 | entry: { 1015 | home: './src/index.js' 1016 | }, 1017 | output: { 1018 | filename: "[name].js", 1019 | path: path.resolve(process.cwd(), 'dist3') 1020 | }, 1021 | module: { 1022 | rules: [ 1023 | { 1024 | test: /\.js$/, 1025 | use: { 1026 | loader: 'babel-loader', 1027 | options: { 1028 | presets: [ 1029 | '@babel/preset-env' 1030 | ] 1031 | } 1032 | } 1033 | }, 1034 | { 1035 | test: /\.css$/, 1036 | use: ['style-loader', 'css-loader', { 1037 | loader: 'postcss-loader', 1038 | options: { 1039 | plugins: (loader) => [ 1040 | require("postcss-custom-properties") 1041 | ] 1042 | } 1043 | }] 1044 | } 1045 | ] 1046 | }, 1047 | plugins: [ 1048 | new HtmlWebpackPlugin({ 1049 | template: './src/index.html', 1050 | filename: 'index.html' 1051 | }) 1052 | ] 1053 | } 1054 | 1055 | ``` 1056 | 1057 | `webpack.dev4.js` 1058 | 1059 | ``` 1060 | let merge = require('webpack-merge') 1061 | let base = require('./webpack.base4.js') 1062 | 1063 | module.exports = merge(base, { 1064 | mode: 'development', 1065 | devServer: {}, 1066 | devtool: 'source-map' 1067 | }) 1068 | 1069 | ``` 1070 | 1071 | `webpack.prod4.js` 1072 | 1073 | ``` 1074 | let merge = require('webpack-merge') 1075 | let base = require('./webpack.base4.js') 1076 | 1077 | module.exports = merge(base, { 1078 | mode: 'production' 1079 | }) 1080 | 1081 | ``` 1082 | 1083 | `package.json` 1084 | 1085 | ``` 1086 | "scripts": { 1087 | "build": "webpack --config webpack.prod4.js", 1088 | "dev": "webpack-dev-server --config webpack.dev4.js" 1089 | }, 1090 | ``` 1091 | 1092 | 1093 | ## webpack 优化 1094 | 1095 | `yarn add webpack webpack-cli html-webpack-plugin @babel/core babel-loader @babel/preset-env @babel/preset-react -D` 1096 | 1097 | `webpack.config.js` 1098 | 1099 | ``` 1100 | let path = require('path') 1101 | let HtmlWebpackPlugin = require('html-webpack-plugin') 1102 | 1103 | 1104 | module.exports = { 1105 | mode: 'development', 1106 | entry: './src/index.js', 1107 | output: { 1108 | filename: 'main.js', 1109 | path: path.resolve(__dirname, 'dist') 1110 | }, 1111 | module: { 1112 | rules: [ 1113 | { 1114 | test: /\.js$/, 1115 | use: { 1116 | loader: 'babel-loader', 1117 | options: { 1118 | presets: [ 1119 | '@babel/preset-env', 1120 | '@babel/preset-react' 1121 | ] 1122 | } 1123 | } 1124 | }, 1125 | ] 1126 | }, 1127 | plugins: [ 1128 | new HtmlWebpackPlugin({ 1129 | template: './src/index.html', 1130 | filename: 'index.html' 1131 | }), 1132 | ] 1133 | } 1134 | 1135 | ``` 1136 | ## 优化:当某些包是独立的个体没有依赖 1137 | 1138 | 以jquery为例,`yarn add jquery -D`,它是一个独立的包没有依赖,可以在webpack配置中,配置它不再查找依赖 1139 | 1140 | ``` 1141 | module: { 1142 | noParse: /jquery/, // 不用解析某些包的依赖 1143 | rules: [ 1144 | { 1145 | test: /\.js$/, 1146 | use: { 1147 | loader: 'babel-loader', 1148 | options: { 1149 | presets: [ 1150 | '@babel/preset-env', 1151 | '@babel/preset-react' 1152 | ] 1153 | } 1154 | } 1155 | }, 1156 | ] 1157 | } 1158 | 1159 | ``` 1160 | 运行`npx webpack` 1161 | 1162 | 从2057ms -> 1946 ms 1163 | 1164 | ## 优化:规则匹配设置范围 1165 | 1166 | ``` 1167 | rules: [ 1168 | { 1169 | test: /\.js$/, 1170 | exclude: '/node_modules/', // 排除 1171 | include: path.resolve('src'), // 在这个范围内 1172 | use: { 1173 | loader: 'babel-loader', 1174 | options: { 1175 | presets: [ 1176 | '@babel/preset-env', 1177 | '@babel/preset-react' 1178 | ] 1179 | } 1180 | } 1181 | } 1182 | ``` 1183 | 1184 | 尽量实用`include`,不使用`exclude`,使用绝对路径 1185 | 1186 | ## 优化:忽略依赖中不必要的语言包 1187 | `yarn add moment webpack-dev-server -D` 1188 | 1189 | 忽略掉`moment`的其他语言包 1190 | 1191 | ``` 1192 | let webpack = require('webpack') 1193 | 1194 | plugins: [ 1195 | new webpack.IgnorePlugin(/\.\/locale/, /moment/) 1196 | ] 1197 | 1198 | ``` 1199 | 1200 | `index.js` 1201 | 1202 | ``` 1203 | import moment from 'moment' 1204 | 1205 | let r = moment().endOf('day').fromNow() // 距离现在多少天 1206 | console.log(r); 1207 | ``` 1208 | 1209 | 1210 | 从 1.2MB 到 800kb 1211 | 1212 | ## 动态链接库 1213 | 1214 | `yarn add react react-dom` 1215 | 1216 | 正常使用 1217 | 1218 | `webpack.config.js` 1219 | 1220 | ``` 1221 | { 1222 | test: /\.js$/, 1223 | exclude: '/node_modules/', 1224 | include: path.resolve('src'), 1225 | use: { 1226 | loader: 'babel-loader', 1227 | options: { 1228 | presets: [ 1229 | '@babel/preset-env', 1230 | '@babel/preset-react' 1231 | ] 1232 | } 1233 | } 1234 | } 1235 | ``` 1236 | 1237 | `index.js` 1238 | ``` 1239 | import React from 'react' 1240 | 1241 | import {render} from 'react-dom' 1242 | 1243 | 1244 | render(

    111111

    , window.root) 1245 | ``` 1246 | 1247 | `index.html` 1248 | 1249 | ``` 1250 |
    1251 | ``` 1252 | 1253 | 独立的将`react react-dom` 打包好, 打包好再引用,从而减少`webpack`每次都要打包`react` 1254 | 1255 | 创建`webpack.config.react.js` 1256 | 1257 | 1258 | ``` 1259 | let path = require('path') 1260 | let webpack = require('webpack') 1261 | module.exports = { 1262 | mode: 'development', 1263 | entry: { 1264 | // test: './src/test.js' 1265 | react: ['react', 'react-dom'] 1266 | }, 1267 | output: { 1268 | filename: '_dll_[name].js', // 产生的文件名 1269 | path: path.resolve(__dirname, 'dist'), 1270 | library: '_dll_[name]', // 给输出的结果加个名字 1271 | // libraryTarget: 'var' // 配置如何暴露 library 1272 | // commonjs 结果放在export属性上, umd统一资源模块, 默认是var 1273 | }, 1274 | plugins: [ 1275 | new webpack.DllPlugin({ 1276 | name: '_dll_[name]', // name === library 1277 | path: path.resolve(__dirname, 'dist', 'manifest.json') // manifest.json 定义了各个模块的路径 1278 | }) 1279 | ] 1280 | } 1281 | ``` 1282 | 1283 | [libraryTarget](https://webpack.docschina.org/configuration/output/#%E6%9A%B4%E9%9C%B2%E4%B8%BA%E4%B8%80%E4%B8%AA%E5%8F%98%E9%87%8F) 1284 | 1285 | `manifest.json`就是一个任务清单or动态链接库,在这个清单里面查找react 1286 | 1287 | `npx webpack --config webpack.config.react.js` 1288 | 1289 | 在`index.html`增加引用 1290 | 1291 | ``` 1292 | 1293 |
    1294 | 1295 | 1296 | ``` 1297 | 1298 | 在webpack.config.js 中配置,现在动态链接库`manifest.json`中查找,如果没有再打包react 1299 | ``` 1300 | plugins: [ 1301 | new webpack.DllReferencePlugin({ 1302 | manifest: path.resolve(__dirname, 'dist', 'manifest.json') 1303 | }) 1304 | ] 1305 | 1306 | ``` 1307 | 1308 | [DLLPlugin 和 DLLReferencePlugin](https://webpack.docschina.org/plugins/dll-plugin/#src/components/Sidebar/Sidebar.jsx) 1309 | 1310 | `npm run build` 1311 | 1312 | 打包后的`bunle.js`文件变小 1313 | 1314 | `npm run dev` 1315 | 1316 | 可以理解为先把react打包,后面每次都直接使用react打包后的结果 1317 | 1318 | ## 多线程打包`happypack` 1319 | 1320 | `yarn add happypack` 1321 | 1322 | `webpack.config.js` 1323 | 1324 | ``` 1325 | let Happypack = require('happypack') 1326 | 1327 | 1328 | rules: [ 1329 | { 1330 | test: /\.js$/, 1331 | exclude: '/node_modules/', 1332 | include: path.resolve('src'), 1333 | use: 'happypack/loader?id=js' 1334 | }, 1335 | ] 1336 | 1337 | plugins: [ 1338 | new Happypack({ 1339 | id: 'js', 1340 | use: [{ 1341 | loader: 'babel-loader', 1342 | options: { 1343 | presets: [ 1344 | '@babel/preset-env', 1345 | '@babel/preset-react' 1346 | ] 1347 | } 1348 | }] 1349 | }) 1350 | ] 1351 | ``` 1352 | 1353 | js启用多线程,由于启用多线程也会浪费时间,因此当项目比较大的时候启用效果更好 1354 | 1355 | css启用多线程 1356 | ``` 1357 | 1358 | { 1359 | test: /\.css$/, 1360 | use: 'happypack/loader?id=css' 1361 | } 1362 | 1363 | new Happypack({ 1364 | id: 'css', 1365 | use: ['style-loader', 'css-loader'] 1366 | }), 1367 | ``` 1368 | 1369 | ## webpack 自带的优化 1370 | 1371 | `test.js` 1372 | 1373 | ``` 1374 | let sum = (a, b) => { 1375 | return a + b + 'sum' 1376 | } 1377 | 1378 | let minus = (a, b) => { 1379 | return a - b + 'minus'; 1380 | } 1381 | 1382 | export default { 1383 | sum, minus 1384 | } 1385 | ``` 1386 | 1387 | 1. 使用import 1388 | 1389 | `index.js` 1390 | 1391 | ``` 1392 | import calc from './test' 1393 | 1394 | console.log(calc.sum(1, 2)); 1395 | ``` 1396 | 1397 | 1398 | import在生产环境下会自动去除没有用的代码`minus`,这叫`tree-shaking`,将没有用的代码自动删除掉 1399 | 1400 | 1401 | `index.js` 1402 | 1403 | ``` 1404 | let calc = require('./test') 1405 | console.log(calc); // es 6导出,是一个default的对象 1406 | console.log(calc.default.sum(1, 2)); 1407 | ``` 1408 | 1409 | require引入es6 模块会把结果放在default上,打包build后并不会把多余`minus`代码删除掉,不支持`tree-shaking` 1410 | 1411 | 1412 | 2. 作用域的提升 1413 | 1414 | `index.js` 1415 | 1416 | ``` 1417 | let a = 1 1418 | let b = 2 1419 | let c = 3 1420 | let d = a + b + c 1421 | 1422 | console.log(d, '---------'); 1423 | ``` 1424 | 打包出来的文件 1425 | 1426 | ``` 1427 | console.log(r.default.sum(1,2));console.log(6,"---------") 1428 | ``` 1429 | 1430 | 在webpack中可以省略一些可以简化的代码 1431 | 1432 | ## 抽取公共代码 1433 | 1434 | 1. 抽离自有模块 1435 | 1436 | `webpack.config.js` 1437 | 1438 | ``` 1439 | module.exports = { 1440 | optimization: { 1441 | splitChunks: { // 分割代码块,针对多入口 1442 | cacheGroups: { // 缓存组 1443 | common: { // 公共模块 1444 | minSize: 0, // 大于多少抽离 1445 | minChunks: 2, // 使用多少次以上抽离抽离 1446 | chunks: 'initial' // 从什么地方开始, 从入口开始 1447 | } 1448 | } 1449 | } 1450 | }, 1451 | } 1452 | ``` 1453 | [SplitChunksPlugin](https://webpack.docschina.org/plugins/split-chunks-plugin/) 1454 | 1455 | 1456 | 分别有a.js和b.js, index.js和other.js分别引入a和b两个js 1457 | 1458 | `index.js` 1459 | 1460 | ``` 1461 | import './a' 1462 | import './b' 1463 | 1464 | console.log('index.js'); 1465 | ``` 1466 | 1467 | `other.js` 1468 | 1469 | ``` 1470 | import './a' 1471 | import './b' 1472 | 1473 | console.log('other.js'); 1474 | ``` 1475 | 1476 | `webpack.config.js` 1477 | 1478 | ``` 1479 | module.exports = { 1480 | optimization: { 1481 | splitChunks: { // 分割代码块,针对多入口 1482 | cacheGroups: { // 缓存组 1483 | common: { // 公共模块 1484 | minSize: 0, // 大于多少抽离 1485 | minChunks: 2, // 使用多少次以上抽离抽离 1486 | chunks: 'initial' // 从什么地方开始, 从入口开始 1487 | } 1488 | } 1489 | } 1490 | }, 1491 | } 1492 | ``` 1493 | 1494 | 2. 抽离第三方模块 1495 | 1496 | 比如jquery 1497 | 1498 | `index.js` 和 `other.js`分别引入 1499 | 1500 | ``` 1501 | import $ from 'jquery' 1502 | 1503 | console.log($); 1504 | ``` 1505 | 1506 | 修改`webpack.config.js`配置: 1507 | 1508 | ``` 1509 | optimization: { 1510 | splitChunks: { // 分割代码块,针对多入口 1511 | cacheGroups: { // 缓存组 1512 | common: { // 公共模块 1513 | minSize: 0, // 大于多少抽离 1514 | minChunks: 2, // 使用多少次以上抽离抽离 1515 | chunks: 'initial' // 从什么地方开始,刚开始 1516 | }, 1517 | vendor: { 1518 | priority: 1, // 增加权重, (先抽离第三方) 1519 | test: /node_modules/, // 把此目录下的抽离 1520 | minSize: 0, // 大于多少抽离 1521 | minChunks: 2, // 使用多少次以上抽离抽离 1522 | chunks: 'initial' // 从什么地方开始,刚开始 1523 | } 1524 | } 1525 | }, 1526 | }, 1527 | ``` 1528 | 1529 | ## 懒加载(延迟加载) 1530 | 1531 | `yarn add @babel/plugin-syntax-dynamic-import -D` 1532 | 1533 | `source.js` 1534 | 1535 | ``` 1536 | export default 'mayufo' 1537 | ``` 1538 | 1539 | `index.js` 1540 | 1541 | ``` 1542 | let button = document.createElement('button') 1543 | button.innerHTML = 'hello' 1544 | button.addEventListener('click', function () { 1545 | console.log('click') 1546 | // es6草案中的语法,jsonp实现动态加载文件 1547 | import('./source.js').then(data => { 1548 | console.log(data.default) 1549 | }) 1550 | }) 1551 | document.body.appendChild(button) 1552 | 1553 | ``` 1554 | 1555 | `webpack.config.js` 1556 | 1557 | ``` 1558 | { 1559 | test: /\.js$/, 1560 | exclude: '/node_modules/', 1561 | include: path.resolve('src'), 1562 | use: [{ 1563 | loader: 'babel-loader', 1564 | options: { 1565 | presets: [ 1566 | '@babel/preset-env', 1567 | '@babel/preset-react' 1568 | ], 1569 | plugins: [ 1570 | '@babel/plugin-syntax-dynamic-import' 1571 | ] 1572 | } 1573 | }] 1574 | } 1575 | ``` 1576 | 1577 | ## 热更新(当页面改变只更新改变的部分,不重新打包) 1578 | 1579 | `webpack.config.js` 1580 | 1581 | ``` 1582 | plugins: [ 1583 | new HtmlWebpackPlugin({ 1584 | template: './src/index.html', 1585 | filename: 'index.html' 1586 | }), 1587 | new webpack.NameModulesPlugin(), // 打印更新的模块路径 1588 | new webpack.HotModuleReplacementPlugin() // 热更新插件 1589 | ] 1590 | ``` 1591 | 1592 | `index.js` 1593 | 1594 | ``` 1595 | 1596 | import str from './source' 1597 | 1598 | console.log(str); 1599 | 1600 | if (module.hot) { 1601 | module.hot.accept('./source', () => { 1602 | console.log('文件更新了'); 1603 | require('./source') 1604 | console.log(str); 1605 | }) 1606 | } 1607 | 1608 | ``` 1609 | 1610 | ## tapable介绍 - SyncHook 1611 | 1612 | [tapable](https://juejin.im/post/5abf33f16fb9a028e46ec352) 1613 | 1614 | `webpack`本质上是一种事件流的机制,它的工作流程就是将各个插件串联起来,而实现这一切的核心就是`Tapable`,`webpack`中最核心的负责编译的`Compiler`和负责创建`bundles`的`Compilation`都是`Tapable`的实例。 1615 | 1616 | `SyncHook` 不关心监听函数的返回值 1617 | 1618 | `yarn add tabable` 1619 | 1620 | `1.use.js` 1621 | 1622 | ``` 1623 | let {SyncHook} = require('tapable') // 结构同步勾子 1624 | 1625 | 1626 | class Lesson { 1627 | constructor () { 1628 | this.hooks = { 1629 | // 订阅勾子 1630 | arch: new SyncHook(['name']), 1631 | } 1632 | } 1633 | start () { 1634 | this.hooks.arch.call('may') 1635 | } 1636 | tap () { // 注册监听函数 1637 | this.hooks.arch.tap('node', function (name) { 1638 | console.log('node', name) 1639 | }) 1640 | this.hooks.arch.tap('react', function (name) { 1641 | console.log('react', name) 1642 | }) 1643 | } 1644 | } 1645 | 1646 | 1647 | let l = new Lesson() 1648 | 1649 | l.tap(); //注册两个函数 1650 | l.start() // 启动勾子 1651 | 1652 | ``` 1653 | 1654 | `1.theory.js` 1655 | 1656 | ``` 1657 | class SyncHook { // 勾子是同步的 1658 | constructor(args) { // args => ['name'] 1659 | this.tasks = [] 1660 | } 1661 | tap (name, task) { 1662 | this.tasks.push(task) 1663 | } 1664 | call (...args) { 1665 | this.tasks.forEach((task) => task(...args)) 1666 | } 1667 | } 1668 | 1669 | let hook = new SyncHook(['name']) 1670 | 1671 | hook.tap('react', function (name) { 1672 | console.log('react', name); 1673 | }) 1674 | 1675 | 1676 | hook.tap('node', function (name) { 1677 | console.log('node', name); 1678 | }) 1679 | 1680 | 1681 | hook.call('jw') 1682 | ``` 1683 | 1684 | 1685 | ## tapable介绍 - SyncBailHook 1686 | 1687 | `SyncBailHook`为勾子加了个保险,当`return`返回不是`undefine`就会停止 1688 | 1689 | `2.use.js` 1690 | 1691 | ``` 1692 | let {SyncBailHook} = require('tapable') // 解构同步勾子 1693 | 1694 | class Lesson { 1695 | constructor () { 1696 | this.hooks = { 1697 | // 订阅勾子 1698 | arch: new SyncBailHook(['name']), 1699 | 1700 | } 1701 | } 1702 | start () { 1703 | // 发布 1704 | this.hooks.arch.call('may') 1705 | } 1706 | tap () { // 注册监听函数,订阅 1707 | this.hooks.arch.tap('node', function (name) { 1708 | console.log('node', name) 1709 | return '停止学习' // 会停止 1710 | // return undefined 1711 | }) 1712 | this.hooks.arch.tap('react', function (name) { 1713 | console.log('react', name) 1714 | }) 1715 | } 1716 | } 1717 | 1718 | 1719 | let l = new Lesson() 1720 | 1721 | l.tap(); //注册两个函数 1722 | l.start() // 启动勾子 1723 | 1724 | ``` 1725 | 1726 | `2.theory.js` 1727 | 1728 | ``` 1729 | class SyncBailHook { // 勾子是同步的 1730 | constructor(args) { // args => ['name'] 1731 | this.tasks = [] 1732 | } 1733 | tap (name, task) { 1734 | this.tasks.push(task) 1735 | } 1736 | call (...args) { 1737 | let ret; // 当前函数的返回值 1738 | let index = 0; // 当前要执行的第一个 1739 | do { 1740 | ret = this.tasks[index](...args) 1741 | } while (ret === undefined && index < this.tasks.length) 1742 | } 1743 | } 1744 | 1745 | let hook = new SyncBailHook(['name']) 1746 | 1747 | hook.tap('react', function (name) { 1748 | console.log('react', name); 1749 | return '停止学习' 1750 | // return undefined 1751 | }) 1752 | 1753 | 1754 | hook.tap('node', function (name) { 1755 | console.log('node', name); 1756 | }) 1757 | 1758 | 1759 | hook.call('jw') 1760 | 1761 | ``` 1762 | 1763 | ## tapable介绍 - SyncWaterfallHook 1764 | 1765 | `SyncWaterfallHook`上一个监听函数的返回值可以传给下一个监听函数 1766 | 1767 | `3.use.js` 1768 | 1769 | ``` 1770 | let {SyncWaterfallHook} = require('tapable') // 解构同步勾子 1771 | 1772 | // waterfall 瀑布 1773 | 1774 | class Lesson { 1775 | constructor () { 1776 | this.hooks = { 1777 | // 订阅勾子 1778 | arch: new SyncWaterfallHook(['name']), 1779 | 1780 | } 1781 | } 1782 | start () { 1783 | // 发布 1784 | this.hooks.arch.call('may') 1785 | } 1786 | tap () { // 注册监听函数,订阅 1787 | this.hooks.arch.tap('node', function (name) { 1788 | console.log('node', name) 1789 | return '学的不错' 1790 | }) 1791 | this.hooks.arch.tap('react', function (name) { 1792 | console.log('react', name) 1793 | }) 1794 | } 1795 | } 1796 | 1797 | 1798 | let l = new Lesson() 1799 | 1800 | l.tap(); //注册两个函数 1801 | l.start() // 启动勾子 1802 | 1803 | ``` 1804 | 1805 | `3.theory.js` 1806 | 1807 | ``` 1808 | class SyncWaterfallHook { // 勾子是同步的 - 瀑布 1809 | constructor(args) { // args => ['name'] 1810 | this.tasks = [] 1811 | } 1812 | tap (name, task) { 1813 | this.tasks.push(task) 1814 | } 1815 | call (...args) { 1816 | let [first, ...others] = this.tasks; 1817 | let ret = first(...args) 1818 | others.reduce((a, b) => { 1819 | return b(a); 1820 | }, ret); 1821 | 1822 | } 1823 | } 1824 | 1825 | let hook = new SyncWaterfallHook(['name']) 1826 | 1827 | hook.tap('react', function (name) { 1828 | console.log('react', name); 1829 | return 'react Ok' 1830 | // return undefined 1831 | }) 1832 | 1833 | 1834 | hook.tap('node', function (name) { 1835 | console.log('node', name); 1836 | return 'node Ok' 1837 | }) 1838 | 1839 | hook.tap('webpack', function (data) { 1840 | console.log('webpack', data); 1841 | }) 1842 | 1843 | 1844 | 1845 | hook.call('jw') 1846 | 1847 | 1848 | ``` 1849 | 1850 | ## tapable介绍 - SyncLoopHook 1851 | 1852 | `SyncLoopHook`当监听函数被触发的时候,如果该监听函数返回`true`时则这个监听函数会反复执行,如果返回 `undefined` 则表示退出循环 1853 | 1854 | `4.use.js` 1855 | 1856 | ``` 1857 | let {SyncLoopHook} = require('tapable') // 解构同步勾子 1858 | 1859 | // 不返回undefined 会多次执行 1860 | 1861 | class Lesson { 1862 | constructor () { 1863 | this.index = 0 1864 | this.hooks = { 1865 | // 订阅勾子 1866 | arch: new SyncLoopHook(['name']), 1867 | 1868 | } 1869 | } 1870 | start () { 1871 | // 发布 1872 | this.hooks.arch.call('may') 1873 | } 1874 | tap () { // 注册监听函数,订阅 1875 | this.hooks.arch.tap('node', (name) => { 1876 | console.log('node', name) 1877 | return ++this.index === 3 ? undefined : '继续学' 1878 | }) 1879 | this.hooks.arch.tap('react', (name) => { 1880 | console.log('react', name) 1881 | }) 1882 | } 1883 | } 1884 | 1885 | 1886 | let l = new Lesson() 1887 | 1888 | l.tap(); //注册两个函数 1889 | l.start() // 启动勾子 1890 | 1891 | ``` 1892 | 1893 | `4.theory.js` 1894 | 1895 | ``` 1896 | class SyncLoopHook { // 勾子是同步的 - 瀑布 1897 | constructor(args) { // args => ['name'] 1898 | this.tasks = [] 1899 | } 1900 | tap (name, task) { 1901 | this.tasks.push(task) 1902 | } 1903 | call (...args) { 1904 | this.tasks.forEach(task => { 1905 | let ret 1906 | do { 1907 | ret = task(...args); 1908 | } while(ret !== undefined) 1909 | }) 1910 | } 1911 | } 1912 | 1913 | let hook = new SyncLoopHook(['name']) 1914 | let total = 0 1915 | hook.tap('react', function (name) { 1916 | console.log('react', name); 1917 | return ++total === 3 ? undefined: '继续学' 1918 | }) 1919 | 1920 | 1921 | hook.tap('node', function (name) { 1922 | console.log('node', name); 1923 | }) 1924 | 1925 | hook.tap('webpack', function (data) { 1926 | console.log('webpack', data); 1927 | }) 1928 | 1929 | 1930 | 1931 | hook.call('jw') 1932 | 1933 | ``` 1934 | 1935 | 1936 | ## `AsyncParallelHook` 与 `AsyncParallelBailHook` 1937 | 1938 | 异步的勾子分两种`串行`和`并行` 1939 | 1940 | `并行`等待所有并发的异步事件执行后执行回调 1941 | 1942 | 注册的三种方法 1943 | 1944 | 1. 异步的注册方法`tap` 1945 | 2. 异步的注册方法`tapAsync`, 还有个回调参数 1946 | 3. `topPromise`,注册`promise` 1947 | 1948 | 调用的三种 1949 | 1950 | 1. call (同步) 1951 | 2. callAsync (异步) 1952 | 3. promise (异步) 1953 | 1954 | 这里介绍的是异步并行的 1955 | 1956 | #### AsyncParallelHook 1957 | 1958 | 不关心监听函数的返回值。 1959 | 1960 | `5.use.js` 1961 | 1962 | ``` 1963 | let {AsyncParallelHook} = require('tapable') // 解构同步勾子 1964 | 1965 | // 不返回undefined 会多次执行 1966 | 1967 | class Lesson { 1968 | constructor() { 1969 | this.index = 0 1970 | this.hooks = { 1971 | // 订阅勾子 1972 | arch: new AsyncParallelHook(['name']), 1973 | 1974 | } 1975 | } 1976 | 1977 | start() { 1978 | // 发布callAsync 1979 | // this.hooks.arch.callAsync('may', function () { 1980 | // console.log('end'); 1981 | // }) 1982 | // 另一种发布promise 1983 | this.hooks.arch.promise('may').then(function () { 1984 | console.log('end'); 1985 | } 1986 | ) 1987 | } 1988 | 1989 | tap() { // 注册监听函数,订阅 1990 | // 注册tapAsync 1991 | // this.hooks.arch.tapAsync('node', (name, callback) => { 1992 | // setTimeout(() => { 1993 | // console.log('node', name) 1994 | // callback() 1995 | // }, 1000) 1996 | // }) 1997 | // this.hooks.arch.tapAsync('react', (name, callback) => { 1998 | // setTimeout(() => { 1999 | // console.log('react', name) 2000 | // callback() 2001 | // }, 1000) 2002 | // }) 2003 | // 另一种订阅 tapPromise 2004 | this.hooks.arch.tapPromise('node', (name) => { 2005 | return new Promise((resolve, reject) => { 2006 | setTimeout(() => { 2007 | console.log('node', name) 2008 | resolve() 2009 | }, 1000) 2010 | }) 2011 | }) 2012 | this.hooks.arch.tapPromise('react', (name) => { 2013 | return new Promise((resolve, reject) => { 2014 | setTimeout(() => { 2015 | console.log('react', name) 2016 | resolve() 2017 | }, 1000) 2018 | }) 2019 | }) 2020 | } 2021 | } 2022 | 2023 | 2024 | let l = new Lesson() 2025 | 2026 | l.tap(); //注册两个函数 2027 | l.start() // 启动勾子 2028 | 2029 | 2030 | ``` 2031 | 2032 | 2033 | `5.theory.js` 2034 | 2035 | ``` 2036 | class AsyncParallelHook { // 勾子是同步的 - 瀑布 2037 | constructor(args) { // args => ['name'] 2038 | this.tasks = [] 2039 | } 2040 | 2041 | tapAsync(name, task) { 2042 | this.tasks.push(task) 2043 | } 2044 | 2045 | tapPromise(name, task) { 2046 | this.tasks.push(task) 2047 | } 2048 | callAsync(...args) { 2049 | let finalCallback = args.pop() // 拿出最终的函数 2050 | let index = 0 2051 | let done = () => { // 类似promise.all的实现 2052 | index++; 2053 | if (index === this.tasks.length) { 2054 | finalCallback(); 2055 | } 2056 | } 2057 | this.tasks.forEach(task => { 2058 | task(...args, done) // 这里的args 已经把最后一个参数删掉 2059 | }) 2060 | } 2061 | 2062 | promise(...args) { 2063 | let tasks = this.tasks.map(task => task(...args)) 2064 | return Promise.all(tasks) 2065 | } 2066 | } 2067 | 2068 | let hook = new AsyncParallelHook(['name']) 2069 | 2070 | 2071 | // hook.tapAsync('react', function (name, callback) { 2072 | // setTimeout(() => { 2073 | // console.log('react', name); 2074 | // callback() 2075 | // }, 1000) 2076 | // }) 2077 | // 2078 | // hook.tapAsync('node', function (name, callback) { 2079 | // setTimeout(() => { 2080 | // console.log('node', name); 2081 | // callback() 2082 | // }, 1000) 2083 | // }) 2084 | 2085 | // hook.tapAsync('webpack', function (name, callback) { 2086 | // setTimeout(() => { 2087 | // console.log('webpack', name); 2088 | // callback() 2089 | // }, 1000) 2090 | // }) 2091 | 2092 | hook.tapPromise('react', function (name, callback) { 2093 | return new Promise((resolve, reject) => { 2094 | setTimeout(() => { 2095 | console.log('react', name); 2096 | resolve() 2097 | }, 1000) 2098 | }) 2099 | }) 2100 | 2101 | hook.tapPromise('node', function (name, callback) { 2102 | return new Promise((resolve, reject) => { 2103 | setTimeout(() => { 2104 | console.log('node', name); 2105 | resolve() 2106 | }, 1000) 2107 | }) 2108 | }) 2109 | 2110 | 2111 | 2112 | // 2113 | // hook.callAsync('jw', function () { 2114 | // console.log('end'); 2115 | // }) 2116 | 2117 | 2118 | hook.promise('jw').then(function () { 2119 | console.log('end'); 2120 | }) 2121 | 2122 | 2123 | ``` 2124 | 2125 | 2126 | #### AsyncParallelBailHook 2127 | 2128 | 只要监听函数的返回值不为 `null`,就会忽略后面的监听函数执行,直接跳跃到`callAsync`等触发函数绑定的回调函数,然后执行这个被绑定的回调函数。 2129 | 2130 | 使用和原理与`SyncBailHook`相似 2131 | 2132 | 2133 | ## 异步串行 —— AsyncSeriesHook 2134 | 2135 | `串行 `one by one 2136 | 2137 | `6.use.js` 2138 | 2139 | ``` 2140 | let {AsyncSeriesHook} = require('tapable') // 解构同步勾子 2141 | 2142 | 2143 | class Lesson { 2144 | constructor() { 2145 | this.index = 0 2146 | this.hooks = { 2147 | // 订阅勾子 2148 | arch: new AsyncSeriesHook(['name']), 2149 | 2150 | } 2151 | } 2152 | 2153 | start() { 2154 | // 发布 2155 | // this.hooks.arch.callAsync('may', function () { 2156 | // console.log('end'); 2157 | // }) 2158 | // 另一种发布 2159 | this.hooks.arch.promise('may').then(function () { 2160 | console.log('end'); 2161 | } 2162 | ) 2163 | } 2164 | 2165 | tap() { // 注册监听函数,订阅 2166 | // this.hooks.arch.tapAsync('node', (name, callback) => { 2167 | // setTimeout(() => { 2168 | // console.log('node', name) 2169 | // callback() 2170 | // }, 1000) 2171 | // }) 2172 | // this.hooks.arch.tapAsync('react', (name, callback) => { 2173 | // setTimeout(() => { 2174 | // console.log('react', name) 2175 | // callback() 2176 | // }, 1000) 2177 | // }) 2178 | // 另一种订阅 2179 | this.hooks.arch.tapPromise('node', (name) => { 2180 | return new Promise((resolve, reject) => { 2181 | setTimeout(() => { 2182 | console.log('node', name) 2183 | resolve() 2184 | }, 1000) 2185 | }) 2186 | }) 2187 | this.hooks.arch.tapPromise('react', (name) => { 2188 | return new Promise((resolve, reject) => { 2189 | setTimeout(() => { 2190 | console.log('react', name) 2191 | resolve() 2192 | }, 1000) 2193 | }) 2194 | }) 2195 | } 2196 | } 2197 | 2198 | 2199 | let l = new Lesson() 2200 | 2201 | l.tap(); //注册两个函数 2202 | l.start(); // 启动勾子 2203 | 2204 | ``` 2205 | 2206 | `6.theory.js` 2207 | 2208 | ``` 2209 | class AsyncSeriesHook { // 2210 | constructor(args) { // args => ['name'] 2211 | this.tasks = [] 2212 | } 2213 | 2214 | tapAsync(name, task) { 2215 | this.tasks.push(task) 2216 | } 2217 | 2218 | tapPromise(name, task) { 2219 | this.tasks.push(task) 2220 | } 2221 | 2222 | callAsync(...args) { 2223 | let finalCallback = args.pop() 2224 | let index = 0; 2225 | let next = () => { 2226 | if (this.tasks.length === index) return finalCallback(); 2227 | let task = this.tasks[index++]; 2228 | task(...args, next); 2229 | } 2230 | next(); 2231 | } 2232 | 2233 | promise(...args) { 2234 | // 将promise串联起来 2235 | let [first, ...other] = this.tasks 2236 | return other.reduce((p, n) => { 2237 | return p.then(() => n (...args)) 2238 | }, first(...args)) 2239 | } 2240 | } 2241 | 2242 | let hook = new AsyncSeriesHook(['name']) 2243 | 2244 | 2245 | // hook.tapAsync('react', function (name, callback) { 2246 | // setTimeout(() => { 2247 | // console.log('react', name); 2248 | // callback() 2249 | // }, 1000) 2250 | // }) 2251 | // 2252 | // hook.tapAsync('node', function (name, callback) { 2253 | // setTimeout(() => { 2254 | // console.log('node', name); 2255 | // callback() 2256 | // }, 1000) 2257 | // }) 2258 | // 2259 | // hook.tapAsync('webpack', function (name, callback) { 2260 | // setTimeout(() => { 2261 | // console.log('webpack', name); 2262 | // callback() 2263 | // }, 1000) 2264 | // }) 2265 | 2266 | 2267 | hook.tapPromise('react', function (name, callback) { 2268 | return new Promise((resolve, reject) => { 2269 | setTimeout(() => { 2270 | console.log('react', name); 2271 | resolve() 2272 | }, 1000) 2273 | }) 2274 | }) 2275 | 2276 | hook.tapPromise('node', function (name, callback) { 2277 | return new Promise((resolve, reject) => { 2278 | setTimeout(() => { 2279 | console.log('node', name); 2280 | resolve() 2281 | }, 1000) 2282 | }) 2283 | }) 2284 | 2285 | 2286 | 2287 | 2288 | // hook.callAsync('jw', function () { 2289 | // console.log('end'); 2290 | // }) 2291 | 2292 | 2293 | hook.promise('jw').then(function () { 2294 | console.log('end'); 2295 | }) 2296 | 2297 | ``` 2298 | 2299 | ## 异步串行 —— AsyncSeriesWaterfallHook 2300 | 2301 | 上一个监听函数的中的`callback(err, data)`的第二个参数,可以作为下一个监听函数的参数 2302 | 2303 | 2304 | `7.use.js` 2305 | 2306 | ``` 2307 | let {AsyncSeriesWaterfallHook} = require('tapable') // 解构同步勾子 2308 | 2309 | 2310 | class Lesson { 2311 | constructor() { 2312 | this.index = 0 2313 | this.hooks = { 2314 | // 订阅勾子 2315 | arch: new AsyncSeriesWaterfallHook(['name']), 2316 | 2317 | } 2318 | } 2319 | 2320 | start() { 2321 | // 发布 2322 | this.hooks.arch.callAsync('may', function () { 2323 | console.log('end'); 2324 | }) 2325 | // 另一种发布 2326 | // this.hooks.arch.promise('may').then(function () { 2327 | // console.log('end'); 2328 | // } 2329 | // ) 2330 | } 2331 | 2332 | tap() { // 注册监听函数,订阅 2333 | this.hooks.arch.tapAsync('node', (name, callback) => { 2334 | setTimeout(() => { 2335 | console.log('node', name) 2336 | // callback(null, 'result') 2337 | callback('error', 'result') // 如果放error, 会跳过直接后面的勾子,直接走到最终的 2338 | 2339 | }, 1000) 2340 | }) 2341 | this.hooks.arch.tapAsync('react', (name, callback) => { 2342 | setTimeout(() => { 2343 | console.log('react', name) 2344 | callback() 2345 | }, 1000) 2346 | }) 2347 | // 另一种订阅 2348 | // this.hooks.arch.tapPromise('node', (name) => { 2349 | // return new Promise((resolve, reject) => { 2350 | // setTimeout(() => { 2351 | // console.log('node', name) 2352 | // resolve() 2353 | // }, 1000) 2354 | // }) 2355 | // }) 2356 | // this.hooks.arch.tapPromise('react', (name) => { 2357 | // return new Promise((resolve, reject) => { 2358 | // setTimeout(() => { 2359 | // console.log('react', name) 2360 | // resolve() 2361 | // }, 1000) 2362 | // }) 2363 | // }) 2364 | } 2365 | } 2366 | 2367 | 2368 | let l = new Lesson() 2369 | 2370 | l.tap(); //注册两个函数 2371 | l.start(); // 启动勾子 2372 | 2373 | ``` 2374 | 2375 | `7.theory.js` 2376 | 2377 | ``` 2378 | class AsyncSeriesWaterfallHook { // 2379 | constructor(args) { // args => ['name'] 2380 | this.tasks = [] 2381 | } 2382 | 2383 | tapAsync(name, task) { 2384 | this.tasks.push(task) 2385 | } 2386 | 2387 | tapPromise(name, task) { 2388 | this.tasks.push(task) 2389 | } 2390 | callAsync(...args) { 2391 | let finalCallback = args.pop() 2392 | let index = 0; 2393 | let next = (err, data) => { 2394 | let task = this.tasks[index] 2395 | if(!task) return finalCallback(); 2396 | if (index === 0) { 2397 | // 执行的第一个 2398 | task(...args, next) 2399 | } else { 2400 | task(data, next) 2401 | } 2402 | index ++ 2403 | } 2404 | next(); 2405 | } 2406 | 2407 | promise(...args) { 2408 | // 将promise串联起来 2409 | let [first, ...other] = this.tasks 2410 | return other.reduce((p, n) => { 2411 | return p.then((data) => n(data)) 2412 | }, first(...args)) 2413 | } 2414 | } 2415 | 2416 | let hook = new AsyncSeriesWaterfallHook(['name']) 2417 | 2418 | 2419 | // hook.tapAsync('react', function (name, callback) { 2420 | // setTimeout(() => { 2421 | // console.log('react', name); 2422 | // callback(null, '结果1') 2423 | // }, 1000) 2424 | // }) 2425 | // 2426 | // hook.tapAsync('node', function (name, callback) { 2427 | // setTimeout(() => { 2428 | // console.log('node', name); 2429 | // callback(null, '结果2') 2430 | // }, 1000) 2431 | // }) 2432 | // 2433 | // hook.tapAsync('webpack', function (name, callback) { 2434 | // setTimeout(() => { 2435 | // console.log('webpack', name); 2436 | // callback() 2437 | // }, 1000) 2438 | // }) 2439 | 2440 | // 2441 | hook.tapPromise('react', function (name, callback) { 2442 | return new Promise((resolve, reject) => { 2443 | setTimeout(() => { 2444 | console.log('react', name); 2445 | resolve('result') 2446 | }, 1000) 2447 | }) 2448 | }) 2449 | 2450 | hook.tapPromise('node', function (name, callback) { 2451 | return new Promise((resolve, reject) => { 2452 | setTimeout(() => { 2453 | console.log('node', name); 2454 | resolve() 2455 | }, 1000) 2456 | }) 2457 | }) 2458 | 2459 | 2460 | // 2461 | // 2462 | // hook.callAsync('jw', function () { 2463 | // console.log('end'); 2464 | // }) 2465 | 2466 | 2467 | hook.promise('jw').then(function () { 2468 | console.log('end'); 2469 | }) 2470 | 2471 | ``` 2472 | 2473 | 2474 | ## 手写webpack 2475 | 2476 | [对应的may-pack项目](https://github.com/mayufo/webpack-training) 2477 | 2478 | 2479 | `yarn add webpack webpack-cli -D` 2480 | 2481 | 2482 | `webpack.config.js` 2483 | 2484 | ``` 2485 | let path = require('path') 2486 | 2487 | module.exports = { 2488 | mode: 'development', 2489 | entry: './src/index.js', 2490 | output: { 2491 | filename: 'bundle.js', 2492 | path: path.resolve(__dirname, 'dist') 2493 | } 2494 | } 2495 | ``` 2496 | 2497 | `npx webpack` 2498 | 2499 | 生成文件`bundle.js` 2500 | 2501 | ``` 2502 | (function (modules) { 2503 | var installedModules = {}; 2504 | 2505 | function __webpack_require__(moduleId) { 2506 | 2507 | if (installedModules[moduleId]) { 2508 | return installedModules[moduleId].exports; 2509 | } 2510 | var module = installedModules[moduleId] = { 2511 | i: moduleId, 2512 | l: false, 2513 | exports: {} 2514 | }; 2515 | 2516 | modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); 2517 | 2518 | module.l = true; 2519 | 2520 | return module.exports; 2521 | } 2522 | 2523 | 2524 | // Load entry module and return exports 2525 | return __webpack_require__(__webpack_require__.s = "./src/index.js"); 2526 | }) 2527 | ({ 2528 | "./src/a.js": 2529 | (function (module, exports, __webpack_require__) { 2530 | eval("let b = __webpack_require__(/*! ./base/b */ \"./src/base/b.js\")\n\nmodule.exports = 'a'+ b\n\n\n\n//# sourceURL=webpack:///./src/a.js?"); 2531 | }), 2532 | "./src/base/b.js": 2533 | (function (module, exports) { 2534 | eval("module.exports = 'b'\n\n\n//# sourceURL=webpack:///./src/base/b.js?"); 2535 | }), 2536 | "./src/index.js": 2537 | (function (module, exports, __webpack_require__) { 2538 | eval(" let str = __webpack_require__(/*! ./a.js */ \"./src/a.js\")\n\n console.log(str);\n\n\n//# sourceURL=webpack:///./src/index.js?"); 2539 | }) 2540 | 2541 | }); 2542 | 2543 | ``` 2544 | 2545 | 新建项目用于自己的`webpack`,这里叫`may-pack` 2546 | 2547 | `yarn init` 2548 | 2549 | 如果在node里想执行命令,创建`bin`文件,再创建`may-pack.js` 2550 | 2551 | 配置`package.json` 2552 | 2553 | ``` 2554 | { 2555 | "name": "may-pack", 2556 | "version": "1.0.0", 2557 | "main": "index.js", 2558 | "license": "MIT", 2559 | "bin": { 2560 | "may-pack": "./bin/may-pack.js" 2561 | } 2562 | } 2563 | ``` 2564 | 2565 | `may-pack.js` 2566 | 2567 | ``` 2568 | #! /usr/bin/env node 2569 | 2570 | // node环境 2571 | 2572 | console.log('start'); 2573 | 2574 | ``` 2575 | 运行`npm link`将npm 模块链接到对应的运行项目中去,方便地对模块进行调试和测试 2576 | 2577 | 在想运行`may-pack`的项目中运行,`npm link may-pack` 得到 `start` 2578 | 2579 | ## webpack分析及处理 2580 | 2581 | `may-pack.js` 2582 | 2583 | ``` 2584 | #! /usr/bin/env node 2585 | 2586 | // node环境 2587 | 2588 | console.log('start'); 2589 | 2590 | let path = require('path') 2591 | 2592 | // 拿到配置文件webpack.config.js 2593 | let config = require(path.resolve('webpack.config.js')); 2594 | 2595 | 2596 | let Compiler = require('../lib/Compiler.js'); 2597 | 2598 | let compiler = new Compiler(config); 2599 | 2600 | // 标识运行编译 2601 | compiler.run() 2602 | 2603 | ``` 2604 | 2605 | 创建`lib`文件`Compiler.js` 2606 | 2607 | ``` 2608 | let path = require('path') 2609 | let fs = require('fs') 2610 | 2611 | class Compiler { 2612 | constructor(config) { 2613 | // entry output 2614 | this.config = config 2615 | // 需要保存入口文件的路径 2616 | this.entryId = ''; // './src/index.js' 2617 | // 需要保存所有的模块依赖 2618 | this.modules = {}; 2619 | this.entry = config.entry // 入口文件 2620 | // 工作目录 2621 | this.root = process.cwd(); // 当前运行npx的路径 2622 | } 2623 | 2624 | // 构建模块 2625 | buildModule(modulePath, isEntry) { 2626 | 2627 | } 2628 | 2629 | // 发射文件 2630 | emitFile() { 2631 | // 用数据 渲染想要的 2632 | } 2633 | 2634 | run() { 2635 | // 执行 创建模块的依赖关系 2636 | this.buildModule(path.resolve(this.root, this.entry), true) // path.resolve(this.root, this.entry) 得到入口文件的绝对路径 2637 | // 发射打包后的文件 2638 | this.emitFile() 2639 | } 2640 | } 2641 | 2642 | module.exports = Compiler 2643 | 2644 | ``` 2645 | 主要两个任务 2646 | 1. 拿到入口Id 2647 | 2. 解析模块,也就是实现`buildModule`方法 2648 | 2649 | ## 创建依赖关系 2650 | 2651 | `may-pack`中`Compiler.js` 2652 | 2653 | ``` 2654 | 2655 | let path = require('path') 2656 | let fs = require('fs') 2657 | // babylon 主要把源码转成ast Babylon 是 Babel 中使用的 JavaScript 解析器。 2658 | // @babel/traverse 对ast解析遍历语法树 负责替换,删除和添加节点 2659 | // @babel/types 用于AST节点的Lodash-esque实用程序库 2660 | // @babel/generator 结果生成 2661 | 2662 | let babylon = require('babylon') 2663 | let traverse = require('@babel/traverse').default; 2664 | let type = require('@babel/types'); 2665 | let generator = require('@babel/generator').default 2666 | class Compiler { 2667 | constructor(config) { 2668 | // entry output 2669 | this.config = config 2670 | // 需要保存入口文件的路径 2671 | this.entryId = ''; // './src/index.js' 2672 | // 需要保存所有的模块依赖 2673 | this.modules = {}; 2674 | this.entry = config.entry // 入口文件 2675 | // 工作目录 2676 | this.root = process.cwd(); // 当前运行npx的路径 2677 | 2678 | 2679 | } 2680 | // 拿到模块内容 2681 | getSource (modulePath) { 2682 | let content = fs.readFileSync(modulePath, 'utf8') 2683 | return content 2684 | } 2685 | parse (source, parentPath) { 2686 | console.log(source, parentPath) 2687 | } 2688 | // 构建模块 2689 | buildModule(modulePath, isEntry) { 2690 | // 拿到模块内容 2691 | let source = this.getSource(modulePath) // 得到入口文件的内容 2692 | // 模块id modulePath(需要相对路径) = modulePath(模块路径) - this.root(项目工作路径) src/index.js 2693 | let moduleName = './' + path.relative(this.root, modulePath) 2694 | console.log(source, moduleName); // 拿到代码 和相对路径 ./src/index.js 2695 | if (isEntry) { 2696 | this.entryId = moduleName 2697 | } 2698 | let {sourceCode, dependencies} = this.parse(source, path.dirname(moduleName)) // ./src 2699 | // 把相对路径和模块中的内容对应起来 2700 | this.modules[moduleName] = sourceCode 2701 | } 2702 | 2703 | // 发射文件 2704 | emitFile() { 2705 | // 用数据 渲染想要的 2706 | } 2707 | 2708 | run() { 2709 | // 执行 创建模块的依赖关系 2710 | this.buildModule(path.resolve(this.root, this.entry), true) // path.resolve(this.root, this.entry) 得到入口文件的绝对路径 2711 | console.log(this.modules, this.entryId); 2712 | // 发射打包后的文件 2713 | this.emitFile() 2714 | } 2715 | 2716 | 2717 | } 2718 | 2719 | module.exports = Compiler 2720 | 2721 | 2722 | ``` 2723 | 2724 | ## ast递归解析 2725 | 2726 | `parse`方法主要靠解析语法树来进行转义 2727 | `babylon` 主要把源码转成ast Babylon 是 Babel 中使用的 JavaScript 解析器。 2728 | `@babel/traverse` 对ast解析遍历语法树 负责替换,删除和添加节点 2729 | `@babel/types` 用于AST节点的Lodash-esque实用程序库 2730 | `@babel/generator` 结果生成 2731 | 2732 | 2733 | `yarn add babylon @babel/traverse @babel/types @babel/generator` 2734 | 2735 | `may-pack`中`Compiler.js` 2736 | 2737 | ``` 2738 | let path = require('path') 2739 | let fs = require('fs') 2740 | // babylon 主要把源码转成ast Babylon 是 Babel 中使用的 JavaScript 解析器。 2741 | // @babel/traverse 对ast解析遍历语法树 负责替换,删除和添加节点 2742 | // @babel/types 用于AST节点的Lodash-esque实用程序库 2743 | // @babel/generator 结果生成 2744 | 2745 | let babylon = require('babylon') 2746 | let traverse = require('@babel/traverse').default; 2747 | let type = require('@babel/types'); 2748 | let generator = require('@babel/generator').default 2749 | class Compiler { 2750 | constructor(config) { 2751 | // entry output 2752 | this.config = config 2753 | // 需要保存入口文件的路径 2754 | this.entryId = ''; // './src/index.js' 2755 | // 需要保存所有的模块依赖 2756 | this.modules = {}; 2757 | this.entry = config.entry // 入口文件 2758 | // 工作目录 2759 | this.root = process.cwd(); // 当前运行npx的路径 2760 | 2761 | 2762 | } 2763 | // 拿到模块内容 2764 | getSource (modulePath) { 2765 | let content = fs.readFileSync(modulePath, 'utf8') 2766 | return content 2767 | } 2768 | parse (source, parentPath) { 2769 | // AST解析语法树 2770 | let ast = babylon.parse(source) 2771 | let dependencies = []; // 依赖的数组 2772 | // https://astexplorer.net/ 2773 | traverse(ast, { 2774 | // 调用表达式 2775 | CallExpression(p) { 2776 | let node = p.node; //对应的节点 2777 | if(node.callee.name === 'require') { 2778 | node.callee.name = '__webpack_require__' 2779 | let moduledName = node.arguments[0].value // 取到模块的引用名字 2780 | moduledName = moduledName + (path.extname(moduledName) ? '': '.js'); // ./a.js 2781 | moduledName = './' + path.join(parentPath, moduledName) // './src/a.js' 2782 | dependencies.push(moduledName) 2783 | node.arguments = [type.stringLiteral(moduledName)] // 改掉源码 2784 | } 2785 | } 2786 | }) 2787 | let sourceCode = generator(ast).code 2788 | return { sourceCode, dependencies } 2789 | } 2790 | // 构建模块 2791 | buildModule(modulePath, isEntry) { 2792 | // 拿到模块内容 2793 | let source = this.getSource(modulePath) // 得到入口文件的内容 2794 | // 模块id modulePath(需要相对路径) = modulePath(模块路径) - this.root(项目工作路径) src/index.js 2795 | let moduleName = './' + path.relative(this.root, modulePath) 2796 | // console.log(source, moduleName); // 拿到代码 和相对路径 ./src/index.js 2797 | if (isEntry) { 2798 | this.entryId = moduleName 2799 | } 2800 | // 解析把source源码进行改造, 返回一个依赖列表 2801 | let {sourceCode, dependencies} = this.parse(source, path.dirname(moduleName)) // ./src 2802 | // 把相对路径和模块中的内容对应起来 2803 | this.modules[moduleName] = sourceCode 2804 | dependencies.forEach(dep => { // 附模块的加载 递归加载 2805 | this.buildModule(path.join(this.root, dep), false) 2806 | }) 2807 | } 2808 | 2809 | // 发射文件 2810 | emitFile() { 2811 | // 用数据 渲染想要的 2812 | } 2813 | 2814 | run() { 2815 | // 执行 创建模块的依赖关系 2816 | this.buildModule(path.resolve(this.root, this.entry), true) // path.resolve(this.root, this.entry) 得到入口文件的绝对路径 2817 | console.log(this.modules, this.entryId); 2818 | // 发射打包后的文件 2819 | this.emitFile() 2820 | } 2821 | 2822 | 2823 | } 2824 | 2825 | module.exports = Compiler 2826 | 2827 | ``` 2828 | 2829 | ## 生成打包工具 2830 | 2831 | 使用ejs模板 2832 | 2833 | `may-pack`中`main.ejs` 2834 | ``` 2835 | (function (modules) { 2836 | var installedModules = {}; 2837 | 2838 | function __webpack_require__(moduleId) { 2839 | 2840 | if (installedModules[moduleId]) { 2841 | return installedModules[moduleId].exports; 2842 | } 2843 | var module = installedModules[moduleId] = { 2844 | i: moduleId, 2845 | l: false, 2846 | exports: {} 2847 | }; 2848 | 2849 | modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); 2850 | 2851 | module.l = true; 2852 | 2853 | return module.exports; 2854 | } 2855 | 2856 | 2857 | // Load entry module and return exports 2858 | return __webpack_require__(__webpack_require__.s = "<%-entryId %>"); 2859 | })({ 2860 | <% for(let key in modules){ %> 2861 | "<%- key %>": 2862 | (function (module, exports,__webpack_require__) { 2863 | eval(`<%-modules[key] %>`); 2864 | }), 2865 | <% } %> 2866 | }); 2867 | 2868 | ``` 2869 | 2870 | [ejs入门](https://ejs.bootcss.com/) 2871 | 2872 | `yarn add ejs` 2873 | 2874 | 2875 | `may-pack`中`Compiler.js` 2876 | 2877 | ``` 2878 | let ejs = require('ejs') 2879 | ``` 2880 | 2881 | ``` 2882 | // 发射文件 2883 | emitFile() { 2884 | // 用数据 渲染想要的 2885 | // 输出到那个目录下 2886 | let main = path.join(this.config.output.path, this.config.output.filename) 2887 | let templateStr = this.getSource(path.join(__dirname, 'main.ejs')) 2888 | let code = ejs.render(templateStr, { entryId: this.entryId, modules: this.modules}) 2889 | this.assets = {} 2890 | // 路径对应的代码 2891 | this.assets[main] = code 2892 | fs.writeFileSync(main, this.assets[main]) 2893 | } 2894 | ``` 2895 | 2896 | 在`webpack-training`项目中运行`npx may-pack`, 得到`bundle.js`,运行得到结果 2897 | 2898 | ## 增加loader 2899 | 2900 | 创建`loader`文件夹,创建`less-loader1.js`和`style-loader1.js` 2901 | 2902 | `yarn add less` 2903 | 2904 | [less使用](http://lesscss.cn/#using-less) 2905 | 2906 | `less-loader1.js` 2907 | 2908 | ``` 2909 | // 将less转为css 2910 | let less = require('less') 2911 | 2912 | function loader(source) { 2913 | let css = '' 2914 | less.render(source, function (err, output) { 2915 | css = output.css 2916 | }) 2917 | css = css.replace(/\n/g, '\\n'); 2918 | return css 2919 | } 2920 | 2921 | module.exports = loader 2922 | 2923 | ``` 2924 | 2925 | `style-loader1.js` 2926 | 2927 | ``` 2928 | // 将css插入到html头部 2929 | function loader(source) { 2930 | console.log(111); 2931 | let style = ` 2932 | let style = document.createElement('style') 2933 | style.innerHTML = ${JSON.stringify(source)} 2934 | document.head.appendChild(style) 2935 | ` 2936 | return style 2937 | } 2938 | module.exports = loader 2939 | 2940 | 2941 | // JSON.stringify(source) 可以将代码转为一行 2942 | 2943 | ``` 2944 | 2945 | `webpack.config.js` 2946 | 2947 | ``` 2948 | let path = require('path') 2949 | 2950 | module.exports = { 2951 | mode: 'development', 2952 | entry: './src/index.js', 2953 | output: { 2954 | filename: 'bundle.js', 2955 | path: path.resolve(__dirname, 'dist') 2956 | }, 2957 | module: { 2958 | rules: [ 2959 | { 2960 | test: /\.less$/, 2961 | use: [ 2962 | path.resolve(__dirname, 'loader', 'style-loader1'), 2963 | path.resolve(__dirname, 'loader', 'less-loader1') 2964 | ] 2965 | } 2966 | ] 2967 | } 2968 | } 2969 | 2970 | ``` 2971 | 2972 | 创建`index.less` 2973 | 2974 | ``` 2975 | body { 2976 | background: red 2977 | } 2978 | ``` 2979 | 2980 | `index.js` 2981 | 2982 | ``` 2983 | let str = require('./a.js') 2984 | 2985 | require('./index.less') 2986 | 2987 | console.log(str); 2988 | 2989 | ``` 2990 | 2991 | `may-pack`中`Compiler.js` 2992 | 2993 | ``` 2994 | // 拿到模块内容 2995 | getSource (modulePath) { 2996 | // 匹配各种文件的规则 2997 | let rules= this.config.module.rules; // webpack.config.js 中rules的数组 2998 | let content = fs.readFileSync(modulePath, 'utf8') 2999 | 3000 | for (let i = 0; i < rules.length; i++) { 3001 | let rule = rules[i] 3002 | let {test, use} = rule 3003 | let len = use.length - 1 3004 | 3005 | if (test.test(modulePath)) { 3006 | // console.log(use[len]); 3007 | function normalLoader () { 3008 | // console.log(use[len--]); 3009 | let loader = require(use[len--]) 3010 | content = loader(content) 3011 | // 递归调用loader 实现转化 3012 | if (len >= 0) { 3013 | normalLoader() 3014 | } 3015 | } 3016 | normalLoader() 3017 | } 3018 | 3019 | } 3020 | return content 3021 | } 3022 | ``` 3023 | 3024 | 运行`npx may-pack` 3025 | 3026 | ## 增加plugins 3027 | 3028 | `yarn add tapable` 3029 | 3030 | `may-pack`中`Compiler.js` 3031 | 3032 | ``` 3033 | constructor(config) { 3034 | // entry output 3035 | this.config = config 3036 | // 需要保存入口文件的路径 3037 | this.entryId = ''; // './src/index.js' 3038 | // 需要保存所有的模块依赖 3039 | this.modules = {}; 3040 | this.entry = config.entry // 入口文件 3041 | // 工作目录 3042 | this.root = process.cwd(); // 当前运行npx的路径 3043 | 3044 | this.hooks = { 3045 | entryOption: new SyncHook(), // 入口选项 3046 | compile: new SyncHook(), // 编译 3047 | afterCompile: new SyncHook(), // 编译完成 3048 | afterPlugins: new SyncHook(), // 编译完插件 3049 | run: new SyncHook(), // 运行 3050 | emit: new SyncHook(), // 发射 3051 | done: new SyncHook() // 完成 3052 | } 3053 | // 如果传递了plugins参数 3054 | let plugins = this.config.plugins 3055 | if (Array.isArray(plugins)) { 3056 | plugins.forEach(plugin => { 3057 | plugin.apply(this); // 这里只是appLy方法不是改变this指向 3058 | }) 3059 | } 3060 | this.hooks.afterPlugins.call() 3061 | } 3062 | ``` 3063 | 3064 | 在`webpack.config.js`中写插件方法 3065 | 3066 | ``` 3067 | class P { 3068 | apply(compiler) { // 这里只是appLy方法不是改变this指向 3069 | // 绑定 3070 | compiler.hooks.emit.tap('emit', function () { 3071 | console.log('emit'); 3072 | }) 3073 | } 3074 | } 3075 | 3076 | class P1 { 3077 | apply(compiler) { // 这里只是appLy方法不是改变this指向 3078 | // 绑定 3079 | compiler.hooks.afterPlugins.tap('emit', function () { 3080 | console.log('afterPlugins'); 3081 | }) 3082 | } 3083 | } 3084 | 3085 | 3086 | 3087 | module.exports = { 3088 | mode: 'development', 3089 | entry: './src/index.js', 3090 | output: { 3091 | filename: 'bundle.js', 3092 | path: path.resolve(__dirname, 'dist') 3093 | }, 3094 | module: { 3095 | rules: [ 3096 | { 3097 | test: /\.less$/, 3098 | use: [ 3099 | path.resolve(__dirname, 'loader', 'style-loader'), 3100 | path.resolve(__dirname, 'loader', 'less-loader') 3101 | ] 3102 | } 3103 | ] 3104 | }, 3105 | plugins: [ 3106 | new P(), 3107 | new P1() 3108 | ] 3109 | } 3110 | ``` 3111 | 3112 | 然后在各个地方调用 3113 | 3114 | `may-pack`中`may-pack.js` 3115 | 3116 | 3117 | ``` 3118 | ..... 3119 | // 调用 3120 | compiler.hooks.entryOption.call() 3121 | // 标识运行编译 3122 | compiler.run() 3123 | ``` 3124 | 3125 | `may-pack`中`Compiler.js` 3126 | ``` 3127 | run() { 3128 | this.hooks.run.call() 3129 | 3130 | this.hooks.compile.call() 3131 | // 执行 创建模块的依赖关系 3132 | this.buildModule(path.resolve(this.root, this.entry), true) // path.resolve(this.root, this.entry) 得到入口文件的绝对路径 3133 | // console.log(this.modules, this.entryId); 3134 | this.hooks.afterCompile.call() 3135 | // 发射打包后的文件 3136 | this.emitFile() 3137 | this.hooks.emit.call() 3138 | this.hooks.done.call() 3139 | } 3140 | ``` 3141 | 3142 | 运行`npx may-pack` 3143 | 3144 | 3145 | ## loader 3146 | 3147 | 3148 | [手写loader](https://juejin.im/post/59e6a5de518825469c7461da) 3149 | 3150 | `webapck.config.js` 3151 | 3152 | ``` 3153 | let path = require('path') 3154 | 3155 | module.exports = { 3156 | mode: 'development', 3157 | entry: './src/index', 3158 | output: { 3159 | filename: 'build.js', 3160 | path: path.resolve(__dirname, 'dist') 3161 | }, 3162 | module: { 3163 | rules: [ 3164 | { 3165 | test: /\.js/, 3166 | use: 'loader1' // 如何找到这个loader1 3167 | } 3168 | ] 3169 | }, 3170 | } 3171 | 3172 | ``` 3173 | 3174 | 创建`loader`文件`loader1.js` 3175 | 3176 | ``` 3177 | console.log(22); 3178 | 3179 | function loader(source) { // loader的参数就是源代码 3180 | return source 3181 | } 3182 | module.exports = loader 3183 | 3184 | ``` 3185 | 3186 | 3187 | `webpack.config.js` 3188 | 3189 | ``` 3190 | let path = require('path') 3191 | 3192 | module.exports = { 3193 | mode: 'development', 3194 | entry: './src/index.js', 3195 | output: { 3196 | filename: 'build.js', 3197 | path: path.resolve(__dirname, 'dist') 3198 | }, 3199 | resolveLoader: { 3200 | // 别名 3201 | // alias: { 3202 | // loader1: path.resolve(__dirname, 'loader', 'loader1') 3203 | // } 3204 | modules: ['node_modules', path.resolve(__dirname, 'loader')] // 先找node_modules, 再去loader中去找 3205 | }, 3206 | module: { 3207 | rules: [ 3208 | { 3209 | test: /\.js$/, 3210 | // use: [path.resolve(__dirname, 'loader', 'loader1')] 3211 | use: 'loader1' // 如何找到这个loader1 3212 | 3213 | }, 3214 | // { 3215 | // test: /\.less$/, 3216 | // use: [ 3217 | // path.resolve(__dirname, 'loader', 'style-loader'), 3218 | // path.resolve(__dirname, 'loader', 'less-loader') 3219 | // ] 3220 | // } 3221 | ] 3222 | }, 3223 | } 3224 | 3225 | ``` 3226 | 如何找到这个`loader1` 3227 | 3228 | 1. 通过配别名`alias` 3229 | 2. 通过`modules` 3230 | 3231 | `npx webpack` 3232 | 3233 | ## 配置多个loader 3234 | 3235 | 1. 数组方式 3236 | 3237 | 先分别在`loader`文件下创建,`loader2.js`和`loader3.js` 3238 | 3239 | ``` 3240 | 3241 | function loader(source) { // loader的参数就是源代码 3242 | console.log('loader2'); // loader3.js 类似 3243 | return source 3244 | } 3245 | module.exports = loader 3246 | 3247 | ``` 3248 | 3249 | `webpack.config.js` 3250 | 3251 | ``` 3252 | rules: [ 3253 | { 3254 | test: /\.js$/, 3255 | use: ['loader3', 'loader2', 'loader1'] 3256 | }, 3257 | ] 3258 | ``` 3259 | 3260 | 运行`npx webpack`,分别打出 3261 | ``` 3262 | loader1 3263 | loader2 3264 | loader3 3265 | ``` 3266 | 3267 | 2. 对象方式 3268 | 3269 | ``` 3270 | rules: [ 3271 | { 3272 | test: /\.js$/, 3273 | use: ['loader3'] 3274 | }, 3275 | { 3276 | test: /\.js$/, 3277 | use: ['loader2'] 3278 | }, 3279 | { 3280 | test: /\.js$/, 3281 | use: ['loader1'] 3282 | } 3283 | ] 3284 | ``` 3285 | 3286 | 运行`npx webpack`,分别打出 3287 | 3288 | ``` 3289 | loader1 3290 | loader2 3291 | loader3 3292 | ``` 3293 | 3294 | 3295 | > `loader`的顺序: 从右到左, 从下到上 3296 | 3297 | 3298 | 也可以通过配置不同的参数改变`loader`的执行顺序,`pre` 前面的, `post`在后面的, `normal`正常 3299 | 3300 | 3301 | ``` 3302 | { 3303 | test: /\.js$/, 3304 | use: ['loader1'], 3305 | enforce: "pre" 3306 | }, 3307 | { 3308 | test: /\.js$/, 3309 | use: ['loader2'] 3310 | }, 3311 | { 3312 | test: /\.js$/, 3313 | use: ['loader3'], 3314 | enforce: "post" 3315 | }, 3316 | ``` 3317 | 3318 | `loader` 带参数执行的顺序: `pre -> normal -> inline -> post` 3319 | 3320 | `inline`为行内`loader` 3321 | 3322 | 在`loader`文件中新建`inlin-loader` 3323 | 3324 | ``` 3325 | 3326 | function loader(source) { // loader的参数就是源代码 3327 | console.log('inline'); 3328 | return source 3329 | } 3330 | module.exports = loader 3331 | 3332 | ``` 3333 | 3334 | `src/a.js` 3335 | 3336 | ``` 3337 | module.exports = 'may' 3338 | ``` 3339 | 3340 | `src/index` 3341 | 3342 | ``` 3343 | console.log('hello') 3344 | let srt = require('-!inline-loader!./a') 3345 | ``` 3346 | 3347 | 1. `-!`禁用`pre-loader`和 `normal-loader`来处理了 3348 | 3349 | ``` 3350 | loader1 3351 | loader2 3352 | loader3 3353 | inline 3354 | loader3 3355 | ``` 3356 | 3357 | 3358 | 3359 | 2. `!`禁用`normal-loader` 3360 | 3361 | ``` 3362 | loader1 3363 | loader2 3364 | loader3 3365 | loader1 3366 | inline 3367 | loader3 3368 | ``` 3369 | 3370 | 3371 | 3372 | 3. `!!` 禁用`pre-loader`、`normal-loader`、`post-loader`,只能行内处理 3373 | 3374 | ``` 3375 | loader1 3376 | loader2 3377 | loader3 3378 | inline 3379 | ``` 3380 | 3381 | loader 默认由两部分组成`pitch`和`normal` 3382 | 3383 | `user: [loader3, loader2, loader1]` 3384 | 3385 | 3386 | 无返回值: 先执行pitch方法,从左到右,再获取资源 3387 | 3388 | 3389 | ``` 3390 | 3391 | pitch loader - 无返回值 3392 | 3393 | pitch loader3 → loader2 → loader1 3394 | ↘ 3395 | 资源 3396 | ↙ 3397 | normal loader3 ← loader2 ← loader1 3398 | ``` 3399 | 3400 | 有返回值: 直接跳过后续所有的`loader`包括自己的,跳到之前的`loader`, 可用于阻断 3401 | 3402 | [loader](https://webpack.docschina.org/api/loaders/) 3403 | 3404 | ``` 3405 | user: [loader3, loader2, loader1] 3406 | 3407 | pitch loader - 有返回值 3408 | 3409 | pitch loader3 → loader2 loader1 3410 | ↙ 3411 | 有返回值 资源 3412 | ↙ 3413 | normal loader3 loader2 loader1 3414 | ``` 3415 | 3416 | `loadeer2.js` 3417 | 3418 | ``` 3419 | 3420 | function loader(source) { // loader的参数就是源代码 3421 | console.log('loader2'); 3422 | return source 3423 | } 3424 | 3425 | loader.pitch = function () { 3426 | return '111' 3427 | } 3428 | module.exports = loader 3429 | 3430 | ``` 3431 | 3432 | 结果 3433 | 3434 | ``` 3435 | loader3 3436 | ``` 3437 | 3438 | ## `babel-loader`实现 3439 | 3440 | `yarn add @babel/core @babel/preset-env` 3441 | 3442 | `webpack.config.js` 3443 | 3444 | ``` 3445 | { 3446 | test: '\.js$/', 3447 | use: { 3448 | loader: 'babel-loader2', 3449 | options: { 3450 | presets: [ 3451 | '@babel/preset-env' 3452 | ] 3453 | } 3454 | } 3455 | } 3456 | ``` 3457 | 3458 | 在`loader`文件创建`babel-loader2.js`(如果你已经装过`babel-loader`) 3459 | 3460 | 拿到`babel`的参数 3461 | 3462 | `yarn add loader-utils` 3463 | 3464 | 3465 | ``` 3466 | // 需要在webpack.config.js拿到babel的预设, 通过预设转换模块, 先引入babel 3467 | let babel = require('@babel/core') 3468 | 3469 | // 拿到babel的参数 需要工具 loaderUtils 3470 | let loaderUtils =require('loader-utils') 3471 | 3472 | 3473 | function loader(source) { // loader的参数就是源代码 这里的this就是loader的上下文 3474 | let options = loaderUtils.getOptions(this) 3475 | console.log(this.resourcePath, 444); // [./src/index.js] 3476 | let callback = this.async(); // babel的转换是异步的,同步的返回是不行的, 不能用return 同步就是直接掉用 异步会在async中 3477 | babel.transform(source, { 3478 | ...options, 3479 | sourceMap: true, // 是否设置sourceMap 还需要再webpack.config.js 中配置 devtool: 'source-map' 3480 | filename: this.resourcePath.split('/').pop() // 给生成的`source-map`指定名字 3481 | }, function (err, result) { 3482 | callback(err, result.code, result.map) // 异步 参数分别是「错误 转化后的代码 和 sourceMap」 3483 | }) 3484 | console.log(options); 3485 | // return source 失效 3486 | } 3487 | 3488 | module.exports = loader 3489 | 3490 | 3491 | ``` 3492 | 3493 | 3494 | `index.js` 3495 | 3496 | ``` 3497 | class May { 3498 | constructor () { 3499 | this.name = 'may' 3500 | } 3501 | getName () { 3502 | return this.name 3503 | } 3504 | } 3505 | 3506 | 3507 | let may = new May() 3508 | 3509 | console.log(may.getName()); 3510 | ``` 3511 | 3512 | `npx webpack` 3513 | 3514 | ## `banner-loader`实现(自创) 3515 | 3516 | 给所有匹配的`js`加一个注释 3517 | 3518 | `webpack.config.js` 3519 | 3520 | ``` 3521 | { // 给所有匹配的`js`加一个注释 3522 | test: /\.js$/, 3523 | use: { 3524 | loader: 'banner-loader', 3525 | options: { 3526 | text: 'may', 3527 | filename: path.resolve(__dirname, 'banner.js') 3528 | } 3529 | } 3530 | } 3531 | ``` 3532 | 3533 | `banner.js` 3534 | 3535 | ``` 3536 | 二次星球中毒 3537 | ``` 3538 | 3539 | 3540 | 在`loader`文件创建`banner-loader.js` 3541 | 3542 | `yarn add schema-utils` 校验自己写的`loader`格式是否正确 3543 | 3544 | [schema-utils](https://github.com/webpack-contrib/schema-utils) 3545 | 3546 | `banner-loader.js` 3547 | 3548 | ``` 3549 | // 拿到loader的配置 3550 | let loaderUtils = require('loader-utils') 3551 | // 校验loader 3552 | let validateOptions = require('schema-utils') 3553 | // 读取文件 3554 | let fs = require('fs') // 异步 3555 | 3556 | function loader(source) { // loader的参数就是源代码 3557 | let options = loaderUtils.getOptions(this) 3558 | let callback = this.async() // 读取文件是异步 3559 | let schema = { 3560 | type: 'object', 3561 | properties: { 3562 | text: { 3563 | type: 'string' 3564 | }, 3565 | filename: { 3566 | type: 'string' 3567 | } 3568 | } 3569 | } 3570 | validateOptions(schema, options, 'banner-loader') // 自己的校验格式, 自己的写的配置, 对应的loader名字 3571 | if (options.filename) { 3572 | this.cacheable(false) // 不要缓存 如果有大量计算 推荐缓存 3573 | // this.cacheable && this.cacheable() 3574 | this.addDependency(options.filename) // 自动增加依赖 3575 | fs.readFile(options.filename, 'utf8', function (err, data) { 3576 | callback(err, `/**${data}**/${source}`) 3577 | }) 3578 | } else { 3579 | callback(null, `/**${options.text}**/${source}`) 3580 | } 3581 | return source 3582 | } 3583 | module.exports = loader 3584 | 3585 | ``` 3586 | 3587 | 优化: 3588 | 3589 | 1. 修改`banner.js`的内容后, `webpack`进行监控,打包`webapck.config.js`配置`watch: true` 3590 | 2. `loader`缓存 3591 | 3592 | ## 实现`file-loader`和`url-loader` 3593 | 3594 | `yarn add mime` 3595 | 3596 | 其主要用途是设置某种扩展名的文件的响应程序类型 3597 | 3598 | [mime](https://github.com/broofa/node-mime#readme) 3599 | 3600 | 创建`file-loader.js1` 3601 | 3602 | ``` 3603 | // 拿到babel的参数 需要工具 loaderUtils 3604 | let loaderUtils = require('loader-utils') 3605 | 3606 | function loader(source) { // loader的参数就是源代码 3607 | // file-loader需要返回路径 3608 | let filename = loaderUtils.interpolateName(this, '[hash].[ext]', {content: source }) 3609 | this.emitFile(filename, source) // 发射文件 3610 | console.log('loader1'); 3611 | return `module.exports="${filename}"` 3612 | } 3613 | loader.raw = true // 二进制 3614 | module.exports = loader 3615 | 3616 | ``` 3617 | 3618 | 创建`url-loader1.js` 3619 | 3620 | ``` 3621 | // 拿到babel的参数 需要工具 loaderUtils 3622 | let loaderUtils = require('loader-utils') 3623 | let mime = require('mime') // 途是设置某种扩展名的文件的响应程序类型 3624 | 3625 | function loader(source) { // loader的参数就是源代码 3626 | let {limit} = loaderUtils.getOptions(this) 3627 | console.log(this.resourcePath); 3628 | if (limit && limit > source.length) { 3629 | return `module.exports="data:${mime.getType(this.resourcePath)};base64,${source.toString('base64')}"` 3630 | } else { 3631 | return require('./file-loader1').call(this, source) 3632 | } 3633 | } 3634 | loader.raw = true // 二进制 3635 | module.exports = loader 3636 | 3637 | ``` 3638 | 3639 | `webpack.config.js` 3640 | 3641 | ``` 3642 | { 3643 | test: /\.png$/, 3644 | // 目的是根据图片生成md5 发射到dist目录下,file-loader 返回当前图片路径 3645 | // use: 'file-loader' 3646 | // 处理路径 3647 | use: { 3648 | loader: 'url-loader1', 3649 | options: { 3650 | limit: 200 * 1024 3651 | } 3652 | } 3653 | } 3654 | ``` 3655 | 3656 | `index.js`引入图片 3657 | 3658 | ``` 3659 | import p from './photo.png' 3660 | 3661 | let img = document.createElement('img') 3662 | img.src = p 3663 | document.body.appendChild(img); 3664 | 3665 | ``` 3666 | 3667 | 3668 | ## `less-loader`和`css-loader` 3669 | 3670 | 3671 | 先安装`less` 3672 | 3673 | 分别创建`style-loader2` `css-loader2` `less-loader2` 3674 | 3675 | `style-loader1` 与 `less-loader1` 同之前的 3676 | 3677 | 3678 | ## `css-loader` 3679 | 3680 | 主要用来处理`css`中的图片链接,需要把`url`转换成`require` 3681 | 3682 | 3683 | `webpack.config.js` 3684 | 3685 | ``` 3686 | { 3687 | test: /\.png$/, 3688 | // 目的是根据图片生成md5 发射到dist目录下,file-loader 返回当前图片路径 3689 | // use: 'file-loader' 3690 | // 处理路径 3691 | use: { 3692 | loader: 'url-loader1', 3693 | options: { 3694 | limit: 200 * 1024 3695 | } 3696 | } 3697 | }, 3698 | { 3699 | test: /\.less$/, 3700 | use: ['style-loader2', 'css-loader2', 'less-loader2'] 3701 | } 3702 | ``` 3703 | 3704 | 创建`index.less` 3705 | 3706 | ``` 3707 | @base: #f938ab; 3708 | body { 3709 | background: @base; 3710 | background: url("./photo.png"); 3711 | } 3712 | ``` 3713 | 3714 | `less-loader2.js` 3715 | 3716 | ``` 3717 | // 将less转为css 3718 | let less = require('less') 3719 | 3720 | function loader(source) { 3721 | let css = '' 3722 | // console.log(source, 2222); 3723 | less.render(source, function (err, output) { 3724 | // console.log(output); 3725 | css = output.css 3726 | }) 3727 | // css = css.replace(/\n/g, '\\n'); 3728 | return css 3729 | } 3730 | 3731 | module.exports = loader 3732 | ``` 3733 | 3734 | 3735 | `css-loader2.js` 3736 | 3737 | ``` 3738 | // css-loader 用来解析@import这种语法,包括css中引入的图片 3739 | function loader(source) { 3740 | let reg = /url\((.+?)\)/g // 匹配括号 3741 | 3742 | let pos = 0; 3743 | let current; 3744 | 3745 | let arr = ['let list = []'] 3746 | 3747 | while (current = reg.exec(source)) { 3748 | let [matchUrl, g] = current // matchUrl -> 'url("./photo.png")', g -> '"./photo.png"' 3749 | // console.log(matchUrl, g, 88); 3750 | let lastIndex = reg.lastIndex - matchUrl.length // 拿到css从开通到地址链接之前的index 3751 | arr.push(`list.push(${JSON.stringify(source.slice(pos, lastIndex))})`) // 拼入开始和地址之前的代码 3752 | pos = reg.lastIndex 3753 | arr.push(`list.push('url('+ require(${g}) +')')`) // 拼入图片地址 3754 | } 3755 | arr.push(`list.push(${JSON.stringify(source.slice(pos))})`) // 拼入地址到结尾的代码 3756 | arr.push(`module.exports = list.join('')`) 3757 | console.log(arr.join('\r\n')); 3758 | // let list = [] 3759 | // list.push("body {\\n background: #f938ab;\\n background: ") 3760 | // list.push('url('+ require("./photo.png") +')') 3761 | // list.push(";\\n}\\n") 3762 | // module.exports = list.join('') 3763 | 3764 | return arr.join('\r\n') 3765 | } 3766 | module.exports = loader 3767 | 3768 | ``` 3769 | 3770 | `style-loader2.js` 3771 | 3772 | ``` 3773 | let loaderUtils = require('loader-utils') 3774 | 3775 | // 将css插入到html头部 3776 | function loader(source) { 3777 | let str = ` 3778 | let style = document.createElement('style') 3779 | style.innerHTML = ${JSON.stringify(source)} 3780 | document.head.appendChild(style) 3781 | ` 3782 | return str 3783 | } 3784 | 3785 | 3786 | // style-loader写了pitch,有返回后面的跳过,自己的写不会走 3787 | loader.pitch = function (remainingRequest) { // 剩余的请求 3788 | console.log(loaderUtils.stringifyRequest(this, '!!' + remainingRequest, 99999999)) 3789 | // 让style-loader 处理 less-loader 和css-loader拼接的结果 3790 | // 得到 /Users/liuhuimin/work/webpack/loader/css-loader2.js!/Users/liuhuimin/work/webpack/loader/less-loader2.js!/Users/liuhuimin/work/webpack/src/index.less 3791 | // 剩余的请求 less-loader!css-loader!./index.less 3792 | // console.log(remainingRequest, 1223); 3793 | // require返回的就是css-loader处理好的结果require('!!css-loader!less-loader!./index.less') 3794 | let str = ` 3795 | let style = document.createElement('style') 3796 | style.innerHTML = require(${loaderUtils.stringifyRequest(this, '!!' + remainingRequest)}) 3797 | document.head.appendChild(style) 3798 | ` 3799 | // stringifyRequest 绝对路径转相对路径 3800 | return str 3801 | } 3802 | module.exports = loader 3803 | 3804 | ``` 3805 | 3806 | 3807 | ``` 3808 | user: ['style-loader2', 'css-loader2', 'less-loader2'] 3809 | 3810 | pitch loader - 有返回值 3811 | 3812 | pitch style-loader2 → css-loader2 less-loader2 3813 | ↙ 3814 | 有返回值 资源 3815 | ↙ 3816 | normal style-loader2 css-loader2 less-loader2 3817 | ``` 3818 | 3819 | 在`style-loader2`中 引用了`less-loader` `css-loader` 和`less`文件 3820 | 3821 | 3822 | ## webpack 中的插件 3823 | 3824 | `yarn add webpack webpack-cil -D` 3825 | 3826 | `webpack.config.js` 3827 | 3828 | ``` 3829 | let path = require('path') 3830 | let DonePlugin = require('./plugins/DonePlugins') 3831 | let AsyncPlugins = require('./plugins/AsyncPlugins') 3832 | 3833 | module.exports = { 3834 | mode: 'development', 3835 | entry: './src/index.js', 3836 | output: { 3837 | filename: 'build.js', 3838 | path: path.resolve(__dirname, 'dist') 3839 | }, 3840 | plugins: [ 3841 | new DonePlugin(), // 同步 3842 | new AsyncPlugins() // 异步 3843 | ] 3844 | } 3845 | 3846 | ``` 3847 | 3848 | `node_modules/webpack/lib`中查看`Compiler.js` 3849 | 3850 | 1. 同步`plugins/DonePlugins` 3851 | 3852 | 打包完成 3853 | 3854 | ``` 3855 | class DonePlugins { 3856 | apply (compiler) { 3857 | console.log(1); 3858 | compiler.hooks.done.tap('DonePlugin', (stats) => { 3859 | console.log('编译完成'); 3860 | }) 3861 | } 3862 | } 3863 | 3864 | 3865 | module.exports = DonePlugins 3866 | 3867 | ``` 3868 | 3869 | 3870 | 2. 异步`plugins/AsyncPlugins` 3871 | 3872 | ``` 3873 | class AsyncPlugins { 3874 | apply (compiler) { 3875 | console.log(2); 3876 | compiler.hooks.emit.tapAsync('AsyncPlugin', (complete, callback) => { 3877 | setTimeout(() => { 3878 | console.log('文件发射出来'); 3879 | callback() 3880 | }, 1000) 3881 | }) 3882 | compiler.hooks.emit.tapPromise('AsyncPlugin', (complete, callback) => { 3883 | return new Promise((resolve, reject) => { 3884 | setTimeout(() => { 3885 | console.log('文件发射出来 222'); 3886 | resolve() 3887 | }, 1000) 3888 | }) 3889 | }) 3890 | } 3891 | } 3892 | 3893 | 3894 | module.exports = AsyncPlugins 3895 | 3896 | ``` 3897 | 3898 | ## 文件列表插件 3899 | 3900 | 希望生成一个文件描述打包出来的文件 3901 | 3902 | 在`plugins`中新建`FileListPlugin` 3903 | 3904 | ``` 3905 | class FileListPlugin { 3906 | constructor ({filename}) { 3907 | this.filename = filename 3908 | } 3909 | apply (compiler) { 3910 | // 文件已经准备好了 要进行发射 3911 | // emit 3912 | compiler.hooks.emit.tap('FileListPlugin', (compilation) => { 3913 | let assets = compilation.assets; 3914 | console.log(assets, 55); 3915 | let content = `## 文件名 资源大小\r\n` 3916 | // [ [bundls.js, {}], [index.html, {}]] 3917 | Object.entries(assets).forEach(([filename, stateObj]) => { 3918 | content += `- ${filename} ${stateObj.size()}\r\n` 3919 | }) 3920 | // 资源对象 3921 | assets[this.filename] = { 3922 | source () { 3923 | return content; 3924 | }, 3925 | size () { 3926 | return content.length 3927 | } 3928 | } 3929 | }) 3930 | } 3931 | } 3932 | 3933 | module.exports = FileListPlugin 3934 | 3935 | ``` 3936 | 3937 | ``` 3938 | let path = require('path') 3939 | let DonePlugin = require('./plugins/DonePlugins') 3940 | let AsyncPlugins = require('./plugins/AsyncPlugins') 3941 | let HtmlWebpackPlugin = require('html-webpack-plugin') 3942 | let FileListPlugin = require('./plugins/FileListPlugin') 3943 | 3944 | module.exports = { 3945 | mode: 'development', 3946 | entry: './src/index.js', 3947 | output: { 3948 | filename: 'build.js', 3949 | path: path.resolve(__dirname, 'dist') 3950 | }, 3951 | plugins: [ 3952 | new DonePlugin(), 3953 | new AsyncPlugins(), 3954 | new HtmlWebpackPlugin({ 3955 | template: './src/index.html', 3956 | filename: 'index.html' 3957 | }), 3958 | new FileListPlugin({ 3959 | filename: 'list.md' 3960 | }) 3961 | ] 3962 | } 3963 | 3964 | ``` 3965 | 3966 | 生成`list.md` 3967 | 3968 | 3969 | ## 内联的`webpack`插件 3970 | 3971 | 新建`index.css`引入`index.js` 3972 | 3973 | `yarn add css-loader mini-css-extract-plugin -D` 3974 | 3975 | 希望打包后`css、js`内联在`index.html`文件中 3976 | 3977 | 创建`plugins`中`InlineSourcePlugins.js` 3978 | 3979 | `yarn add --dev html-webpack-plugin@next` 3980 | 3981 | [HTML Webpack Plugin](https://github.com/jantimon/html-webpack-plugin) 3982 | 3983 | `webpack.config.js` 3984 | 3985 | ``` 3986 | let path = require('path') 3987 | let DonePlugin = require('./plugins/DonePlugins') 3988 | let AsyncPlugins = require('./plugins/AsyncPlugins') 3989 | let HtmlWebpackPlugin = require('html-webpack-plugin') 3990 | let FileListPlugin = require('./plugins/FileListPlugin') 3991 | 3992 | let InlineSourcePlugins = require('./plugins/InlineSourcePlugins') 3993 | 3994 | let MiniCssExtractPlugin = require('mini-css-extract-plugin') 3995 | 3996 | module.exports = { 3997 | mode: 'production', 3998 | entry: './src/index.js', 3999 | output: { 4000 | filename: 'bundle.js', 4001 | path: path.resolve(__dirname, 'dist') 4002 | }, 4003 | module: { 4004 | rules: [ 4005 | { 4006 | test: /\.css$/, 4007 | use: [MiniCssExtractPlugin.loader, 'css-loader'] 4008 | } 4009 | ] 4010 | }, 4011 | plugins: [ 4012 | // new DonePlugin(), 4013 | // new AsyncPlugins(), 4014 | new HtmlWebpackPlugin({ 4015 | template: './src/index.html', 4016 | filename: 'index.html' 4017 | }), 4018 | new MiniCssExtractPlugin({ 4019 | filename: 'index.css' 4020 | }), 4021 | new InlineSourcePlugins({ 4022 | match: /\.(js|css)/ 4023 | }), 4024 | // new FileListPlugin({ 4025 | // filename: 'list.md' 4026 | // }) 4027 | ] 4028 | } 4029 | 4030 | ``` 4031 | 4032 | `InlineSourcePlugins.js` 4033 | 4034 | ``` 4035 | const HtmlWebpackPlugin = require('html-webpack-plugin') 4036 | 4037 | // 把外链的标签编程内联的标签 4038 | class InlineSourcePlugins { 4039 | constructor({match}) { 4040 | this.reg = match // 正则 4041 | } 4042 | 4043 | // 处理某一个标签 4044 | processTag(tag, compilation) { 4045 | let newTag = {} 4046 | let url = '' 4047 | if (tag.tagName === 'link' && this.reg.test(tag.attributes.href)) { 4048 | newTag = { 4049 | tagName: 'style', 4050 | attributes: {type: 'text/css'} 4051 | } 4052 | url = tag.attributes.href 4053 | } else if (tag.tagName === 'script' && this.reg.test(tag.attributes.src)) { 4054 | newTag = { 4055 | tagName: 'script', 4056 | attributes: {type: 'application/javascript'} 4057 | } 4058 | url = tag.attributes.src 4059 | } 4060 | if (url) { 4061 | newTag.innerHTML = compilation.assets[url].source(); // 文件内容放到innerHTML属性中 4062 | delete compilation.assets[url] // 删除原有的资源 4063 | return newTag 4064 | // console.log(compilation.assets[url].source()); 4065 | } 4066 | return tag 4067 | } 4068 | 4069 | // 处理引入标签的数据 4070 | processTags(data, compilation) { 4071 | let headTags = [] 4072 | let bodyTags = [] 4073 | data.headTags.forEach(headTag => { 4074 | headTags.push(this.processTag(headTag, compilation)) 4075 | }) 4076 | data.bodyTags.forEach(bodyTag => { 4077 | bodyTags.push(this.processTag(bodyTag, compilation)) 4078 | }) 4079 | console.log({...data, headTags, bodyTags}) 4080 | return {...data, headTags, bodyTags} 4081 | } 4082 | 4083 | 4084 | 4085 | apply(compiler) { 4086 | // 通过webpackPlugin来实现 npm搜索 html-webpack-plugin 4087 | compiler.hooks.compilation.tap('InlineSourcePlugins', (compilation) => { 4088 | HtmlWebpackPlugin.getHooks(compilation).alterAssetTagGroups.tapAsync( 4089 | 'alertPlugin', 4090 | (data, callback) => { 4091 | // console.log('======'); 4092 | // console.log(data) // 插入html标签的数据 4093 | // console.log('======'); 4094 | data = this.processTags(data, compilation) // compilation.assets 资源的链接 4095 | callback(null, data) 4096 | }) 4097 | }) 4098 | 4099 | } 4100 | } 4101 | 4102 | module.exports = InlineSourcePlugins 4103 | 4104 | ``` 4105 | 4106 | 4107 | ## 打包后自动发布 4108 | 4109 | 打包好的文件自动上传致七牛 4110 | 4111 | 需要这几个参数 4112 | 4113 | ``` 4114 | bucket: '' // 七牛的存储空间 4115 | domain: '', 4116 | accessKey: '', // 七牛云的两对密匙 4117 | secretKey: '' // 七牛云的两对密匙 4118 | ``` 4119 | 4120 | 注册七牛,并在对象存储里面,新建存储空间列表`test`,`bucket: 'test'` 4121 | 4122 | 内容管理外链接默认域名 `domain: 'xxxxxxxx'` 4123 | 4124 | 右上角个人面板里面个人中心,密钥管理分别对应`accessKey`和`secretKey` 4125 | 4126 | [进入开发者中心](https://developer.qiniu.com/) -> SDK&工具 -> 官方SDK -> Node服务端文档 —> 文件上传 4127 | 4128 | 4129 | [node文件上传](https://developer.qiniu.com/kodo/sdk/1289/nodejs) 4130 | 4131 | 4132 | 4133 | `npm install qiniu` 4134 | 4135 | [compiler-hooks](https://webpack.docschina.org/api/compiler-hooks) 4136 | 4137 | 4138 | `webpack.config.js` 4139 | 4140 | ``` 4141 | plugins: [ 4142 | new HtmlWebpackPlugin({ 4143 | template: './src/index.html', 4144 | filename: 'index.html' 4145 | }), 4146 | new MiniCssExtractPlugin({ 4147 | filename: 'index.css' 4148 | }), 4149 | new UploadPlugin({ 4150 | bucket: 'test', // 七牛的存储空间 4151 | domain: 'poyrjyh1b.bkt.clouddn.com', 4152 | accessKey: 'xxxxxx', // 七牛云的两对密匙 4153 | secretKey: 'yyyyyy' // 七牛云的两对密匙 4154 | }) 4155 | ] 4156 | ``` 4157 | 4158 | `UploadPlugin.js` 4159 | 4160 | ``` 4161 | let qiniu = require('qiniu') 4162 | let path = require('path') 4163 | 4164 | class UploadPlugin { 4165 | constructor (options = {}) { 4166 | // 参考 https://developer.qiniu.com/kodo/sdk/1289/nodejs 4167 | let { bucket = '', domain = '', accessKey = '', secretKey = ''} = options 4168 | let mac = new qiniu.auth.digest.Mac(accessKey, secretKey) 4169 | let putPolicy = new qiniu.rs.PutPolicy({ 4170 | scope: bucket 4171 | }); 4172 | this.uploadToken = putPolicy.uploadToken(mac) 4173 | let config = new qiniu.conf.Config(); 4174 | this.formUploader = new qiniu.form_up.FormUploader(config) 4175 | this.putExtra = new qiniu.form_up.PutExtra() 4176 | } 4177 | apply (compiler) { 4178 | compiler.hooks.afterEmit.tapPromise('UploadPlugin', (complication) => { 4179 | let assets = complication.assets 4180 | let promise = [] 4181 | Object.keys(assets).forEach(filename => { 4182 | promise.push(this.upload(filename)) 4183 | }) 4184 | return Promise.all(promise) 4185 | }) 4186 | } 4187 | 4188 | upload (filename) { 4189 | return new Promise((resolve, reject) => { 4190 | let localFile = path.resolve(__dirname, '../dist', filename) 4191 | this.formUploader.putFile(this.uploadToken, filename, localFile, this.putExtra, function(respErr, 4192 | respBody, respInfo) { 4193 | if (respErr) { 4194 | reject(respErr) 4195 | } 4196 | if (respInfo.statusCode == 200) { 4197 | resolve(respBody) 4198 | } else { 4199 | console.log(respInfo.statusCode) 4200 | console.log(respBody) 4201 | } 4202 | }); 4203 | }) 4204 | } 4205 | } 4206 | 4207 | module.exports = UploadPlugin 4208 | 4209 | ``` 4210 | --------------------------------------------------------------------------------