cainiaoblog 23.2.205 → 23.2.207

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/search.xml CHANGED
@@ -8,7 +8,7 @@
8
8
  <link href="/article/9c3c56c.html"/>
9
9
  <url>/article/9c3c56c.html</url>
10
10
 
11
- <content type="html"><![CDATA[<p>vue,react,webpack,babel 要点总结</p><span id="more"></span><h2 id="Vue"><a href="#Vue" class="headerlink" title="Vue"></a>Vue</h2><h3 id="基本使用"><a href="#基本使用" class="headerlink" title="基本使用"></a>基本使用</h3><h4 id="v-html"><a href="#v-html" class="headerlink" title="v-html"></a>v-html</h4><p>原始html内容,有xss防线</p><h4 id="computed"><a href="#computed" class="headerlink" title="computed"></a>computed</h4><ul><li>有缓存,data不变不会重新计算</li></ul><h4 id="watch"><a href="#watch" class="headerlink" title="watch"></a>watch</h4><p>监听引用类型,使用下面方法深度监听,但是<strong>拿不到<code>oldVal</code></strong></p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">watch</span>:&#123;</span><br><span class="line"> <span class="attr">info</span>: &#123;</span><br><span class="line"> handler (oldVal, val) &#123;</span><br><span class="line"> <span class="variable language_">console</span>.<span class="title function_">log</span>((oldVal, val)</span><br><span class="line"> &#125;,</span><br><span class="line"> <span class="attr">deep</span>: <span class="literal">true</span></span><br><span class="line"> &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h4 id="class-写法"><a href="#class-写法" class="headerlink" title="class 写法"></a>class 写法</h4><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">data () &#123;</span><br><span class="line"> <span class="attr">isblack</span>: <span class="literal">true</span>,</span><br><span class="line"> <span class="attr">isRed</span>: <span class="literal">false</span>,</span><br><span class="line"> </span><br><span class="line"> <span class="attr">black</span>: <span class="string">&quot;black&quot;</span>,</span><br><span class="line"> <span class="attr">red</span>: <span class="string">&quot;red&quot;</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">:<span class="keyword">class</span>=&#123;<span class="attr">black</span>: isblack, <span class="attr">red</span>: isRed&#125;</span><br></pre></td></tr></table></figure><p>或者</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">:<span class="keyword">class</span>=[black, red]</span><br></pre></td></tr></table></figure><h4 id="style"><a href="#style" class="headerlink" title="style"></a>style</h4><p>object 类型,驼峰写法</p><h4 id="v-if-和-v-show-区别"><a href="#v-if-和-v-show-区别" class="headerlink" title="v-if 和 v-show 区别"></a>v-if 和 v-show 区别</h4><p>v-if 是否渲染,更新不频繁使用</p><p>v-show 是通过 css 的 display 控制显示与隐藏,频繁切换使用</p><h4 id="v-for"><a href="#v-for" class="headerlink" title="v-for"></a>v-for</h4><ul><li>也可以遍历对象</li><li>key,不能乱写,尽量不用index,不用random</li><li><code>v-for</code> 优先级比<code>v-if</code>高</li></ul><h4 id="事件"><a href="#事件" class="headerlink" title="事件"></a>事件</h4><h5 id="event参数,自定义参数"><a href="#event参数,自定义参数" class="headerlink" title="event参数,自定义参数"></a>event参数,自定义参数</h5><ul><li>没有参数,直接可以获取</li><li>有参数,用<code>$event</code></li><li><code>event</code>是原生对象</li></ul><h5 id="事件修饰符,按键修饰符"><a href="#事件修饰符,按键修饰符" class="headerlink" title="事件修饰符,按键修饰符"></a>事件修饰符,按键修饰符</h5><p><code>@click.stop.prevent</code></p><p><code>@click.ctrl.exact</code></p><h5 id="!(观察)事件被绑定到哪里"><a href="#!(观察)事件被绑定到哪里" class="headerlink" title="!(观察)事件被绑定到哪里"></a>!(观察)事件被绑定到哪里</h5><p><code>event.target</code>的值表明事件是挂在<strong>当前元素</strong>上的</p><h4 id="表单"><a href="#表单" class="headerlink" title="表单"></a>表单</h4><h5 id="v-model"><a href="#v-model" class="headerlink" title="v-model"></a>v-model</h5><p>v-model.trim</p><p>v-model.lazy</p><p>v-model.number</p><h2 id="组件使用"><a href="#组件使用" class="headerlink" title="组件使用"></a>组件使用</h2><h3 id="props"><a href="#props" class="headerlink" title="props"></a>props</h3><p>简写</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">props</span>:[<span class="string">&#x27;list&#x27;</span>]</span><br></pre></td></tr></table></figure><p>复杂写法,常用,可以定义类型和默认值</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">props</span>: &#123;</span><br><span class="line"> <span class="attr">type</span>: <span class="title class_">Array</span>,</span><br><span class="line"> <span class="attr">default</span>:&#123;</span><br><span class="line"> <span class="keyword">return</span> []</span><br><span class="line"> &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="emit"><a href="#emit" class="headerlink" title="$emit"></a>$emit</h3><p>父组件</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">&lt;<span class="title class_">Input</span> @add=<span class="string">&quot;clickhandler&quot;</span>/&gt;</span><br></pre></td></tr></table></figure><p>子组件</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">methods</span>:&#123;</span><br><span class="line"> <span class="title function_">addTitle</span>(<span class="params"></span>) &#123;</span><br><span class="line"> <span class="variable language_">this</span>.$emit(<span class="string">&#x27;add&#x27;</span>, title)</span><br><span class="line"> &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="组件通信-自定义事件(兄弟组件通信)"><a href="#组件通信-自定义事件(兄弟组件通信)" class="headerlink" title="组件通信 - 自定义事件(兄弟组件通信)"></a>组件通信 - 自定义事件(兄弟组件通信)</h3><p><strong>vue 具有自定义事件能力 $on、$off、$emit</strong></p><p><strong>组件卸载时,解除事件绑定,销毁子组件,定时器等</strong></p><h3 id="组件生命周期"><a href="#组件生命周期" class="headerlink" title="组件生命周期"></a>组件生命周期</h3><ul><li>挂载阶段</li><li>更新阶段</li><li>销毁阶段</li></ul><h4 id="单个组件"><a href="#单个组件" class="headerlink" title="单个组件"></a>单个组件</h4><p>created:vue实例化完成</p><p>mounted:渲染完成</p><h4 id="父子组件"><a href="#父子组件" class="headerlink" title="父子组件"></a>父子组件</h4><p>组件实例创建: 先父后子</p><p>组件渲染: 先子后父【子组件渲染完,才能挂载】</p><h3 id="高级特性"><a href="#高级特性" class="headerlink" title="高级特性"></a>高级特性</h3><h4 id="自定义-v-model"><a href="#自定义-v-model" class="headerlink" title="自定义 v-model"></a>自定义 v-model</h4><p>父组件中</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">&lt;CunstomVModal v-model=&#123;name&#125;&gt;</span><br></pre></td></tr></table></figure><p>子组件中 CunstomVModal</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line">&lt;template&gt;</span><br><span class="line"> &lt;input type=&quot;text&quot; :value=&quot;txt&quot; @input=&quot;$emit(&#x27;chage&#x27;, $event.target.value)&quot;</span><br><span class="line">&lt;/template&gt;</span><br><span class="line">&lt;script&gt;</span><br><span class="line"> export default &#123;</span><br><span class="line"> model:&#123;</span><br><span class="line"> prop: &#x27;txt&#x27;,</span><br><span class="line"> event:&#x27;chage&#x27;</span><br><span class="line"> &#125;,</span><br><span class="line"> props:&#123;</span><br><span class="line"> txt: String,</span><br><span class="line"> default () &#123;</span><br><span class="line"> return &#x27;&#x27;</span><br><span class="line"> &#125;</span><br><span class="line"> &#125;</span><br><span class="line"> &#125;</span><br><span class="line">&lt;/script&gt;</span><br></pre></td></tr></table></figure><h4 id="nextTick、refs"><a href="#nextTick、refs" class="headerlink" title="$nextTick、refs"></a>$nextTick、refs</h4><ul><li>vue 是异步渲染</li><li>data 改变之后,dom不会like渲染</li><li>$nextTick 会在dom渲染之后被处罚,以获取最新的dom节点</li></ul><h4 id="slot"><a href="#slot" class="headerlink" title="slot"></a>slot</h4><ul><li>基本使用</li><li>作用于插槽</li><li>具名插槽</li></ul><h4 id="动态组件"><a href="#动态组件" class="headerlink" title="动态组件"></a>动态组件</h4><ul><li>:is&#x3D;”component-name”</li><li>需要根据数据,动态渲染的场景。即组件类型不确定</li></ul><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line">&lt;template&gt;</span><br><span class="line"> &lt;component :is=&quot;compName&quot;&gt;</span><br><span class="line">&lt;/template&gt;</span><br><span class="line">&lt;script&gt;</span><br><span class="line"> import myComp from &#x27;./myComp&#x27;</span><br><span class="line"> components:&#123;</span><br><span class="line"> myComp</span><br><span class="line"> &#125;,</span><br><span class="line"> data () &#123;</span><br><span class="line"> return &#123;</span><br><span class="line"> compName: &#x27;myComp&#x27;</span><br><span class="line"> &#125;</span><br><span class="line"> &#125;</span><br><span class="line"> </span><br><span class="line">&lt;/script&gt;</span><br></pre></td></tr></table></figure><h4 id="异步组件"><a href="#异步组件" class="headerlink" title="异步组件"></a>异步组件</h4><ul><li>import()</li><li>按需加载,异步加载大组件</li></ul><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">export default &#123;</span><br><span class="line"> components:&#123;</span><br><span class="line"> Demo: () =&gt; import(&#x27;./ImportDemo&#x27;)</span><br><span class="line"> &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h4 id="keep-alive"><a href="#keep-alive" class="headerlink" title="keep-alive"></a>keep-alive</h4><ul><li>缓存组件</li><li>频繁切换,不需要重复渲染</li><li>vue常见性能优化</li></ul><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">// 每个组件只渲染一次,且不会销毁。</span><br><span class="line">// 简单的用v-show就行,稍微复杂的用keep-alive</span><br><span class="line">&lt;keep-alive&gt;</span><br><span class="line"> &lt;A v-if=&quot;a&quot;&gt;&lt;/A&gt;</span><br><span class="line"> &lt;B v-if=&quot;b&quot;&gt;&lt;/B&gt;</span><br><span class="line"> &lt;C v-if=&quot;c&quot;&gt;&lt;/C&gt;</span><br><span class="line">&lt;/keep-alive&gt;</span><br></pre></td></tr></table></figure><h4 id="mixin"><a href="#mixin" class="headerlink" title="mixin"></a>mixin</h4><ul><li>多个组件有相同的逻辑,抽离出来</li><li>mixin并不是完美的解决方案,会有一些问题</li><li>vue 3 提出的 composition API 旨在解决这些问题</li></ul><hr><h5 id="问题"><a href="#问题" class="headerlink" title="问题"></a>问题</h5><ul><li>变量来源不明确,不利于阅读</li><li>多个mixin可能会造成命名冲突</li><li>mixin和组件可能会出现多对多的关系,复杂度较高</li></ul><hr><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">&lt;sript&gt;</span><br><span class="line"> import Mixin from &#x27;./mixin&#x27;</span><br><span class="line"> export default &#123;</span><br><span class="line"> mixins: [Mixin],</span><br><span class="line"> data () &#123;</span><br><span class="line"> return &#123;&#125;</span><br><span class="line"> &#125;</span><br><span class="line"> &#125;</span><br><span class="line">&lt;/sript&gt;</span><br></pre></td></tr></table></figure><h4 id="vuex-使用"><a href="#vuex-使用" class="headerlink" title="vuex 使用"></a>vuex 使用</h4><ul><li>基本概念、基本使用、API</li><li>state的数据结构设计</li></ul><h5 id="vuex基本概念"><a href="#vuex基本概念" class="headerlink" title="vuex基本概念"></a>vuex基本概念</h5><ul><li>state</li><li>gettters</li><li>action</li><li>mutation</li></ul><h5 id="用于vue组件"><a href="#用于vue组件" class="headerlink" title="用于vue组件"></a>用于vue组件</h5><ul><li>dispatch</li><li>commit</li><li>mapstate</li><li>mapGetters</li><li>mapActions</li><li>mapMutations</li></ul><h4 id="vue-router"><a href="#vue-router" class="headerlink" title="vue-router"></a>vue-router</h4><ul><li>路由模式:<ul><li>hash<ul><li>window.onhashchange</li><li>会出发网页跳转,</li><li>可前进后退,</li><li>不会提交到server端,强制刷新也不会</li><li>可刷新</li></ul></li><li>H5 history<ul><li>history.pushState</li><li>window.onpopstate</li><li>不可刷新(需要后端支持)</li></ul></li></ul></li><li>路由配置:<ul><li>动态路由 {path: ‘&#x2F;user&#x2F;:id?’, component: User}</li><li>懒加载 {path: ‘&#x2F;user&#x2F;:id?’, component: () &#x3D;&gt; import(‘.&#x2F;user’)}</li></ul></li></ul><h3 id="Vue原理"><a href="#Vue原理" class="headerlink" title="Vue原理"></a>Vue原理</h3><h4 id="如何里面MVVM"><a href="#如何里面MVVM" class="headerlink" title="如何里面MVVM"></a>如何里面MVVM</h4><ul><li>很久以前的视图<ul><li>asp jsp php 已经有组件化了</li><li>传统组件,只是静态渲染,更新依赖于操作dom</li></ul></li><li>数据驱动视图(vue MVVM,react setState)</li></ul><h4 id="vue-响应式"><a href="#vue-响应式" class="headerlink" title="vue 响应式"></a>vue 响应式</h4><ul><li>组件data的数据一旦变化,立刻出发视图更新</li><li>核心API - Object.defineProperty<ul><li>get</li><li>set</li></ul></li><li>Object.defineProperty 的一些缺点 (Vue3.0 使用 Proxy)</li><li>proxy 兼容性不太好,且无法 polyfill</li></ul><h5 id="Object-defineProperty-缺点"><a href="#Object-defineProperty-缺点" class="headerlink" title="Object.defineProperty 缺点"></a>Object.defineProperty 缺点</h5><ul><li>深度监听,需要递归到底,一次性计算量大</li><li>新增,删除 无法监听,需要用 Vue.set 和 Vue.delete</li><li>无法监听数组,需要专门处理</li></ul><h4 id="虚拟DOM-和-diff"><a href="#虚拟DOM-和-diff" class="headerlink" title="虚拟DOM 和 diff"></a>虚拟DOM 和 diff</h4><ul><li>DOM操作非常耗性能,js是执行非常快的</li><li>vue和react是数据驱动视图</li></ul><h5 id="vdom"><a href="#vdom" class="headerlink" title="vdom"></a>vdom</h5><p>用js模拟dom结构,计算出最小的变更,操作dom</p><h5 id="snabbdom"><a href="#snabbdom" class="headerlink" title="snabbdom"></a>snabbdom</h5><p>h函数,返回vnode(js)<br>patch(container, vnode) 初次渲染<br>patch(oldVnode, newVnode) 更新,新旧vnode都有children,那么updateChildren<br>patch(oldVnode, null) 销毁清空</p><h5 id="diff"><a href="#diff" class="headerlink" title="diff"></a>diff</h5><ul><li>只比较同一层级,不跨级比较</li><li>tag不相同,直接删掉重建,不再深度比较</li><li>tag和key都相同,则认为是相同节点,不再深度比较</li></ul><h5 id="模板编译"><a href="#模板编译" class="headerlink" title="模板编译"></a>模板编译</h5><ul><li>vue将模板编译为render函数</li><li>执行render函数生成vnode,patch</li><li>触发响应式,监听data属性getter setter</li></ul><blockquote><ul><li>with语法</li><li>vue组件可以用render代替template</li></ul></blockquote><h6 id="初次渲染过程"><a href="#初次渲染过程" class="headerlink" title="初次渲染过程"></a>初次渲染过程</h6><ul><li>解析为render函数</li><li>触发响应式,监听data属性getter setter</li><li>执行render函数,生成vnode,patch</li></ul><h6 id="更新过程"><a href="#更新过程" class="headerlink" title="更新过程"></a>更新过程</h6><ul><li>修改data,触发setter,(此前在getter中已被监听)</li><li>重新执行render函数,生成newVnode</li><li>patch(oldVnode, newVnode)</li></ul><h5 id="组件异步渲染"><a href="#组件异步渲染" class="headerlink" title="组件异步渲染"></a>组件异步渲染</h5><ul><li>$nextTick</li><li>汇总data修改,一次性更新视图</li><li>减少dom操作次数,提高性能</li></ul><h4 id="Vue3"><a href="#Vue3" class="headerlink" title="Vue3"></a>Vue3</h4><h5 id="新功能"><a href="#新功能" class="headerlink" title="新功能"></a>新功能</h5><ul><li><p>createApp</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">// vue2</span><br><span class="line">const app = new Vue(&#123;&#125;)</span><br><span class="line">Vue.use()</span><br><span class="line">Vue.mixin()</span><br><span class="line">Vue.component()</span><br><span class="line">Vue.directive()</span><br><span class="line">// vue3</span><br><span class="line">const app = Vue.createApp(&#123;&#125;)</span><br><span class="line">app.use()</span><br><span class="line">app.mixin()</span><br><span class="line">app.component()</span><br><span class="line">app.directive()</span><br></pre></td></tr></table></figure></li><li><p>emits属性</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">emits: [&#x27;onClickhandler&#x27;] // 建议 onXxx</span><br></pre></td></tr></table></figure></li><li><p>生命周期</p></li><li><p>多事件处理, @click&#x3D;”one($event),two($event)”</p></li><li><p>Fragment, 模板不需要包在一个根组件中了,template下面可以写多个标签</p></li><li><p>移除.sync改为v-model参数</p></li><li><p>异步组件的引用方式, defindAsyncComponent</p></li><li><p>移除filter,如:v-if&#x3D;”boolA | boolB”</p></li><li><p>Teleport,<teleport to="body"></teleport></p></li><li><p>Suspense</p></li></ul><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">&lt;Suspense&gt;</span><br><span class="line"> &lt;template&gt;</span><br><span class="line"> &lt;Test1/&gt; &lt;!-- 是一个异步组件 --&gt;</span><br><span class="line"> &lt;/template&gt;</span><br><span class="line"> &lt;!-- 具名插槽 </span><br><span class="line"> 即:Suspense 组件内部,有两个slot,</span><br><span class="line"> 其中一个具名为 fallback</span><br><span class="line"> --&gt;</span><br><span class="line"> &lt;tempalte #fallback&gt;</span><br><span class="line"> Loading...</span><br><span class="line"> &lt;/tempalte&gt;</span><br><span class="line">&lt;/Suspense&gt;</span><br></pre></td></tr></table></figure><ul><li>Compositio API<ul><li>reactive</li><li>ref toRef toRefs</li><li>readonly</li><li>computed</li><li>watch watchEffect</li><li>钩子函数生命周期</li></ul></li></ul><h5 id="原理"><a href="#原理" class="headerlink" title="原理"></a>原理</h5><ul><li>proxy 实现响应式<ul><li>对值类型无能为力,所以要用 ref</li></ul></li><li>编译优化<ul><li>PatchFlag 静态标记</li><li>hoistStatic 静态提升</li><li>cacheHandler 缓存事件</li><li>SSR 优化</li><li>Tree-shaking 优化</li></ul></li></ul><h5 id="Vue3-比-vue2-优势"><a href="#Vue3-比-vue2-优势" class="headerlink" title="Vue3 比 vue2 优势"></a>Vue3 比 vue2 优势</h5><ul><li>性能更好</li><li>体积更小</li><li>更好的ts支持</li><li>更好的代码组织</li><li>更好的逻辑抽离</li><li>更多的新功能</li></ul><h5 id="生命周期"><a href="#生命周期" class="headerlink" title="生命周期"></a>生命周期</h5><ul><li>beforeDestroy 改为 beforeUnmount</li><li>destroyed 改为 unmounted</li><li>其他沿用vue2的生命周期</li><li>setup等于 beforeCreate 和 created</li></ul><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">import &#123; onBeforeMount &#125; from &#x27;vue&#x27;</span><br><span class="line"></span><br><span class="line">export default &#123;</span><br><span class="line"> &lt;!-- 其他代码 --&gt;</span><br><span class="line"> setup () &#123;</span><br><span class="line"> onBeforeMount(() =&gt; &#123;</span><br><span class="line"> console.log(&#x27;onBeforeMount&#x27;)</span><br><span class="line"> &#125;)</span><br><span class="line"> &#125;</span><br><span class="line"> &lt;!-- 其他代码 --&gt;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h5 id="Composition-API-对比-Options-API"><a href="#Composition-API-对比-Options-API" class="headerlink" title="Composition API 对比 Options API"></a>Composition API 对比 Options API</h5><ul><li>更好的代码组织</li><li>更好的逻辑复用(options api 分散于生命周期中,代码越多越明显)</li><li>更好的类型推导(直接this.a 和 this.fn,不利于类型判断 )</li><li>不建议共用</li></ul><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line">export default &#123;</span><br><span class="line"> data () &#123;</span><br><span class="line"> return &#123;</span><br><span class="line"> a: 10</span><br><span class="line"> &#125;</span><br><span class="line"> &#125;,</span><br><span class="line"> methods: &#123;</span><br><span class="line"> fn () &#123;</span><br><span class="line"> const a = this.a</span><br><span class="line"> &#125;</span><br><span class="line"> &#125;,</span><br><span class="line"> mounted: &#123;</span><br><span class="line"> this.fn()</span><br><span class="line"> &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h5 id="ref-toRef-toRefs"><a href="#ref-toRef-toRefs" class="headerlink" title="ref toRef toRefs"></a>ref toRef toRefs</h5><ul><li>toRef 将一个响应式对象中的某个属性变为ref</li><li>toRefs 将一个响应式对象变为普通对象,这个普通对象的每个属性变为ref</li></ul><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br></pre></td><td class="code"><pre><span class="line">&lt;template&gt;</span><br><span class="line"> &lt;div&gt;&#123;&#123;name&#125;&#125;&#123;&#123;ageRef&#125;&#125;&lt;/div&gt;</span><br><span class="line"> &lt;div ref=&quot;eleRef&quot;&gt;我是文字&lt;/div&gt;</span><br><span class="line">&lt;/template&gt;</span><br><span class="line"></span><br><span class="line">&lt;script&gt;</span><br><span class="line"> import &#123; ref, toRef, toRefs, reactive, onMounted &#125; from &#x27;vue&#x27;</span><br><span class="line"></span><br><span class="line"> export default &#123;</span><br><span class="line"> name: &#x27;Ref&#x27;,</span><br><span class="line"> setup() &#123;</span><br><span class="line"> const eleRef = ref(null)</span><br><span class="line"> const ageRef = ref(20)</span><br><span class="line"> // 两者保持引用关系↓</span><br><span class="line"> // 不能返回解构的state,会丢失响应式</span><br><span class="line"> const state = reactive(&#123;</span><br><span class="line"> name: &#x27;张三&#x27;</span><br><span class="line"> &#125;)</span><br><span class="line"> const nameRef = toRef(state, &#x27;name&#x27;)</span><br><span class="line"> nameRef.value = &#x27;李四&#x27; // state中的name也会改变</span><br><span class="line"> state.name = &#x27;王五&#x27; // nameRef的值也会改变</span><br><span class="line"> // 两者保持引用关系 ↑</span><br><span class="line"> ageRef.value = 30</span><br><span class="line"> onMounted(() =&gt; &#123;</span><br><span class="line"> console.log(eleRef.value.innerHTML)</span><br><span class="line"> &#125;)</span><br><span class="line"></span><br><span class="line"> // const stateRefs = toRefs(state)</span><br><span class="line"> // const &#123;name: nameRef&#125; = stateRefs</span><br><span class="line"> // return stateRefs // toRefs的返回</span><br><span class="line"></span><br><span class="line"> // 其他返回</span><br><span class="line"> return &#123;</span><br><span class="line"> ageRef,</span><br><span class="line"> eleRef</span><br><span class="line"> &#125;</span><br><span class="line"> &#125;</span><br><span class="line"> &#125;</span><br><span class="line">&lt;/script&gt;</span><br><span class="line"></span><br></pre></td></tr></table></figure><h5 id="为什么要用-Ref"><a href="#为什么要用-Ref" class="headerlink" title="为什么要用 Ref"></a>为什么要用 Ref</h5><ul><li>返回 值类型,会丢失响应式</li><li>如在setup、computed、合成函数,都有可能返回 值类型</li><li>vue如果不定义ref。用户将自造ref,反而更混乱</li></ul><h5 id="为什么要-value"><a href="#为什么要-value" class="headerlink" title="为什么要.value"></a>为什么要.value</h5><ul><li>ref 是个对象(不丢失响应式),value存储值</li><li>通过.value属性的get和set实现响应式</li><li>用于模板、reactive时,(vue可以自己控制)不需要.value,其他情况都需要</li></ul><h5 id="Composition-API-实现逻辑复用"><a href="#Composition-API-实现逻辑复用" class="headerlink" title="Composition API 实现逻辑复用"></a>Composition API 实现逻辑复用</h5><ul><li>抽离逻辑代码到一个函数</li><li>函数命名约定为 useXxx 格式</li><li>setup中引用 useXxx 函数</li></ul><h5 id="Proxy-实现响应式"><a href="#Proxy-实现响应式" class="headerlink" title="Proxy 实现响应式"></a>Proxy 实现响应式</h5><ul><li>基本使用</li><li>Reflect</li><li>实现响应式</li><li>对比 defineProperty 提高性能<ul><li>set第一层属性的时候,触发set</li><li>set第二层属性的时候触发第一层属性的get</li><li>所以在get的时候,返回一个 新的proxy对象即可 深度监听,而且是惰性监听</li><li>所以性能比defineProperty好</li></ul></li></ul><h5 id="watch-和-watchEffect-的区别"><a href="#watch-和-watchEffect-的区别" class="headerlink" title="watch 和 watchEffect 的区别"></a>watch 和 watchEffect 的区别</h5><ul><li>两者都可以监听data属性变化</li><li>watch 需要明确监听哪个属性</li><li>watchEffect会根据其中的属性,自动监听其变化</li></ul><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line">// watch 默认不会立即执行</span><br><span class="line">// 监听ref</span><br><span class="line">watch(nameRef, (newVal, oldVal) =&gt; &#123;&#125;, [options])</span><br><span class="line">// 监听响应式</span><br><span class="line">watch(() =&gt; state.name, (newState, oldState) =&gt; &#123;&#125;, [options])</span><br><span class="line"></span><br><span class="line">watchEffect(() =&gt; &#123;</span><br><span class="line"> // 初始化就会执行一次,收集监听的数据</span><br><span class="line"> console.log(&#x27;watchEffect&#x27;)</span><br><span class="line">&#125;)</span><br><span class="line">watchEffect(() =&gt; &#123;</span><br><span class="line"> // 写了 state.name 就监听, 不写的不会监听</span><br><span class="line"> console.log(state.name)</span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure><h5 id="setup中如何获取组件实例"><a href="#setup中如何获取组件实例" class="headerlink" title="setup中如何获取组件实例"></a>setup中如何获取组件实例</h5><ul><li>在setup 和 Composition API 中没有this</li><li>可通过 getCurrentInstance 获取当前实例</li><li>若使用 options API 可照常使用 this</li></ul><h5 id="vue3-为何比-vue2-快"><a href="#vue3-为何比-vue2-快" class="headerlink" title="vue3 为何比 vue2 快"></a>vue3 为何比 vue2 快</h5><ul><li>Proxy 响应式</li><li>PatchFlag &#x2F;&#x2F; 标记节点类型</li><li>hoistStatic &#x2F;&#x2F; 缓存静态节点,多个节点合并</li><li>cacheHandler &#x2F;&#x2F;</li><li>SSR 优化</li><li>tree-shaking</li></ul><h6 id="PatchFlag"><a href="#PatchFlag" class="headerlink" title="PatchFlag"></a>PatchFlag</h6><ul><li>编译模板时,动态节点做标记</li><li>标记分为不同类型,如 TEXT PROPS</li><li>diff算法时,可以区分静态节点,以及不同类型的节点</li></ul><h6 id="hoistStatic"><a href="#hoistStatic" class="headerlink" title="hoistStatic"></a>hoistStatic</h6><ul><li>将静态节点的定义,提升到父作用域,缓存起来</li><li>多个相邻的静态节点,会被合并起来</li><li>典型的拿空间换时间的优化策略</li></ul><h6 id="cacheHandler"><a href="#cacheHandler" class="headerlink" title="cacheHandler"></a>cacheHandler</h6><ul><li>将事件缓存起来</li></ul><h5 id="vite-ES6-module"><a href="#vite-ES6-module" class="headerlink" title="vite - ES6 module"></a>vite - ES6 module</h5><h2 id="React"><a href="#React" class="headerlink" title="React"></a>React</h2><h3 id="基本使用-1"><a href="#基本使用-1" class="headerlink" title="基本使用"></a>基本使用</h3><h4 id="setState"><a href="#setState" class="headerlink" title="setState"></a>setState</h4><ul><li>不可变值</li><li>可能异步可能同步</li><li>可能会被合并</li></ul><h5 id="是同步还是异步"><a href="#是同步还是异步" class="headerlink" title="是同步还是异步"></a>是同步还是异步</h5><ul><li>直接使用是异步的</li><li>在setTimeout或者自定义的dom事件中是同步的</li></ul><h5 id="何时会合并"><a href="#何时会合并" class="headerlink" title="何时会合并"></a>何时会合并</h5><ul><li>对象形式,会被合并</li><li>函数形式,不会合并</li></ul><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">this.setState((state, props) =&gt; &#123;&#125;)</span><br></pre></td></tr></table></figure><ul><li>直接修改state的值,影响性能</li></ul><h4 id="事件为何要bind-this"><a href="#事件为何要bind-this" class="headerlink" title="事件为何要bind this"></a>事件为何要bind this</h4><h4 id="Reaact-高级特性"><a href="#Reaact-高级特性" class="headerlink" title="Reaact 高级特性"></a>Reaact 高级特性</h4><ul><li>函数组件</li><li>非受控组件</li><li>Portals ReactDOM.createPortals()</li><li>context</li><li>异步组件,import() React.lazy React.Suspense</li><li>性能优化,SCU 核心问题是 不可变值</li><li>高阶组件HOC</li><li>render props</li></ul><h5 id="jsx-本质是什么"><a href="#jsx-本质是什么" class="headerlink" title="jsx 本质是什么"></a>jsx 本质是什么</h5><ul><li>React.createElement 函数,执行返回 vnode</li></ul><h5 id="事件合成机制"><a href="#事件合成机制" class="headerlink" title="事件合成机制"></a>事件合成机制</h5><ul><li>更好的兼容性和跨平台</li><li>减少内存小号,避免频繁解绑</li><li>方便统一管理(如事务机制)</li></ul><h5 id="transaction-事务机制"><a href="#transaction-事务机制" class="headerlink" title="transaction 事务机制"></a>transaction 事务机制</h5><p>Transaction类的主要作用使用提供的包装(Wrapper)来包装一个函数。<br>Transaction会接受一个方法 func,和一组Wrapper。Transaction会在func执行之前,执行一组Wrapper中的initialize方法。而后执行func方法,在func方法执行完了之后,执行Wrapper提供的所有close方法。</p><h5 id="fiber"><a href="#fiber" class="headerlink" title="fiber"></a>fiber</h5><ul><li>props state</li><li>render 生成 vnode</li><li>patch(ele, vnode)<ul><li>reconciliation 执行diff算法,纯js计算<ul><li>进行任务拆分</li><li>DOM需要渲染时暂停,空闲时恢复</li><li>window.requestUdleCallback</li></ul></li><li>commit 阶段,将diff结果渲染到dom中</li></ul></li></ul><h5 id="react-性能优化"><a href="#react-性能优化" class="headerlink" title="react 性能优化"></a>react 性能优化</h5><ul><li>渲染列表key</li><li>及时销毁事件</li><li>合理使用异步组件</li><li>减少 bind 次数</li><li>合理使用 scu pure memo</li><li>合理使用 immutablejs</li><li>打包webpack优化</li><li>前端通用优化 图片懒加载等</li><li>SSR</li></ul><h5 id="React-和-Vue-区别"><a href="#React-和-Vue-区别" class="headerlink" title="React 和 Vue 区别"></a>React 和 Vue 区别</h5><p>1、共同</p><ul><li>支持组件化</li><li>数据驱动视图</li><li>vdom操作dom</li></ul><p>2、不同</p><ul><li>React 使用 jsx 拥抱js(文件都是.js结尾),Vue使用模板拥抱html(文件是.vue结尾)</li><li>React 函数式编程,Vue 声明式编程</li><li>React 更多需要自力更生,Vue 把想要的都给你</li></ul><h2 id="Webpack"><a href="#Webpack" class="headerlink" title="Webpack"></a>Webpack</h2><h3 id="升级webpack5-级周边插件后。需要做的调整"><a href="#升级webpack5-级周边插件后。需要做的调整" class="headerlink" title="升级webpack5 级周边插件后。需要做的调整"></a>升级webpack5 级周边插件后。需要做的调整</h3><ul><li>package.json的dev-server命令改了,<ul><li>4: “dev”:”webpack-dev-server –config build&#x2F;webpack.dev.js”</li><li>5: “dev”:”webpack serve –config build&#x2F;webpack.dev.js”</li></ul></li><li>webpack-merge<ul><li>4: const { smart } &#x3D; require(‘webpack-merge’)</li><li>5: const { merge } &#x3D; require(‘webpack-merge’)</li></ul></li><li>CleanWebpackplugin<ul><li>4: const CleanWebpackplugin &#x3D; require(‘clean-webpack-plugin’)</li><li>5: const { CleanWebpackplugin } &#x3D; require(‘clean-webpack-plugin’)</li></ul></li><li>modules.rules<ul><li>4: loader: [‘xxx-loader’]</li><li>5: use: [‘xxx-loader’]</li></ul></li><li>filename hash h小写<ul><li>4: filename: ‘bundle.[contentHash:8].js’</li><li>5: filename: ‘bundle.[contenthash:8].js’</li></ul></li></ul><h3 id="基本配置"><a href="#基本配置" class="headerlink" title="基本配置"></a>基本配置</h3><ul><li><p>拆分配置 和 merge</p></li><li><p>启动本地服务 webpack-dev-server</p></li><li><p>处理 ES6</p> <figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">modules.<span class="property">exports</span> = &#123;</span><br><span class="line"> <span class="attr">entry</span>: path.<span class="title function_">join</span>(srcPath, <span class="string">&#x27;index&#x27;</span>),</span><br><span class="line"> <span class="attr">modules</span>: &#123;</span><br><span class="line"> <span class="attr">rules</span>: [</span><br><span class="line"> &#123;</span><br><span class="line"> <span class="attr">test</span>: <span class="regexp">/\.js$/</span>,</span><br><span class="line"> <span class="attr">loader</span>: [<span class="string">&#x27;babel-loader&#x27;</span>],</span><br><span class="line"> <span class="attr">include</span>: srcPath,</span><br><span class="line"> <span class="attr">exclude</span>: <span class="regexp">/node_modules/</span></span><br><span class="line"> &#125;</span><br><span class="line"> ]</span><br><span class="line"> &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure> <figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line"> &quot;presets&quot;: [&quot;@babel/preset-env&quot;],</span><br><span class="line"> &quot;plugins&quot;: []</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></li><li><p>处理样式</p></li><li><p>处理图片</p><ul><li>file-loader 将图片变为url形式</li><li>url-loader 将图片变为base64形式,减少http请求,很小没必要再进行http请求</li></ul></li><li><p>模块化</p></li></ul><h3 id="高级配置"><a href="#高级配置" class="headerlink" title="高级配置"></a>高级配置</h3><h4 id="多入口"><a href="#多入口" class="headerlink" title="多入口"></a>多入口</h4><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">entry</span>: &#123;</span><br><span class="line"> <span class="attr">index</span>: path.<span class="title function_">join</span>(srcPath, <span class="string">&#x27;index.js&#x27;</span>),</span><br><span class="line"> <span class="attr">other</span>: path.<span class="title function_">join</span>(srcPath, <span class="string">&#x27;other.js&#x27;</span>),</span><br><span class="line">&#125;,</span><br><span class="line"><span class="attr">output</span>: &#123;</span><br><span class="line"> <span class="attr">filename</span>: <span class="string">&#x27;[name].[contentHash:8].js&#x27;</span>,</span><br><span class="line"> <span class="attr">path</span>: distPath</span><br><span class="line">&#125;</span><br><span class="line"><span class="attr">plugins</span>: [</span><br><span class="line"> <span class="keyword">new</span> <span class="title class_">HtmlWebpackPlugin</span>(&#123;</span><br><span class="line"> <span class="attr">template</span>: path.<span class="title function_">join</span>(srcPath, <span class="string">&#x27;index.html&#x27;</span>),</span><br><span class="line"> <span class="attr">filename</span>: <span class="string">&#x27;index.html&#x27;</span>,</span><br><span class="line"> <span class="attr">chunks</span>: [<span class="string">&#x27;index&#x27;</span>] <span class="comment">// 要引入什么js文件</span></span><br><span class="line"> &#125;),</span><br><span class="line"> <span class="keyword">new</span> <span class="title class_">HtmlWebpackPlugin</span>(&#123;</span><br><span class="line"> <span class="attr">template</span>: path.<span class="title function_">join</span>(srcPath, <span class="string">&#x27;other.html&#x27;</span>),</span><br><span class="line"> <span class="attr">filename</span>: <span class="string">&#x27;other.html&#x27;</span>,</span><br><span class="line"> <span class="attr">chunks</span>: [<span class="string">&#x27;other&#x27;</span>]</span><br><span class="line"> &#125;)</span><br><span class="line">]</span><br></pre></td></tr></table></figure><h4 id="抽离css文件"><a href="#抽离css文件" class="headerlink" title="抽离css文件"></a>抽离css文件</h4><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br></pre></td><td class="code"><pre><span class="line">coonst <span class="title class_">MiniCssExtracctplugin</span> = <span class="built_in">require</span>(<span class="string">&#x27;mini-css-extract-plugin&#x27;</span>)</span><br><span class="line"><span class="keyword">const</span> <span class="title class_">TerserJSPlugin</span> = <span class="built_in">require</span>(<span class="string">&#x27;terser-webpack-plugin&#x27;</span>)</span><br><span class="line"><span class="keyword">const</span> <span class="title class_">OptimizeCssAssetsplugin</span> = <span class="built_in">require</span>(<span class="string">&#x27;optimize-css-assets-webpack-plugin&#x27;</span>)</span><br><span class="line"></span><br><span class="line"><span class="variable language_">module</span>.<span class="property">exports</span> = &#123;</span><br><span class="line"> <span class="attr">module</span>: &#123;</span><br><span class="line"> <span class="attr">rules</span>: [</span><br><span class="line"> &#123;</span><br><span class="line"> <span class="attr">test</span>: <span class="regexp">/\.css$/</span>,</span><br><span class="line"> <span class="attr">loader</span>: [</span><br><span class="line"> <span class="title class_">MiniCssExtracctplugin</span>.<span class="property">loader</span>,</span><br><span class="line"> <span class="string">&#x27;css-loader&#x27;</span>,</span><br><span class="line"> <span class="string">&#x27;postcss-loader&#x27;</span></span><br><span class="line"> ]</span><br><span class="line"> &#125;</span><br><span class="line"> ]</span><br><span class="line"> &#125;,</span><br><span class="line"> <span class="attr">plugins</span>: [</span><br><span class="line"> <span class="keyword">new</span> <span class="title class_">MiniCssExtracctplugin</span>(&#123;</span><br><span class="line"> <span class="attr">filename</span>: <span class="string">&#x27;css/main.[contentHash:8].css&#x27;</span></span><br><span class="line"> &#125;)</span><br><span class="line"> ],</span><br><span class="line"> <span class="attr">optimization</span>: &#123;</span><br><span class="line"> <span class="attr">minimizer</span>: [<span class="keyword">new</span> <span class="title class_">TerserJSPlugin</span>(&#123;&#125;), <span class="keyword">new</span> <span class="title class_">OptimizeCssAssetsplugin</span>(&#123;&#125;)]</span><br><span class="line"> &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure><h4 id="压缩-optimization"><a href="#压缩-optimization" class="headerlink" title="压缩 optimization"></a>压缩 optimization</h4><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> <span class="title class_">TerserJSPlugin</span> = <span class="built_in">require</span>(<span class="string">&#x27;terser-webpack-plugin&#x27;</span>)</span><br><span class="line"><span class="keyword">const</span> <span class="title class_">OptimizeCssAssetsplugin</span> = <span class="built_in">require</span>(<span class="string">&#x27;optimize-css-assets-webpack-plugin&#x27;</span>)</span><br><span class="line"></span><br><span class="line"><span class="variable language_">module</span>.<span class="property">exports</span> = &#123;</span><br><span class="line"> &lt;!-- 其他配置 --&gt;</span><br><span class="line"> <span class="attr">optimization</span>: &#123;</span><br><span class="line"> <span class="attr">minimizer</span>: [<span class="keyword">new</span> <span class="title class_">TerserJSPlugin</span>(&#123;&#125;), <span class="keyword">new</span> <span class="title class_">OptimizeCssAssetsplugin</span>(&#123;&#125;)]</span><br><span class="line"> &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h4 id="抽离公共代码"><a href="#抽离公共代码" class="headerlink" title="抽离公共代码"></a>抽离公共代码</h4><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br></pre></td><td class="code"><pre><span class="line"><span class="variable language_">module</span>.<span class="property">exports</span> = &#123;</span><br><span class="line"> &lt;!-- 其他配置 --&gt;</span><br><span class="line"> <span class="attr">optimization</span>: &#123;</span><br><span class="line"> <span class="attr">minimizer</span>: [],</span><br><span class="line"> <span class="attr">splitChunks</span>: &#123;</span><br><span class="line"> <span class="comment">// initial 不处理异步</span></span><br><span class="line"> <span class="comment">// async 只处理异步</span></span><br><span class="line"> <span class="comment">// all 全部</span></span><br><span class="line"> <span class="attr">chunks</span>: <span class="string">&#x27;all&#x27;</span>,</span><br><span class="line"> <span class="attr">cacheGroups</span>: &#123;</span><br><span class="line"> <span class="comment">// 引用关系 关联 HtmlWebpackPlugin chunks 配置</span></span><br><span class="line"> <span class="comment">// 第三方模块</span></span><br><span class="line"> <span class="attr">vendor</span>: &#123;</span><br><span class="line"> <span class="attr">name</span>: <span class="string">&#x27;vendor&#x27;</span>, <span class="comment">// chunk 名称</span></span><br><span class="line"> <span class="attr">priority</span>: <span class="number">1</span>, <span class="comment">// 权限更高,有限抽离,重要!</span></span><br><span class="line"> <span class="attr">test</span>: <span class="regexp">/node_modules/</span>, <span class="comment">// </span></span><br><span class="line"> <span class="attr">minSize</span>: <span class="number">0</span>, <span class="comment">// 大小限制,实际开发不要写0</span></span><br><span class="line"> <span class="attr">minChunks</span>: <span class="number">1</span>, <span class="comment">// 最少复用go几次</span></span><br><span class="line"> &#125;,</span><br><span class="line"> <span class="attr">vendor</span>: &#123;</span><br><span class="line"> <span class="attr">name</span>: <span class="string">&#x27;common&#x27;</span>, <span class="comment">// chunk 名称</span></span><br><span class="line"> <span class="attr">priority</span>: <span class="number">0</span>, <span class="comment">// 权限更高,有限抽离,重要!</span></span><br><span class="line"> <span class="attr">minSize</span>: <span class="number">0</span>, <span class="comment">// 大小限制</span></span><br><span class="line"> <span class="attr">minChunks</span>: <span class="number">2</span>, <span class="comment">// 最少复用过几次</span></span><br><span class="line"> &#125;</span><br><span class="line"> &#125;</span><br><span class="line"> &#125;</span><br><span class="line"> &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure><h4 id="异步加载"><a href="#异步加载" class="headerlink" title="异步加载"></a>异步加载</h4><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 定义 chunk</span></span><br><span class="line"><span class="keyword">import</span>(<span class="string">&#x27;./dynamic-data.js&#x27;</span>).<span class="title function_">then</span>(<span class="function"><span class="params">res</span> =&gt;</span> &#123;</span><br><span class="line"> <span class="variable language_">console</span>.<span class="title function_">log</span>(res.<span class="property">default</span>.<span class="property">message</span>)</span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure><h3 id="module-chunk-bundle-区别"><a href="#module-chunk-bundle-区别" class="headerlink" title="module chunk bundle 区别"></a>module chunk bundle 区别</h3><ul><li>module:就是写的代码,只要可以引入的都是module</li><li>chunk:多模块合并成的,比如:index.js以及它引入的其他文件的集合</li><li>bundle:最终的输出文件</li></ul><h3 id="性能优化,构建速度"><a href="#性能优化,构建速度" class="headerlink" title="性能优化,构建速度"></a>性能优化,构建速度</h3><ul><li>优化babel-loader</li><li>IgnorePlugin</li><li>noParse</li><li>happypack</li><li>parallelUglifyPlugin</li><li>自动刷新</li><li>热更新</li><li>DllPlugin</li></ul><h4 id="优化babel-loader"><a href="#优化babel-loader" class="headerlink" title="优化babel-loader"></a>优化babel-loader</h4><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line"> <span class="attr">test</span>: <span class="regexp">/\.js$/</span>,</span><br><span class="line"> <span class="attr">use</span>: [<span class="string">&#x27;babel-loader?cacheDirectory&#x27;</span>], <span class="comment">// 开启缓存</span></span><br><span class="line"> <span class="attr">include</span>: path.<span class="title function_">resolve</span>(__dirname, <span class="string">&#x27;src&#x27;</span>), <span class="comment">// 明确范围</span></span><br><span class="line"> <span class="comment">// 排除范围, 两者选一即可</span></span><br><span class="line"> <span class="comment">// exclude: path.resolve(__dirname, &#x27;node_modules&#x27;)</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h4 id="IgnorePlugin,直接不引入,代码中没有"><a href="#IgnorePlugin,直接不引入,代码中没有" class="headerlink" title="IgnorePlugin,直接不引入,代码中没有"></a>IgnorePlugin,直接不引入,代码中没有</h4><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 不引入所有语言包</span></span><br><span class="line"><span class="keyword">new</span> webpack.<span class="title class_">IgnorePlugin</span>(<span class="regexp">/\.\/locale/</span>, <span class="regexp">/moment/</span>)</span><br></pre></td></tr></table></figure><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 手动引入中文</span></span><br><span class="line">moment.<span class="title function_">locale</span>(<span class="string">&#x27;zh-cn&#x27;</span>)</span><br></pre></td></tr></table></figure><h4 id="noParse,引入,但不打包"><a href="#noParse,引入,但不打包" class="headerlink" title="noParse,引入,但不打包"></a>noParse,引入,但不打包</h4><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">modules.<span class="property">exports</span>=&#123;</span><br><span class="line"> <span class="attr">module</span>: &#123;</span><br><span class="line"> <span class="attr">noParse</span>: [<span class="regexp">/react\.min\.js/</span>],</span><br><span class="line"> <span class="attr">rules</span>: [</span><br><span class="line"> &#123;&#125;</span><br><span class="line"> ]</span><br><span class="line"> &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure><h4 id="happyPack-多线程打包"><a href="#happyPack-多线程打包" class="headerlink" title="happyPack 多线程打包"></a>happyPack 多线程打包</h4><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> <span class="title class_">HappyPack</span> = <span class="built_in">require</span>(<span class="string">&#x27;happypack&#x27;</span>)</span><br><span class="line"></span><br><span class="line"><span class="variable language_">module</span>.<span class="property">exports</span> = &#123;</span><br><span class="line"> <span class="attr">modules</span>: &#123;</span><br><span class="line"> <span class="attr">rules</span>: [</span><br><span class="line"> &#123;</span><br><span class="line"> <span class="attr">test</span>: <span class="regexp">/\.js$/</span>,</span><br><span class="line"> <span class="attr">use</span>: [<span class="string">&#x27;happypack/loader?id=babel&#x27;</span>],</span><br><span class="line"> <span class="attr">include</span>: srcPath</span><br><span class="line"> &#125;</span><br><span class="line"> ]</span><br><span class="line"> &#125;,</span><br><span class="line"> <span class="attr">plugins</span>: [</span><br><span class="line"> <span class="keyword">new</span> <span class="title class_">Happypack</span>(&#123;</span><br><span class="line"> <span class="attr">id</span>: <span class="string">&#x27;babel&#x27;</span>,</span><br><span class="line"> <span class="attr">loaders</span>: [<span class="string">&#x27;babel-loader?cacheDirectory&#x27;</span>]</span><br><span class="line"> &#125;)</span><br><span class="line"> ]</span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure><h4 id="ParallelUglifyPlugin-多进程压缩js"><a href="#ParallelUglifyPlugin-多进程压缩js" class="headerlink" title="ParallelUglifyPlugin 多进程压缩js"></a>ParallelUglifyPlugin 多进程压缩js</h4><p>new ParallelUglifyPlugin({<br> uglifyJS: {<br> &#x2F;&#x2F; 还是使用 uglifyjs 压缩,只是开启了多进程<br> output: {<br> beautify: false, &#x2F;&#x2F; 最紧凑的输出<br> comments: false &#x2F;&#x2F; 删除所有注释<br> },<br> compress: {<br> &#x2F;&#x2F; 删除所有 console 语句,可兼容ie<br> drop_console: true,<br> }<br> }<br>})</p><h4 id="自动刷新"><a href="#自动刷新" class="headerlink" title="自动刷新"></a>自动刷新</h4><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="variable language_">module</span>.<span class="property">exports</span> = &#123;</span><br><span class="line"> <span class="comment">// 开启之后, webpack-dev-server 会自动开启刷新浏览器</span></span><br><span class="line"> <span class="attr">watch</span>: <span class="literal">true</span>, <span class="comment">// 默认false </span></span><br><span class="line"> <span class="attr">watchOptions</span>: &#123;</span><br><span class="line"> <span class="attr">ignored</span>: <span class="regexp">/node_modules/</span>, <span class="comment">// 忽略</span></span><br><span class="line"> <span class="attr">aggregateTimeout</span>: <span class="number">300</span>, <span class="comment">// 监听变化后等300ms再执行,防止太快</span></span><br><span class="line"> <span class="attr">poll</span>: <span class="number">1000</span> <span class="comment">// 每隔1000ms 询问一次</span></span><br><span class="line"> &#125;</span><br><span class="line"></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h4 id="DllPlugin"><a href="#DllPlugin" class="headerlink" title="DllPlugin"></a>DllPlugin</h4><ul><li>前端框架如 vue react , 体积大,构建慢</li><li>较稳定,不常升级版本</li><li>同一个版本只构建一次即可,不用每次都重新构建</li><li>webpack 内置</li><li>打包出dll文件</li><li>DllReferencePlugin - 使用dll文件</li></ul><p>1、打包dll配置 webpacl.dll.js</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="variable language_">module</span>.<span class="property">exports</span> = &#123;</span><br><span class="line"> <span class="attr">mode</span>: <span class="string">&#x27;development&#x27;</span>,</span><br><span class="line"></span><br><span class="line"> <span class="attr">entry</span>: &#123;</span><br><span class="line"> <span class="comment">// 把 React 相关模块的放到一个单独的动态链接库</span></span><br><span class="line"> <span class="attr">react</span>: [<span class="string">&#x27;react&#x27;</span>, <span class="string">&#x27;react-dom&#x27;</span>]</span><br><span class="line"> &#125;,</span><br><span class="line"> <span class="attr">output</span>: &#123;</span><br><span class="line"> <span class="attr">filename</span>: <span class="string">&#x27;[name].dll.js&#x27;</span>,</span><br><span class="line"> <span class="attr">path</span>: distPath,</span><br><span class="line"> <span class="attr">library</span>: <span class="string">&#x27;_dll_[name]&#x27;</span></span><br><span class="line"> &#125;,</span><br><span class="line"> <span class="attr">plugins</span>: [</span><br><span class="line"> <span class="keyword">new</span> <span class="title class_">DllPlugin</span>(&#123;</span><br><span class="line"> <span class="attr">name</span>: <span class="string">&#x27;_dll_[name]&#x27;</span>,</span><br><span class="line"> <span class="attr">path</span>: path.<span class="title function_">join</span>(distPath, <span class="string">&#x27;[name].mainfest.json&#x27;</span>),</span><br><span class="line"> &#125;)</span><br><span class="line"> ]</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>2、修改index.html模板</p><p>添加</p><script src="./react.dll.js"></script><p>3、配置webpack.dev.js</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line"><span class="attr">moduls</span>: &#123;</span><br><span class="line"></span><br><span class="line"> <span class="attr">rules</span>: [</span><br><span class="line"> &#123;</span><br><span class="line"> <span class="attr">test</span>: <span class="regexp">/\.js$/</span>,</span><br><span class="line"> <span class="attr">loader</span>: [<span class="string">&#x27;babel-loader&#x27;</span>],</span><br><span class="line"> <span class="attr">include</span>: srcPath,</span><br><span class="line"> <span class="attr">exclude</span>: <span class="regexp">/node_modules/</span> <span class="comment">// 使用dll,所以不需要转换了</span></span><br><span class="line"> &#125;</span><br><span class="line"> ]</span><br><span class="line"></span><br><span class="line">&#125;,</span><br><span class="line"></span><br><span class="line"><span class="attr">plugins</span>: [</span><br><span class="line"></span><br><span class="line"> <span class="keyword">new</span> <span class="title class_">DllReferencePlugin</span>(&#123;</span><br><span class="line"> <span class="attr">mainfest</span>: <span class="built_in">require</span>(path.<span class="title function_">join</span>(distPath, <span class="string">&#x27;react.mainfest.json&#x27;</span>))</span><br><span class="line"> &#125;)</span><br><span class="line"></span><br><span class="line">]</span><br><span class="line"></span><br></pre></td></tr></table></figure><h4 id="webpack-优化构建速度-(可用于生产环境)"><a href="#webpack-优化构建速度-(可用于生产环境)" class="headerlink" title="webpack 优化构建速度 (可用于生产环境)"></a>webpack 优化构建速度 (可用于生产环境)</h4><ul><li>优化 babel-loader</li><li>IgnorePlugin</li><li>noParse</li><li>happyPack</li><li>parellelUglifyPlugin</li></ul><h4 id="webpack-优化构建速度-(不可用于生产环境)"><a href="#webpack-优化构建速度-(不可用于生产环境)" class="headerlink" title="webpack 优化构建速度 (不可用于生产环境)"></a>webpack 优化构建速度 (不可用于生产环境)</h4><ul><li>自动刷新</li><li>热更新</li><li>DllPlugin</li></ul><h4 id="webpack性能优化-产出代码"><a href="#webpack性能优化-产出代码" class="headerlink" title="webpack性能优化 - 产出代码"></a>webpack性能优化 - 产出代码</h4><ul><li><p>体积更小</p></li><li><p>合理分包,不重复引用</p></li><li><p>速度更快,内损使用更少</p></li><li><p>小图片base64编码</p></li><li><p>bundle + hash</p></li><li><p>懒加载 import 语法</p></li><li><p>提取公共代码 splitChunks</p></li><li><p>IgnorePlugin</p></li><li><p>cdn加速</p></li><li><p>使用 production</p></li><li><p>Scope Hosting 改变打包作用域</p></li></ul><h5 id="使用-production"><a href="#使用-production" class="headerlink" title="使用 production"></a>使用 production</h5><ul><li>自动开启代码压缩</li><li>Vue React 等会自动删除调试代码(如开发环境的warning)</li><li>启动 Tree-Shaking</li></ul><h5 id="Scope-Hosting"><a href="#Scope-Hosting" class="headerlink" title="Scope Hosting"></a>Scope Hosting</h5><ul><li>代码体积更小</li><li>创建函数作用域更少</li><li>代码可读性更好</li></ul><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> <span class="title class_">ModuleConcatenationPlugin</span> = <span class="built_in">require</span>(<span class="string">&#x27;webpack/lib/optmize/ModuleConcatenationPlugin&#x27;</span>)</span><br><span class="line"></span><br><span class="line"><span class="variable language_">module</span>.<span class="property">exports</span> = &#123;</span><br><span class="line"></span><br><span class="line"> <span class="attr">resolve</span>: &#123;</span><br><span class="line"> <span class="comment">// 针对npm中的第三方模块有限采用 jsnext:main 中指向的es6 模块化语法的文件</span></span><br><span class="line"> <span class="attr">mainFields</span>: [<span class="string">&#x27;jsnext:main&#x27;</span>, <span class="string">&#x27;browser&#x27;</span>, <span class="string">&#x27;main&#x27;</span>]</span><br><span class="line"> &#125;,</span><br><span class="line"> <span class="attr">plugins</span>: [</span><br><span class="line"> <span class="comment">// 开启 Scope Hosting</span></span><br><span class="line"> <span class="keyword">new</span> <span class="title class_">ModuleConcatenationPlugin</span>()</span><br><span class="line"> ]</span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure><h2 id="babel"><a href="#babel" class="headerlink" title="babel"></a>babel</h2><ul><li>环境搭建 , 基本配置<ul><li>.babelrc 配置</li><li>presets 和 plugins</li></ul></li><li>babel-polyfill</li><li>babel-runtime</li></ul><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line"> &quot;presets&quot;: [</span><br><span class="line"> [</span><br><span class="line"> &quot;@babel/preset-env&quot;,</span><br><span class="line"> &#123;</span><br><span class="line"> &quot;useBuiltins&quot;: &quot;usage&quot;, // 按需引入</span><br><span class="line"> &quot;corejs&quot;: 3 // 版本3</span><br><span class="line"> &#125;</span><br><span class="line"> ]</span><br><span class="line"> ],</span><br><span class="line"> plugins: []</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="babel-polyfill"><a href="#babel-polyfill" class="headerlink" title="babel-polyfill"></a>babel-polyfill</h3><ul><li>babel 7.4 之后弃用 babel-polyfill</li><li>推荐直接使用 core-js 和 regenerator</li></ul><h3 id="babel-polyfill-问题"><a href="#babel-polyfill-问题" class="headerlink" title="babel-polyfill 问题"></a>babel-polyfill 问题</h3><ul><li>污染全局环境,使用babel-runtime解决</li></ul><p>。babelrc中</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line">plugins: [</span><br><span class="line"> [</span><br><span class="line"> &quot;@babel/plugin-transform-runtime&quot;,</span><br><span class="line"> &#123;</span><br><span class="line"> &quot;absoluteRuntime&quot;: false,</span><br><span class="line"> &quot;corejs&quot;: 3,</span><br><span class="line"> &quot;helpers&quot;: true,</span><br><span class="line"> &quot;regenerator&quot;: true,</span><br><span class="line"> &quot;useESModules&quot;: false</span><br><span class="line"> &#125;</span><br><span class="line"> ]</span><br><span class="line">]</span><br><span class="line"></span><br><span class="line"></span><br></pre></td></tr></table></figure>]]></content>
11
+ <content type="html"><![CDATA[<p>vue,react,webpack,babel 要点总结</p><span id="more"></span><p><a href="https://juejin.cn/post/7146973901166215176">掘金大牛:「历时8个月」10万字前端知识体系总结(基础知识篇)</a></p><h2 id="Vue"><a href="#Vue" class="headerlink" title="Vue"></a>Vue</h2><h3 id="基本使用"><a href="#基本使用" class="headerlink" title="基本使用"></a>基本使用</h3><h4 id="v-html"><a href="#v-html" class="headerlink" title="v-html"></a>v-html</h4><p>原始html内容,有xss防线</p><h4 id="computed"><a href="#computed" class="headerlink" title="computed"></a>computed</h4><ul><li>有缓存,data不变不会重新计算</li></ul><h4 id="watch"><a href="#watch" class="headerlink" title="watch"></a>watch</h4><p>监听引用类型,使用下面方法深度监听,但是<strong>拿不到<code>oldVal</code></strong></p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">watch</span>:&#123;</span><br><span class="line"> <span class="attr">info</span>: &#123;</span><br><span class="line"> handler (oldVal, val) &#123;</span><br><span class="line"> <span class="variable language_">console</span>.<span class="title function_">log</span>((oldVal, val)</span><br><span class="line"> &#125;,</span><br><span class="line"> <span class="attr">deep</span>: <span class="literal">true</span></span><br><span class="line"> &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h4 id="class-写法"><a href="#class-写法" class="headerlink" title="class 写法"></a>class 写法</h4><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">data () &#123;</span><br><span class="line"> <span class="attr">isblack</span>: <span class="literal">true</span>,</span><br><span class="line"> <span class="attr">isRed</span>: <span class="literal">false</span>,</span><br><span class="line"> </span><br><span class="line"> <span class="attr">black</span>: <span class="string">&quot;black&quot;</span>,</span><br><span class="line"> <span class="attr">red</span>: <span class="string">&quot;red&quot;</span></span><br><span class="line">&#125;</span><br><span class="line">:<span class="keyword">class</span>=&#123;<span class="attr">black</span>: isblack, <span class="attr">red</span>: isRed&#125;</span><br></pre></td></tr></table></figure><p>或者</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">:<span class="keyword">class</span>=[black, red]</span><br></pre></td></tr></table></figure><h4 id="style"><a href="#style" class="headerlink" title="style"></a>style</h4><p>object 类型,驼峰写法</p><h4 id="v-if-和-v-show-区别"><a href="#v-if-和-v-show-区别" class="headerlink" title="v-if 和 v-show 区别"></a>v-if 和 v-show 区别</h4><p>v-if 是否渲染,更新不频繁使用</p><p>v-show 是通过 css 的 display 控制显示与隐藏,频繁切换使用</p><h4 id="v-for"><a href="#v-for" class="headerlink" title="v-for"></a>v-for</h4><ul><li>也可以遍历对象</li><li>key,不能乱写,尽量不用index,不用random</li><li><code>v-for</code> 优先级比<code>v-if</code>高</li></ul><h4 id="事件"><a href="#事件" class="headerlink" title="事件"></a>事件</h4><h5 id="event参数,自定义参数"><a href="#event参数,自定义参数" class="headerlink" title="event参数,自定义参数"></a>event参数,自定义参数</h5><ul><li>没有参数,直接可以获取</li><li>有参数,用<code>$event</code></li><li><code>event</code>是原生对象</li></ul><h5 id="事件修饰符,按键修饰符"><a href="#事件修饰符,按键修饰符" class="headerlink" title="事件修饰符,按键修饰符"></a>事件修饰符,按键修饰符</h5><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">@click.stop.prevent</span><br><span class="line">@click.ctrl.exact</span><br></pre></td></tr></table></figure><h5 id="!(观察)事件被绑定到哪里"><a href="#!(观察)事件被绑定到哪里" class="headerlink" title="!(观察)事件被绑定到哪里"></a>!(观察)事件被绑定到哪里</h5><p><code>event.target</code>的值表明事件是挂在<strong>当前元素</strong>上的</p><h4 id="表单"><a href="#表单" class="headerlink" title="表单"></a>表单</h4><h5 id="v-model"><a href="#v-model" class="headerlink" title="v-model"></a>v-model</h5><p>v-model.trim</p><p>v-model.lazy</p><p>v-model.number</p><h2 id="组件使用"><a href="#组件使用" class="headerlink" title="组件使用"></a>组件使用</h2><h3 id="props"><a href="#props" class="headerlink" title="props"></a>props</h3><p>简写</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">props</span>:[<span class="string">&#x27;list&#x27;</span>]</span><br></pre></td></tr></table></figure><p>复杂写法,常用,可以定义类型和默认值</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">props</span>: &#123;</span><br><span class="line"> <span class="attr">type</span>: <span class="title class_">Array</span>,</span><br><span class="line"> <span class="attr">default</span>:&#123;</span><br><span class="line"> <span class="keyword">return</span> []</span><br><span class="line"> &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="emit"><a href="#emit" class="headerlink" title="$emit"></a>$emit</h3><p>父组件</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">&lt;<span class="title class_">Input</span> @add=<span class="string">&quot;clickhandler&quot;</span>/&gt;</span><br></pre></td></tr></table></figure><p>子组件</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">methods</span>:&#123;</span><br><span class="line"> <span class="title function_">addTitle</span>(<span class="params"></span>) &#123;</span><br><span class="line"> <span class="variable language_">this</span>.$emit(<span class="string">&#x27;add&#x27;</span>, title)</span><br><span class="line"> &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="组件通信-自定义事件(兄弟组件通信)"><a href="#组件通信-自定义事件(兄弟组件通信)" class="headerlink" title="组件通信 - 自定义事件(兄弟组件通信)"></a>组件通信 - 自定义事件(兄弟组件通信)</h3><p><strong>vue 具有自定义事件能力 $on、$off、$emit</strong></p><p><strong>组件卸载时,解除事件绑定,销毁子组件,定时器等</strong></p><h3 id="组件生命周期"><a href="#组件生命周期" class="headerlink" title="组件生命周期"></a>组件生命周期</h3><ul><li>挂载阶段</li><li>更新阶段</li><li>销毁阶段</li></ul><h4 id="单个组件"><a href="#单个组件" class="headerlink" title="单个组件"></a>单个组件</h4><p>created:vue实例化完成</p><p>mounted:渲染完成</p><h4 id="父子组件"><a href="#父子组件" class="headerlink" title="父子组件"></a>父子组件</h4><p>组件实例创建: 先父后子</p><p>组件渲染: 先子后父【子组件渲染完,才能挂载】</p><h3 id="高级特性"><a href="#高级特性" class="headerlink" title="高级特性"></a>高级特性</h3><h4 id="自定义-v-model"><a href="#自定义-v-model" class="headerlink" title="自定义 v-model"></a>自定义 v-model</h4><p>父组件中</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">&lt;CunstomVModal v-model=&#123;name&#125;&gt;</span><br></pre></td></tr></table></figure><p>子组件中 CunstomVModal</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line">&lt;template&gt;</span><br><span class="line"> &lt;input type=&quot;text&quot; :value=&quot;txt&quot; @input=&quot;$emit(&#x27;chage&#x27;, $event.target.value)&quot;</span><br><span class="line">&lt;/template&gt;</span><br><span class="line">&lt;script&gt;</span><br><span class="line"> export default &#123;</span><br><span class="line"> model:&#123;</span><br><span class="line"> prop: &#x27;txt&#x27;,</span><br><span class="line"> event:&#x27;chage&#x27;</span><br><span class="line"> &#125;,</span><br><span class="line"> props:&#123;</span><br><span class="line"> txt: String,</span><br><span class="line"> default () &#123;</span><br><span class="line"> return &#x27;&#x27;</span><br><span class="line"> &#125;</span><br><span class="line"> &#125;</span><br><span class="line"> &#125;</span><br><span class="line">&lt;/script&gt;</span><br></pre></td></tr></table></figure><h4 id="nextTick、refs"><a href="#nextTick、refs" class="headerlink" title="$nextTick、refs"></a>$nextTick、refs</h4><ul><li>vue 是异步渲染</li><li>data 改变之后,dom不会like渲染</li><li>$nextTick 会在dom渲染之后被处罚,以获取最新的dom节点</li></ul><h4 id="slot"><a href="#slot" class="headerlink" title="slot"></a>slot</h4><ul><li>基本使用</li><li>作用于插槽</li><li>具名插槽</li></ul><h4 id="动态组件"><a href="#动态组件" class="headerlink" title="动态组件"></a>动态组件</h4><ul><li>:is&#x3D;”component-name”</li><li>需要根据数据,动态渲染的场景。即组件类型不确定</li></ul><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line">&lt;template&gt;</span><br><span class="line"> &lt;component :is=&quot;compName&quot;&gt;</span><br><span class="line">&lt;/template&gt;</span><br><span class="line">&lt;script&gt;</span><br><span class="line"> import myComp from &#x27;./myComp&#x27;</span><br><span class="line"> components:&#123;</span><br><span class="line"> myComp</span><br><span class="line"> &#125;,</span><br><span class="line"> data () &#123;</span><br><span class="line"> return &#123;</span><br><span class="line"> compName: &#x27;myComp&#x27;</span><br><span class="line"> &#125;</span><br><span class="line"> &#125;</span><br><span class="line"> </span><br><span class="line">&lt;/script&gt;</span><br></pre></td></tr></table></figure><h4 id="异步组件"><a href="#异步组件" class="headerlink" title="异步组件"></a>异步组件</h4><ul><li>import()</li><li>按需加载,异步加载大组件</li></ul><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">export default &#123;</span><br><span class="line"> components:&#123;</span><br><span class="line"> Demo: () =&gt; import(&#x27;./ImportDemo&#x27;)</span><br><span class="line"> &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h4 id="keep-alive"><a href="#keep-alive" class="headerlink" title="keep-alive"></a>keep-alive</h4><ul><li>缓存组件</li><li>频繁切换,不需要重复渲染</li><li>vue常见性能优化</li></ul><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">// 每个组件只渲染一次,且不会销毁。</span><br><span class="line">// 简单的用v-show就行,稍微复杂的用keep-alive</span><br><span class="line">&lt;keep-alive&gt;</span><br><span class="line"> &lt;A v-if=&quot;a&quot;&gt;&lt;/A&gt;</span><br><span class="line"> &lt;B v-if=&quot;b&quot;&gt;&lt;/B&gt;</span><br><span class="line"> &lt;C v-if=&quot;c&quot;&gt;&lt;/C&gt;</span><br><span class="line">&lt;/keep-alive&gt;</span><br></pre></td></tr></table></figure><h4 id="mixin"><a href="#mixin" class="headerlink" title="mixin"></a>mixin</h4><ul><li>多个组件有相同的逻辑,抽离出来</li><li>mixin并不是完美的解决方案,会有一些问题</li><li>vue 3 提出的 composition API 旨在解决这些问题</li></ul><hr><h5 id="问题"><a href="#问题" class="headerlink" title="问题"></a>问题</h5><ul><li>变量来源不明确,不利于阅读</li><li>多个mixin可能会造成命名冲突</li><li>mixin和组件可能会出现多对多的关系,复杂度较高</li></ul><hr><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">&lt;sript&gt;</span><br><span class="line"> import Mixin from &#x27;./mixin&#x27;</span><br><span class="line"> export default &#123;</span><br><span class="line"> mixins: [Mixin],</span><br><span class="line"> data () &#123;</span><br><span class="line"> return &#123;&#125;</span><br><span class="line"> &#125;</span><br><span class="line"> &#125;</span><br><span class="line">&lt;/sript&gt;</span><br></pre></td></tr></table></figure><h4 id="vuex-使用"><a href="#vuex-使用" class="headerlink" title="vuex 使用"></a>vuex 使用</h4><ul><li>基本概念、基本使用、API</li><li>state的数据结构设计</li></ul><h5 id="vuex基本概念"><a href="#vuex基本概念" class="headerlink" title="vuex基本概念"></a>vuex基本概念</h5><ul><li>state</li><li>gettters</li><li>action</li><li>mutation</li></ul><h5 id="用于vue组件"><a href="#用于vue组件" class="headerlink" title="用于vue组件"></a>用于vue组件</h5><ul><li>dispatch</li><li>commit</li><li>mapstate</li><li>mapGetters</li><li>mapActions</li><li>mapMutations</li></ul><h4 id="vue-router"><a href="#vue-router" class="headerlink" title="vue-router"></a>vue-router</h4><ul><li>路由模式:<ul><li>hash<ul><li>window.onhashchange</li><li>会出发网页跳转,</li><li>可前进后退,</li><li>不会提交到server端,强制刷新也不会</li><li>可刷新</li></ul></li><li>H5 history<ul><li>history.pushState</li><li>window.onpopstate</li><li>不可刷新(需要后端支持)</li></ul></li></ul></li><li>路由配置:<ul><li>动态路由 {path: ‘&#x2F;user&#x2F;:id?’, component: User}</li><li>懒加载 {path: ‘&#x2F;user&#x2F;:id?’, component: () &#x3D;&gt; import(‘.&#x2F;user’)}</li></ul></li></ul><h3 id="Vue原理"><a href="#Vue原理" class="headerlink" title="Vue原理"></a>Vue原理</h3><h4 id="如何里面MVVM"><a href="#如何里面MVVM" class="headerlink" title="如何里面MVVM"></a>如何里面MVVM</h4><ul><li>很久以前的视图<ul><li>asp jsp php 已经有组件化了</li><li>传统组件,只是静态渲染,更新依赖于操作dom</li></ul></li><li>数据驱动视图(vue MVVM,react setState)</li></ul><h4 id="vue-响应式"><a href="#vue-响应式" class="headerlink" title="vue 响应式"></a>vue 响应式</h4><ul><li>组件data的数据一旦变化,立刻出发视图更新</li><li>核心API - Object.defineProperty<ul><li>get</li><li>set</li></ul></li><li>Object.defineProperty 的一些缺点 (Vue3.0 使用 Proxy)</li><li>proxy 兼容性不太好,且无法 polyfill</li></ul><h5 id="Object-defineProperty-缺点"><a href="#Object-defineProperty-缺点" class="headerlink" title="Object.defineProperty 缺点"></a>Object.defineProperty 缺点</h5><ul><li>深度监听,需要递归到底,一次性计算量大</li><li>新增,删除 无法监听,需要用 Vue.set 和 Vue.delete</li><li>无法监听数组,需要专门处理</li></ul><h5 id="Proxy-实现响应式"><a href="#Proxy-实现响应式" class="headerlink" title="Proxy 实现响应式"></a>Proxy 实现响应式</h5><ul><li>基本使用</li><li>Reflect</li><li>实现响应式</li><li>对比 defineProperty 提高性能<ul><li>set第一层属性的时候,触发set</li><li>set第二层属性的时候触发第一层属性的get</li><li>所以在get的时候,返回一个 新的proxy对象即可 深度监听,而且是惰性监听</li><li>所以性能比defineProperty好</li></ul></li></ul><h4 id="虚拟DOM-和-diff"><a href="#虚拟DOM-和-diff" class="headerlink" title="虚拟DOM 和 diff"></a>虚拟DOM 和 diff</h4><ul><li>DOM操作非常耗性能,js是执行非常快的</li><li>vue和react是数据驱动视图</li></ul><h5 id="vdom"><a href="#vdom" class="headerlink" title="vdom"></a>vdom</h5><p>用js模拟dom结构,计算出最小的变更,操作dom</p><h5 id="snabbdom"><a href="#snabbdom" class="headerlink" title="snabbdom"></a>snabbdom</h5><p>h函数,返回vnode(js)<br>patch(container, vnode) 初次渲染<br>patch(oldVnode, newVnode) 更新,新旧vnode都有children,那么updateChildren<br>patch(oldVnode, null) 销毁清空</p><h5 id="diff"><a href="#diff" class="headerlink" title="diff"></a>diff</h5><ul><li>只比较同一层级,不跨级比较</li><li>tag不相同,直接删掉重建,不再深度比较</li><li>tag和key都相同,则认为是相同节点,不再深度比较</li></ul><h5 id="模板编译"><a href="#模板编译" class="headerlink" title="模板编译"></a>模板编译</h5><ul><li>vue将模板编译为render函数</li><li>执行render函数生成vnode,patch</li><li>触发响应式,监听data属性getter setter</li></ul><blockquote><ul><li>with语法</li><li>vue组件可以用render代替template</li></ul></blockquote><h6 id="初次渲染过程"><a href="#初次渲染过程" class="headerlink" title="初次渲染过程"></a>初次渲染过程</h6><ul><li>解析为render函数</li><li>触发响应式,监听data属性getter setter</li><li>执行render函数,生成vnode,patch</li></ul><h6 id="更新过程"><a href="#更新过程" class="headerlink" title="更新过程"></a>更新过程</h6><ul><li>修改data,触发setter,(此前在getter中已被监听)</li><li>重新执行render函数,生成newVnode</li><li>patch(oldVnode, newVnode)</li></ul><h5 id="组件异步渲染"><a href="#组件异步渲染" class="headerlink" title="组件异步渲染"></a>组件异步渲染</h5><ul><li>$nextTick</li><li>汇总data修改,一次性更新视图</li><li>减少dom操作次数,提高性能</li></ul><h4 id="Vue3"><a href="#Vue3" class="headerlink" title="Vue3"></a>Vue3</h4><h5 id="新功能"><a href="#新功能" class="headerlink" title="新功能"></a>新功能</h5><ul><li><p>createApp</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">// vue2</span><br><span class="line">const app = new Vue(&#123;&#125;)</span><br><span class="line">Vue.use()</span><br><span class="line">Vue.mixin()</span><br><span class="line">Vue.component()</span><br><span class="line">Vue.directive()</span><br><span class="line">// vue3</span><br><span class="line">const app = Vue.createApp(&#123;&#125;)</span><br><span class="line">app.use()</span><br><span class="line">app.mixin()</span><br><span class="line">app.component()</span><br><span class="line">app.directive()</span><br></pre></td></tr></table></figure></li><li><p>emits属性</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">emits: [&#x27;onClickhandler&#x27;] // 建议 onXxx</span><br></pre></td></tr></table></figure></li><li><p>生命周期</p></li><li><p>多事件处理, @click&#x3D;”one($event),two($event)”</p></li><li><p>Fragment, 模板不需要包在一个根组件中了,template下面可以写多个标签</p></li><li><p>移除.sync改为v-model参数</p></li><li><p>异步组件的引用方式, defindAsyncComponent</p></li><li><p>移除filter,如:v-if&#x3D;”boolA | boolB”</p></li><li><p>Teleport,<teleport to="body"></teleport></p></li><li><p>Suspense</p></li></ul><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">&lt;Suspense&gt;</span><br><span class="line"> &lt;template&gt;</span><br><span class="line"> &lt;Test1/&gt; &lt;!-- 是一个异步组件 --&gt;</span><br><span class="line"> &lt;/template&gt;</span><br><span class="line"> &lt;!-- 具名插槽 </span><br><span class="line"> 即:Suspense 组件内部,有两个slot,</span><br><span class="line"> 其中一个具名为 fallback</span><br><span class="line"> --&gt;</span><br><span class="line"> &lt;tempalte #fallback&gt;</span><br><span class="line"> Loading...</span><br><span class="line"> &lt;/tempalte&gt;</span><br><span class="line">&lt;/Suspense&gt;</span><br></pre></td></tr></table></figure><ul><li>Compositio API<ul><li>reactive</li><li>ref toRef toRefs</li><li>readonly</li><li>computed</li><li>watch watchEffect</li><li>钩子函数生命周期</li></ul></li></ul><h5 id="原理"><a href="#原理" class="headerlink" title="原理"></a>原理</h5><ul><li>proxy 实现响应式<ul><li>对值类型无能为力,所以要用 ref</li></ul></li><li>编译优化<ul><li>PatchFlag 静态标记</li><li>hoistStatic 静态提升</li><li>cacheHandler 缓存事件</li><li>SSR 优化</li><li>Tree-shaking 优化</li></ul></li></ul><h5 id="Vue3-比-vue2-优势"><a href="#Vue3-比-vue2-优势" class="headerlink" title="Vue3 比 vue2 优势"></a>Vue3 比 vue2 优势</h5><ul><li>性能更好</li><li>体积更小</li><li>更好的ts支持</li><li>更好的代码组织</li><li>更好的逻辑抽离</li><li>更多的新功能</li></ul><h5 id="生命周期"><a href="#生命周期" class="headerlink" title="生命周期"></a>生命周期</h5><ul><li>beforeDestroy 改为 beforeUnmount</li><li>destroyed 改为 unmounted</li><li>其他沿用vue2的生命周期</li><li>setup等于 beforeCreate 和 created</li></ul><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">import &#123; onBeforeMount &#125; from &#x27;vue&#x27;</span><br><span class="line"></span><br><span class="line">export default &#123;</span><br><span class="line"> &lt;!-- 其他代码 --&gt;</span><br><span class="line"> setup () &#123;</span><br><span class="line"> onBeforeMount(() =&gt; &#123;</span><br><span class="line"> console.log(&#x27;onBeforeMount&#x27;)</span><br><span class="line"> &#125;)</span><br><span class="line"> &#125;</span><br><span class="line"> &lt;!-- 其他代码 --&gt;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h5 id="Composition-API-对比-Options-API"><a href="#Composition-API-对比-Options-API" class="headerlink" title="Composition API 对比 Options API"></a>Composition API 对比 Options API</h5><ul><li>更好的代码组织</li><li>更好的逻辑复用(options api 分散于生命周期中,代码越多越明显)</li><li>更好的类型推导(直接this.a 和 this.fn,不利于类型判断 )</li><li>不建议共用</li></ul><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line">export default &#123;</span><br><span class="line"> data () &#123;</span><br><span class="line"> return &#123;</span><br><span class="line"> a: 10</span><br><span class="line"> &#125;</span><br><span class="line"> &#125;,</span><br><span class="line"> methods: &#123;</span><br><span class="line"> fn () &#123;</span><br><span class="line"> const a = this.a</span><br><span class="line"> &#125;</span><br><span class="line"> &#125;,</span><br><span class="line"> mounted: &#123;</span><br><span class="line"> this.fn()</span><br><span class="line"> &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h5 id="ref-toRef-toRefs"><a href="#ref-toRef-toRefs" class="headerlink" title="ref toRef toRefs"></a>ref toRef toRefs</h5><ul><li>toRef 将一个响应式对象中的某个属性变为ref</li><li>toRefs 将一个响应式对象变为普通对象,这个普通对象的每个属性变为ref</li></ul><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br></pre></td><td class="code"><pre><span class="line">&lt;template&gt;</span><br><span class="line"> &lt;div&gt;&#123;&#123;name&#125;&#125;&#123;&#123;ageRef&#125;&#125;&lt;/div&gt;</span><br><span class="line"> &lt;div ref=&quot;eleRef&quot;&gt;我是文字&lt;/div&gt;</span><br><span class="line">&lt;/template&gt;</span><br><span class="line"></span><br><span class="line">&lt;script&gt;</span><br><span class="line"> import &#123; ref, toRef, toRefs, reactive, onMounted &#125; from &#x27;vue&#x27;</span><br><span class="line"></span><br><span class="line"> export default &#123;</span><br><span class="line"> name: &#x27;Ref&#x27;,</span><br><span class="line"> setup() &#123;</span><br><span class="line"> const eleRef = ref(null)</span><br><span class="line"> const ageRef = ref(20)</span><br><span class="line"> // 两者保持引用关系↓</span><br><span class="line"> // 不能返回解构的state,会丢失响应式</span><br><span class="line"> const state = reactive(&#123;</span><br><span class="line"> name: &#x27;张三&#x27;</span><br><span class="line"> &#125;)</span><br><span class="line"> const nameRef = toRef(state, &#x27;name&#x27;)</span><br><span class="line"> nameRef.value = &#x27;李四&#x27; // state中的name也会改变</span><br><span class="line"> state.name = &#x27;王五&#x27; // nameRef的值也会改变</span><br><span class="line"> // 两者保持引用关系 ↑</span><br><span class="line"> ageRef.value = 30</span><br><span class="line"> onMounted(() =&gt; &#123;</span><br><span class="line"> console.log(eleRef.value.innerHTML)</span><br><span class="line"> &#125;)</span><br><span class="line"></span><br><span class="line"> // const stateRefs = toRefs(state)</span><br><span class="line"> // const &#123;name: nameRef&#125; = stateRefs</span><br><span class="line"> // return stateRefs // toRefs的返回</span><br><span class="line"></span><br><span class="line"> // 其他返回</span><br><span class="line"> return &#123;</span><br><span class="line"> ageRef,</span><br><span class="line"> eleRef</span><br><span class="line"> &#125;</span><br><span class="line"> &#125;</span><br><span class="line"> &#125;</span><br><span class="line">&lt;/script&gt;</span><br><span class="line"></span><br></pre></td></tr></table></figure><h5 id="为什么要用-Ref"><a href="#为什么要用-Ref" class="headerlink" title="为什么要用 Ref"></a>为什么要用 Ref</h5><ul><li>返回 值类型,会丢失响应式</li><li>如在setup、computed、合成函数,都有可能返回 值类型</li><li>vue如果不定义ref。用户将自造ref,反而更混乱</li></ul><h5 id="为什么要-value"><a href="#为什么要-value" class="headerlink" title="为什么要.value"></a>为什么要.value</h5><ul><li>ref 是个对象(不丢失响应式),value存储值</li><li>通过.value属性的get和set实现响应式</li><li>用于模板、reactive时,(vue可以自己控制)不需要.value,其他情况都需要</li></ul><h5 id="Composition-API-实现逻辑复用"><a href="#Composition-API-实现逻辑复用" class="headerlink" title="Composition API 实现逻辑复用"></a>Composition API 实现逻辑复用</h5><ul><li>抽离逻辑代码到一个函数</li><li>函数命名约定为 useXxx 格式</li><li>setup中引用 useXxx 函数</li></ul><h5 id="Proxy-实现响应式-1"><a href="#Proxy-实现响应式-1" class="headerlink" title="Proxy 实现响应式"></a>Proxy 实现响应式</h5><ul><li>基本使用</li><li>Reflect</li><li>实现响应式</li><li>对比 defineProperty 提高性能<ul><li>set第一层属性的时候,触发set</li><li>set第二层属性的时候触发第一层属性的get</li><li>所以在get的时候,返回一个 新的proxy对象即可 深度监听,而且是惰性监听</li><li>所以性能比defineProperty好</li></ul></li></ul><h5 id="watch-和-watchEffect-的区别"><a href="#watch-和-watchEffect-的区别" class="headerlink" title="watch 和 watchEffect 的区别"></a>watch 和 watchEffect 的区别</h5><ul><li>两者都可以监听data属性变化</li><li>watch 需要明确监听哪个属性</li><li>watchEffect会根据其中的属性,自动监听其变化</li></ul><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line">// watch 默认不会立即执行</span><br><span class="line">// 监听ref</span><br><span class="line">watch(nameRef, (newVal, oldVal) =&gt; &#123;&#125;, [options])</span><br><span class="line">// 监听响应式</span><br><span class="line">watch(() =&gt; state.name, (newState, oldState) =&gt; &#123;&#125;, [options])</span><br><span class="line"></span><br><span class="line">watchEffect(() =&gt; &#123;</span><br><span class="line"> // 初始化就会执行一次,收集监听的数据</span><br><span class="line"> console.log(&#x27;watchEffect&#x27;)</span><br><span class="line">&#125;)</span><br><span class="line">watchEffect(() =&gt; &#123;</span><br><span class="line"> // 写了 state.name 就监听, 不写的不会监听</span><br><span class="line"> console.log(state.name)</span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure><h5 id="setup中如何获取组件实例"><a href="#setup中如何获取组件实例" class="headerlink" title="setup中如何获取组件实例"></a>setup中如何获取组件实例</h5><ul><li>在setup 和 Composition API 中没有this</li><li>可通过 getCurrentInstance 获取当前实例</li><li>若使用 options API 可照常使用 this</li></ul><h5 id="vue3-为何比-vue2-快"><a href="#vue3-为何比-vue2-快" class="headerlink" title="vue3 为何比 vue2 快"></a>vue3 为何比 vue2 快</h5><ul><li>Proxy 响应式</li><li>PatchFlag &#x2F;&#x2F; 标记节点类型</li><li>hoistStatic &#x2F;&#x2F; 缓存静态节点,多个节点合并</li><li>cacheHandler &#x2F;&#x2F;</li><li>SSR 优化</li><li>tree-shaking</li></ul><h6 id="PatchFlag"><a href="#PatchFlag" class="headerlink" title="PatchFlag"></a>PatchFlag</h6><ul><li>编译模板时,动态节点做标记</li><li>标记分为不同类型,如 TEXT PROPS</li><li>diff算法时,可以区分静态节点,以及不同类型的节点</li></ul><h6 id="hoistStatic"><a href="#hoistStatic" class="headerlink" title="hoistStatic"></a>hoistStatic</h6><ul><li>将静态节点的定义,提升到父作用域,缓存起来</li><li>多个相邻的静态节点,会被合并起来</li><li>典型的拿空间换时间的优化策略</li></ul><h6 id="cacheHandler"><a href="#cacheHandler" class="headerlink" title="cacheHandler"></a>cacheHandler</h6><ul><li>将事件缓存起来</li></ul><h5 id="vite-ES6-module"><a href="#vite-ES6-module" class="headerlink" title="vite - ES6 module"></a>vite - ES6 module</h5><h2 id="React"><a href="#React" class="headerlink" title="React"></a>React</h2><h3 id="基本使用-1"><a href="#基本使用-1" class="headerlink" title="基本使用"></a>基本使用</h3><h4 id="setState"><a href="#setState" class="headerlink" title="setState"></a>setState</h4><ul><li>不可变值</li><li>可能异步可能同步</li><li>可能会被合并</li></ul><h5 id="是同步还是异步"><a href="#是同步还是异步" class="headerlink" title="是同步还是异步"></a>是同步还是异步</h5><ul><li>直接使用是异步的</li><li>在setTimeout或者自定义的dom事件中是同步的</li></ul><h5 id="何时会合并"><a href="#何时会合并" class="headerlink" title="何时会合并"></a>何时会合并</h5><ul><li>对象形式,会被合并</li><li>函数形式,不会合并</li></ul><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">this.setState((state, props) =&gt; &#123;&#125;)</span><br></pre></td></tr></table></figure><ul><li>直接修改state的值,影响性能</li></ul><h4 id="事件为何要bind-this"><a href="#事件为何要bind-this" class="headerlink" title="事件为何要bind this"></a>事件为何要bind this</h4><h4 id="Reaact-高级特性"><a href="#Reaact-高级特性" class="headerlink" title="Reaact 高级特性"></a>Reaact 高级特性</h4><ul><li>函数组件</li><li>非受控组件</li><li>Portals ReactDOM.createPortals()</li><li>context</li><li>异步组件,import() React.lazy React.Suspense</li><li>性能优化,SCU 核心问题是 不可变值</li><li>高阶组件HOC</li><li>render props</li></ul><h5 id="jsx-本质是什么"><a href="#jsx-本质是什么" class="headerlink" title="jsx 本质是什么"></a>jsx 本质是什么</h5><ul><li>React.createElement 函数,执行返回 vnode</li></ul><h5 id="事件合成机制"><a href="#事件合成机制" class="headerlink" title="事件合成机制"></a>事件合成机制</h5><ul><li>更好的兼容性和跨平台</li><li>减少内存小号,避免频繁解绑</li><li>方便统一管理(如事务机制)</li></ul><h5 id="transaction-事务机制"><a href="#transaction-事务机制" class="headerlink" title="transaction 事务机制"></a>transaction 事务机制</h5><p>Transaction类的主要作用使用提供的包装(Wrapper)来包装一个函数。<br>Transaction会接受一个方法 func,和一组Wrapper。Transaction会在func执行之前,执行一组Wrapper中的initialize方法。而后执行func方法,在func方法执行完了之后,执行Wrapper提供的所有close方法。</p><h5 id="fiber"><a href="#fiber" class="headerlink" title="fiber"></a>fiber</h5><ul><li>props state</li><li>render 生成 vnode</li><li>patch(ele, vnode)<ul><li>reconciliation 执行diff算法,纯js计算<ul><li>进行任务拆分</li><li>DOM需要渲染时暂停,空闲时恢复</li><li>window.requestIdleCallback</li></ul></li><li>commit 阶段,将diff结果渲染到dom中</li></ul></li></ul><h5 id="react-性能优化"><a href="#react-性能优化" class="headerlink" title="react 性能优化"></a>react 性能优化</h5><ul><li>渲染列表key</li><li>及时销毁事件</li><li>合理使用异步组件</li><li>减少 bind 次数</li><li>合理使用 scu pure memo</li><li>合理使用 immutablejs</li><li>打包webpack优化</li><li>前端通用优化 图片懒加载等</li><li>SSR</li></ul><h5 id="React-和-Vue-区别"><a href="#React-和-Vue-区别" class="headerlink" title="React 和 Vue 区别"></a>React 和 Vue 区别</h5><p>1、共同</p><ul><li>支持组件化</li><li>数据驱动视图</li><li>vdom操作dom</li></ul><p>2、不同</p><ul><li>React 使用 jsx 拥抱js(文件都是.js结尾),Vue使用模板拥抱html(文件是.vue结尾)</li><li>React 函数式编程,Vue 声明式编程</li><li>React 更多需要自力更生,Vue 把想要的都给你</li></ul><h2 id="Webpack"><a href="#Webpack" class="headerlink" title="Webpack"></a>Webpack</h2><h3 id="升级webpack5-级周边插件后。需要做的调整"><a href="#升级webpack5-级周边插件后。需要做的调整" class="headerlink" title="升级webpack5 级周边插件后。需要做的调整"></a>升级webpack5 级周边插件后。需要做的调整</h3><ul><li>package.json的dev-server命令改了,<ul><li>4: “dev”:”webpack-dev-server –config build&#x2F;webpack.dev.js”</li><li>5: “dev”:”webpack serve –config build&#x2F;webpack.dev.js”</li></ul></li><li>webpack-merge<ul><li>4: const { smart } &#x3D; require(‘webpack-merge’)</li><li>5: const { merge } &#x3D; require(‘webpack-merge’)</li></ul></li><li>CleanWebpackplugin<ul><li>4: const CleanWebpackplugin &#x3D; require(‘clean-webpack-plugin’)</li><li>5: const { CleanWebpackplugin } &#x3D; require(‘clean-webpack-plugin’)</li></ul></li><li>modules.rules<ul><li>4: loader: [‘xxx-loader’]</li><li>5: use: [‘xxx-loader’]</li></ul></li><li>filename hash h小写<ul><li>4: filename: ‘bundle.[contentHash:8].js’</li><li>5: filename: ‘bundle.[contenthash:8].js’</li></ul></li></ul><h3 id="基本配置"><a href="#基本配置" class="headerlink" title="基本配置"></a>基本配置</h3><ul><li><p>拆分配置 和 merge</p></li><li><p>启动本地服务 webpack-dev-server</p></li><li><p>处理 ES6</p> <figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">modules.<span class="property">exports</span> = &#123;</span><br><span class="line"> <span class="attr">entry</span>: path.<span class="title function_">join</span>(srcPath, <span class="string">&#x27;index&#x27;</span>),</span><br><span class="line"> <span class="attr">modules</span>: &#123;</span><br><span class="line"> <span class="attr">rules</span>: [</span><br><span class="line"> &#123;</span><br><span class="line"> <span class="attr">test</span>: <span class="regexp">/\.js$/</span>,</span><br><span class="line"> <span class="attr">loader</span>: [<span class="string">&#x27;babel-loader&#x27;</span>],</span><br><span class="line"> <span class="attr">include</span>: srcPath,</span><br><span class="line"> <span class="attr">exclude</span>: <span class="regexp">/node_modules/</span></span><br><span class="line"> &#125;</span><br><span class="line"> ]</span><br><span class="line"> &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure> <figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line"> &quot;presets&quot;: [&quot;@babel/preset-env&quot;],</span><br><span class="line"> &quot;plugins&quot;: []</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></li><li><p>处理样式</p></li><li><p>处理图片</p><ul><li>file-loader 将图片变为url形式</li><li>url-loader 将图片变为base64形式,减少http请求,很小没必要再进行http请求</li></ul></li><li><p>模块化</p></li></ul><h3 id="高级配置"><a href="#高级配置" class="headerlink" title="高级配置"></a>高级配置</h3><h4 id="多入口"><a href="#多入口" class="headerlink" title="多入口"></a>多入口</h4><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">entry</span>: &#123;</span><br><span class="line"> <span class="attr">index</span>: path.<span class="title function_">join</span>(srcPath, <span class="string">&#x27;index.js&#x27;</span>),</span><br><span class="line"> <span class="attr">other</span>: path.<span class="title function_">join</span>(srcPath, <span class="string">&#x27;other.js&#x27;</span>),</span><br><span class="line">&#125;,</span><br><span class="line"><span class="attr">output</span>: &#123;</span><br><span class="line"> <span class="attr">filename</span>: <span class="string">&#x27;[name].[contentHash:8].js&#x27;</span>,</span><br><span class="line"> <span class="attr">path</span>: distPath</span><br><span class="line">&#125;</span><br><span class="line"><span class="attr">plugins</span>: [</span><br><span class="line"> <span class="keyword">new</span> <span class="title class_">HtmlWebpackPlugin</span>(&#123;</span><br><span class="line"> <span class="attr">template</span>: path.<span class="title function_">join</span>(srcPath, <span class="string">&#x27;index.html&#x27;</span>),</span><br><span class="line"> <span class="attr">filename</span>: <span class="string">&#x27;index.html&#x27;</span>,</span><br><span class="line"> <span class="attr">chunks</span>: [<span class="string">&#x27;index&#x27;</span>] <span class="comment">// 要引入什么js文件</span></span><br><span class="line"> &#125;),</span><br><span class="line"> <span class="keyword">new</span> <span class="title class_">HtmlWebpackPlugin</span>(&#123;</span><br><span class="line"> <span class="attr">template</span>: path.<span class="title function_">join</span>(srcPath, <span class="string">&#x27;other.html&#x27;</span>),</span><br><span class="line"> <span class="attr">filename</span>: <span class="string">&#x27;other.html&#x27;</span>,</span><br><span class="line"> <span class="attr">chunks</span>: [<span class="string">&#x27;other&#x27;</span>]</span><br><span class="line"> &#125;)</span><br><span class="line">]</span><br></pre></td></tr></table></figure><h4 id="抽离css文件"><a href="#抽离css文件" class="headerlink" title="抽离css文件"></a>抽离css文件</h4><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br></pre></td><td class="code"><pre><span class="line">coonst <span class="title class_">MiniCssExtracctplugin</span> = <span class="built_in">require</span>(<span class="string">&#x27;mini-css-extract-plugin&#x27;</span>)</span><br><span class="line"><span class="keyword">const</span> <span class="title class_">TerserJSPlugin</span> = <span class="built_in">require</span>(<span class="string">&#x27;terser-webpack-plugin&#x27;</span>)</span><br><span class="line"><span class="keyword">const</span> <span class="title class_">OptimizeCssAssetsplugin</span> = <span class="built_in">require</span>(<span class="string">&#x27;optimize-css-assets-webpack-plugin&#x27;</span>)</span><br><span class="line"></span><br><span class="line"><span class="variable language_">module</span>.<span class="property">exports</span> = &#123;</span><br><span class="line"> <span class="attr">module</span>: &#123;</span><br><span class="line"> <span class="attr">rules</span>: [</span><br><span class="line"> &#123;</span><br><span class="line"> <span class="attr">test</span>: <span class="regexp">/\.css$/</span>,</span><br><span class="line"> <span class="attr">loader</span>: [</span><br><span class="line"> <span class="title class_">MiniCssExtracctplugin</span>.<span class="property">loader</span>,</span><br><span class="line"> <span class="string">&#x27;css-loader&#x27;</span>,</span><br><span class="line"> <span class="string">&#x27;postcss-loader&#x27;</span></span><br><span class="line"> ]</span><br><span class="line"> &#125;</span><br><span class="line"> ]</span><br><span class="line"> &#125;,</span><br><span class="line"> <span class="attr">plugins</span>: [</span><br><span class="line"> <span class="keyword">new</span> <span class="title class_">MiniCssExtracctplugin</span>(&#123;</span><br><span class="line"> <span class="attr">filename</span>: <span class="string">&#x27;css/main.[contentHash:8].css&#x27;</span></span><br><span class="line"> &#125;)</span><br><span class="line"> ],</span><br><span class="line"> <span class="attr">optimization</span>: &#123;</span><br><span class="line"> <span class="attr">minimizer</span>: [<span class="keyword">new</span> <span class="title class_">TerserJSPlugin</span>(&#123;&#125;), <span class="keyword">new</span> <span class="title class_">OptimizeCssAssetsplugin</span>(&#123;&#125;)]</span><br><span class="line"> &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure><h4 id="压缩-optimization"><a href="#压缩-optimization" class="headerlink" title="压缩 optimization"></a>压缩 optimization</h4><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> <span class="title class_">TerserJSPlugin</span> = <span class="built_in">require</span>(<span class="string">&#x27;terser-webpack-plugin&#x27;</span>)</span><br><span class="line"><span class="keyword">const</span> <span class="title class_">OptimizeCssAssetsplugin</span> = <span class="built_in">require</span>(<span class="string">&#x27;optimize-css-assets-webpack-plugin&#x27;</span>)</span><br><span class="line"></span><br><span class="line"><span class="variable language_">module</span>.<span class="property">exports</span> = &#123;</span><br><span class="line"> &lt;!-- 其他配置 --&gt;</span><br><span class="line"> <span class="attr">optimization</span>: &#123;</span><br><span class="line"> <span class="attr">minimizer</span>: [<span class="keyword">new</span> <span class="title class_">TerserJSPlugin</span>(&#123;&#125;), <span class="keyword">new</span> <span class="title class_">OptimizeCssAssetsplugin</span>(&#123;&#125;)]</span><br><span class="line"> &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h4 id="抽离公共代码"><a href="#抽离公共代码" class="headerlink" title="抽离公共代码"></a>抽离公共代码</h4><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br></pre></td><td class="code"><pre><span class="line"><span class="variable language_">module</span>.<span class="property">exports</span> = &#123;</span><br><span class="line"> &lt;!-- 其他配置 --&gt;</span><br><span class="line"> <span class="attr">optimization</span>: &#123;</span><br><span class="line"> <span class="attr">minimizer</span>: [],</span><br><span class="line"> <span class="attr">splitChunks</span>: &#123;</span><br><span class="line"> <span class="comment">// initial 不处理异步</span></span><br><span class="line"> <span class="comment">// async 只处理异步</span></span><br><span class="line"> <span class="comment">// all 全部</span></span><br><span class="line"> <span class="attr">chunks</span>: <span class="string">&#x27;all&#x27;</span>,</span><br><span class="line"> <span class="attr">cacheGroups</span>: &#123;</span><br><span class="line"> <span class="comment">// 引用关系 关联 HtmlWebpackPlugin chunks 配置</span></span><br><span class="line"> <span class="comment">// 第三方模块</span></span><br><span class="line"> <span class="attr">vendor</span>: &#123;</span><br><span class="line"> <span class="attr">name</span>: <span class="string">&#x27;vendor&#x27;</span>, <span class="comment">// chunk 名称</span></span><br><span class="line"> <span class="attr">priority</span>: <span class="number">1</span>, <span class="comment">// 权限更高,有限抽离,重要!</span></span><br><span class="line"> <span class="attr">test</span>: <span class="regexp">/node_modules/</span>, <span class="comment">// </span></span><br><span class="line"> <span class="attr">minSize</span>: <span class="number">0</span>, <span class="comment">// 大小限制,实际开发不要写0</span></span><br><span class="line"> <span class="attr">minChunks</span>: <span class="number">1</span>, <span class="comment">// 最少复用过几次</span></span><br><span class="line"> &#125;,</span><br><span class="line"> <span class="attr">vendor</span>: &#123;</span><br><span class="line"> <span class="attr">name</span>: <span class="string">&#x27;common&#x27;</span>, <span class="comment">// chunk 名称</span></span><br><span class="line"> <span class="attr">priority</span>: <span class="number">0</span>, <span class="comment">// 权限更高,有限抽离,重要!</span></span><br><span class="line"> <span class="attr">minSize</span>: <span class="number">0</span>, <span class="comment">// 大小限制</span></span><br><span class="line"> <span class="attr">minChunks</span>: <span class="number">2</span>, <span class="comment">// 最少复用过几次</span></span><br><span class="line"> &#125;</span><br><span class="line"> &#125;</span><br><span class="line"> &#125;</span><br><span class="line"> &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure><h4 id="异步加载"><a href="#异步加载" class="headerlink" title="异步加载"></a>异步加载</h4><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 定义 chunk</span></span><br><span class="line"><span class="keyword">import</span>(<span class="string">&#x27;./dynamic-data.js&#x27;</span>).<span class="title function_">then</span>(<span class="function"><span class="params">res</span> =&gt;</span> &#123;</span><br><span class="line"> <span class="variable language_">console</span>.<span class="title function_">log</span>(res.<span class="property">default</span>.<span class="property">message</span>)</span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure><h3 id="module-chunk-bundle-区别"><a href="#module-chunk-bundle-区别" class="headerlink" title="module chunk bundle 区别"></a>module chunk bundle 区别</h3><ul><li>module:就是写的代码,只要可以引入的都是module</li><li>chunk:多模块合并成的,比如:index.js以及它引入的其他文件的集合</li><li>bundle:最终的输出文件</li></ul><h3 id="性能优化,构建速度"><a href="#性能优化,构建速度" class="headerlink" title="性能优化,构建速度"></a>性能优化,构建速度</h3><ul><li>优化babel-loader</li><li>IgnorePlugin</li><li>noParse</li><li>happypack</li><li>parallelUglifyPlugin</li><li>自动刷新</li><li>热更新</li><li>DllPlugin</li></ul><h4 id="优化babel-loader"><a href="#优化babel-loader" class="headerlink" title="优化babel-loader"></a>优化babel-loader</h4><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line"> <span class="attr">test</span>: <span class="regexp">/\.js$/</span>,</span><br><span class="line"> <span class="attr">use</span>: [<span class="string">&#x27;babel-loader?cacheDirectory&#x27;</span>], <span class="comment">// 开启缓存</span></span><br><span class="line"> <span class="attr">include</span>: path.<span class="title function_">resolve</span>(__dirname, <span class="string">&#x27;src&#x27;</span>), <span class="comment">// 明确范围</span></span><br><span class="line"> <span class="comment">// 排除范围, 两者选一即可</span></span><br><span class="line"> <span class="comment">// exclude: path.resolve(__dirname, &#x27;node_modules&#x27;)</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h4 id="IgnorePlugin,直接不引入,代码中没有"><a href="#IgnorePlugin,直接不引入,代码中没有" class="headerlink" title="IgnorePlugin,直接不引入,代码中没有"></a>IgnorePlugin,直接不引入,代码中没有</h4><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 不引入所有语言包</span></span><br><span class="line"><span class="keyword">new</span> webpack.<span class="title class_">IgnorePlugin</span>(<span class="regexp">/\.\/locale/</span>, <span class="regexp">/moment/</span>)</span><br><span class="line"><span class="comment">// 手动引入中文</span></span><br><span class="line">moment.<span class="title function_">locale</span>(<span class="string">&#x27;zh-cn&#x27;</span>)</span><br></pre></td></tr></table></figure><h4 id="noParse,引入,但不打包"><a href="#noParse,引入,但不打包" class="headerlink" title="noParse,引入,但不打包"></a>noParse,引入,但不打包</h4><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">modules.<span class="property">exports</span>=&#123;</span><br><span class="line"> <span class="attr">module</span>: &#123;</span><br><span class="line"> <span class="attr">noParse</span>: [<span class="regexp">/react\.min\.js/</span>],</span><br><span class="line"> <span class="attr">rules</span>: [</span><br><span class="line"> &#123;&#125;</span><br><span class="line"> ]</span><br><span class="line"> &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure><h4 id="happyPack-多线程打包"><a href="#happyPack-多线程打包" class="headerlink" title="happyPack 多线程打包"></a>happyPack 多线程打包</h4><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> <span class="title class_">HappyPack</span> = <span class="built_in">require</span>(<span class="string">&#x27;happypack&#x27;</span>)</span><br><span class="line"></span><br><span class="line"><span class="variable language_">module</span>.<span class="property">exports</span> = &#123;</span><br><span class="line"> <span class="attr">modules</span>: &#123;</span><br><span class="line"> <span class="attr">rules</span>: [</span><br><span class="line"> &#123;</span><br><span class="line"> <span class="attr">test</span>: <span class="regexp">/\.js$/</span>,</span><br><span class="line"> <span class="attr">use</span>: [<span class="string">&#x27;happypack/loader?id=babel&#x27;</span>],</span><br><span class="line"> <span class="attr">include</span>: srcPath</span><br><span class="line"> &#125;</span><br><span class="line"> ]</span><br><span class="line"> &#125;,</span><br><span class="line"> <span class="attr">plugins</span>: [</span><br><span class="line"> <span class="keyword">new</span> <span class="title class_">Happypack</span>(&#123;</span><br><span class="line"> <span class="attr">id</span>: <span class="string">&#x27;babel&#x27;</span>,</span><br><span class="line"> <span class="attr">loaders</span>: [<span class="string">&#x27;babel-loader?cacheDirectory&#x27;</span>]</span><br><span class="line"> &#125;)</span><br><span class="line"> ]</span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure><h4 id="ParallelUglifyPlugin-多进程压缩js"><a href="#ParallelUglifyPlugin-多进程压缩js" class="headerlink" title="ParallelUglifyPlugin 多进程压缩js"></a>ParallelUglifyPlugin 多进程压缩js</h4><p>new ParallelUglifyPlugin({<br> uglifyJS: {<br> &#x2F;&#x2F; 还是使用 uglifyjs 压缩,只是开启了多进程<br> output: {<br> beautify: false, &#x2F;&#x2F; 最紧凑的输出<br> comments: false &#x2F;&#x2F; 删除所有注释<br> },<br> compress: {<br> &#x2F;&#x2F; 删除所有 console 语句,可兼容ie<br> drop_console: true,<br> }<br> }<br>})</p><h4 id="自动刷新"><a href="#自动刷新" class="headerlink" title="自动刷新"></a>自动刷新</h4><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="variable language_">module</span>.<span class="property">exports</span> = &#123;</span><br><span class="line"> <span class="comment">// 开启之后, webpack-dev-server 会自动开启刷新浏览器</span></span><br><span class="line"> <span class="attr">watch</span>: <span class="literal">true</span>, <span class="comment">// 默认false </span></span><br><span class="line"> <span class="attr">watchOptions</span>: &#123;</span><br><span class="line"> <span class="attr">ignored</span>: <span class="regexp">/node_modules/</span>, <span class="comment">// 忽略</span></span><br><span class="line"> <span class="attr">aggregateTimeout</span>: <span class="number">300</span>, <span class="comment">// 监听变化后等300ms再执行,防止太快</span></span><br><span class="line"> <span class="attr">poll</span>: <span class="number">1000</span> <span class="comment">// 每隔1000ms 询问一次</span></span><br><span class="line"> &#125;</span><br><span class="line"></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h4 id="DllPlugin"><a href="#DllPlugin" class="headerlink" title="DllPlugin"></a>DllPlugin</h4><ul><li>前端框架如 vue react , 体积大,构建慢</li><li>较稳定,不常升级版本</li><li>同一个版本只构建一次即可,不用每次都重新构建</li><li>webpack 内置</li><li>打包出dll文件</li><li>DllReferencePlugin - 使用dll文件</li></ul><p>1、打包dll配置 webpacl.dll.js</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="variable language_">module</span>.<span class="property">exports</span> = &#123;</span><br><span class="line"> <span class="attr">mode</span>: <span class="string">&#x27;development&#x27;</span>,</span><br><span class="line"></span><br><span class="line"> <span class="attr">entry</span>: &#123;</span><br><span class="line"> <span class="comment">// 把 React 相关模块的放到一个单独的动态链接库</span></span><br><span class="line"> <span class="attr">react</span>: [<span class="string">&#x27;react&#x27;</span>, <span class="string">&#x27;react-dom&#x27;</span>]</span><br><span class="line"> &#125;,</span><br><span class="line"> <span class="attr">output</span>: &#123;</span><br><span class="line"> <span class="attr">filename</span>: <span class="string">&#x27;[name].dll.js&#x27;</span>,</span><br><span class="line"> <span class="attr">path</span>: distPath,</span><br><span class="line"> <span class="attr">library</span>: <span class="string">&#x27;_dll_[name]&#x27;</span></span><br><span class="line"> &#125;,</span><br><span class="line"> <span class="attr">plugins</span>: [</span><br><span class="line"> <span class="keyword">new</span> <span class="title class_">DllPlugin</span>(&#123;</span><br><span class="line"> <span class="attr">name</span>: <span class="string">&#x27;_dll_[name]&#x27;</span>,</span><br><span class="line"> <span class="attr">path</span>: path.<span class="title function_">join</span>(distPath, <span class="string">&#x27;[name].mainfest.json&#x27;</span>),</span><br><span class="line"> &#125;)</span><br><span class="line"> ]</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>2、修改index.html模板</p><p>添加</p><script src="./react.dll.js"></script><p>3、配置webpack.dev.js</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line"><span class="attr">moduls</span>: &#123;</span><br><span class="line"></span><br><span class="line"> <span class="attr">rules</span>: [</span><br><span class="line"> &#123;</span><br><span class="line"> <span class="attr">test</span>: <span class="regexp">/\.js$/</span>,</span><br><span class="line"> <span class="attr">loader</span>: [<span class="string">&#x27;babel-loader&#x27;</span>],</span><br><span class="line"> <span class="attr">include</span>: srcPath,</span><br><span class="line"> <span class="attr">exclude</span>: <span class="regexp">/node_modules/</span> <span class="comment">// 使用dll,所以不需要转换了</span></span><br><span class="line"> &#125;</span><br><span class="line"> ]</span><br><span class="line"></span><br><span class="line">&#125;,</span><br><span class="line"></span><br><span class="line"><span class="attr">plugins</span>: [</span><br><span class="line"></span><br><span class="line"> <span class="keyword">new</span> <span class="title class_">DllReferencePlugin</span>(&#123;</span><br><span class="line"> <span class="attr">mainfest</span>: <span class="built_in">require</span>(path.<span class="title function_">join</span>(distPath, <span class="string">&#x27;react.mainfest.json&#x27;</span>))</span><br><span class="line"> &#125;)</span><br><span class="line"></span><br><span class="line">]</span><br><span class="line"></span><br></pre></td></tr></table></figure><h4 id="webpack-优化构建速度-(可用于生产环境)"><a href="#webpack-优化构建速度-(可用于生产环境)" class="headerlink" title="webpack 优化构建速度 (可用于生产环境)"></a>webpack 优化构建速度 (可用于生产环境)</h4><ul><li>优化 babel-loader</li><li>IgnorePlugin</li><li>noParse</li><li>happyPack</li><li>parellelUglifyPlugin</li></ul><h4 id="webpack-优化构建速度-(不可用于生产环境)"><a href="#webpack-优化构建速度-(不可用于生产环境)" class="headerlink" title="webpack 优化构建速度 (不可用于生产环境)"></a>webpack 优化构建速度 (不可用于生产环境)</h4><ul><li>自动刷新</li><li>热更新</li><li>DllPlugin</li></ul><h4 id="webpack性能优化-产出代码"><a href="#webpack性能优化-产出代码" class="headerlink" title="webpack性能优化 - 产出代码"></a>webpack性能优化 - 产出代码</h4><ul><li><p>体积更小</p></li><li><p>合理分包,不重复引用</p></li><li><p>速度更快,内损使用更少</p></li><li><p>小图片base64编码</p></li><li><p>bundle + hash</p></li><li><p>懒加载 import 语法</p></li><li><p>提取公共代码 splitChunks</p></li><li><p>IgnorePlugin</p></li><li><p>cdn加速</p></li><li><p>使用 production</p></li><li><p>Scope Hosting 改变打包作用域</p></li></ul><h5 id="使用-production"><a href="#使用-production" class="headerlink" title="使用 production"></a>使用 production</h5><ul><li>自动开启代码压缩</li><li>Vue React 等会自动删除调试代码(如开发环境的warning)</li><li>启动 Tree-Shaking</li></ul><h5 id="Scope-Hosting"><a href="#Scope-Hosting" class="headerlink" title="Scope Hosting"></a>Scope Hosting</h5><ul><li>代码体积更小</li><li>创建函数作用域更少</li><li>代码可读性更好</li></ul><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> <span class="title class_">ModuleConcatenationPlugin</span> = <span class="built_in">require</span>(<span class="string">&#x27;webpack/lib/optmize/ModuleConcatenationPlugin&#x27;</span>)</span><br><span class="line"></span><br><span class="line"><span class="variable language_">module</span>.<span class="property">exports</span> = &#123;</span><br><span class="line"></span><br><span class="line"> <span class="attr">resolve</span>: &#123;</span><br><span class="line"> <span class="comment">// 针对npm中的第三方模块有限采用 jsnext:main 中指向的es6 模块化语法的文件</span></span><br><span class="line"> <span class="attr">mainFields</span>: [<span class="string">&#x27;jsnext:main&#x27;</span>, <span class="string">&#x27;browser&#x27;</span>, <span class="string">&#x27;main&#x27;</span>]</span><br><span class="line"> &#125;,</span><br><span class="line"> <span class="attr">plugins</span>: [</span><br><span class="line"> <span class="comment">// 开启 Scope Hosting</span></span><br><span class="line"> <span class="keyword">new</span> <span class="title class_">ModuleConcatenationPlugin</span>()</span><br><span class="line"> ]</span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure><h2 id="babel"><a href="#babel" class="headerlink" title="babel"></a>babel</h2><ul><li>环境搭建 , 基本配置<ul><li>.babelrc 配置</li><li>presets 和 plugins</li></ul></li><li>babel-polyfill</li><li>babel-runtime</li></ul><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line"> &quot;presets&quot;: [</span><br><span class="line"> [</span><br><span class="line"> &quot;@babel/preset-env&quot;,</span><br><span class="line"> &#123;</span><br><span class="line"> &quot;useBuiltins&quot;: &quot;usage&quot;, // 按需引入</span><br><span class="line"> &quot;corejs&quot;: 3 // 版本3</span><br><span class="line"> &#125;</span><br><span class="line"> ]</span><br><span class="line"> ],</span><br><span class="line"> plugins: []</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="babel-polyfill"><a href="#babel-polyfill" class="headerlink" title="babel-polyfill"></a>babel-polyfill</h3><ul><li>babel 7.4 之后弃用 babel-polyfill</li><li>推荐直接使用 core-js 和 regenerator</li></ul><h3 id="babel-polyfill-问题"><a href="#babel-polyfill-问题" class="headerlink" title="babel-polyfill 问题"></a>babel-polyfill 问题</h3><ul><li>污染全局环境,使用babel-runtime解决</li></ul><p>。babelrc中</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">plugins: [</span><br><span class="line"> [</span><br><span class="line"> &quot;@babel/plugin-transform-runtime&quot;,</span><br><span class="line"> &#123;</span><br><span class="line"> &quot;absoluteRuntime&quot;: false,</span><br><span class="line"> &quot;corejs&quot;: 3,</span><br><span class="line"> &quot;helpers&quot;: true,</span><br><span class="line"> &quot;regenerator&quot;: true,</span><br><span class="line"> &quot;useESModules&quot;: false</span><br><span class="line"> &#125;</span><br><span class="line"> ]</span><br><span class="line">]</span><br><span class="line"></span><br></pre></td></tr></table></figure>]]></content>
12
12
 
13
13
 
14
14
 
@@ -2535,7 +2535,7 @@
2535
2535
  <link href="/article/14192.html"/>
2536
2536
  <url>/article/14192.html</url>
2537
2537
 
2538
- <content type="html"><![CDATA[<p><code>原文地址:https://juejin.im/post/5dff8a26e51d4558105420ed</code><br>这段时间面试官都挺忙的,频频出现在博客文章标题,虽然我不是特别想蹭热度,但是实在想不到好的标题了-。-,蹭蹭就蹭蹭 :)<br>事实上我在面试的时候确实被问到了这个问题,而且是一道在线 coding 的编程题,当时虽然思路正确,可惜最终也并不算完全答对<br>结束后花了一段时间整理了下思路,那么究竟该如何实现一个大文件上传,以及在上传中如何实现断点续传的功能呢?<br>本文将从零搭建前端和服务端,实现一个大文件上传和断点续传的 demo</p><span id="more"></span><p>前端:<code>vue``element-ui</code><br>服务端:<code>nodejs</code></p><p><code>文章有误解的地方,欢迎指出,将在第一时间改正,有更好的实现方式希望留下你的评论</code></p><h1 id="大文件上传"><a href="#大文件上传" class="headerlink" title="大文件上传"></a>大文件上传</h1><h2 id="整体思路"><a href="#整体思路" class="headerlink" title="整体思路"></a>整体思路</h2><h3 id="前端"><a href="#前端" class="headerlink" title="前端"></a>前端</h3><p>前端大文件上传网上的大部分文章已经给出了解决方案,核心是<code>利用 Blob.prototype.slice</code> 方法,和数组的 slice 方法相似,调用的 slice 方法可以返回<code>原文件的某个切片</code></p><p>这样我们就可以根据预先设置好的切片最大数量将文件切分为一个个切片,然后借助 http 的可并发性,同时上传多个切片,这样从原本传一个大文件,变成了<code>同时</code>传多个小的文件切片,可以大大减少上传时间</p><p>另外由于是并发,传输到服务端的顺序可能会发生变化,所以我们还需要给每个切片记录顺序</p><h3 id="服务端"><a href="#服务端" class="headerlink" title="服务端"></a>服务端</h3><p>服务端需要负责接受这些切片,并在接收到所有切片后<code>合并</code>切片</p><p>这里又引伸出两个问题</p><ol><li>何时合并切片,即切片什么时候传输完成</li><li>如何合并切片</li></ol><p>第一个问题需要前端进行配合,前端在每个切片中都携带切片最大数量的信息,当服务端接受到这个数量的切片时自动合并,也可以额外发一个请求主动通知服务端进行切片的合并</p><p>第二个问题,具体如何合并切片呢?这里可以使用 nodejs 的 api <code>fs.appendFileSync</code>,它可以同步地将数据追加到指定文件,也就是说,当服务端接受到所有切片后,先创建一个最终的文件,然后将所有切片逐步合并到这个文件中</p><p><code>talk is cheap,show me the code</code>,接着我们用代码实现上面的思路</p><h2 id="前端部分"><a href="#前端部分" class="headerlink" title="前端部分"></a>前端部分</h2><p>前端使用 Vue 作为开发框架,对界面没有太大要求,原生也可以,考虑到美观使用 element-ui 作为 UI 框架</p><h3 id="上传控件"><a href="#上传控件" class="headerlink" title="上传控件"></a>上传控件</h3><p>首先创建选择文件的控件,监听 change 事件以及上传按钮</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><span class="line">&lt;template&gt;</span><br><span class="line"> &lt;div&gt;</span><br><span class="line"> &lt;input type=&quot;file&quot; @change=&quot;handleFileChange&quot; /&gt;</span><br><span class="line"> &lt;el-button @click=&quot;handleUpload&quot;&gt;上传&lt;/el-button&gt;</span><br><span class="line"> &lt;/div&gt;</span><br><span class="line">&lt;/template&gt;</span><br><span class="line"></span><br><span class="line">&lt;script&gt;</span><br><span class="line">export default &#123;</span><br><span class="line"> data: () =&gt; (&#123;</span><br><span class="line"> container: &#123;</span><br><span class="line"> file: null</span><br><span class="line"> &#125;</span><br><span class="line"> &#125;),</span><br><span class="line"> methods: &#123;</span><br><span class="line"> handleFileChange(e) &#123;</span><br><span class="line"> const [file] = e.target.files;</span><br><span class="line"> if (!file) return;</span><br><span class="line"> Object.assign(this.$data, this.$options.data());</span><br><span class="line"> this.container.file = file;</span><br><span class="line"> &#125;,</span><br><span class="line"> async handleUpload() &#123;&#125;</span><br><span class="line"> &#125;</span><br><span class="line">&#125;;</span><br><span class="line">&lt;/script&gt;</span><br><span class="line"></span><br></pre></td></tr></table></figure><p><img src="https://user-gold-cdn.xitu.io/2020/1/7/16f7ff98adb6bcdd?imageView2/0/w/1280/h/960/format/webp/ignore-error/1" alt="xuehuayu.cn"></p><h3 id="请求逻辑"><a href="#请求逻辑" class="headerlink" title="请求逻辑"></a>请求逻辑</h3><p>考虑到通用性,这里没有用第三方的请求库,而是用原生 XMLHttpRequest 做一层简单的封装来发请求</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line">request(&#123;</span><br><span class="line"> url,</span><br><span class="line"> method = &quot;post&quot;,</span><br><span class="line"> data,</span><br><span class="line"> headers = &#123;&#125;,</span><br><span class="line"> requestList</span><br><span class="line"> &#125;) &#123;</span><br><span class="line"> returnnewPromise(resolve =&gt; &#123;</span><br><span class="line"> const xhr = new XMLHttpRequest();</span><br><span class="line"> xhr.open(method, url);</span><br><span class="line"> Object.keys(headers).forEach(key =&gt;</span><br><span class="line"> xhr.setRequestHeader(key, headers[key])</span><br><span class="line"> );</span><br><span class="line"> xhr.send(data);</span><br><span class="line"> xhr.onload = e =&gt; &#123;</span><br><span class="line"> resolve(&#123;</span><br><span class="line"> data: e.target.response</span><br><span class="line"> &#125;);</span><br><span class="line"> &#125;;</span><br><span class="line"> &#125;);</span><br><span class="line"> &#125;</span><br></pre></td></tr></table></figure><h3 id="上传切片"><a href="#上传切片" class="headerlink" title="上传切片"></a>上传切片</h3><p>接着实现比较重要的上传功能,上传需要做两件事</p><ul><li>对文件进行切片</li><li>将切片传输给服务端</li></ul><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br></pre></td><td class="code"><pre><span class="line">&lt;template&gt;</span><br><span class="line"> &lt;div&gt;</span><br><span class="line"> &lt;input type=&quot;file&quot; @change=&quot;handleFileChange&quot; /&gt;</span><br><span class="line"> &lt;el-button @click=&quot;handleUpload&quot;&gt;上传&lt;/el-button&gt;</span><br><span class="line"> &lt;/div&gt;</span><br><span class="line">&lt;/template&gt;</span><br><span class="line"></span><br><span class="line">&lt;script&gt;</span><br><span class="line">+ const LENGTH = 10; // 切片数量</span><br><span class="line"></span><br><span class="line">export default &#123;</span><br><span class="line"> data: () =&gt; (&#123;</span><br><span class="line"> container: &#123;</span><br><span class="line"> file: null,</span><br><span class="line">+ data: []</span><br><span class="line"> &#125;</span><br><span class="line"> &#125;),</span><br><span class="line"> methods: &#123;</span><br><span class="line"> request() &#123;&#125;,</span><br><span class="line"> handleFileChange() &#123;&#125;,</span><br><span class="line">+ // 生成文件切片</span><br><span class="line">+ createFileChunk(file, length = LENGTH) &#123;</span><br><span class="line">+ const fileChunkList = [];</span><br><span class="line">+ const chunkSize = Math.ceil(file.size / length);</span><br><span class="line">+ let cur = 0;</span><br><span class="line">+ while (cur &lt; file.size) &#123;</span><br><span class="line">+ fileChunkList.push(&#123; file: file.slice(cur, cur + chunkSize) &#125;);</span><br><span class="line">+ cur += chunkSize;+ &#125;</span><br><span class="line">+ return fileChunkList;+ &#125;,</span><br><span class="line">+ // 上传切片</span><br><span class="line">+ async uploadChunks() &#123;</span><br><span class="line">+ const requestList = this.data</span><br><span class="line">+ .map((&#123; chunk,hash &#125;) =&gt; &#123;</span><br><span class="line">+ const formData = new FormData();</span><br><span class="line">+ formData.append(&quot;chunk&quot;, chunk);</span><br><span class="line">+ formData.append(&quot;hash&quot;, hash);</span><br><span class="line">+ formData.append(&quot;filename&quot;, this.container.file.name);</span><br><span class="line">+ return &#123; formData &#125;;</span><br><span class="line">+ &#125;)</span><br><span class="line">+ .map(async (&#123; formData &#125;) =&gt;</span><br><span class="line">+ this.request(&#123;</span><br><span class="line">+ url: &quot;http://localhost:3000&quot;,</span><br><span class="line">+ data: formData+ &#125;)</span><br><span class="line">+ );</span><br><span class="line">+ await Promise.all(requestList); // 并发切片</span><br><span class="line">+ &#125;,</span><br><span class="line">+ async handleUpload() &#123;</span><br><span class="line">+ if (!this.container.file) return;</span><br><span class="line">+ const fileChunkList = this.createFileChunk(this.container.file);</span><br><span class="line">+ this.data = fileChunkList.map((&#123; file &#125;,index) =&gt; (&#123;</span><br><span class="line">+ chunk: file,</span><br><span class="line">+ hash: this.container.file.name + &quot;-&quot; + index // 文件名 + 数组下标</span><br><span class="line">+ &#125;));</span><br><span class="line">+ await this.uploadChunks();</span><br><span class="line">+ &#125;</span><br><span class="line"> &#125;</span><br><span class="line">&#125;;</span><br><span class="line">&lt;/script&gt;</span><br></pre></td></tr></table></figure><p>当点击上传按钮时,调用 <code>createFileChunk</code> 将文件切片,切片数量通过一个常量 Length 控制,这里设置为 10,即将文件分成 10 个切片上传</p><p>createFileChunk 内使用 while 循环和 slice 方法将切片放入 <code>fileChunkList</code> 数组中返回</p><p>在生成文件切片时,需要给每个切片一个标识作为 hash,这里暂时使用<code>文件名 + 下标</code>,这样后端可以知道当前切片是第几个切片,用于之后的合并切片</p><p>随后调用 <code>uploadChunks</code> 上传所有的文件切片,将文件切片,切片 hash,以及文件名放入 FormData 中,再调用上一步的 <code>request</code> 函数返回一个 proimise,最后调用 Promise.all 并发上传所有的切片</p><h3 id="发送合并请求"><a href="#发送合并请求" class="headerlink" title="发送合并请求"></a>发送合并请求</h3><p>这里使用整体思路中提到的第二种合并切片的方式,即前端主动通知服务端进行合并,所以前端还需要额外发请求,服务端接受到这个请求时主动合并切片</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br></pre></td><td class="code"><pre><span class="line">&lt;template&gt;</span><br><span class="line"> &lt;div&gt;</span><br><span class="line"> &lt;input type=&quot;file&quot; @change=&quot;handleFileChange&quot; /&gt;</span><br><span class="line"> &lt;el-button @click=&quot;handleUpload&quot;&gt;上传&lt;/el-button&gt;</span><br><span class="line"> &lt;/div&gt;</span><br><span class="line">&lt;/template&gt;</span><br><span class="line"></span><br><span class="line">&lt;script&gt;</span><br><span class="line">export default &#123;</span><br><span class="line"> data: () =&gt; (&#123;</span><br><span class="line"> container: &#123;</span><br><span class="line"> file: null</span><br><span class="line"> &#125;,</span><br><span class="line"> data: []</span><br><span class="line"> &#125;),</span><br><span class="line"> methods: &#123;</span><br><span class="line"> request() &#123;&#125;,</span><br><span class="line"> handleFileChange() &#123;&#125;,</span><br><span class="line"> createFileChunk() &#123;&#125;,</span><br><span class="line"> // 上传切片,同时过滤已上传的切片</span><br><span class="line"> async uploadChunks() &#123;</span><br><span class="line"> const requestList = this.data</span><br><span class="line"> .map((&#123; chunk,hash &#125;) =&gt; &#123;</span><br><span class="line"> const formData = new FormData();</span><br><span class="line"> formData.append(&quot;chunk&quot;, chunk);</span><br><span class="line"> formData.append(&quot;hash&quot;, hash);</span><br><span class="line"> formData.append(&quot;filename&quot;, this.container.file.name);</span><br><span class="line"> return &#123; formData &#125;;</span><br><span class="line"> &#125;)</span><br><span class="line"> .map(async (&#123; formData &#125;) =&gt;</span><br><span class="line"> this.request(&#123;</span><br><span class="line"> url: &quot;http://localhost:3000&quot;,</span><br><span class="line"> data: formData</span><br><span class="line"> &#125;)</span><br><span class="line"> );</span><br><span class="line"> await Promise.all(requestList);</span><br><span class="line">+ // 合并切片+ await this.mergeRequest();</span><br><span class="line"> &#125;,</span><br><span class="line">+ async mergeRequest() &#123;</span><br><span class="line">+ await this.request(&#123;</span><br><span class="line">+ url: &quot;http://localhost:3000/merge&quot;,</span><br><span class="line">+ headers: &#123;</span><br><span class="line">+ &quot;content-type&quot;: &quot;application/json&quot;</span><br><span class="line">+ &#125;,</span><br><span class="line">+ data: JSON.stringify(&#123;</span><br><span class="line">+ filename: this.container.file.name+ &#125;)</span><br><span class="line">+ &#125;);</span><br><span class="line">+ &#125;,</span><br><span class="line"> async handleUpload() &#123;&#125;</span><br><span class="line"> &#125;</span><br><span class="line">&#125;;</span><br><span class="line">&lt;/script&gt;</span><br></pre></td></tr></table></figure><h2 id="服务端部分"><a href="#服务端部分" class="headerlink" title="服务端部分"></a>服务端部分</h2><p>简单使用 http 模块搭建服务端</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line">const http = require(&quot;http&quot;);</span><br><span class="line">const server = http.createServer();</span><br><span class="line"></span><br><span class="line">server.on(&quot;request&quot;, async (req, res) =&gt; &#123;</span><br><span class="line"> res.setHeader(&quot;Access-Control-Allow-Origin&quot;, &quot;*&quot;);</span><br><span class="line"> res.setHeader(&quot;Access-Control-Allow-Headers&quot;, &quot;*&quot;);</span><br><span class="line"> if (req.method === &quot;OPTIONS&quot;) &#123;</span><br><span class="line"> res.status = 200;</span><br><span class="line"> res.end();</span><br><span class="line"> return;</span><br><span class="line"> &#125;</span><br><span class="line">&#125;);</span><br><span class="line"></span><br><span class="line">server.listen(3000, () =&gt; console.log(&quot;正在监听 3000 端口&quot;));</span><br></pre></td></tr></table></figure><h3 id="接受切片"><a href="#接受切片" class="headerlink" title="接受切片"></a>接受切片</h3><p>使用 <code>multiparty</code> 包处理前端传来的 FormData</p><p>在 multiparty.parse 的回调中,files 参数保存了 FormData 中文件,fields 参数保存了 FormData 中非文件的字段</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br></pre></td><td class="code"><pre><span class="line">const http = require(&quot;http&quot;);</span><br><span class="line">const path = require(&quot;path&quot;);</span><br><span class="line">const fse = require(&quot;fs-extra&quot;);</span><br><span class="line">const multiparty = require(&quot;multiparty&quot;);</span><br><span class="line"></span><br><span class="line">const server = http.createServer();</span><br><span class="line">+ const UPLOAD_DIR = path.resolve(__dirname, &quot;..&quot;, &quot;target&quot;); // 大文件存储目录</span><br><span class="line"></span><br><span class="line">server.on(&quot;request&quot;, async (req, res) =&gt; &#123;</span><br><span class="line"> res.setHeader(&quot;Access-Control-Allow-Origin&quot;, &quot;*&quot;);</span><br><span class="line"> res.setHeader(&quot;Access-Control-Allow-Headers&quot;, &quot;*&quot;);</span><br><span class="line"> if (req.method === &quot;OPTIONS&quot;) &#123;</span><br><span class="line"> res.status = 200;</span><br><span class="line"> res.end();</span><br><span class="line"> return;</span><br><span class="line"> &#125;</span><br><span class="line"></span><br><span class="line">+ const multipart = new multiparty.Form();</span><br><span class="line">+ multipart.parse(req, async (err, fields, files) =&gt; &#123;</span><br><span class="line">+ if (err) &#123;</span><br><span class="line">+ return;</span><br><span class="line">+ &#125;</span><br><span class="line">+ const [chunk] = files.chunk;</span><br><span class="line">+ const [hash] = fields.hash;</span><br><span class="line">+ const [filename] = fields.filename;</span><br><span class="line">+ const chunkDir = `$&#123;UPLOAD_DIR&#125;/$&#123;filename&#125;`;</span><br><span class="line">+ // 切片目录不存在,创建切片目录</span><br><span class="line">+ if (!fse.existsSync(chunkDir)) &#123;</span><br><span class="line">+ await fse.mkdirs(chunkDir);</span><br><span class="line">+ &#125;</span><br><span class="line">+ // fs-extra 专用方法,类似 fs.rename 并且跨平台</span><br><span class="line">+ // fs-extra 的 rename 方法 windows 平台会有权限问题</span><br><span class="line">+ // https://github.com/meteor/meteor/issues/7852#issuecomment-255767835</span><br><span class="line">+ await fse.move(chunk.path, `$&#123;chunkDir&#125;/$&#123;hash&#125;`);</span><br><span class="line">+ res.end(&quot;received file chunk&quot;);</span><br><span class="line">+ &#125;);</span><br><span class="line">&#125;);</span><br><span class="line"></span><br><span class="line">server.listen(3000, () =&gt; console.log(&quot;正在监听 3000 端口&quot;));</span><br></pre></td></tr></table></figure><p><img src="https://user-gold-cdn.xitu.io/2020/1/11/16f905fb6a626f47?imageView2/0/w/1280/h/960/format/webp/ignore-error/1" alt="image-20200110215559194"><br>查看 multiparty 处理后的 chunk 对象,path 是存储临时文件的路径,size 是临时文件大小,在 multiparty 文档中提到可以使用 fs.rename(由于我用的是 fs-extra,它的 rename 方法 windows 平台权限问题,所以换成了 fse.move) 移动临时文件,即移动文件切片</p><p>在接受文件切片时,需要先创建存储切片的文件夹,由于前端在发送每个切片时额外携带了唯一值 hash,所以以 hash 作为文件名,将切片从临时路径移动切片文件夹中,最后的结果如下<br><img src="https://user-gold-cdn.xitu.io/2020/1/8/16f83c3caac8eb65?imageView2/0/w/1280/h/960/format/webp/ignore-error/1" alt="xuehuayu.cn"></p><h3 id="合并切片"><a href="#合并切片" class="headerlink" title="合并切片"></a>合并切片</h3><p>在接收到前端发送的合并请求后,服务端将文件夹下的所有切片进行合并</p><pre><code>const http = require(&quot;http&quot;);const path = require(&quot;path&quot;);const fse = require(&quot;fs-extra&quot;);const server = http.createServer();const UPLOAD_DIR = path.resolve(__dirname, &quot;..&quot;, &quot;target&quot;); // 大文件存储目录+ const resolvePost = req =&gt;+ new Promise(resolve =&gt; &#123;+ let chunk = &quot;&quot;;+ req.on(&quot;data&quot;, data =&gt; &#123;+ chunk += data;+ &#125;);+ req.on(&quot;end&quot;, () =&gt; &#123;+ resolve(JSON.parse(chunk));+ &#125;);+ &#125;);+ // 合并切片+ const mergeFileChunk = async (filePath, filename) =&gt; &#123;+ const chunkDir = `$&#123;UPLOAD_DIR&#125;/$&#123;filename&#125;`;+ const chunkPaths = await fse.readdir(chunkDir);+ await fse.writeFile(filePath, &quot;&quot;);+ chunkPaths.forEach(chunkPath =&gt; &#123;+ fse.appendFileSync(filePath, fse.readFileSync(`$&#123;chunkDir&#125;/$&#123;chunkPath&#125;`));+ fse.unlinkSync(`$&#123;chunkDir&#125;/$&#123;chunkPath&#125;`);+ &#125;);+ fse.rmdirSync(chunkDir); // 合并后删除保存切片的目录+ &#125;;server.on(&quot;request&quot;, async (req, res) =&gt; &#123; res.setHeader(&quot;Access-Control-Allow-Origin&quot;, &quot;*&quot;); res.setHeader(&quot;Access-Control-Allow-Headers&quot;, &quot;*&quot;); if (req.method === &quot;OPTIONS&quot;) &#123; res.status = 200; res.end(); return; &#125;+ if (req.url === &quot;/merge&quot;) &#123;+ const data = await resolvePost(req);+ const &#123; filename &#125; = data;+ const filePath = `$&#123;UPLOAD_DIR&#125;/$&#123;filename&#125;`;+ await mergeFileChunk(filePath, filename);+ res.end(+ JSON.stringify(&#123;+ code: 0,+ message: &quot;file merged success&quot;+ &#125;)+ );+ &#125;&#125;);server.listen(3000, () =&gt; console.log(&quot;正在监听 3000 端口&quot;));复制代码</code></pre><p>由于前端在发送合并请求时会携带文件名,服务端根据文件名可以找到上一步创建的切片文件夹</p><p>接着使用 fs.writeFileSync 先创建一个空文件,这个空文件的文件名就是<strong>切片文件夹名 + 后缀名</strong>组合而成,随后通过 fs.appendFileSync 从切片文件夹中不断将切片合并到空文件中,每次合并完成后删除这个切片,等所有切片都合并完毕后最后删除切片文件夹<br><img src="https://user-gold-cdn.xitu.io/2020/1/8/16f8414c7fae1c1f?imageView2/0/w/1280/h/960/format/webp/ignore-error/1" alt="xuehuayu.cn"><br>至此一个简单的大文件上传就完成了,接下来我们再此基础上扩展一些额外的功能</p><h2 id="显示上传进度条"><a href="#显示上传进度条" class="headerlink" title="显示上传进度条"></a>显示上传进度条</h2><p>上传进度分两种,一个是每个切片的上传进度,另一个是整个文件的上传进度,而整个文件的上传进度是基于每个切片上传进度计算而来,所以我们先实现切片的上传进度</p><h3 id="切片进度条"><a href="#切片进度条" class="headerlink" title="切片进度条"></a>切片进度条</h3><p>XMLHttpRequest 原生支持上传进度的监听,只需要监听 upload.onprogress 即可,我们在原来的 request 基础上传入 onProgress 参数,给 XMLHttpRequest 注册监听事件</p><pre><code> // xhr request(&#123; url, method = &quot;post&quot;, data, headers = &#123;&#125;,+ onProgress = e =&gt; e, requestList &#125;) &#123; return new Promise(resolve =&gt; &#123; const xhr = new XMLHttpRequest();+ xhr.upload.onprogress = onProgress; xhr.open(method, url); Object.keys(headers).forEach(key =&gt; xhr.setRequestHeader(key, headers[key]) ); xhr.send(data); xhr.onload = e =&gt; &#123; resolve(&#123; data: e.target.response &#125;); &#125;; &#125;); &#125;复制代码</code></pre><p>由于每个切片都需要触发独立的监听事件,所以还需要一个工厂函数,根据传入的切片返回不同的监听函数</p><p>在原先的前端上传逻辑中新增监听函数部分</p><pre><code> // 上传切片,同时过滤已上传的切片 async uploadChunks(uploadedList = []) &#123; const requestList = this.data+ .map((&#123; chunk,hash,index &#125;) =&gt; &#123; const formData = new FormData(); formData.append(&quot;chunk&quot;, chunk); formData.append(&quot;hash&quot;, hash); formData.append(&quot;filename&quot;, this.container.file.name);+ return &#123; formData,index &#125;; &#125;)+ .map(async (&#123; formData,index &#125;) =&gt; this.request(&#123; url: &quot;http://localhost:3000&quot;, data: formData,+ onProgress: this.createProgressHandler(this.data[index]), &#125;) ); await Promise.all(requestList); // 合并切片 await this.mergeRequest(); &#125;, async handleUpload() &#123; if (!this.container.file) return; const fileChunkList = this.createFileChunk(this.container.file); this.data = fileChunkList.map((&#123; file &#125;,index) =&gt; (&#123; chunk: file,+ index, hash: this.container.file.name + &quot;-&quot; + index+ percentage:0 &#125;)); await this.uploadChunks(); &#125; + createProgressHandler(item) &#123;+ return e =&gt; &#123;+ item.percentage = parseInt(String((e.loaded / e.total) * 100));+ &#125;;+ &#125;复制代码</code></pre><p>每个切片在上传时都会通过监听函数更新 data 数组对应元素的 percentage 属性,之后把将 data 数组放到视图中展示即可</p><h3 id="文件进度条"><a href="#文件进度条" class="headerlink" title="文件进度条"></a>文件进度条</h3><p>将每个切片已上传的部分累加,除以整个文件的大小,就能得出当前文件的上传进度,所以这里使用 Vue 计算属性</p><pre><code> computed: &#123; uploadPercentage() &#123; if (!this.container.file || !this.data.length) return0; const loaded = this.data .map(item =&gt; item.size * item.percentage) .reduce((acc, cur) =&gt; acc + cur); returnparseInt((loaded / this.container.file.size).toFixed(2)); &#125; &#125;复制代码</code></pre><p>最终视图如下<br><img src="https://user-gold-cdn.xitu.io/2020/1/8/16f84ad6b4848423?imageView2/0/w/1280/h/960/format/webp/ignore-error/1" alt="xuehuayu.cn"></p><h1 id="断点续传"><a href="#断点续传" class="headerlink" title="断点续传"></a>断点续传</h1><p>断点续传的原理在于前端&#x2F;服务端需要<code>记住</code>已上传的切片,这样下次上传就可以跳过之前已上传的部分,有两种方案实现记忆的功能</p><ul><li>前端使用 localStorage 记录已上传的切片 hash</li><li>服务端保存已上传的切片 hash,前端每次上传前向服务端获取已上传的切片</li></ul><p>第一种是前端的解决方案,第二种是服务端,而前端方案有一个缺陷,如果换了个浏览器就失去了记忆的效果,所以这里选取后者</p><h2 id="生成-hash"><a href="#生成-hash" class="headerlink" title="生成 hash"></a>生成 hash</h2><p>无论是前端还是服务端,都必须要生成文件和切片的 hash,<code>之前我们使用文件名 + 切片下标作为切片 hash</code>,这样做文件名一旦修改就失去了效果,而事实上只要文件内容不变,hash 就不应该变化,所以正确的做法是<code>根据文件内容生成 hash</code>,所以我们修改一下 hash 的生成规则</p><p>这里用到另一个库 <code>spark-md5</code>,它可以根据文件内容计算出文件的 hash 值,另外考虑到如果上传一个超大文件,读取文件内容计算 hash 是非常耗费时间的,并且会<code>引起 UI 的阻塞</code>,导致页面假死状态,所以我们使用 web-worker 在 worker 线程计算 hash,这样用户仍可以在主界面正常的交互</p><p>由于实例化 web-worker 时,参数是一个 js 文件路径且不能跨域,所以我们单独创建一个 hash.js 文件放在 public 目录下,另外在 worker 中也是不允许访问 dom 的,但它提供了<code>importScripts</code> 函数用于导入外部脚本,通过它导入 spark-md5</p><pre><code>// /public/hash.jsself.importScripts(&quot;/spark-md5.min.js&quot;); // 导入脚本// 生成文件 hashself.onmessage = e =&gt; &#123; const &#123; fileChunkList &#125; = e.data; const spark = new self.SparkMD5.ArrayBuffer(); let percentage = 0; let count = 0; const loadNext = index =&gt; &#123; const reader = new FileReader(); reader.readAsArrayBuffer(fileChunkList[index].file); reader.onload = e =&gt; &#123; count++; spark.append(e.target.result); if (count === fileChunkList.length) &#123; self.postMessage(&#123; percentage: 100, hash: spark.end() &#125;); self.close(); &#125; else &#123; percentage += 100 / fileChunkList.length; self.postMessage(&#123; percentage &#125;); // 递归计算下一个切片 loadNext(count); &#125; &#125;; &#125;; loadNext(0);&#125;;复制代码</code></pre><p>在 worker 线程中,接受文件切片 fileChunkList,利用 FileReader 读取每个切片的 ArrayBuffer 并不断传入 spark-md5 中,每计算完一个切片通过 postMessage 向主线程发送一个进度事件,全部完成后将最终的 hash 发送给主线程</p><p><code>spark-md5 需要根据所有切片才能算出一个 hash 值,不能直接将整个文件放入计算,否则即使不同文件也会有相同的 hash,具体可以看官方文档</code></p><p><a href="https://www.npmjs.com/package/spark-md5">spark-md5</a></p><p>接着编写主线程与 worker 线程通讯的逻辑</p><pre><code>+ // 生成文件 hash(web-worker)+ calculateHash(fileChunkList) &#123;+ return new Promise(resolve =&gt; &#123;+ // 添加 worker 属性+ this.container.worker = new Worker(&quot;/hash.js&quot;);+ this.container.worker.postMessage(&#123; fileChunkList &#125;);+ this.container.worker.onmessage = e =&gt; &#123;+ const &#123; percentage, hash &#125; = e.data;+ this.hashPercentage = percentage;+ if (hash) &#123;+ resolve(hash);+ &#125;+ &#125;;+ &#125;); &#125;, async handleUpload() &#123; if (!this.container.file) return; const fileChunkList = this.createFileChunk(this.container.file);+ this.container.hash = await this.calculateHash(fileChunkList); this.data = fileChunkList.map((&#123; file &#125;,index) =&gt; (&#123;+ fileHash: this.container.hash, chunk: file, hash: this.container.file.name + &quot;-&quot; + index, // 文件名 + 数组下标 percentage:0 &#125;)); await this.uploadChunks(); &#125; 复制代码</code></pre><p>主线程使用 <code>postMessage</code> 给 worker 线程传入所有切片 fileChunkList,并监听 worker 线程发出的 postMessage 事件拿到文件 hash</p><p>加上显示计算 hash 的进度条,看起来像这样<br><img src="https://user-gold-cdn.xitu.io/2020/1/8/16f84ae4f233e891?imageView2/0/w/1280/h/960/format/webp/ignore-error/1" alt="xuehuayu.cn"><br>至此前端需要将之前用文件名作为 hash 的地方改写为 workder 返回的这个 hash<br><img src="https://user-gold-cdn.xitu.io/2020/1/8/16f84b3bba953304?imageView2/0/w/1280/h/960/format/webp/ignore-error/1" alt="xuehuayu.cn"><br>服务端则使用 hash 作为切片文件夹名,hash + 下标作为切片名,hash + 扩展名作为文件名,没有新增的逻辑<br><img src="https://user-gold-cdn.xitu.io/2020/1/8/16f84b50117e4db2?imageView2/0/w/1280/h/960/format/webp/ignore-error/1" alt="xuehuayu.cn"><img src="https://user-gold-cdn.xitu.io/2020/1/8/16f84b33bbceaa48?imageView2/0/w/1280/h/960/format/webp/ignore-error/1" alt="xuehuayu.cn"></p><h2 id="文件秒传"><a href="#文件秒传" class="headerlink" title="文件秒传"></a>文件秒传</h2><p>在实现断点续传前先简单介绍一下文件秒传</p><p>所谓的文件秒传,即在服务端已经存在了上传的资源,所以当用户<code>再次上传</code>时会直接提示上传成功</p><p>文件秒传需要依赖上一步生成的 hash,即在<code>上传前</code>,先计算出文件 hash,并把 hash 发送给服务端进行验证,由于 hash 的唯一性,所以一旦服务端能找到 hash 相同的文件,则直接返回上传成功的信息即可</p><pre><code>+ async verifyUpload(filename, fileHash) &#123;+ const &#123; data &#125; = await this.request(&#123;+ url: &quot;http://localhost:3000/verify&quot;,+ headers: &#123;+ &quot;content-type&quot;: &quot;application/json&quot;+ &#125;,+ data: JSON.stringify(&#123;+ filename,+ fileHash+ &#125;)+ &#125;);+ return JSON.parse(data);+ &#125;, async handleUpload() &#123; if (!this.container.file) return; const fileChunkList = this.createFileChunk(this.container.file); this.container.hash = await this.calculateHash(fileChunkList);+ const &#123; shouldUpload &#125; = await this.verifyUpload(+ this.container.file.name,+ this.container.hash+ );+ if (!shouldUpload) &#123;+ this.$message.success(&quot;秒传:上传成功&quot;);+ return;+ &#125; this.data = fileChunkList.map((&#123; file &#125;, index) =&gt; (&#123; fileHash: this.container.hash, index, hash: this.container.hash + &quot;-&quot; + index, chunk: file, percentage: 0 &#125;)); await this.uploadChunks(); &#125; 复制代码</code></pre><p>秒传其实就是给用户看的障眼法,实质上根本没有上传<br><img src="https://user-gold-cdn.xitu.io/2020/1/9/16f890e5967106c6?imageView2/0/w/1280/h/960/format/webp/ignore-error/1" alt="image-20200109143511277"><br>:)</p><p>服务端的逻辑非常简单,新增一个验证接口,验证文件是否存在即可</p><pre><code>+ const extractExt = filename =&gt;+ filename.slice(filename.lastIndexOf(&quot;.&quot;), filename.length); // 提取后缀名const UPLOAD_DIR = path.resolve(__dirname, &quot;..&quot;, &quot;target&quot;); // 大文件存储目录const resolvePost = req =&gt; new Promise(resolve =&gt; &#123; let chunk = &quot;&quot;; req.on(&quot;data&quot;, data =&gt; &#123; chunk += data; &#125;); req.on(&quot;end&quot;, () =&gt; &#123; resolve(JSON.parse(chunk)); &#125;); &#125;);server.on(&quot;request&quot;, async (req, res) =&gt; &#123; if (req.url === &quot;/verify&quot;) &#123;+ const data = await resolvePost(req);+ const &#123; fileHash, filename &#125; = data;+ const ext = extractExt(filename);+ const filePath = `$&#123;UPLOAD_DIR&#125;/$&#123;fileHash&#125;$&#123;ext&#125;`;+ if (fse.existsSync(filePath)) &#123;+ res.end(+ JSON.stringify(&#123;+ shouldUpload: false+ &#125;)+ );+ &#125; else &#123;+ res.end(+ JSON.stringify(&#123;+ shouldUpload: true+ &#125;)+ );+ &#125; &#125;&#125;);server.listen(3000, () =&gt; console.log(&quot;正在监听 3000 端口&quot;));复制代码</code></pre><h2 id="暂停上传"><a href="#暂停上传" class="headerlink" title="暂停上传"></a>暂停上传</h2><p>讲完了生成 hash 和文件秒传,回到断点续传</p><p>断点续传顾名思义即断点 + 续传,所以我们第一步先实现“断点”,也就是暂停上传</p><p>原理是使用 XMLHttpRequest 的 <code>abort</code> 方法,可以取消一个 xhr 请求的发送,为此我们需要将上传每个切片的 xhr 对象保存起来,我们再改造一下 request 方法</p><pre><code> request(&#123; url, method = &quot;post&quot;, data, headers = &#123;&#125;, onProgress = e =&gt; e,+ requestList &#125;) &#123; return new Promise(resolve =&gt; &#123; const xhr = new XMLHttpRequest(); xhr.upload.onprogress = onProgress; xhr.open(method, url); Object.keys(headers).forEach(key =&gt; xhr.setRequestHeader(key, headers[key]) ); xhr.send(data); xhr.onload = e =&gt; &#123;+ // 将请求成功的 xhr 从列表中删除+ if (requestList) &#123;+ const xhrIndex = requestList.findIndex(item =&gt; item === xhr);+ requestList.splice(xhrIndex, 1);+ &#125; resolve(&#123; data: e.target.response &#125;); &#125;;+ // 暴露当前 xhr 给外部+ requestList?.push(xhr); &#125;); &#125;,复制代码</code></pre><p>这样在上传切片时传入 requestList 数组作为参数,request 方法就会将所有的 xhr 保存在数组中了<br><img src="https://user-gold-cdn.xitu.io/2020/1/8/16f8563491ea7793?imageView2/0/w/1280/h/960/format/webp/ignore-error/1" alt="xuehuayu.cn"><br>每当一个切片上传成功时,将对应的 xhr 从 requestList 中删除,所以 requestList 中只保存<code>正在上传切片的 xhr</code></p><p>之后新建一个暂停按钮,当点击按钮时,调用保存在 requestList 中 xhr 的 abort 方法,即取消并清空所有正在上传的切片</p><pre><code> handlePause() &#123; this.requestList.forEach(xhr =&gt; xhr?.abort()); this.requestList = [];&#125;复制代码</code></pre><p><img src="https://user-gold-cdn.xitu.io/2020/1/9/16f890e598dd6afb?imageView2/0/w/1280/h/960/format/webp/ignore-error/1" alt="image-20200109143737924"><br>点击暂停按钮可以看到 xhr 都被取消了<br><img src="https://user-gold-cdn.xitu.io/2020/1/8/16f85644dc27459e?imageView2/0/w/1280/h/960/format/webp/ignore-error/1" alt="xuehuayu.cn"></p><h2 id="恢复上传"><a href="#恢复上传" class="headerlink" title="恢复上传"></a>恢复上传</h2><p>之前在介绍断点续传的时提到使用第二种服务端存储的方式实现续传</p><p>由于当文件切片上传后,服务端会建立一个文件夹存储所有上传的切片,所以每次前端上传前可以调用一个接口,服务端将已上传的切片的切片名返回,前端再跳过这些已经上传切片,这样就实现了“续传”的效果</p><p>而这个接口可以和之前秒传的验证接口合并,前端每次上传前发送一个验证的请求,返回两种结果</p><ul><li>服务端已存在该文件,不需要再次上传</li><li>服务端不存在该文件或者已上传部分文件切片,通知前端进行上传,并把<strong>已上传</strong>的文件切片返回给前端</li></ul><p>所以我们改造一下之前文件秒传的服务端验证接口</p><pre><code>const extractExt = filename =&gt; filename.slice(filename.lastIndexOf(&quot;.&quot;), filename.length); // 提取后缀名const UPLOAD_DIR = path.resolve(__dirname, &quot;..&quot;, &quot;target&quot;); // 大文件存储目录const resolvePost = req =&gt; new Promise(resolve =&gt; &#123; let chunk = &quot;&quot;; req.on(&quot;data&quot;, data =&gt; &#123; chunk += data; &#125;); req.on(&quot;end&quot;, () =&gt; &#123; resolve(JSON.parse(chunk)); &#125;); &#125;); + // 返回已经上传切片名列表+ const createUploadedList = async fileHash =&gt;+ fse.existsSync(`$&#123;UPLOAD_DIR&#125;/$&#123;fileHash&#125;`)+ ? await fse.readdir(`$&#123;UPLOAD_DIR&#125;/$&#123;fileHash&#125;`)+ : [];server.on(&quot;request&quot;, async (req, res) =&gt; &#123; if (req.url === &quot;/verify&quot;) &#123; const data = await resolvePost(req); const &#123; fileHash, filename &#125; = data; const ext = extractExt(filename); const filePath = `$&#123;UPLOAD_DIR&#125;/$&#123;fileHash&#125;$&#123;ext&#125;`; if (fse.existsSync(filePath)) &#123; res.end( JSON.stringify(&#123; shouldUpload: false &#125;) ); &#125; else &#123; res.end( JSON.stringify(&#123; shouldUpload: true,+ uploadedList: await createUploadedList(fileHash) &#125;) ); &#125; &#125;&#125;);server.listen(3000, () =&gt; console.log(&quot;正在监听 3000 端口&quot;));复制代码</code></pre><p>接着回到前端,前端有两个地方需要调用验证的接口</p><ul><li>点击上传时,检查是否需要上传和已上传的切片</li><li>点击暂停后的恢复上传,返回已上传的切片</li></ul><p>新增恢复按钮并改造原来上传切片的逻辑</p><pre><code>&lt;template&gt; &lt;div id=&quot;app&quot;&gt; &lt;input type=&quot;file&quot; @change=&quot;handleFileChange&quot; /&gt; &lt;el-button @click=&quot;handleUpload&quot;&gt;上传&lt;/el-button&gt; &lt;el-button @click=&quot;handlePause&quot; v-if=&quot;isPaused&quot;&gt;暂停&lt;/el-button&gt;+ &lt;el-button @click=&quot;handleResume&quot; v-else&gt;恢复&lt;/el-button&gt; //... &lt;/div&gt;&lt;/template&gt;+ async handleResume() &#123;+ const &#123; uploadedList &#125; = await this.verifyUpload(+ this.container.file.name,+ this.container.hash+ );+ await this.uploadChunks(uploadedList); &#125;, async handleUpload() &#123; if (!this.container.file) return; const fileChunkList = this.createFileChunk(this.container.file); this.container.hash = await this.calculateHash(fileChunkList);+ const &#123; shouldUpload, uploadedList &#125; = await this.verifyUpload( this.container.file.name, this.container.hash ); if (!shouldUpload) &#123; this.$message.success(&quot;秒传:上传成功&quot;); return; &#125; this.data = fileChunkList.map((&#123; file &#125;, index) =&gt; (&#123; fileHash: this.container.hash, index, hash: this.container.hash + &quot;-&quot; + index, chunk: file, percentage: 0 &#125;));+ await this.uploadChunks(uploadedList); &#125;, // 上传切片,同时过滤已上传的切片+ async uploadChunks(uploadedList = []) &#123; const requestList = this.data+ .filter((&#123; hash &#125;) =&gt; !uploadedList.includes(hash)) .map((&#123; chunk, hash, index &#125;) =&gt; &#123; const formData = new FormData(); formData.append(&quot;chunk&quot;, chunk); formData.append(&quot;hash&quot;, hash); formData.append(&quot;filename&quot;, this.container.file.name); formData.append(&quot;fileHash&quot;, this.container.hash); return &#123; formData, index &#125;; &#125;) .map(async (&#123; formData, index &#125;) =&gt; this.request(&#123; url: &quot;http://localhost:3000&quot;, data: formData, onProgress: this.createProgressHandler(this.data[index]), requestList: this.requestList &#125;) ); await Promise.all(requestList); // 之前上传的切片数量 + 本次上传的切片数量 = 所有切片数量时 // 合并切片+ if (uploadedList.length + requestList.length === this.data.length) &#123; await this.mergeRequest();+ &#125; &#125;复制代码</code></pre><p>![image-20200109144331326](data:image&#x2F;svg+xml;utf8,<?xml version=&quot;1.0&quot;?><svg xmlns=&quot;http://www.w3.org/2000/svg&quot; version=&quot;1.1&quot; width=&quot;463&quot; height=&quot;54&quot;></svg>)<br>这里给原来上传切片的函数新增 uploadedList 参数,即上图中服务端返回的切片名列表,通过 filter 过滤掉已上传的切片,并且由于新增了已上传的部分,所以之前合并接口的触发条件做了一些改动</p><p>到这里断点续传的功能基本完成了</p><h2 id="进度条改进"><a href="#进度条改进" class="headerlink" title="进度条改进"></a>进度条改进</h2><p>虽然实现了断点续传,但还需要修改一下进度条的显示规则,否则在暂停上传&#x2F;接收到已上传切片时的进度条会出现偏差</p><h3 id="切片进度条-1"><a href="#切片进度条-1" class="headerlink" title="切片进度条"></a>切片进度条</h3><p>由于在点击上传&#x2F;恢复上传时,会调用验证接口返回已上传的切片,所以需要将已上传切片的进度变成 100%</p><pre><code> async handleUpload() &#123; if (!this.container.file) return; const fileChunkList = this.createFileChunk(this.container.file); this.container.hash = await this.calculateHash(fileChunkList); const &#123; shouldUpload, uploadedList &#125; = await this.verifyUpload( this.container.file.name, this.container.hash ); if (!shouldUpload) &#123; this.$message.success(&quot;秒传:上传成功&quot;); return; &#125; this.data = fileChunkList.map((&#123; file &#125;, index) =&gt; (&#123; fileHash: this.container.hash, index, hash: this.container.hash + &quot;-&quot; + index, chunk: file,+ percentage: uploadedList.includes(index) ? 100 : 0 &#125;)); await this.uploadChunks(uploadedList); &#125;,复制代码</code></pre><p>uploadedList 会返回已上传的切片,在遍历所有切片时判断当前切片是否在已上传列表里即可</p><h3 id="文件进度条-1"><a href="#文件进度条-1" class="headerlink" title="文件进度条"></a>文件进度条</h3><p>之前说到文件进度条是一个计算属性,根据所有切片的上传进度计算而来,这就遇到了一个问题<br><img src="https://user-gold-cdn.xitu.io/2020/1/9/16f8845dcca1427c?imageView2/0/w/1280/h/960/format/webp/ignore-error/1" alt="xuehuayu.cn"><br>点击暂停会取消并清空切片的 xhr 请求,此时如果已经上传了一部分,就会发现文件进度条有<code>倒退</code>的现象<br><img src="https://user-gold-cdn.xitu.io/2020/1/9/16f884633f3acfd9?imageView2/0/w/1280/h/960/format/webp/ignore-error/1" alt="xuehuayu.cn"><br>当点击恢复时,由于重新创建了 xhr 导致切片进度清零,所以总进度条就会倒退</p><p>解决方案是创建一个“假”的进度条,这个假进度条基于文件进度条,但只会停止和增加,然后给用户展示这个假的进度条</p><p>这里我们使用 Vue 的监听属性</p><pre><code> data: () =&gt; (&#123;+ fakeUploadPercentage: 0 &#125;), computed: &#123; uploadPercentage() &#123; if (!this.container.file || !this.data.length) return 0; const loaded = this.data .map(item =&gt; item.size * item.percentage) .reduce((acc, cur) =&gt; acc + cur); return parseInt((loaded / this.container.file.size).toFixed(2)); &#125; &#125;, watch: &#123;+ uploadPercentage(now) &#123;+ if (now &gt; this.fakeUploadPercentage) &#123;+ this.fakeUploadPercentage = now;+ &#125; &#125; &#125;,复制代码</code></pre><p>当 uploadPercentage 即真的文件进度条增加时,fakeUploadPercentage 也增加,一旦文件进度条后退,假的进度条只需停止即可</p><p>至此一个大文件上传 + 断点续传的解决方案就完成了</p><h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><p>大文件上传</p><ul><li>前端上传大文件时使用 Blob.prototype.slice 将文件切片,并发上传多个切片,最后发送一个合并的请求通知服务端合并切片</li><li>服务端接收切片并存储,收到合并请求后使用 fs.appendFileSync 对多个切片进行合并</li><li>原生 XMLHttpRequest 的 upload.onprogress 对切片上传进度的监听</li><li>使用 Vue 计算属性根据每个切片的进度算出整个文件的上传进度</li></ul><p>断点续传</p><ul><li>使用 spark-md5 根据文件内容算出文件 hash</li><li>通过 hash 可以判断服务端是否已经上传该文件,从而直接提示用户上传成功(秒传)</li><li>通过 XMLHttpRequest 的 abort 方法暂停切片的上传</li><li>上传前服务端返回已经上传的切片名,前端跳过这些切片的上传</li></ul><h1 id="源代码"><a href="#源代码" class="headerlink" title="源代码"></a>源代码</h1><p>源代码增加了一些按钮的状态,交互更加友好,文章表达比较晦涩的地方可以跳转到源代码查看</p><p><a href="https://github.com/yeyan1996/file-upload">file-upload</a><br><img src="https://user-gold-cdn.xitu.io/2020/1/9/16f8859bdc09fa25?imageView2/0/w/1280/h/960/format/webp/ignore-error/1" alt="xuehuayu.cn"><br>谢谢观看 :)</p><h1 id="字节跳动-EA(Enterprise-Application)前端团队招人啦~"><a href="#字节跳动-EA(Enterprise-Application)前端团队招人啦~" class="headerlink" title="字节跳动 EA(Enterprise Application)前端团队招人啦~"></a>字节跳动 EA(Enterprise Application)前端团队招人啦~</h1><p>坐标上海&#x2F;北京,校招社招都有,hc 无上限,有兴趣的欢迎投递简历至 <a href="mailto:&#x31;&#57;&#x39;&#x36;&#121;&#101;&#x79;&#97;&#x6e;&#64;&#103;&#x6d;&#x61;&#x69;&#x6c;&#46;&#x63;&#111;&#x6d;">&#x31;&#57;&#x39;&#x36;&#121;&#101;&#x79;&#97;&#x6e;&#64;&#103;&#x6d;&#x61;&#x69;&#x6c;&#46;&#x63;&#111;&#x6d;</a>,校招内推码 Q7QUGMV</p><h1 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h1><p><a href="https://juejin.im/post/5da14778f265da5bb628e590">写给新手前端的各种文件上传攻略,从小图片到大文件断点续传</a></p><p><a href="https://developer.mozilla.org/zh-CN/docs/Web/API/Blob/slice">Blob.slice</a><a href="https://developer.mozilla.org/zh-CN/docs/Web/API/Blob/slice">developer.mozilla.org&#x2F;zh-CN&#x2F;docs&#x2F;…</a>)</p>]]></content>
2538
+ <content type="html"><![CDATA[<p><code>原文地址:https://juejin.im/post/5dff8a26e51d4558105420ed</code><br>这段时间面试官都挺忙的,频频出现在博客文章标题,虽然我不是特别想蹭热度,但是实在想不到好的标题了-。-,蹭蹭就蹭蹭 :)<br>事实上我在面试的时候确实被问到了这个问题,而且是一道在线 coding 的编程题,当时虽然思路正确,可惜最终也并不算完全答对<br>结束后花了一段时间整理了下思路,那么究竟该如何实现一个大文件上传,以及在上传中如何实现断点续传的功能呢?<br>本文将从零搭建前端和服务端,实现一个大文件上传和断点续传的 demo</p><span id="more"></span><p>前端:<code>vue``element-ui</code><br>服务端:<code>nodejs</code></p><p><code>文章有误解的地方,欢迎指出,将在第一时间改正,有更好的实现方式希望留下你的评论</code></p><h1 id="大文件上传"><a href="#大文件上传" class="headerlink" title="大文件上传"></a>大文件上传</h1><h2 id="整体思路"><a href="#整体思路" class="headerlink" title="整体思路"></a>整体思路</h2><h3 id="前端"><a href="#前端" class="headerlink" title="前端"></a>前端</h3><p>前端大文件上传网上的大部分文章已经给出了解决方案,核心是<code>利用 Blob.prototype.slice</code> 方法,和数组的 slice 方法相似,调用的 slice 方法可以返回<code>原文件的某个切片</code></p><p>这样我们就可以根据预先设置好的切片最大数量将文件切分为一个个切片,然后借助 http 的可并发性,同时上传多个切片,这样从原本传一个大文件,变成了<code>同时</code>传多个小的文件切片,可以大大减少上传时间</p><p>另外由于是并发,传输到服务端的顺序可能会发生变化,所以我们还需要给每个切片记录顺序</p><h3 id="服务端"><a href="#服务端" class="headerlink" title="服务端"></a>服务端</h3><p>服务端需要负责接受这些切片,并在接收到所有切片后<code>合并</code>切片</p><p>这里又引伸出两个问题</p><ol><li>何时合并切片,即切片什么时候传输完成</li><li>如何合并切片</li></ol><p>第一个问题需要前端进行配合,前端在每个切片中都携带切片最大数量的信息,当服务端接受到这个数量的切片时自动合并,也可以额外发一个请求主动通知服务端进行切片的合并</p><p>第二个问题,具体如何合并切片呢?这里可以使用 nodejs 的 api <code>fs.appendFileSync</code>,它可以同步地将数据追加到指定文件,也就是说,当服务端接受到所有切片后,先创建一个最终的文件,然后将所有切片逐步合并到这个文件中</p><p><code>talk is cheap,show me the code</code>,接着我们用代码实现上面的思路</p><h2 id="前端部分"><a href="#前端部分" class="headerlink" title="前端部分"></a>前端部分</h2><p>前端使用 Vue 作为开发框架,对界面没有太大要求,原生也可以,考虑到美观使用 element-ui 作为 UI 框架</p><h3 id="上传控件"><a href="#上传控件" class="headerlink" title="上传控件"></a>上传控件</h3><p>首先创建选择文件的控件,监听 change 事件以及上传按钮</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><span class="line">&lt;template&gt;</span><br><span class="line"> &lt;div&gt;</span><br><span class="line"> &lt;input type=&quot;file&quot; @change=&quot;handleFileChange&quot; /&gt;</span><br><span class="line"> &lt;el-button @click=&quot;handleUpload&quot;&gt;上传&lt;/el-button&gt;</span><br><span class="line"> &lt;/div&gt;</span><br><span class="line">&lt;/template&gt;</span><br><span class="line"></span><br><span class="line">&lt;script&gt;</span><br><span class="line">export default &#123;</span><br><span class="line"> data: () =&gt; (&#123;</span><br><span class="line"> container: &#123;</span><br><span class="line"> file: null</span><br><span class="line"> &#125;</span><br><span class="line"> &#125;),</span><br><span class="line"> methods: &#123;</span><br><span class="line"> handleFileChange(e) &#123;</span><br><span class="line"> const [file] = e.target.files;</span><br><span class="line"> if (!file) return;</span><br><span class="line"> Object.assign(this.$data, this.$options.data());</span><br><span class="line"> this.container.file = file;</span><br><span class="line"> &#125;,</span><br><span class="line"> async handleUpload() &#123;&#125;</span><br><span class="line"> &#125;</span><br><span class="line">&#125;;</span><br><span class="line">&lt;/script&gt;</span><br><span class="line"></span><br></pre></td></tr></table></figure><p><img src="https://user-gold-cdn.xitu.io/2020/1/7/16f7ff98adb6bcdd?imageView2/0/w/1280/h/960/format/webp/ignore-error/1" alt="xuehuayu.cn"></p><h3 id="请求逻辑"><a href="#请求逻辑" class="headerlink" title="请求逻辑"></a>请求逻辑</h3><p>考虑到通用性,这里没有用第三方的请求库,而是用原生 XMLHttpRequest 做一层简单的封装来发请求</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line">request(&#123;</span><br><span class="line"> url,</span><br><span class="line"> method = &quot;post&quot;,</span><br><span class="line"> data,</span><br><span class="line"> headers = &#123;&#125;,</span><br><span class="line"> requestList</span><br><span class="line"> &#125;) &#123;</span><br><span class="line"> returnnewPromise(resolve =&gt; &#123;</span><br><span class="line"> const xhr = new XMLHttpRequest();</span><br><span class="line"> xhr.open(method, url);</span><br><span class="line"> Object.keys(headers).forEach(key =&gt;</span><br><span class="line"> xhr.setRequestHeader(key, headers[key])</span><br><span class="line"> );</span><br><span class="line"> xhr.send(data);</span><br><span class="line"> xhr.onload = e =&gt; &#123;</span><br><span class="line"> resolve(&#123;</span><br><span class="line"> data: e.target.response</span><br><span class="line"> &#125;);</span><br><span class="line"> &#125;;</span><br><span class="line"> &#125;);</span><br><span class="line"> &#125;</span><br></pre></td></tr></table></figure><h3 id="上传切片"><a href="#上传切片" class="headerlink" title="上传切片"></a>上传切片</h3><p>接着实现比较重要的上传功能,上传需要做两件事</p><ul><li>对文件进行切片</li><li>将切片传输给服务端</li></ul><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br></pre></td><td class="code"><pre><span class="line">&lt;template&gt;</span><br><span class="line"> &lt;div&gt;</span><br><span class="line"> &lt;input type=&quot;file&quot; @change=&quot;handleFileChange&quot; /&gt;</span><br><span class="line"> &lt;el-button @click=&quot;handleUpload&quot;&gt;上传&lt;/el-button&gt;</span><br><span class="line"> &lt;/div&gt;</span><br><span class="line">&lt;/template&gt;</span><br><span class="line"></span><br><span class="line">&lt;script&gt;</span><br><span class="line">+ const LENGTH = 10; // 切片数量</span><br><span class="line"></span><br><span class="line">export default &#123;</span><br><span class="line"> data: () =&gt; (&#123;</span><br><span class="line"> container: &#123;</span><br><span class="line"> file: null,</span><br><span class="line">+ data: []</span><br><span class="line"> &#125;</span><br><span class="line"> &#125;),</span><br><span class="line"> methods: &#123;</span><br><span class="line"> request() &#123;&#125;,</span><br><span class="line"> handleFileChange() &#123;&#125;,</span><br><span class="line">+ // 生成文件切片</span><br><span class="line">+ createFileChunk(file, length = LENGTH) &#123;</span><br><span class="line">+ const fileChunkList = [];</span><br><span class="line">+ const chunkSize = Math.ceil(file.size / length);</span><br><span class="line">+ let cur = 0;</span><br><span class="line">+ while (cur &lt; file.size) &#123;</span><br><span class="line">+ fileChunkList.push(&#123; file: file.slice(cur, cur + chunkSize) &#125;);</span><br><span class="line">+ cur += chunkSize;+ &#125;</span><br><span class="line">+ return fileChunkList;+ &#125;,</span><br><span class="line">+ // 上传切片</span><br><span class="line">+ async uploadChunks() &#123;</span><br><span class="line">+ const requestList = this.data</span><br><span class="line">+ .map((&#123; chunk,hash &#125;) =&gt; &#123;</span><br><span class="line">+ const formData = new FormData();</span><br><span class="line">+ formData.append(&quot;chunk&quot;, chunk);</span><br><span class="line">+ formData.append(&quot;hash&quot;, hash);</span><br><span class="line">+ formData.append(&quot;filename&quot;, this.container.file.name);</span><br><span class="line">+ return &#123; formData &#125;;</span><br><span class="line">+ &#125;)</span><br><span class="line">+ .map(async (&#123; formData &#125;) =&gt;</span><br><span class="line">+ this.request(&#123;</span><br><span class="line">+ url: &quot;http://localhost:3000&quot;,</span><br><span class="line">+ data: formData+ &#125;)</span><br><span class="line">+ );</span><br><span class="line">+ await Promise.all(requestList); // 并发切片</span><br><span class="line">+ &#125;,</span><br><span class="line">+ async handleUpload() &#123;</span><br><span class="line">+ if (!this.container.file) return;</span><br><span class="line">+ const fileChunkList = this.createFileChunk(this.container.file);</span><br><span class="line">+ this.data = fileChunkList.map((&#123; file &#125;,index) =&gt; (&#123;</span><br><span class="line">+ chunk: file,</span><br><span class="line">+ hash: this.container.file.name + &quot;-&quot; + index // 文件名 + 数组下标</span><br><span class="line">+ &#125;));</span><br><span class="line">+ await this.uploadChunks();</span><br><span class="line">+ &#125;</span><br><span class="line"> &#125;</span><br><span class="line">&#125;;</span><br><span class="line">&lt;/script&gt;</span><br></pre></td></tr></table></figure><p>当点击上传按钮时,调用 <code>createFileChunk</code> 将文件切片,切片数量通过一个常量 Length 控制,这里设置为 10,即将文件分成 10 个切片上传</p><p>createFileChunk 内使用 while 循环和 slice 方法将切片放入 <code>fileChunkList</code> 数组中返回</p><p>在生成文件切片时,需要给每个切片一个标识作为 hash,这里暂时使用<code>文件名 + 下标</code>,这样后端可以知道当前切片是第几个切片,用于之后的合并切片</p><p>随后调用 <code>uploadChunks</code> 上传所有的文件切片,将文件切片,切片 hash,以及文件名放入 FormData 中,再调用上一步的 <code>request</code> 函数返回一个 proimise,最后调用 Promise.all 并发上传所有的切片</p><h3 id="发送合并请求"><a href="#发送合并请求" class="headerlink" title="发送合并请求"></a>发送合并请求</h3><p>这里使用整体思路中提到的第二种合并切片的方式,即前端主动通知服务端进行合并,所以前端还需要额外发请求,服务端接受到这个请求时主动合并切片</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br></pre></td><td class="code"><pre><span class="line">&lt;template&gt;</span><br><span class="line"> &lt;div&gt;</span><br><span class="line"> &lt;input type=&quot;file&quot; @change=&quot;handleFileChange&quot; /&gt;</span><br><span class="line"> &lt;el-button @click=&quot;handleUpload&quot;&gt;上传&lt;/el-button&gt;</span><br><span class="line"> &lt;/div&gt;</span><br><span class="line">&lt;/template&gt;</span><br><span class="line"></span><br><span class="line">&lt;script&gt;</span><br><span class="line">export default &#123;</span><br><span class="line"> data: () =&gt; (&#123;</span><br><span class="line"> container: &#123;</span><br><span class="line"> file: null</span><br><span class="line"> &#125;,</span><br><span class="line"> data: []</span><br><span class="line"> &#125;),</span><br><span class="line"> methods: &#123;</span><br><span class="line"> request() &#123;&#125;,</span><br><span class="line"> handleFileChange() &#123;&#125;,</span><br><span class="line"> createFileChunk() &#123;&#125;,</span><br><span class="line"> // 上传切片,同时过滤已上传的切片</span><br><span class="line"> async uploadChunks() &#123;</span><br><span class="line"> const requestList = this.data</span><br><span class="line"> .map((&#123; chunk,hash &#125;) =&gt; &#123;</span><br><span class="line"> const formData = new FormData();</span><br><span class="line"> formData.append(&quot;chunk&quot;, chunk);</span><br><span class="line"> formData.append(&quot;hash&quot;, hash);</span><br><span class="line"> formData.append(&quot;filename&quot;, this.container.file.name);</span><br><span class="line"> return &#123; formData &#125;;</span><br><span class="line"> &#125;)</span><br><span class="line"> .map(async (&#123; formData &#125;) =&gt;</span><br><span class="line"> this.request(&#123;</span><br><span class="line"> url: &quot;http://localhost:3000&quot;,</span><br><span class="line"> data: formData</span><br><span class="line"> &#125;)</span><br><span class="line"> );</span><br><span class="line"> await Promise.all(requestList);</span><br><span class="line">+ // 合并切片+ await this.mergeRequest();</span><br><span class="line"> &#125;,</span><br><span class="line">+ async mergeRequest() &#123;</span><br><span class="line">+ await this.request(&#123;</span><br><span class="line">+ url: &quot;http://localhost:3000/merge&quot;,</span><br><span class="line">+ headers: &#123;</span><br><span class="line">+ &quot;content-type&quot;: &quot;application/json&quot;</span><br><span class="line">+ &#125;,</span><br><span class="line">+ data: JSON.stringify(&#123;</span><br><span class="line">+ filename: this.container.file.name+ &#125;)</span><br><span class="line">+ &#125;);</span><br><span class="line">+ &#125;,</span><br><span class="line"> async handleUpload() &#123;&#125;</span><br><span class="line"> &#125;</span><br><span class="line">&#125;;</span><br><span class="line">&lt;/script&gt;</span><br></pre></td></tr></table></figure><h2 id="服务端部分"><a href="#服务端部分" class="headerlink" title="服务端部分"></a>服务端部分</h2><p>简单使用 http 模块搭建服务端</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line">const http = require(&quot;http&quot;);</span><br><span class="line">const server = http.createServer();</span><br><span class="line"></span><br><span class="line">server.on(&quot;request&quot;, async (req, res) =&gt; &#123;</span><br><span class="line"> res.setHeader(&quot;Access-Control-Allow-Origin&quot;, &quot;*&quot;);</span><br><span class="line"> res.setHeader(&quot;Access-Control-Allow-Headers&quot;, &quot;*&quot;);</span><br><span class="line"> if (req.method === &quot;OPTIONS&quot;) &#123;</span><br><span class="line"> res.status = 200;</span><br><span class="line"> res.end();</span><br><span class="line"> return;</span><br><span class="line"> &#125;</span><br><span class="line">&#125;);</span><br><span class="line"></span><br><span class="line">server.listen(3000, () =&gt; console.log(&quot;正在监听 3000 端口&quot;));</span><br></pre></td></tr></table></figure><h3 id="接受切片"><a href="#接受切片" class="headerlink" title="接受切片"></a>接受切片</h3><p>使用 <code>multiparty</code> 包处理前端传来的 FormData</p><p>在 multiparty.parse 的回调中,files 参数保存了 FormData 中文件,fields 参数保存了 FormData 中非文件的字段</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br></pre></td><td class="code"><pre><span class="line">const http = require(&quot;http&quot;);</span><br><span class="line">const path = require(&quot;path&quot;);</span><br><span class="line">const fse = require(&quot;fs-extra&quot;);</span><br><span class="line">const multiparty = require(&quot;multiparty&quot;);</span><br><span class="line"></span><br><span class="line">const server = http.createServer();</span><br><span class="line">+ const UPLOAD_DIR = path.resolve(__dirname, &quot;..&quot;, &quot;target&quot;); // 大文件存储目录</span><br><span class="line"></span><br><span class="line">server.on(&quot;request&quot;, async (req, res) =&gt; &#123;</span><br><span class="line"> res.setHeader(&quot;Access-Control-Allow-Origin&quot;, &quot;*&quot;);</span><br><span class="line"> res.setHeader(&quot;Access-Control-Allow-Headers&quot;, &quot;*&quot;);</span><br><span class="line"> if (req.method === &quot;OPTIONS&quot;) &#123;</span><br><span class="line"> res.status = 200;</span><br><span class="line"> res.end();</span><br><span class="line"> return;</span><br><span class="line"> &#125;</span><br><span class="line"></span><br><span class="line">+ const multipart = new multiparty.Form();</span><br><span class="line">+ multipart.parse(req, async (err, fields, files) =&gt; &#123;</span><br><span class="line">+ if (err) &#123;</span><br><span class="line">+ return;</span><br><span class="line">+ &#125;</span><br><span class="line">+ const [chunk] = files.chunk;</span><br><span class="line">+ const [hash] = fields.hash;</span><br><span class="line">+ const [filename] = fields.filename;</span><br><span class="line">+ const chunkDir = `$&#123;UPLOAD_DIR&#125;/$&#123;filename&#125;`;</span><br><span class="line">+ // 切片目录不存在,创建切片目录</span><br><span class="line">+ if (!fse.existsSync(chunkDir)) &#123;</span><br><span class="line">+ await fse.mkdirs(chunkDir);</span><br><span class="line">+ &#125;</span><br><span class="line">+ // fs-extra 专用方法,类似 fs.rename 并且跨平台</span><br><span class="line">+ // fs-extra 的 rename 方法 windows 平台会有权限问题</span><br><span class="line">+ // https://github.com/meteor/meteor/issues/7852#issuecomment-255767835</span><br><span class="line">+ await fse.move(chunk.path, `$&#123;chunkDir&#125;/$&#123;hash&#125;`);</span><br><span class="line">+ res.end(&quot;received file chunk&quot;);</span><br><span class="line">+ &#125;);</span><br><span class="line">&#125;);</span><br><span class="line"></span><br><span class="line">server.listen(3000, () =&gt; console.log(&quot;正在监听 3000 端口&quot;));</span><br></pre></td></tr></table></figure><p><img src="https://user-gold-cdn.xitu.io/2020/1/11/16f905fb6a626f47?imageView2/0/w/1280/h/960/format/webp/ignore-error/1" alt="image-20200110215559194"><br>查看 multiparty 处理后的 chunk 对象,path 是存储临时文件的路径,size 是临时文件大小,在 multiparty 文档中提到可以使用 fs.rename(由于我用的是 fs-extra,它的 rename 方法 windows 平台权限问题,所以换成了 fse.move) 移动临时文件,即移动文件切片</p><p>在接受文件切片时,需要先创建存储切片的文件夹,由于前端在发送每个切片时额外携带了唯一值 hash,所以以 hash 作为文件名,将切片从临时路径移动切片文件夹中,最后的结果如下<br><img src="https://user-gold-cdn.xitu.io/2020/1/8/16f83c3caac8eb65?imageView2/0/w/1280/h/960/format/webp/ignore-error/1" alt="xuehuayu.cn"></p><h3 id="合并切片"><a href="#合并切片" class="headerlink" title="合并切片"></a>合并切片</h3><p>在接收到前端发送的合并请求后,服务端将文件夹下的所有切片进行合并</p><pre><code>const http = require(&quot;http&quot;);const path = require(&quot;path&quot;);const fse = require(&quot;fs-extra&quot;);const server = http.createServer();const UPLOAD_DIR = path.resolve(__dirname, &quot;..&quot;, &quot;target&quot;); // 大文件存储目录+ const resolvePost = req =&gt;+ new Promise(resolve =&gt; &#123;+ let chunk = &quot;&quot;;+ req.on(&quot;data&quot;, data =&gt; &#123;+ chunk += data;+ &#125;);+ req.on(&quot;end&quot;, () =&gt; &#123;+ resolve(JSON.parse(chunk));+ &#125;);+ &#125;);+ // 合并切片+ const mergeFileChunk = async (filePath, filename) =&gt; &#123;+ const chunkDir = `$&#123;UPLOAD_DIR&#125;/$&#123;filename&#125;`;+ const chunkPaths = await fse.readdir(chunkDir);+ await fse.writeFile(filePath, &quot;&quot;);+ chunkPaths.forEach(chunkPath =&gt; &#123;+ fse.appendFileSync(filePath, fse.readFileSync(`$&#123;chunkDir&#125;/$&#123;chunkPath&#125;`));+ fse.unlinkSync(`$&#123;chunkDir&#125;/$&#123;chunkPath&#125;`);+ &#125;);+ fse.rmdirSync(chunkDir); // 合并后删除保存切片的目录+ &#125;;server.on(&quot;request&quot;, async (req, res) =&gt; &#123; res.setHeader(&quot;Access-Control-Allow-Origin&quot;, &quot;*&quot;); res.setHeader(&quot;Access-Control-Allow-Headers&quot;, &quot;*&quot;); if (req.method === &quot;OPTIONS&quot;) &#123; res.status = 200; res.end(); return; &#125;+ if (req.url === &quot;/merge&quot;) &#123;+ const data = await resolvePost(req);+ const &#123; filename &#125; = data;+ const filePath = `$&#123;UPLOAD_DIR&#125;/$&#123;filename&#125;`;+ await mergeFileChunk(filePath, filename);+ res.end(+ JSON.stringify(&#123;+ code: 0,+ message: &quot;file merged success&quot;+ &#125;)+ );+ &#125;&#125;);server.listen(3000, () =&gt; console.log(&quot;正在监听 3000 端口&quot;));复制代码</code></pre><p>由于前端在发送合并请求时会携带文件名,服务端根据文件名可以找到上一步创建的切片文件夹</p><p>接着使用 fs.writeFileSync 先创建一个空文件,这个空文件的文件名就是<strong>切片文件夹名 + 后缀名</strong>组合而成,随后通过 fs.appendFileSync 从切片文件夹中不断将切片合并到空文件中,每次合并完成后删除这个切片,等所有切片都合并完毕后最后删除切片文件夹<br><img src="https://user-gold-cdn.xitu.io/2020/1/8/16f8414c7fae1c1f?imageView2/0/w/1280/h/960/format/webp/ignore-error/1" alt="xuehuayu.cn"><br>至此一个简单的大文件上传就完成了,接下来我们再此基础上扩展一些额外的功能</p><h2 id="显示上传进度条"><a href="#显示上传进度条" class="headerlink" title="显示上传进度条"></a>显示上传进度条</h2><p>上传进度分两种,一个是每个切片的上传进度,另一个是整个文件的上传进度,而整个文件的上传进度是基于每个切片上传进度计算而来,所以我们先实现切片的上传进度</p><h3 id="切片进度条"><a href="#切片进度条" class="headerlink" title="切片进度条"></a>切片进度条</h3><p>XMLHttpRequest 原生支持上传进度的监听,只需要监听 upload.onprogress 即可,我们在原来的 request 基础上传入 onProgress 参数,给 XMLHttpRequest 注册监听事件</p><pre><code> // xhr request(&#123; url, method = &quot;post&quot;, data, headers = &#123;&#125;,+ onProgress = e =&gt; e, requestList &#125;) &#123; return new Promise(resolve =&gt; &#123; const xhr = new XMLHttpRequest();+ xhr.upload.onprogress = onProgress; xhr.open(method, url); Object.keys(headers).forEach(key =&gt; xhr.setRequestHeader(key, headers[key]) ); xhr.send(data); xhr.onload = e =&gt; &#123; resolve(&#123; data: e.target.response &#125;); &#125;; &#125;); &#125;复制代码</code></pre><p>由于每个切片都需要触发独立的监听事件,所以还需要一个工厂函数,根据传入的切片返回不同的监听函数</p><p>在原先的前端上传逻辑中新增监听函数部分</p><pre><code> // 上传切片,同时过滤已上传的切片 async uploadChunks(uploadedList = []) &#123; const requestList = this.data+ .map((&#123; chunk,hash,index &#125;) =&gt; &#123; const formData = new FormData(); formData.append(&quot;chunk&quot;, chunk); formData.append(&quot;hash&quot;, hash); formData.append(&quot;filename&quot;, this.container.file.name);+ return &#123; formData,index &#125;; &#125;)+ .map(async (&#123; formData,index &#125;) =&gt; this.request(&#123; url: &quot;http://localhost:3000&quot;, data: formData,+ onProgress: this.createProgressHandler(this.data[index]), &#125;) ); await Promise.all(requestList); // 合并切片 await this.mergeRequest(); &#125;, async handleUpload() &#123; if (!this.container.file) return; const fileChunkList = this.createFileChunk(this.container.file); this.data = fileChunkList.map((&#123; file &#125;,index) =&gt; (&#123; chunk: file,+ index, hash: this.container.file.name + &quot;-&quot; + index+ percentage:0 &#125;)); await this.uploadChunks(); &#125; + createProgressHandler(item) &#123;+ return e =&gt; &#123;+ item.percentage = parseInt(String((e.loaded / e.total) * 100));+ &#125;;+ &#125;复制代码</code></pre><p>每个切片在上传时都会通过监听函数更新 data 数组对应元素的 percentage 属性,之后把将 data 数组放到视图中展示即可</p><h3 id="文件进度条"><a href="#文件进度条" class="headerlink" title="文件进度条"></a>文件进度条</h3><p>将每个切片已上传的部分累加,除以整个文件的大小,就能得出当前文件的上传进度,所以这里使用 Vue 计算属性</p><pre><code> computed: &#123; uploadPercentage() &#123; if (!this.container.file || !this.data.length) return0; const loaded = this.data .map(item =&gt; item.size * item.percentage) .reduce((acc, cur) =&gt; acc + cur); returnparseInt((loaded / this.container.file.size).toFixed(2)); &#125; &#125;复制代码</code></pre><p>最终视图如下<br><img src="https://user-gold-cdn.xitu.io/2020/1/8/16f84ad6b4848423?imageView2/0/w/1280/h/960/format/webp/ignore-error/1" alt="xuehuayu.cn"></p><h1 id="断点续传"><a href="#断点续传" class="headerlink" title="断点续传"></a>断点续传</h1><p>断点续传的原理在于前端&#x2F;服务端需要<code>记住</code>已上传的切片,这样下次上传就可以跳过之前已上传的部分,有两种方案实现记忆的功能</p><ul><li>前端使用 localStorage 记录已上传的切片 hash</li><li>服务端保存已上传的切片 hash,前端每次上传前向服务端获取已上传的切片</li></ul><p>第一种是前端的解决方案,第二种是服务端,而前端方案有一个缺陷,如果换了个浏览器就失去了记忆的效果,所以这里选取后者</p><h2 id="生成-hash"><a href="#生成-hash" class="headerlink" title="生成 hash"></a>生成 hash</h2><p>无论是前端还是服务端,都必须要生成文件和切片的 hash,<code>之前我们使用文件名 + 切片下标作为切片 hash</code>,这样做文件名一旦修改就失去了效果,而事实上只要文件内容不变,hash 就不应该变化,所以正确的做法是<code>根据文件内容生成 hash</code>,所以我们修改一下 hash 的生成规则</p><p>这里用到另一个库 <code>spark-md5</code>,它可以根据文件内容计算出文件的 hash 值,另外考虑到如果上传一个超大文件,读取文件内容计算 hash 是非常耗费时间的,并且会<code>引起 UI 的阻塞</code>,导致页面假死状态,所以我们使用 web-worker 在 worker 线程计算 hash,这样用户仍可以在主界面正常的交互</p><p>由于实例化 web-worker 时,参数是一个 js 文件路径且不能跨域,所以我们单独创建一个 hash.js 文件放在 public 目录下,另外在 worker 中也是不允许访问 dom 的,但它提供了<code>importScripts</code> 函数用于导入外部脚本,通过它导入 spark-md5</p><pre><code>// /public/hash.jsself.importScripts(&quot;/spark-md5.min.js&quot;); // 导入脚本// 生成文件 hashself.onmessage = e =&gt; &#123; const &#123; fileChunkList &#125; = e.data; const spark = new self.SparkMD5.ArrayBuffer(); let percentage = 0; let count = 0; const loadNext = index =&gt; &#123; const reader = new FileReader(); reader.readAsArrayBuffer(fileChunkList[index].file); reader.onload = e =&gt; &#123; count++; spark.append(e.target.result); if (count === fileChunkList.length) &#123; self.postMessage(&#123; percentage: 100, hash: spark.end() &#125;); self.close(); &#125; else &#123; percentage += 100 / fileChunkList.length; self.postMessage(&#123; percentage &#125;); // 递归计算下一个切片 loadNext(count); &#125; &#125;; &#125;; loadNext(0);&#125;;复制代码</code></pre><p>在 worker 线程中,接受文件切片 fileChunkList,利用 FileReader 读取每个切片的 ArrayBuffer 并不断传入 spark-md5 中,每计算完一个切片通过 postMessage 向主线程发送一个进度事件,全部完成后将最终的 hash 发送给主线程</p><p><code>spark-md5 需要根据所有切片才能算出一个 hash 值,不能直接将整个文件放入计算,否则即使不同文件也会有相同的 hash,具体可以看官方文档</code></p><p><a href="https://www.npmjs.com/package/spark-md5">spark-md5</a></p><p>接着编写主线程与 worker 线程通讯的逻辑</p><pre><code>+ // 生成文件 hash(web-worker)+ calculateHash(fileChunkList) &#123;+ return new Promise(resolve =&gt; &#123;+ // 添加 worker 属性+ this.container.worker = new Worker(&quot;/hash.js&quot;);+ this.container.worker.postMessage(&#123; fileChunkList &#125;);+ this.container.worker.onmessage = e =&gt; &#123;+ const &#123; percentage, hash &#125; = e.data;+ this.hashPercentage = percentage;+ if (hash) &#123;+ resolve(hash);+ &#125;+ &#125;;+ &#125;); &#125;, async handleUpload() &#123; if (!this.container.file) return; const fileChunkList = this.createFileChunk(this.container.file);+ this.container.hash = await this.calculateHash(fileChunkList); this.data = fileChunkList.map((&#123; file &#125;,index) =&gt; (&#123;+ fileHash: this.container.hash, chunk: file, hash: this.container.file.name + &quot;-&quot; + index, // 文件名 + 数组下标 percentage:0 &#125;)); await this.uploadChunks(); &#125; 复制代码</code></pre><p>主线程使用 <code>postMessage</code> 给 worker 线程传入所有切片 fileChunkList,并监听 worker 线程发出的 postMessage 事件拿到文件 hash</p><p>加上显示计算 hash 的进度条,看起来像这样<br><img src="https://user-gold-cdn.xitu.io/2020/1/8/16f84ae4f233e891?imageView2/0/w/1280/h/960/format/webp/ignore-error/1" alt="xuehuayu.cn"><br>至此前端需要将之前用文件名作为 hash 的地方改写为 workder 返回的这个 hash<br><img src="https://user-gold-cdn.xitu.io/2020/1/8/16f84b3bba953304?imageView2/0/w/1280/h/960/format/webp/ignore-error/1" alt="xuehuayu.cn"><br>服务端则使用 hash 作为切片文件夹名,hash + 下标作为切片名,hash + 扩展名作为文件名,没有新增的逻辑<br><img src="https://user-gold-cdn.xitu.io/2020/1/8/16f84b50117e4db2?imageView2/0/w/1280/h/960/format/webp/ignore-error/1" alt="xuehuayu.cn"><img src="https://user-gold-cdn.xitu.io/2020/1/8/16f84b33bbceaa48?imageView2/0/w/1280/h/960/format/webp/ignore-error/1" alt="xuehuayu.cn"></p><h2 id="文件秒传"><a href="#文件秒传" class="headerlink" title="文件秒传"></a>文件秒传</h2><p>在实现断点续传前先简单介绍一下文件秒传</p><p>所谓的文件秒传,即在服务端已经存在了上传的资源,所以当用户<code>再次上传</code>时会直接提示上传成功</p><p>文件秒传需要依赖上一步生成的 hash,即在<code>上传前</code>,先计算出文件 hash,并把 hash 发送给服务端进行验证,由于 hash 的唯一性,所以一旦服务端能找到 hash 相同的文件,则直接返回上传成功的信息即可</p><pre><code>+ async verifyUpload(filename, fileHash) &#123;+ const &#123; data &#125; = await this.request(&#123;+ url: &quot;http://localhost:3000/verify&quot;,+ headers: &#123;+ &quot;content-type&quot;: &quot;application/json&quot;+ &#125;,+ data: JSON.stringify(&#123;+ filename,+ fileHash+ &#125;)+ &#125;);+ return JSON.parse(data);+ &#125;, async handleUpload() &#123; if (!this.container.file) return; const fileChunkList = this.createFileChunk(this.container.file); this.container.hash = await this.calculateHash(fileChunkList);+ const &#123; shouldUpload &#125; = await this.verifyUpload(+ this.container.file.name,+ this.container.hash+ );+ if (!shouldUpload) &#123;+ this.$message.success(&quot;秒传:上传成功&quot;);+ return;+ &#125; this.data = fileChunkList.map((&#123; file &#125;, index) =&gt; (&#123; fileHash: this.container.hash, index, hash: this.container.hash + &quot;-&quot; + index, chunk: file, percentage: 0 &#125;)); await this.uploadChunks(); &#125; 复制代码</code></pre><p>秒传其实就是给用户看的障眼法,实质上根本没有上传<br><img src="https://user-gold-cdn.xitu.io/2020/1/9/16f890e5967106c6?imageView2/0/w/1280/h/960/format/webp/ignore-error/1" alt="image-20200109143511277"><br>:)</p><p>服务端的逻辑非常简单,新增一个验证接口,验证文件是否存在即可</p><pre><code>+ const extractExt = filename =&gt;+ filename.slice(filename.lastIndexOf(&quot;.&quot;), filename.length); // 提取后缀名const UPLOAD_DIR = path.resolve(__dirname, &quot;..&quot;, &quot;target&quot;); // 大文件存储目录const resolvePost = req =&gt; new Promise(resolve =&gt; &#123; let chunk = &quot;&quot;; req.on(&quot;data&quot;, data =&gt; &#123; chunk += data; &#125;); req.on(&quot;end&quot;, () =&gt; &#123; resolve(JSON.parse(chunk)); &#125;); &#125;);server.on(&quot;request&quot;, async (req, res) =&gt; &#123; if (req.url === &quot;/verify&quot;) &#123;+ const data = await resolvePost(req);+ const &#123; fileHash, filename &#125; = data;+ const ext = extractExt(filename);+ const filePath = `$&#123;UPLOAD_DIR&#125;/$&#123;fileHash&#125;$&#123;ext&#125;`;+ if (fse.existsSync(filePath)) &#123;+ res.end(+ JSON.stringify(&#123;+ shouldUpload: false+ &#125;)+ );+ &#125; else &#123;+ res.end(+ JSON.stringify(&#123;+ shouldUpload: true+ &#125;)+ );+ &#125; &#125;&#125;);server.listen(3000, () =&gt; console.log(&quot;正在监听 3000 端口&quot;));复制代码</code></pre><h2 id="暂停上传"><a href="#暂停上传" class="headerlink" title="暂停上传"></a>暂停上传</h2><p>讲完了生成 hash 和文件秒传,回到断点续传</p><p>断点续传顾名思义即断点 + 续传,所以我们第一步先实现“断点”,也就是暂停上传</p><p>原理是使用 XMLHttpRequest 的 <code>abort</code> 方法,可以取消一个 xhr 请求的发送,为此我们需要将上传每个切片的 xhr 对象保存起来,我们再改造一下 request 方法</p><pre><code> request(&#123; url, method = &quot;post&quot;, data, headers = &#123;&#125;, onProgress = e =&gt; e,+ requestList &#125;) &#123; return new Promise(resolve =&gt; &#123; const xhr = new XMLHttpRequest(); xhr.upload.onprogress = onProgress; xhr.open(method, url); Object.keys(headers).forEach(key =&gt; xhr.setRequestHeader(key, headers[key]) ); xhr.send(data); xhr.onload = e =&gt; &#123;+ // 将请求成功的 xhr 从列表中删除+ if (requestList) &#123;+ const xhrIndex = requestList.findIndex(item =&gt; item === xhr);+ requestList.splice(xhrIndex, 1);+ &#125; resolve(&#123; data: e.target.response &#125;); &#125;;+ // 暴露当前 xhr 给外部+ requestList?.push(xhr); &#125;); &#125;,复制代码</code></pre><p>这样在上传切片时传入 requestList 数组作为参数,request 方法就会将所有的 xhr 保存在数组中了<br><img src="https://user-gold-cdn.xitu.io/2020/1/8/16f8563491ea7793?imageView2/0/w/1280/h/960/format/webp/ignore-error/1" alt="xuehuayu.cn"><br>每当一个切片上传成功时,将对应的 xhr 从 requestList 中删除,所以 requestList 中只保存<code>正在上传切片的 xhr</code></p><p>之后新建一个暂停按钮,当点击按钮时,调用保存在 requestList 中 xhr 的 abort 方法,即取消并清空所有正在上传的切片</p><pre><code> handlePause() &#123; this.requestList.forEach(xhr =&gt; xhr?.abort()); this.requestList = [];&#125;复制代码</code></pre><p><img src="https://user-gold-cdn.xitu.io/2020/1/9/16f890e598dd6afb?imageView2/0/w/1280/h/960/format/webp/ignore-error/1" alt="image-20200109143737924"><br>点击暂停按钮可以看到 xhr 都被取消了<br><img src="https://user-gold-cdn.xitu.io/2020/1/8/16f85644dc27459e?imageView2/0/w/1280/h/960/format/webp/ignore-error/1" alt="xuehuayu.cn"></p><h2 id="恢复上传"><a href="#恢复上传" class="headerlink" title="恢复上传"></a>恢复上传</h2><p>之前在介绍断点续传的时提到使用第二种服务端存储的方式实现续传</p><p>由于当文件切片上传后,服务端会建立一个文件夹存储所有上传的切片,所以每次前端上传前可以调用一个接口,服务端将已上传的切片的切片名返回,前端再跳过这些已经上传切片,这样就实现了“续传”的效果</p><p>而这个接口可以和之前秒传的验证接口合并,前端每次上传前发送一个验证的请求,返回两种结果</p><ul><li>服务端已存在该文件,不需要再次上传</li><li>服务端不存在该文件或者已上传部分文件切片,通知前端进行上传,并把<strong>已上传</strong>的文件切片返回给前端</li></ul><p>所以我们改造一下之前文件秒传的服务端验证接口</p><pre><code>const extractExt = filename =&gt; filename.slice(filename.lastIndexOf(&quot;.&quot;), filename.length); // 提取后缀名const UPLOAD_DIR = path.resolve(__dirname, &quot;..&quot;, &quot;target&quot;); // 大文件存储目录const resolvePost = req =&gt; new Promise(resolve =&gt; &#123; let chunk = &quot;&quot;; req.on(&quot;data&quot;, data =&gt; &#123; chunk += data; &#125;); req.on(&quot;end&quot;, () =&gt; &#123; resolve(JSON.parse(chunk)); &#125;); &#125;); + // 返回已经上传切片名列表+ const createUploadedList = async fileHash =&gt;+ fse.existsSync(`$&#123;UPLOAD_DIR&#125;/$&#123;fileHash&#125;`)+ ? await fse.readdir(`$&#123;UPLOAD_DIR&#125;/$&#123;fileHash&#125;`)+ : [];server.on(&quot;request&quot;, async (req, res) =&gt; &#123; if (req.url === &quot;/verify&quot;) &#123; const data = await resolvePost(req); const &#123; fileHash, filename &#125; = data; const ext = extractExt(filename); const filePath = `$&#123;UPLOAD_DIR&#125;/$&#123;fileHash&#125;$&#123;ext&#125;`; if (fse.existsSync(filePath)) &#123; res.end( JSON.stringify(&#123; shouldUpload: false &#125;) ); &#125; else &#123; res.end( JSON.stringify(&#123; shouldUpload: true,+ uploadedList: await createUploadedList(fileHash) &#125;) ); &#125; &#125;&#125;);server.listen(3000, () =&gt; console.log(&quot;正在监听 3000 端口&quot;));复制代码</code></pre><p>接着回到前端,前端有两个地方需要调用验证的接口</p><ul><li>点击上传时,检查是否需要上传和已上传的切片</li><li>点击暂停后的恢复上传,返回已上传的切片</li></ul><p>新增恢复按钮并改造原来上传切片的逻辑</p><pre><code>&lt;template&gt; &lt;div id=&quot;app&quot;&gt; &lt;input type=&quot;file&quot; @change=&quot;handleFileChange&quot; /&gt; &lt;el-button @click=&quot;handleUpload&quot;&gt;上传&lt;/el-button&gt; &lt;el-button @click=&quot;handlePause&quot; v-if=&quot;isPaused&quot;&gt;暂停&lt;/el-button&gt;+ &lt;el-button @click=&quot;handleResume&quot; v-else&gt;恢复&lt;/el-button&gt; //... &lt;/div&gt;&lt;/template&gt;+ async handleResume() &#123;+ const &#123; uploadedList &#125; = await this.verifyUpload(+ this.container.file.name,+ this.container.hash+ );+ await this.uploadChunks(uploadedList); &#125;, async handleUpload() &#123; if (!this.container.file) return; const fileChunkList = this.createFileChunk(this.container.file); this.container.hash = await this.calculateHash(fileChunkList);+ const &#123; shouldUpload, uploadedList &#125; = await this.verifyUpload( this.container.file.name, this.container.hash ); if (!shouldUpload) &#123; this.$message.success(&quot;秒传:上传成功&quot;); return; &#125; this.data = fileChunkList.map((&#123; file &#125;, index) =&gt; (&#123; fileHash: this.container.hash, index, hash: this.container.hash + &quot;-&quot; + index, chunk: file, percentage: 0 &#125;));+ await this.uploadChunks(uploadedList); &#125;, // 上传切片,同时过滤已上传的切片+ async uploadChunks(uploadedList = []) &#123; const requestList = this.data+ .filter((&#123; hash &#125;) =&gt; !uploadedList.includes(hash)) .map((&#123; chunk, hash, index &#125;) =&gt; &#123; const formData = new FormData(); formData.append(&quot;chunk&quot;, chunk); formData.append(&quot;hash&quot;, hash); formData.append(&quot;filename&quot;, this.container.file.name); formData.append(&quot;fileHash&quot;, this.container.hash); return &#123; formData, index &#125;; &#125;) .map(async (&#123; formData, index &#125;) =&gt; this.request(&#123; url: &quot;http://localhost:3000&quot;, data: formData, onProgress: this.createProgressHandler(this.data[index]), requestList: this.requestList &#125;) ); await Promise.all(requestList); // 之前上传的切片数量 + 本次上传的切片数量 = 所有切片数量时 // 合并切片+ if (uploadedList.length + requestList.length === this.data.length) &#123; await this.mergeRequest();+ &#125; &#125;复制代码</code></pre><p>![image-20200109144331326](data:image&#x2F;svg+xml;utf8,<?xml version=&quot;1.0&quot;?><svg xmlns=&quot;http://www.w3.org/2000/svg&quot; version=&quot;1.1&quot; width=&quot;463&quot; height=&quot;54&quot;></svg>)<br>这里给原来上传切片的函数新增 uploadedList 参数,即上图中服务端返回的切片名列表,通过 filter 过滤掉已上传的切片,并且由于新增了已上传的部分,所以之前合并接口的触发条件做了一些改动</p><p>到这里断点续传的功能基本完成了</p><h2 id="进度条改进"><a href="#进度条改进" class="headerlink" title="进度条改进"></a>进度条改进</h2><p>虽然实现了断点续传,但还需要修改一下进度条的显示规则,否则在暂停上传&#x2F;接收到已上传切片时的进度条会出现偏差</p><h3 id="切片进度条-1"><a href="#切片进度条-1" class="headerlink" title="切片进度条"></a>切片进度条</h3><p>由于在点击上传&#x2F;恢复上传时,会调用验证接口返回已上传的切片,所以需要将已上传切片的进度变成 100%</p><pre><code> async handleUpload() &#123; if (!this.container.file) return; const fileChunkList = this.createFileChunk(this.container.file); this.container.hash = await this.calculateHash(fileChunkList); const &#123; shouldUpload, uploadedList &#125; = await this.verifyUpload( this.container.file.name, this.container.hash ); if (!shouldUpload) &#123; this.$message.success(&quot;秒传:上传成功&quot;); return; &#125; this.data = fileChunkList.map((&#123; file &#125;, index) =&gt; (&#123; fileHash: this.container.hash, index, hash: this.container.hash + &quot;-&quot; + index, chunk: file,+ percentage: uploadedList.includes(index) ? 100 : 0 &#125;)); await this.uploadChunks(uploadedList); &#125;,复制代码</code></pre><p>uploadedList 会返回已上传的切片,在遍历所有切片时判断当前切片是否在已上传列表里即可</p><h3 id="文件进度条-1"><a href="#文件进度条-1" class="headerlink" title="文件进度条"></a>文件进度条</h3><p>之前说到文件进度条是一个计算属性,根据所有切片的上传进度计算而来,这就遇到了一个问题<br><img src="https://user-gold-cdn.xitu.io/2020/1/9/16f8845dcca1427c?imageView2/0/w/1280/h/960/format/webp/ignore-error/1" alt="xuehuayu.cn"><br>点击暂停会取消并清空切片的 xhr 请求,此时如果已经上传了一部分,就会发现文件进度条有<code>倒退</code>的现象<br><img src="https://user-gold-cdn.xitu.io/2020/1/9/16f884633f3acfd9?imageView2/0/w/1280/h/960/format/webp/ignore-error/1" alt="xuehuayu.cn"><br>当点击恢复时,由于重新创建了 xhr 导致切片进度清零,所以总进度条就会倒退</p><p>解决方案是创建一个“假”的进度条,这个假进度条基于文件进度条,但只会停止和增加,然后给用户展示这个假的进度条</p><p>这里我们使用 Vue 的监听属性</p><pre><code> data: () =&gt; (&#123;+ fakeUploadPercentage: 0 &#125;), computed: &#123; uploadPercentage() &#123; if (!this.container.file || !this.data.length) return 0; const loaded = this.data .map(item =&gt; item.size * item.percentage) .reduce((acc, cur) =&gt; acc + cur); return parseInt((loaded / this.container.file.size).toFixed(2)); &#125; &#125;, watch: &#123;+ uploadPercentage(now) &#123;+ if (now &gt; this.fakeUploadPercentage) &#123;+ this.fakeUploadPercentage = now;+ &#125; &#125; &#125;,复制代码</code></pre><p>当 uploadPercentage 即真的文件进度条增加时,fakeUploadPercentage 也增加,一旦文件进度条后退,假的进度条只需停止即可</p><p>至此一个大文件上传 + 断点续传的解决方案就完成了</p><h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><p>大文件上传</p><ul><li>前端上传大文件时使用 Blob.prototype.slice 将文件切片,并发上传多个切片,最后发送一个合并的请求通知服务端合并切片</li><li>服务端接收切片并存储,收到合并请求后使用 fs.appendFileSync 对多个切片进行合并</li><li>原生 XMLHttpRequest 的 upload.onprogress 对切片上传进度的监听</li><li>使用 Vue 计算属性根据每个切片的进度算出整个文件的上传进度</li></ul><p>断点续传</p><ul><li>使用 spark-md5 根据文件内容算出文件 hash</li><li>通过 hash 可以判断服务端是否已经上传该文件,从而直接提示用户上传成功(秒传)</li><li>通过 XMLHttpRequest 的 abort 方法暂停切片的上传</li><li>上传前服务端返回已经上传的切片名,前端跳过这些切片的上传</li></ul><h1 id="源代码"><a href="#源代码" class="headerlink" title="源代码"></a>源代码</h1><p>源代码增加了一些按钮的状态,交互更加友好,文章表达比较晦涩的地方可以跳转到源代码查看</p><p><a href="https://github.com/yeyan1996/file-upload">file-upload</a><br><img src="https://user-gold-cdn.xitu.io/2020/1/9/16f8859bdc09fa25?imageView2/0/w/1280/h/960/format/webp/ignore-error/1" alt="xuehuayu.cn"><br>谢谢观看 :)</p><h1 id="字节跳动-EA(Enterprise-Application)前端团队招人啦~"><a href="#字节跳动-EA(Enterprise-Application)前端团队招人啦~" class="headerlink" title="字节跳动 EA(Enterprise Application)前端团队招人啦~"></a>字节跳动 EA(Enterprise Application)前端团队招人啦~</h1><p>坐标上海&#x2F;北京,校招社招都有,hc 无上限,有兴趣的欢迎投递简历至 <a href="mailto:&#x31;&#x39;&#x39;&#x36;&#121;&#101;&#x79;&#97;&#110;&#64;&#x67;&#109;&#97;&#105;&#x6c;&#46;&#99;&#x6f;&#109;">&#x31;&#x39;&#x39;&#x36;&#121;&#101;&#x79;&#97;&#110;&#64;&#x67;&#109;&#97;&#105;&#x6c;&#46;&#99;&#x6f;&#109;</a>,校招内推码 Q7QUGMV</p><h1 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h1><p><a href="https://juejin.im/post/5da14778f265da5bb628e590">写给新手前端的各种文件上传攻略,从小图片到大文件断点续传</a></p><p><a href="https://developer.mozilla.org/zh-CN/docs/Web/API/Blob/slice">Blob.slice</a><a href="https://developer.mozilla.org/zh-CN/docs/Web/API/Blob/slice">developer.mozilla.org&#x2F;zh-CN&#x2F;docs&#x2F;…</a>)</p>]]></content>
2539
2539
 
2540
2540
 
2541
2541
  <categories>
@@ -5714,7 +5714,7 @@
5714
5714
  <link href="/article/53962.html"/>
5715
5715
  <url>/article/53962.html</url>
5716
5716
 
5717
- <content type="html"><![CDATA[<p>React16.3.0开始,生命周期进行了一些变化。本文主要介绍React16.3.0之后的生命周期。<br>16版本之前的react组件的生命周期相信大家已经很熟悉。16版本的react对组件的生命周期函数进行了一些修改,下面进行详细说明。</p><ul><li><a href="https://github.com/YvetteLau/Blog/issues/3">React新旧生命周期一览</a><span id="more"></span></li></ul><h3 id="React16-3-0之前生命周期"><a href="#React16-3-0之前生命周期" class="headerlink" title="React16.3.0之前生命周期"></a>React16.3.0之前生命周期</h3><blockquote><p>创建期:</p></blockquote><ol><li>constructor(props, context)</li><li>componentWillMount()</li><li>render()</li><li>componentDidMount()</li></ol><blockquote><p>运行时:</p></blockquote><p><strong>props发生变化时</strong></p><ol><li>componentWillReceiveProps(nextProps, nextContext)</li><li>shouldComponentUpdate(nextProps, nextState, nextContext)</li><li>componentWillUpdate(nextProps, nextState, nextContext)</li><li>render</li><li>componentDidUpdate(prevProps, prevState, snapshot)</li></ol><p><strong>state发生变化时</strong></p><ol><li>shouldComponentUpdate(nextProps, nextState, nextContext)</li><li>componentWillUpdate(nextProps, nextState, nextContext)</li><li>render</li><li>componentDidUpdate(prevProps, prevState, snapshot)</li></ol><blockquote><p>卸载时</p></blockquote><p>componentWillUnmount()</p><h3 id="React16-3-0之后的生命周期"><a href="#React16-3-0之后的生命周期" class="headerlink" title="React16.3.0之后的生命周期"></a>React16.3.0之后的生命周期</h3><blockquote><p>创建期:</p></blockquote><ol><li>constructor(props, context)</li><li>static getDerivedStateFromProps(props, status)</li><li>render()</li><li>componentDidMount()</li></ol><p>或者如下生命周期:</p><ol><li>constructor(props, context)</li><li>componentWillMount() &#x2F; UNSAFE_componentWillMount()</li><li>render()</li><li>componentDidMount()</li></ol><p>注意: getDerivedStateFromProps&#x2F;getSnapshotBeforeUpdate 和 componentWillMount&#x2F;componentWillReceiveProps&#x2F;componentWillUpdate 如果同时存在,React会在控制台给出警告信息,且仅执行 getDerivedStateFromProps&#x2F;getSnapshotBeforeUpdate 【<a href="mailto:&#82;&#x65;&#x61;&#x63;&#116;&#64;&#x31;&#x36;&#x2e;&#55;&#x2e;&#x30;">&#82;&#x65;&#x61;&#x63;&#116;&#64;&#x31;&#x36;&#x2e;&#55;&#x2e;&#x30;</a>】</p><blockquote><p>运行时:</p></blockquote><p><strong>props发生变化时</strong></p><ol><li>static getDerivedStateFromProps(props, status)</li><li>shouldComponentUpdate(nextProps, nextState, nextContext)</li><li>render</li><li>getSnapshotBeforeUpdate(prevProps, prevState)</li><li>componentDidUpdate(prevProps, prevState, snapshot)</li></ol><p>或者如下生命周期:</p><ol><li>componentWillReceiveProps(nextProps, nextContext)&#x2F;UNSAFE_componentWillReceiveProps</li><li>shouldComponentUpdate(nextProps, nextState, nextContext)</li><li>componentWillUpdate(nextProps, nextState, nextContext)</li><li>render</li><li>componentDidUpdate(prevProps, prevState, snapshot)</li></ol><p><strong>state发生变化时</strong></p><ol><li>static getDerivedStateFromProps(props, status)</li><li>shouldComponentUpdate(nextProps, nextState, nextContext)</li><li>render</li><li>getSnapshotBeforeUpdate(prevProps, prevState)</li><li>componentDidUpdate(prevProps, prevState, snapshot)</li></ol><p>或者如下生命周期:</p><ol><li>shouldComponentUpdate(nextProps, nextState, nextContext)</li><li>componentWillUpdate(nextProps, nextState, nextContext)&#x2F;UNSAFE_componentWillUpdate</li><li>render</li><li>componentDidUpdate(prevProps, prevState, snapshot)</li></ol><blockquote><p>销毁时</p></blockquote><p>componentWillUnmount()</p><p>新的生命周期图示:</p><p><a href="https://camo.githubusercontent.com/1077b94360d6754685ea2052ab5ca32c9c338bf7/68747470733a2f2f6e6f74652e796f7564616f2e636f6d2f7977732f7075626c69632f7265736f757263652f66343537303163633431303530346537316462626362643838363165386430632f786d6c6e6f74652f5745425245534f5552434537633738613536653634323335626364626434323638363861346133383762332f3239353236"><img src="https://camo.githubusercontent.com/1077b94360d6754685ea2052ab5ca32c9c338bf7/68747470733a2f2f6e6f74652e796f7564616f2e636f6d2f7977732f7075626c69632f7265736f757263652f66343537303163633431303530346537316462626362643838363165386430632f786d6c6e6f74652f5745425245534f5552434537633738613536653634323335626364626434323638363861346133383762332f3239353236" alt="lifeCycle"></a></p><h2 id="生命周期详解"><a href="#生命周期详解" class="headerlink" title="生命周期详解"></a>生命周期详解</h2><h4 id="1-constructor-props-context"><a href="#1-constructor-props-context" class="headerlink" title="1.constructor(props, context)"></a>1.constructor(props, context)</h4><p>constructor生命周期,如不需要,可缺省。通常会在 constructor 方法中初始化 state 和绑定事件处理程序。</p><p>但是,如果写了constructor,那么必须在其中调用super(props);否则可能会引起报错。</p><p>如:</p><pre><code>class Base extends Component &#123; constructor(props) &#123; super(); //应该为 super(props); &#125; state = &#123; name: this.props.name &#125; //....code&#125;</code></pre><p>抛出异常: Uncaught TypeError: Cannot read property ‘name’ of undefined.</p><p>同样,如果定义了context,在state中需要使用this.context去获取context上的内容,则需要super(props, context);</p><p>不过,如果你缺省constructor,那么在state中,可以放心的使用 this.props 或者是 this.context,不会引起报错。</p><pre><code>class Base extends Component &#123; state = &#123; name: this.props.name, color: this.context.color &#125; //....code&#125;</code></pre><p>初始化的state同样可以在constructor中定义。如果需要给方法绑定this,那么统一在constructor中进行。</p><h4 id="2-static-getDerivedStateFromProps-props-state"><a href="#2-static-getDerivedStateFromProps-props-state" class="headerlink" title="2.static getDerivedStateFromProps(props, state)"></a>2.static getDerivedStateFromProps(props, state)</h4><p>当组件的state需要根据props来改变的时候可调用此方法。这个方法是在 render() 前会被执行,每次触发render前,都会触发此方法。</p><p>该方法有两个参数props和state; 返回值为state对象, 不需要返回整体state,把需要改变的state返回即可。如果不需要,可以返回null.</p><pre><code>classBaseextendsComponent &#123; state = &#123; age:20 &#125; staticgetDerivedStateFromProps(props, state) &#123; return &#123; age:50 &#125; &#125; render() &#123; // 50return ( &lt;div&gt;&#123;this.state.age&#125;&lt;/div&gt; ) &#125;&#125;</code></pre><p>这个方法允许组件基于 props 的变更来更新其内部状态,以这种方式获得的组件状态被称为派生状态。应该谨慎使用派生状态,可能会引入潜在的错误</p><h4 id="3-render"><a href="#3-render" class="headerlink" title="3.render"></a>3.render</h4><p>React组件中必须要提供的方法。当state或者props任一数据有更新时都会执行。</p><p>render() 是一个纯函数,因此,不要在其中执行setState诸如此类的操作。render必须有一个返回值,返回的数据类型可以有:</p><ul><li>null、String、Number、Array、Boolean。</li><li>React elements</li><li>Fragment</li><li>Portal</li></ul><p>注意不要在render中调用setState</p><h4 id="4-componentDidMount"><a href="#4-componentDidMount" class="headerlink" title="4.componentDidMount"></a>4.componentDidMount</h4><p>componentDidMount()方法是在组件加载完后立即执行,也就是当该组件相关的dom节点插入到dom树中时。该方法在组件生命中只执行一次。</p><p>一般情况,我们会在这里setState(),或者进行接口请求,设置订阅等。</p><pre><code>classBaseextendsComponent &#123; state = &#123; age:20 &#125; componentDidMount() &#123; this.fetchDate(); &#125; render() &#123; return ( &lt;div&gt;&#123;this.state.age&#125;&lt;/div&gt; ) &#125; //other code&#125;</code></pre><h4 id="5-shouldComponentUpdate-nextProps-nextState-nextContext"><a href="#5-shouldComponentUpdate-nextProps-nextState-nextContext" class="headerlink" title="5.shouldComponentUpdate(nextProps, nextState, nextContext)"></a>5.shouldComponentUpdate(nextProps, nextState, nextContext)</h4><p>在渲染新的props或state前,shouldComponentUpdate被调用,默认返回true。forceUpdate()时不会调用此方法。</p><p>如果shouldComponentUpdate()返回false,那么getSnapshotBeforeUpdate,render和componentDidUpdate不会被调用。</p><p>此生命周期主要用于优化性能。</p><h4 id="6-getSnapshotBeforeUpdate-prevProps-prevState"><a href="#6-getSnapshotBeforeUpdate-prevProps-prevState" class="headerlink" title="6.getSnapshotBeforeUpdate(prevProps, prevState)"></a>6.getSnapshotBeforeUpdate(prevProps, prevState)</h4><p>在render()的输出被渲染到DOM之前被调用。使组件能够在它们被更改之前捕获当前值(如滚动位置)。这个生命周期返回的任何值都将作为第三个参数传递给componentDidUpdate().</p><h4 id="7-componentDidUpdate-prevProps-prevState-snapshot"><a href="#7-componentDidUpdate-prevProps-prevState-snapshot" class="headerlink" title="7.componentDidUpdate(prevProps, prevState, snapshot)"></a>7.componentDidUpdate(prevProps, prevState, snapshot)</h4><p>在更新发生后调用 componentDidUpdate()。当组件更新时,将此作为一个机会来操作DOM。如将当前的props与以前的props进行比较(例如,如果props没有改变,则可能不需要网络请求。</p><p>如果组件使用 getSnapshotBeforeUpdate(),则它返回的值将作为第三个“快照”参数传递给 componentDidUpdate()。否则,这个参数是undefined。</p><h4 id="8-componentWillUnmount"><a href="#8-componentWillUnmount" class="headerlink" title="8.componentWillUnmount()"></a>8.componentWillUnmount()</h4><p>在组件被卸载并销毁之前立即被调用。在此方法中执行任何必要的清理,例如使定时器无效,取消网络请求或清理在componentDidMount()中创建的任何监听。</p><p><strong>最后,说明一点:</strong></p><p>componentWillMount,componentWillReceiveProps,componentWillUpdate这三个生命周期在React未来版本中会被废弃。</p><p>而UNSAFE_componentWillUpdate,UNSAFE_componentWillReceiveProps,UNSAFE_componentWillUpdate 未来版本中仍可继续使用。</p><p>初始化阶段(父组件和子组件):</p><p><a href="https://camo.githubusercontent.com/76fe4b48fa45ef5e389eed9098cc22fa483efabc/68747470733a2f2f6e6f74652e796f7564616f2e636f6d2f7977732f7075626c69632f7265736f757263652f66343537303163633431303530346537316462626362643838363165386430632f786d6c6e6f74652f5745425245534f5552434562303335306262373362633661396165346163313136303131393863313766322f3239353233"><img src="https://camo.githubusercontent.com/76fe4b48fa45ef5e389eed9098cc22fa483efabc/68747470733a2f2f6e6f74652e796f7564616f2e636f6d2f7977732f7075626c69632f7265736f757263652f66343537303163633431303530346537316462626362643838363165386430632f786d6c6e6f74652f5745425245534f5552434562303335306262373362633661396165346163313136303131393863313766322f3239353233" alt="mount"></a></p><p>运行阶段:父组件props&#x2F;state更新</p><p>子组件的shouldComponentUpdate返回false,则子组件其后的生命周期都不在进行,但是父组件的生命周期继续执行。</p><p><a href="https://camo.githubusercontent.com/9ecc4121b82ff19a4a31d4c5cc1785e7893f03d2/68747470733a2f2f6e6f74652e796f7564616f2e636f6d2f7977732f7075626c69632f7265736f757263652f66343537303163633431303530346537316462626362643838363165386430632f786d6c6e6f74652f5745425245534f5552434561643737363439393431396531633937386133633534336561313364393334652f3239353234"><img src="https://camo.githubusercontent.com/9ecc4121b82ff19a4a31d4c5cc1785e7893f03d2/68747470733a2f2f6e6f74652e796f7564616f2e636f6d2f7977732f7075626c69632f7265736f757263652f66343537303163633431303530346537316462626362643838363165386430632f786d6c6e6f74652f5745425245534f5552434561643737363439393431396531633937386133633534336561313364393334652f3239353234" alt="update"></a></p><p>卸载阶段: 卸载父组件</p><p><a href="https://user-images.githubusercontent.com/20203157/54731656-792d4e00-4bca-11e9-9f61-53fe34688878.png"><img src="https://user-images.githubusercontent.com/20203157/54731656-792d4e00-4bca-11e9-9f61-53fe34688878.png" alt="unmount"></a></p><p>参考:</p><ol><li><a href="https://segmentfault.com/a/1190000016935092">https://segmentfault.com/a/1190000016935092</a></li><li><a href="https://www.jianshu.com/p/514fe21b9914">https://www.jianshu.com/p/514fe21b9914</a></li><li><a href="https://blog.csdn.net/qq_29311407/article/details/79861522">https://blog.csdn.net/qq_29311407&#x2F;article&#x2F;details&#x2F;79861522</a></li></ol>]]></content>
5717
+ <content type="html"><![CDATA[<p>React16.3.0开始,生命周期进行了一些变化。本文主要介绍React16.3.0之后的生命周期。<br>16版本之前的react组件的生命周期相信大家已经很熟悉。16版本的react对组件的生命周期函数进行了一些修改,下面进行详细说明。</p><ul><li><a href="https://github.com/YvetteLau/Blog/issues/3">React新旧生命周期一览</a><span id="more"></span></li></ul><h3 id="React16-3-0之前生命周期"><a href="#React16-3-0之前生命周期" class="headerlink" title="React16.3.0之前生命周期"></a>React16.3.0之前生命周期</h3><blockquote><p>创建期:</p></blockquote><ol><li>constructor(props, context)</li><li>componentWillMount()</li><li>render()</li><li>componentDidMount()</li></ol><blockquote><p>运行时:</p></blockquote><p><strong>props发生变化时</strong></p><ol><li>componentWillReceiveProps(nextProps, nextContext)</li><li>shouldComponentUpdate(nextProps, nextState, nextContext)</li><li>componentWillUpdate(nextProps, nextState, nextContext)</li><li>render</li><li>componentDidUpdate(prevProps, prevState, snapshot)</li></ol><p><strong>state发生变化时</strong></p><ol><li>shouldComponentUpdate(nextProps, nextState, nextContext)</li><li>componentWillUpdate(nextProps, nextState, nextContext)</li><li>render</li><li>componentDidUpdate(prevProps, prevState, snapshot)</li></ol><blockquote><p>卸载时</p></blockquote><p>componentWillUnmount()</p><h3 id="React16-3-0之后的生命周期"><a href="#React16-3-0之后的生命周期" class="headerlink" title="React16.3.0之后的生命周期"></a>React16.3.0之后的生命周期</h3><blockquote><p>创建期:</p></blockquote><ol><li>constructor(props, context)</li><li>static getDerivedStateFromProps(props, status)</li><li>render()</li><li>componentDidMount()</li></ol><p>或者如下生命周期:</p><ol><li>constructor(props, context)</li><li>componentWillMount() &#x2F; UNSAFE_componentWillMount()</li><li>render()</li><li>componentDidMount()</li></ol><p>注意: getDerivedStateFromProps&#x2F;getSnapshotBeforeUpdate 和 componentWillMount&#x2F;componentWillReceiveProps&#x2F;componentWillUpdate 如果同时存在,React会在控制台给出警告信息,且仅执行 getDerivedStateFromProps&#x2F;getSnapshotBeforeUpdate 【<a href="mailto:&#82;&#x65;&#97;&#x63;&#116;&#64;&#49;&#54;&#x2e;&#x37;&#x2e;&#x30;">&#82;&#x65;&#97;&#x63;&#116;&#64;&#49;&#54;&#x2e;&#x37;&#x2e;&#x30;</a>】</p><blockquote><p>运行时:</p></blockquote><p><strong>props发生变化时</strong></p><ol><li>static getDerivedStateFromProps(props, status)</li><li>shouldComponentUpdate(nextProps, nextState, nextContext)</li><li>render</li><li>getSnapshotBeforeUpdate(prevProps, prevState)</li><li>componentDidUpdate(prevProps, prevState, snapshot)</li></ol><p>或者如下生命周期:</p><ol><li>componentWillReceiveProps(nextProps, nextContext)&#x2F;UNSAFE_componentWillReceiveProps</li><li>shouldComponentUpdate(nextProps, nextState, nextContext)</li><li>componentWillUpdate(nextProps, nextState, nextContext)</li><li>render</li><li>componentDidUpdate(prevProps, prevState, snapshot)</li></ol><p><strong>state发生变化时</strong></p><ol><li>static getDerivedStateFromProps(props, status)</li><li>shouldComponentUpdate(nextProps, nextState, nextContext)</li><li>render</li><li>getSnapshotBeforeUpdate(prevProps, prevState)</li><li>componentDidUpdate(prevProps, prevState, snapshot)</li></ol><p>或者如下生命周期:</p><ol><li>shouldComponentUpdate(nextProps, nextState, nextContext)</li><li>componentWillUpdate(nextProps, nextState, nextContext)&#x2F;UNSAFE_componentWillUpdate</li><li>render</li><li>componentDidUpdate(prevProps, prevState, snapshot)</li></ol><blockquote><p>销毁时</p></blockquote><p>componentWillUnmount()</p><p>新的生命周期图示:</p><p><a href="https://camo.githubusercontent.com/1077b94360d6754685ea2052ab5ca32c9c338bf7/68747470733a2f2f6e6f74652e796f7564616f2e636f6d2f7977732f7075626c69632f7265736f757263652f66343537303163633431303530346537316462626362643838363165386430632f786d6c6e6f74652f5745425245534f5552434537633738613536653634323335626364626434323638363861346133383762332f3239353236"><img src="https://camo.githubusercontent.com/1077b94360d6754685ea2052ab5ca32c9c338bf7/68747470733a2f2f6e6f74652e796f7564616f2e636f6d2f7977732f7075626c69632f7265736f757263652f66343537303163633431303530346537316462626362643838363165386430632f786d6c6e6f74652f5745425245534f5552434537633738613536653634323335626364626434323638363861346133383762332f3239353236" alt="lifeCycle"></a></p><h2 id="生命周期详解"><a href="#生命周期详解" class="headerlink" title="生命周期详解"></a>生命周期详解</h2><h4 id="1-constructor-props-context"><a href="#1-constructor-props-context" class="headerlink" title="1.constructor(props, context)"></a>1.constructor(props, context)</h4><p>constructor生命周期,如不需要,可缺省。通常会在 constructor 方法中初始化 state 和绑定事件处理程序。</p><p>但是,如果写了constructor,那么必须在其中调用super(props);否则可能会引起报错。</p><p>如:</p><pre><code>class Base extends Component &#123; constructor(props) &#123; super(); //应该为 super(props); &#125; state = &#123; name: this.props.name &#125; //....code&#125;</code></pre><p>抛出异常: Uncaught TypeError: Cannot read property ‘name’ of undefined.</p><p>同样,如果定义了context,在state中需要使用this.context去获取context上的内容,则需要super(props, context);</p><p>不过,如果你缺省constructor,那么在state中,可以放心的使用 this.props 或者是 this.context,不会引起报错。</p><pre><code>class Base extends Component &#123; state = &#123; name: this.props.name, color: this.context.color &#125; //....code&#125;</code></pre><p>初始化的state同样可以在constructor中定义。如果需要给方法绑定this,那么统一在constructor中进行。</p><h4 id="2-static-getDerivedStateFromProps-props-state"><a href="#2-static-getDerivedStateFromProps-props-state" class="headerlink" title="2.static getDerivedStateFromProps(props, state)"></a>2.static getDerivedStateFromProps(props, state)</h4><p>当组件的state需要根据props来改变的时候可调用此方法。这个方法是在 render() 前会被执行,每次触发render前,都会触发此方法。</p><p>该方法有两个参数props和state; 返回值为state对象, 不需要返回整体state,把需要改变的state返回即可。如果不需要,可以返回null.</p><pre><code>classBaseextendsComponent &#123; state = &#123; age:20 &#125; staticgetDerivedStateFromProps(props, state) &#123; return &#123; age:50 &#125; &#125; render() &#123; // 50return ( &lt;div&gt;&#123;this.state.age&#125;&lt;/div&gt; ) &#125;&#125;</code></pre><p>这个方法允许组件基于 props 的变更来更新其内部状态,以这种方式获得的组件状态被称为派生状态。应该谨慎使用派生状态,可能会引入潜在的错误</p><h4 id="3-render"><a href="#3-render" class="headerlink" title="3.render"></a>3.render</h4><p>React组件中必须要提供的方法。当state或者props任一数据有更新时都会执行。</p><p>render() 是一个纯函数,因此,不要在其中执行setState诸如此类的操作。render必须有一个返回值,返回的数据类型可以有:</p><ul><li>null、String、Number、Array、Boolean。</li><li>React elements</li><li>Fragment</li><li>Portal</li></ul><p>注意不要在render中调用setState</p><h4 id="4-componentDidMount"><a href="#4-componentDidMount" class="headerlink" title="4.componentDidMount"></a>4.componentDidMount</h4><p>componentDidMount()方法是在组件加载完后立即执行,也就是当该组件相关的dom节点插入到dom树中时。该方法在组件生命中只执行一次。</p><p>一般情况,我们会在这里setState(),或者进行接口请求,设置订阅等。</p><pre><code>classBaseextendsComponent &#123; state = &#123; age:20 &#125; componentDidMount() &#123; this.fetchDate(); &#125; render() &#123; return ( &lt;div&gt;&#123;this.state.age&#125;&lt;/div&gt; ) &#125; //other code&#125;</code></pre><h4 id="5-shouldComponentUpdate-nextProps-nextState-nextContext"><a href="#5-shouldComponentUpdate-nextProps-nextState-nextContext" class="headerlink" title="5.shouldComponentUpdate(nextProps, nextState, nextContext)"></a>5.shouldComponentUpdate(nextProps, nextState, nextContext)</h4><p>在渲染新的props或state前,shouldComponentUpdate被调用,默认返回true。forceUpdate()时不会调用此方法。</p><p>如果shouldComponentUpdate()返回false,那么getSnapshotBeforeUpdate,render和componentDidUpdate不会被调用。</p><p>此生命周期主要用于优化性能。</p><h4 id="6-getSnapshotBeforeUpdate-prevProps-prevState"><a href="#6-getSnapshotBeforeUpdate-prevProps-prevState" class="headerlink" title="6.getSnapshotBeforeUpdate(prevProps, prevState)"></a>6.getSnapshotBeforeUpdate(prevProps, prevState)</h4><p>在render()的输出被渲染到DOM之前被调用。使组件能够在它们被更改之前捕获当前值(如滚动位置)。这个生命周期返回的任何值都将作为第三个参数传递给componentDidUpdate().</p><h4 id="7-componentDidUpdate-prevProps-prevState-snapshot"><a href="#7-componentDidUpdate-prevProps-prevState-snapshot" class="headerlink" title="7.componentDidUpdate(prevProps, prevState, snapshot)"></a>7.componentDidUpdate(prevProps, prevState, snapshot)</h4><p>在更新发生后调用 componentDidUpdate()。当组件更新时,将此作为一个机会来操作DOM。如将当前的props与以前的props进行比较(例如,如果props没有改变,则可能不需要网络请求。</p><p>如果组件使用 getSnapshotBeforeUpdate(),则它返回的值将作为第三个“快照”参数传递给 componentDidUpdate()。否则,这个参数是undefined。</p><h4 id="8-componentWillUnmount"><a href="#8-componentWillUnmount" class="headerlink" title="8.componentWillUnmount()"></a>8.componentWillUnmount()</h4><p>在组件被卸载并销毁之前立即被调用。在此方法中执行任何必要的清理,例如使定时器无效,取消网络请求或清理在componentDidMount()中创建的任何监听。</p><p><strong>最后,说明一点:</strong></p><p>componentWillMount,componentWillReceiveProps,componentWillUpdate这三个生命周期在React未来版本中会被废弃。</p><p>而UNSAFE_componentWillUpdate,UNSAFE_componentWillReceiveProps,UNSAFE_componentWillUpdate 未来版本中仍可继续使用。</p><p>初始化阶段(父组件和子组件):</p><p><a href="https://camo.githubusercontent.com/76fe4b48fa45ef5e389eed9098cc22fa483efabc/68747470733a2f2f6e6f74652e796f7564616f2e636f6d2f7977732f7075626c69632f7265736f757263652f66343537303163633431303530346537316462626362643838363165386430632f786d6c6e6f74652f5745425245534f5552434562303335306262373362633661396165346163313136303131393863313766322f3239353233"><img src="https://camo.githubusercontent.com/76fe4b48fa45ef5e389eed9098cc22fa483efabc/68747470733a2f2f6e6f74652e796f7564616f2e636f6d2f7977732f7075626c69632f7265736f757263652f66343537303163633431303530346537316462626362643838363165386430632f786d6c6e6f74652f5745425245534f5552434562303335306262373362633661396165346163313136303131393863313766322f3239353233" alt="mount"></a></p><p>运行阶段:父组件props&#x2F;state更新</p><p>子组件的shouldComponentUpdate返回false,则子组件其后的生命周期都不在进行,但是父组件的生命周期继续执行。</p><p><a href="https://camo.githubusercontent.com/9ecc4121b82ff19a4a31d4c5cc1785e7893f03d2/68747470733a2f2f6e6f74652e796f7564616f2e636f6d2f7977732f7075626c69632f7265736f757263652f66343537303163633431303530346537316462626362643838363165386430632f786d6c6e6f74652f5745425245534f5552434561643737363439393431396531633937386133633534336561313364393334652f3239353234"><img src="https://camo.githubusercontent.com/9ecc4121b82ff19a4a31d4c5cc1785e7893f03d2/68747470733a2f2f6e6f74652e796f7564616f2e636f6d2f7977732f7075626c69632f7265736f757263652f66343537303163633431303530346537316462626362643838363165386430632f786d6c6e6f74652f5745425245534f5552434561643737363439393431396531633937386133633534336561313364393334652f3239353234" alt="update"></a></p><p>卸载阶段: 卸载父组件</p><p><a href="https://user-images.githubusercontent.com/20203157/54731656-792d4e00-4bca-11e9-9f61-53fe34688878.png"><img src="https://user-images.githubusercontent.com/20203157/54731656-792d4e00-4bca-11e9-9f61-53fe34688878.png" alt="unmount"></a></p><p>参考:</p><ol><li><a href="https://segmentfault.com/a/1190000016935092">https://segmentfault.com/a/1190000016935092</a></li><li><a href="https://www.jianshu.com/p/514fe21b9914">https://www.jianshu.com/p/514fe21b9914</a></li><li><a href="https://blog.csdn.net/qq_29311407/article/details/79861522">https://blog.csdn.net/qq_29311407&#x2F;article&#x2F;details&#x2F;79861522</a></li></ol>]]></content>
5718
5718
 
5719
5719
 
5720
5720
  <categories>