一、Ajax深入

(1)同步代码和异步代码

首先,我们要了解同步代码和异步代码分别是什么。

我们都知道在代码中,执行顺序关乎着结果,执行顺序不一样,结果就会大相径庭。所以当我们了解了同步代码和异步代码后,对我们实际的编写会有帮助。

同步代码:逐行执行,前一步的结果影响后面的结果,只有上一步进行完后才能进行下一步

异步代码:可能需要长期运行的,后面的任务不需要等待前面的任务完成,最后在完成后通过回调函数得到结果

这里举一个例子

<body>
  <button class="btn">点我</button>
  <script>
    // 目标:阅读并回答代码执行和打印的顺序
    const result = 0 + 1
    console.log(result)
    setTimeout(() => {
      console.log(2)
    }, 2000)
    document.querySelector('.btn').addEventListener('click', () => {
      console.log(3)
    })
    document.body.style.backgroundColor = 'pink'
    console.log(4)
  </script>
</body>

在这段代码中,const result = 0 + 1,console.log(result)优先执行,这时候输出1,之后遇到了一个函数和监听器,这两个东西优先级在console.log(4)之后,即1后输出4。注意,监听器给了btn一个按钮上,当click时候才会输出3,所以2就在3之前,之后点击按钮才会输出3。最终结果就是142,点一下输出一个3

方便记忆的一方面,赋值,打印都提前,函数,监听器都滞后

这里举几个异步代码的例子:事件,Ajax,setTimeout/setInterval

异步代码一定是通过回调函数来接收结果

(2)回调函数地狱

回调函数地狱就是在回调函数中嵌套回调函数,一直嵌套下去就成了地狱。这类代码本身可读性差,无法捕获异常,耦合性严重

举一个例子

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>回调地狱</title>
</head>

<body>
  <form>
    <span>省份:</span>
    <select>
      <option class="province"></option>
    </select>
    <span>城市:</span>
    <select>
      <option class="city"></option>
    </select>
    <span>地区:</span>
    <select>
      <option class="area"></option>
    </select>
  </form>
  <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
  <script>
    /**
     * 目标:演示回调函数地狱
     * 需求:获取默认第一个省,第一个市,第一个地区并展示在下拉菜单中
     * 概念:在回调函数中嵌套回调函数,一直嵌套下去就形成了回调函数地狱
     * 缺点:可读性差,异常无法获取,耦合性严重,牵一发动全身
    */
    // 1. 获取默认第一个省份的名字
    axios({url: 'http://hmajax.itheima.net/api/province'}).then(result => {
      const pname = result.data.list[0]
      document.querySelector('.province').innerHTML = pname
      // 2. 获取默认第一个城市的名字
      axios({url: 'http://hmajax.itheima.net/api/city', params: { pname }}).then(result => {
        const cname = result.data.list[0]
        document.querySelector('.city').innerHTML = cname
        // 3. 获取默认第一个地区的名字
        axios({url: 'http://hmajax.itheima.net/api/area', params: { pname, cname }}).then(result => {
          console.log(result)
          const areaName = result.data.list[0]
          document.querySelector('.area').innerHTML = areaName
        })
      })
    }).catch(error => {
      console.dir(error)
    })
  </script>
</body>

</html>

(3)Promise链式调用

new Promise()后的.then()方法会创建一个新的Promise对象,继续串联下一个任务,then()回调函数中的返回值会影响新生成的Promise对象最终状态和结果,可以使用链式调用解决回调函数地狱

这里举一个例子

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Promise链式调用_解决回调地狱</title>
</head>

<body>
  <form>
    <span>省份:</span>
    <select>
      <option class="province"></option>
    </select>
    <span>城市:</span>
    <select>
      <option class="city"></option>
    </select>
    <span>地区:</span>
    <select>
      <option class="area"></option>
    </select>
  </form>
  <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
  <script>
    /**
     * 目标:把回调函数嵌套代码,改成Promise链式调用结构
     * 需求:获取默认第一个省,第一个市,第一个地区并展示在下拉菜单中
    */
    let pname = ''
    // 1. 得到-获取省份Promise对象
    axios({url: 'http://hmajax.itheima.net/api/province'}).then(result => {
      pname = result.data.list[0]
      document.querySelector('.province').innerHTML = pname
      // 2. 得到-获取城市Promise对象
      return axios({url: 'http://hmajax.itheima.net/api/city', params: { pname }})
    }).then(result => {
      const cname = result.data.list[0]
      document.querySelector('.city').innerHTML = cname
      // 3. 得到-获取地区Promise对象
      return axios({url: 'http://hmajax.itheima.net/api/area', params: { pname, cname }})
    }).then(result => {
      console.log(result)
      const areaName = result.data.list[0]
      document.querySelector('.area').innerHTML = areaName
    })
  </script>
</body>

</html>

在这段代码中,我们依次获得了省市区promise对象,要在每一次获取对象后使用.then方法,这样才是链式调用。因为是在前面的基础上再获取。

1.async函数和await关键字

async 函数是使用 async 关键字声明的函数。async 函数是AsyncFunction 构造函数的实例,并且其中允许使用 await 关键字。async 和 await 关键字让我们可以用一种更简洁的方式写出基于 promise 的异步行为,而无需刻意地链式调用 promise。

在 async 函数内,使用 await 关键字取代 then 函数,等待获取 Promise 对象成功状态的结果值

这里使用相同的例子

  <script>
    /**
     * 目标:掌握async和await语法,解决回调函数地狱
     * 概念:在async函数内,使用await关键字,获取Promise对象"成功状态"结果值
     * 注意:await必须用在async修饰的函数内(await会阻止"异步函数内"代码继续执行,原地等待结果)
    */
    // 1. 定义async修饰函数
    async function getData() {
      // 2. await等待Promise对象成功的结果
      const pObj = await axios({url: 'http://hmajax.itheima.net/api/province'})
      const pname = pObj.data.list[0]
      const cObj = await axios({url: 'http://hmajax.itheima.net/api/city', params: { pname }})
      const cname = cObj.data.list[0]
      const aObj = await axios({url: 'http://hmajax.itheima.net/api/area', params: { pname, cname }})
      const areaName = aObj.data.list[0]

      document.querySelector('.province').innerHTML = pname
      document.querySelector('.city').innerHTML = cname
      document.querySelector('.area').innerHTML = areaName
    }

    getData()
  </script>

await是在等待前面结果后在进行本段的运行,取代了.then,可以持续的获取对象

2.async函数和await捕获错误

使用try…catch语句块

try {
//要执行的代码
} catch(error){//error接收的是,错误信息
    //try里代码,如果有错误,直接进入这里执行
	}

使用相同案例

  <script>
    /**
     * 目标:async和await_错误捕获
    */
    async function getData() {
      // 1. try包裹可能产生错误的代码
      try {
        const pObj = await axios({ url: 'http://hmajax.itheima.net/api/province' })
        const pname = pObj.data.list[0]
        const cObj = await axios({ url: 'http://hmajax.itheima.net/api/city', params: { pname } })
        const cname = cObj.data.list[0]
        const aObj = await axios({ url: 'http://hmajax.itheima.net/api/area', params: { pname, cname } })
        const areaName = aObj.data.list[0]

        document.querySelector('.province').innerHTML = pname
        document.querySelector('.city').innerHTML = cname
        document.querySelector('.area').innerHTML = areaName
      } catch (error) {
        // 2. 接着调用catch块,接收错误信息
        // 如果try里某行代码报错后,try中剩余的代码不会执行了
        console.dir(error)
      }
    }

    getData()
  </script>

(4)事件循环

JavaScript 单线程(某一刻只能执行一行代码),为了让耗时代码不阻塞其他代码运行,设计了事件循环模型

执行代码和收集异步任务的模型,在调用栈空闲,反复调用任务队列里回调函数的执行机制,就叫事件循环

JavaScript 内代码如何执行?
➢ 执行同步代码,遇到异步代码交给宿主浏览器环境执行
➢ 异步有了结果后,把回调函数放入任务队列排队
➢ 当调用栈空闲后,反复调用任务队列里的回调函数

这里举一个例子进行联系

  <script>
    /**
     * 目标:阅读并回答执行的顺序结果
    */
    console.log(1)
    setTimeout(() => {
      console.log(2)
    }, 0)
    function myFn() {
      console.log(3)
    }
    function ajaxFn() {
      const xhr = new XMLHttpRequest()
      xhr.open('GET', 'http://hmajax.itheima.net/api/province')
      xhr.addEventListener('loadend', () => {
        console.log(4)
      })
      xhr.send()
    }
    for (let i = 0; i < 1; i++) {
      console.log(5)
    }
    ajaxFn()
    document.addEventListener('click', () => {
      console.log(6)
    })
    myFn()
    // 1 5 3 2 4 点击一次document就会执行一次打印6
  </script>

(5)宏任务和微任务

ES6 之后引入了 Promise 对象, 让 JS 引擎也可以发起异步任务
异步任务分为:
➢宏任务:由浏览器环境执行的异步代码
➢微任务:由 JS 引擎环境执行的异步代码

在目前学习阶段,执行所在环境如下

在这里插入图片描述

在这里举一个例子,宏任务和微任务的嵌套

  <script>
    // 目标:回答代码执行顺序
    console.log(1)
    setTimeout(() => {
      console.log(2)
      const p = new Promise(resolve => resolve(3))
      p.then(result => console.log(result))
    }, 0)
    const p = new Promise(resolve => {
      setTimeout(() => {
        console.log(4)
      }, 0)
      resolve(5)
    })
    p.then(result => console.log(result))
    const p2 = new Promise(resolve => resolve(6))
    p2.then(result => console.log(result))
    console.log(7)
    // 1 7 5 6 2 3 4
  </script>

(6)Promise.all静态方法

合并多个 Promise 对象,等待所有同时成功完成(或某一个失败),做后续逻辑

代码如下

const p=Promise.a11([Promise对象,Promise对象,...])
p.then(result =>{
//result结果:[Promise对象成功结果,Promise对象成功结果..]
}).catch(error =>{
//第一-个失败的Promise对象,抛出的异常
}

举例如下

<body>
  <ul class="my-ul"></ul>
  <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
  <script>
    /**
     * 目标:掌握Promise的all方法作用,和使用场景
     * 业务:当我需要同一时间显示多个请求的结果时,就要把多请求合并
     * 例如:默认显示"北京", "上海", "广州", "深圳"的天气在首页查看
     * code:
     * 北京-110100
     * 上海-310100
     * 广州-440100
     * 深圳-440300
    */
    // 1. 请求城市天气,得到Promise对象
    const bjPromise = axios({ url: 'http://hmajax.itheima.net/api/weather', params: { city: '110100' } })
    const shPromise = axios({ url: 'http://hmajax.itheima.net/api/weather', params: { city: '310100' } })
    const gzPromise = axios({ url: 'http://hmajax.itheima.net/api/weather', params: { city: '440100' } })
    const szPromise = axios({ url: 'http://hmajax.itheima.net/api/weather', params: { city: '440300' } })

    // 2. 使用Promise.all,合并多个Promise对象
    const p = Promise.all([bjPromise, shPromise, gzPromise, szPromise])
    p.then(result => {
      // 注意:结果数组顺序和合并时顺序是一致
      console.log(result)
      const htmlStr = result.map(item => {
        return `<li>${item.data.data.area} --- ${item.data.data.weather}</li>`//这里是映射成<li></li>的结构
      }).join('')
      document.querySelector('.my-ul').innerHTML = htmlStr
    }).catch(error => {
      console.dir(error)
    })
  </script>
</body>

二、Node.js和Webpack

(1)Node.js

Node.js 是一个跨平台 JavaScript 运行环境,使开发者可以搭建服务器端的 JavaScript 应用程序

作用:使用 Node.js 编写服务器端程序
✓ 编写数据接口,提供网页资源浏览功能等等
✓ 前端工程化:为后续学习 Vue 和 React 等框架做铺垫

它包含两种语法标准,CommonJS标准和ECMAScript标准

前端工程化:开发项目直到上线,过程中集成的所有工具和技术
Node.js 是前端工程化的基础(因为 Node.js 可以主动读取前端代码内容)

在这里插入图片描述

Node.js 为何能执行 JS?

首先:浏览器能执行 JS 代码,依靠的是内核中的 V8 引擎(C++ 程序)
其次:Node.js 是基于 Chrome V8 引擎进行封装(运行环境)
区别:都支持 ECMAScript 标准语法,Node.js 有独立的 API

注意!Node.js没有DOM和BOM

在这里插入图片描述

1.配置Node.js

Node.js可以让js代码独立运行,在此之前需要独立配置。

\1. 安装 Node.js

首先,确保你已经安装了 Node.js。如果没有,请从 Node.js 官网 下载并安装。

\2. 配置环境变量

如果 Node.js 已经安装,但仍然出现错误,可能是因为环境变量未正确配置。请按照以下步骤进行配置:

找到 Node.js 的安装路径。例如:D:\Node.js打开系统属性: 按 Win + R,输入 sysdm.cpl,点击确定。在系统属性窗口中,选择“高级”选项卡,然后点击“环境变量”。在系统变量中找到 Path,点击编辑。点击新建,将 Node.js 的安装路径添加进去。例如:D:\Node.js点击确定保存设置。

\3. 验证安装

配置完成后,打开新的命令提示符窗口,输入以下命令验证安装是否成功:

node -v

npm -v

如果显示 Node.js 和 npm 的版本号,则表示配置成功。

2.js文件初体验

// 目标:编写 js 代码,在 Node 环境下使用 node 命令来独立执行
console.log('Hello, Node.js')
for (let i = 0; i < 3; i++) {
  console.log(6)
}

在vscode中,创建.js文件,直接输入js代码,在命令行进行运行。步骤如下:

在vscode中右击js文件,选择在集成终端中打开,此时命令行会自动切换到相应文件路径下。

在这里插入图片描述

在输入(node 文件名)就可以运行

在这里插入图片描述

3.fs模块-读写文件

模块:类似插件,封装了方法/属性
fs 模块:封装了与本机文件系统进行交互的,方法/属性
语法:

  1. 加载 fs 模块对象
  2. 写入文件内容
  3. 读取文件内容

结构如下

const fs = require('fs')//fs 是模块标识符:模块的名宁

fs.writeFi1e('文件路径','写入内容',err => {
             //写入后的回调函数
})

fs.readFile('文件路径',(err,data)=>{
//读取后的回调函数
//data 是文件内容的 Buffer 数据流
})

体验

/**
 * 目标:基于 fs 模块读写文件内容
 *  1. 加载 fs 模块对象
 *  2. 写入文件内容
 *  3. 读取文件内容
 */
// 1. 加载 fs 模块对象
const fs = require('fs')
// 2. 写入文件内容
fs.writeFile('./test.txt', 'hello, Node.js', (err) => {
  if (err) console.log(err)
  else console.log('写入成功')
})
// 3. 读取文件内容
fs.readFile('./test.txt', (err, data) => {
  if (err) console.log(err)
  // data 是 buffer 16 进制数据流对象
  // .toString() 转换成字符串
  else console.log(data.toString())
})

直接读取时发现输出buffer数据流,需要使用toString输出字符串内容

4.path模块-路径处理

问题:Node.js 代码中,相对路径是根据终端所在路径来查找的,可能无法找到想要的文件

建议:在 Node.js 代码中,使用绝对路径
补充:__dirname 内置变量(获取当前模块目录-绝对路径)——拿到文件所在的文件夹
✓ windows: E:\html5\node.js和webpack\01_Node.js入门\03\index.js
注意:path.join() 会使用特定于平台的分隔符,作为定界符,将所有给定的路径片段连接在一起
语法:

  1. 加载 path 模块
  2. 使用 path.join 方法,拼接路径
path.join('03','dist/js','index.js')
//windows:'03\dist\js\index.js
//mac:'03/dist/js/index.js
const path = require('path')
path.join('路径1','路径2',...)

体验

/**
 * 目标:在 Node.js 环境的代码中,应使用绝对路径
 * 原因:代码的相对路径是以终端所在文件夹为起点,而不是 Vscode 资源管理器
 *  容易造成目标文件找不到的错误
 */
const fs = require('fs')
// 1. 引入 path 模块对象
const path = require('path')
// 2. 调用 path.join() 配合 __dirname 组成目标文件的绝对路径
console.log(__dirname)
fs.readFile(path.join(__dirname, '../test.txt'), (err, data) => {
  if (err) console.log(err)
  else console.log(data.toString())
})

5.压缩案例

/**
 * 目标1:压缩 html 代码
 * 需求:把回车符 \r,换行符 \n 去掉,写入到新 html 文件中
 *  1.1 读取源 html 文件内容
 *  1.2 正则替换字符串
 *  1.3 写入到新的 html 文件中
 */
// 1.1 读取源 html 文件内容
const fs = require('fs')
const path = require('path')
fs.readFile(path.join(__dirname, 'public/index.html'), (err, data) => {
  if (err) console.log(err)
  else {
    const htmlStr = data.toString()
    // 1.2 正则替换字符串
    const resultStr = htmlStr.replace(/[\r\n]/g, '')//这里替换回车符 \r,换行符 \n ,g是全局替换
    console.log(resultStr)
    // 1.3 写入到新的 html 文件中
    fs.writeFile(path.join(__dirname, 'dist/index.html'), resultStr, err => {
      if (err) console.log(err)
      else console.log('写入成功')
    })
  }
})

6.URL中的端口号

URL:统一资源定位符,简称网址,用于访问服务器里的资源
端口号:标记服务器里不同功能的服务程序
端口号范围:0-65535 之间的任意整数

例如在http://hmajax.itheima.net:80/api/province中:80就是端口号

因为http 协议,默认访问 80 端口

Web 服务程序:用于提供网上信息浏览功能
注意:0-1023 和一些特定端口号被占用,我们自己编写服务程序请避开使用

7.HTTP模块-创建Web服务

需求:创建 Web 服务并响应内容给浏览器
步骤:

  1. 加载 http 模块,创建 Web 服务对象
  2. 监听 request 请求事件,设置响应头和响应体
  3. 配置端口号并启动 Web 服务
  4. 浏览器请求 http://localhost:3000 测试
    (localhost:固定代表本机的域名)
/**
 * 目标:基于 http 模块创建 Web 服务程序
 *  1.1 加载 http 模块,创建 Web 服务对象
 *  1.2 监听 request 请求事件,设置响应头和响应体
 *  1.3 配置端口号并启动 Web 服务
 *  1.4 浏览器请求(http://localhost:3000)测试
 */
// 1.1 加载 http 模块,创建 Web 服务对象
const http = require('http')//请求http拿到对象
const server = http.createServer()
// 1.2 监听 request 请求事件,设置响应头和响应体——当有请求时,触发对应回调函数
server.on('request', (req, res) => {
  // 设置响应头-内容类型-普通文本以及中文编码格式
  res.setHeader('Content-Type', 'text/plain;charset=utf-8')
  // 设置响应体内容,结束本次请求与响应
  res.end('欢迎使用 Node.js 和 http 模块创建的 Web 服务')
})
// 1.3 配置端口号并启动 Web 服务
server.listen(3000, () => {
  console.log('Web 服务启动成功了')
})

在成功运行代码后,服务启动,进入浏览器请求(http://localhost:3000)测试,停止服务在终端ctrl+c

(2)Node.js模块化

什么是模块化?

CommonJS 模块是为 Node.is 打包,Javascript 代码的原始方式。 Node.js 还支持浏览器和其他 JavaScript 运行时使用的 ECMAScript 模块 标准
在 Node.js 中,每个文件都被视为一个单独的模块。

概念:项目是由很多个模块文件组成的
好处:提高代码复用性,按需加载,独立作用域
使用:需要标准语法导出和导入进行使用

导出:module.exports = {}

导入:require(‘模块名或路径’)

比如,我们在上述过程中引入fs,path,http,querystring模块。

1.初体验

在这里建两个文件在同一目录下。

//第一个utils.js
/**
 * 目标:基于 CommonJS 标准语法,封装属性和方法并导出
 */
const baseURL = 'http://hmajax.itheima.net'
const getArraySum = arr => arr.reduce((sum, item) => sum += item, 0)

// 导出
module.exports = {
  url: baseURL,
  arraySum: getArraySum
}
//第二个index.js
/**
 * 目标:基于 CommonJS 标准语法,导入工具属性和方法使用
 */
// 导入
const obj = require('./utils.js')
console.log(obj)
const result = obj.arraySum([5, 1, 2, 3])
console.log(result)

在该代码中,getArraySum是求数组和,所以在调用的时候先把utils.js的方法进行了封装导出,这样就可以在别的.js文件中引用,只不过要进行导入。

2.ECMAScript 标准 - 默认导出和导入

默认标准使用:

  1. 导出:export default {}
  2. 导入:import 变量名 from ‘模块名或路径’

注意:Node.js 默认支持 CommonJS 标准语法

如需使用 ECMAScript 标准语法,在运行模块所在文件夹新建 package.json 文件,并设置 { “type” : “module” }

举一个例子。建三个文件,其中utils.js同上

//第二个package.json
{
  "type": "module"
}
//第三个index.js
/**
 * 目标:基于 ECMAScript 标准语法,"默认"导入,工具属性和方法使用
 */
// 默认导入
import obj from './utils.js'
console.log(obj)
const result = obj.arraySum([10, 20, 30])
console.log(result)

为什么设置 { “type” : “module” }?这是因为在 package.json 中设置 { “type”: “module” } 是为了明确告诉 Node.js 将该包(项目)中的所有 .js 文件都作为 ES Modules (ESM) 来处理。

3.ECMAScript 标准 - 命名导出和导入

命名标准使用:

  1. 导出:export 修饰定义语句
  2. 导入:import { 同名变量 } from '模块名或路径‘

如何选择:
按需加载,使用命名导出和导入
全部加载,使用默认导出和导入

4.包

包:将模块,代码,其他资料聚合成一个文件夹
包分类:
✓ 项目包:主要用于编写项目和业务逻辑
✓ 软件包:封装工具和方法进行使用
要求:根目录中,必须有 package.json 文件(记录包的清单信息)
注意:导入软件包时,引入的默认是 index.js 模块文件 / main 属性指定的模块文件

//一般的 package.json 
{
  "name": "cz_utils",//软件包名称
  "version": "1.0.0",//软件包当前版本
  "description": "一个数组和字符串常用工具方法的包",//软件包描述
  "main": "index.js",//软件包入口点
  "author": "rain",//软件包作者
  "license": "MIT"//软件包许可证
}

(3)npm

npm 是 Node.js 标准的软件包管理器

使用:

  1. 初始化清单文件 :npm init -y(得到 package.json 文件,有则略过此命令,先检查有无package.json文件,没有必须使用)
  2. 下载软件包 :npm i 软件包名称
  3. 使用软件包

注意,使用时内置模块和npm模块都是直接写文件名字,自己创建的需要写路径

1.安装所有依赖

在拿取别人项目后,项目不包含node_modules,不能正常运行,node_modules包含该项目中所需要的所有依赖。自己下载的速度快与拷贝的速度。

如何解决该问题?项目终端输入命令:npm i,自动会下载 package.json 中记录的所有软件包

2.全局软件包 nodemon

软件包区别:
➢ 本地软件包:当前项目内使用,封装属性和方法,存在于 node_modules
➢ 全局软件包:本机所有项目使用,封装命令和工具,存在于系统设置的位置
nodemon 作用:替代 node 命令,检测代码更改,自动重启程序
使用:

  1. 安装:npm i nodemon -g(-g 代表安装到全局环境中)
  2. 运行:nodemon 待执行的目标 js 文件

使用时nodemon 文件名,这样能够实时监测代码,输出最新结果

3.总结

/**
 * 总结:
 * Node.js模块:
 *  概念:每个文件就是一个模块,独立作用域,按需加载,需使用特定语法导出导入
 *  CommonJS 标准语法:
 *    导出:module.exports = {}
 *    导入:require('模块名或路径')
 *  ECMAScript 标准语法:
 *    默认导出:export default {}
 *    默认导入:import 变量名 from '模块名或路径'
 *    命名导出:export 修饰定义语句
 *    命名导入:import { 同名变量 } from '模块名或路径'
 * Node.js包:
 *  概念:把模块文件,代码文件,其他资料聚合成一个文件夹
 *  项目包:编写项目需求和业务逻辑的文件夹
 *  软件包:封装工具/方法的文件夹(一般用 npm 管理)
 *    本地软件包:封装属性/方法,在当前项目中使用,例如:dayjs,lodash
 *    全局软件包:封装工具/命令,在本机中使用,例如:nodemon
 * 常用命令:
 *  执行 js 文件:node xxx
 *  初始化 package.json: npm init -y
 *  下载本地软件包:npm i 软件包名
 *  下载全局软件包:npm i 软件包名 -g
 *  删除软件包:npm uni 软件包名
 */

(4)Webpack

本质上,webpack 是一个用于现代 Javascript应用程序的静态模块打包工具。当webpack处理应用程序时,它会在内部从一个或多个入口点构建一个依赖图(dependency graph),然后将你项目中所需的每一个模块组合成一个或多个bundles,它们均为静态资源,用于展示你的内容。

静态模块:指的是编写代码过程中的,html,css,js,图片等固定内容的文件
打包:把静态模块内容,压缩,整合,转译等(前端工程化)
✓ 把 less / sass 转成 css 代码
✓ 把 ES6+ 降级成 ES5
✓ 支持多种模块标准语法
问题:为何不学 vite ?
因为:很多项目还是基于 Webpack 构建,并为 Vue
React 脚手架使用做铺垫!

所有内容都可以在webpack官网搜索

1.使用Webpack

  1. 新建并初始化项目,编写业务源代码

  2. 下载 webpack webpack-cli 到当前项目中(版本独立),并配置局部自定义命令

    npm i webpack webpack-cli --save-dev进行下载

    创建文件"scripts":{“build”: "webpack}这里的"build"可以更改为自定义名称

  3. 运行打包命令,自动产生 dist 分发文件夹(压缩和优化后,用于最终运行的代码)

    npm run build运行

2.修改Webpack打包入口和出口

Webpack 配置:影响 Webpack 打包过程和结果
步骤:

  1. 项目根目录,新建 webpack.config.js 配置文件
  2. 导出配置对象,配置入口,出口文件的路径

注意:只有和入口产生直接/间接的引入关系,才会被打包

const config = {
  // 打包模式(development 开发模式-使用相关内置优化)
  // mode: 'development',
  // 入口
  // entry: path.resolve(__dirname, 'src/login/index.js'),
  entry: {
    'login': path.resolve(__dirname, 'src/login/index.js'),
    'content': path.resolve(__dirname, 'src/content/index.js'),
    'publish': path.resolve(__dirname, 'src/publish/index.js')
  },
  // 出口
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: './[name]/index.js',
    clean: true // 生成打包后内容之前,清空输出目录
  },

3.自动创建HTML文件

插件 html-webpack-plugin: 在 Webpack 打包时生成 html 文件
步骤:

  1. 下载 html-webpack-plugin 本地软件包
  2. 配置 webpack.config.js 让 Webpack 拥有插件功能

下载时在终端输入npm i html-webpack-plugin --save-dev

配置时在plugin中

  // 插件(给 Webpack 提供更多功能)
  plugins: [
    new HtmlWebpackPlugin({
      template: path.resolve(__dirname, 'public/login.html'), // 模板文件
      filename: path.resolve(__dirname, 'dist/login/index.html'), // 输出文件
      useCdn: process.env.NODE_ENV === 'production', // 生产模式下使用 cdn 引入的地址
      chunks: ['login'] // 引入哪些打包后的模块(和 entry 的 key 一致)
    }),
    new HtmlWebpackPlugin({
      template: path.resolve(__dirname, 'public/content.html'), // 模板文件
      filename: path.resolve(__dirname, 'dist/content/index.html'), // 输出文件
      useCdn: process.env.NODE_ENV === 'production', // 生产模式下使用 cdn 引入的地址
      chunks: ['content']
    }),
    new HtmlWebpackPlugin({
      template: path.resolve(__dirname, 'public/publish.html'), // 模板文件
      filename: path.resolve(__dirname, 'dist/publish/index.html'), // 输出文件
      useCdn: process.env.NODE_ENV === 'production', // 生产模式下使用 cdn 引入的地址
      chunks: ['publish']
    }),
    new MiniCssExtractPlugin({
      filename: './[name]/index.css'
    }), // 生成 css 文件
    new webpack.DefinePlugin({
      'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV)
    })
  ],

4.打包css代码

打包css代码需要使用加载器

加载器 css-loader:解析 css 代码
加载器 style-loader:把解析后的 css 代码插入到 DOM
步骤:

  1. 准备 css 文件代码引入到 src/login/index.js 中(压缩转译处理等)
  2. 下载 css-loader 和 style-loader 本地软件包
  3. 配置 webpack.config.js 让 Webpack 拥有该加载器功能

注意:Webpack 默认只识别 js 代码

下载时在终端输入npm i css-loader style-loader --save-dev

配置在module中

// 加载器(让 webpack 识别更多模块文件内容)
module: {
  rules: [
    // CSS 文件处理规则
    {
      test: /\.css$/i,  // 匹配所有 .css 文件(不区分大小写)
      // use: ['style-loader', "css-loader"],  // 被注释的原始配置
      
      // 根据环境选择不同的 loader
      use: [
        process.env.NODE_ENV === 'development' 
          ? 'style-loader'           // 开发环境:使用 style-loader 将 CSS 注入到 JS 中
          : MiniCssExtractPlugin.loader, // 生产环境:提取 CSS 到独立文件
        "css-loader"                 // 解析 CSS 文件中的 @import 和 url()
      ]
    },
    
    // Less 文件处理规则
    {
      test: /\.less$/i,  // 匹配所有 .less 文件(不区分大小写)
      use: [
        // 将 Less 编译为 CSS
        process.env.NODE_ENV === 'development' 
          ? 'style-loader'           // 开发环境:注入到 JS
          : MiniCssExtractPlugin.loader, // 生产环境:提取为独立文件
        'css-loader',                // 解析 CSS 依赖
        'less-loader',               // 将 Less 编译为 CSS
      ],
    },
    
    // 图片文件处理规则
    {
      test: /\.(png|jpg|jpeg|gif)$/i,  // 匹配常见图片格式
      type: 'asset',                   // Webpack 5 的资源模块类型
      generator: {
        filename: 'assets/[hash][ext][query]'  
         // 输出路径和命名规则:
         // assets/ 目录下
         // [hash] 文件内容哈希值
         // [ext] 原文件扩展名  
         // [query] 可能的查询参数
      }
    }
  ],
},

5.提取css代码

插件 mini-css-extract-plugin:提取 css 代码
步骤:

  1. 下载 mini-css-extract-plugin 本地软件包
  2. 配置 webpack.config.js 让 Webpack 拥有该插件功能

下载时在终端输入npm install --save-dev mini-css-extract-plugin

格式如下

const MiniCssExtractPlugin = require("mini-css-extract-plugin")

module.exports = {
    // ...
    module: {
        rules: [
            {
                test: /\.css/i,
                // use: ['style-loader', 'css-loader']
                use: [MiniCssExtractPlugin.loader, "css-loader"],
            },
        ],
    },
    plugins: [
        // ...
        new MiniCssExtractPlugin()
    ]
};

6.压缩css代码

使用 css-minimizer-webpack-plugin 插件
步骤:

  1. 下载 css-minimizer-webpack-plugin 本地软件包
  2. 配置 webpack.config.js 让 webpack 拥有该功能

下载时在终端输入npm install css-minimizer-webpack-plugin --save-dev

格式如下

const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");

module.exports = {
    // ...
    // 优化
    optimization: {
        // 最小化
        minimizer: [
            // 在 webpack5 中,你可以使用 `...` 语法来扩展现有的 minimizer(即 terser-webpack-plugin),将下一行取消注释(保证 JS 代码还能被压缩处理)
            // ...,
            new CssMinimizerPlugin(),
        ],
    }
};

7.打包less代码

加载器 less-loader:把 less 代码编译为 css 代码
步骤:

  1. 新建 less 代码(设置背景图)并引入到 src/login/index.js 中
  2. 下载 less 和 less-loader 本地软件包
  3. 配置 webpack.config.js 让 Webpack 拥有功能

注意:less-loader 需要配合 less 软件包使用

下载时在终端输入npm i less less-loader --save-dev

格式如下

// ...
module.exports = {
    // ...
    module: {
        rules: [
            // ...
            {
                test: /\.less$/i,
                use: [MiniCssExtractPlugin.loader, "css-loader", "less-loader"]
            }
        ]
    }
}

8.打包图片

资源模块:Webpack5 内置资源模块(字体,图片等)打包,无需额外 loader
步骤:

配置 webpack.config.js 让 Webpack 拥有打包图片功能
✓ 占位符 【hash】对模块内容做算法计算,得到映射的数字字母组合的字符串
✓ 占位符 【ext】使用当前模块原本的占位符,例如:.png / .jpg 等字符串
✓ 占位符 【query】保留引入文件时代码中查询参数(只有 URL 下生效)

注意:判断临界值默认为 8KB
✓ 大于 8KB 文件:发送一个单独的文件并导出 URL 地址
✓ 小于 8KB 文件:导出一个 data URI(base64字符串

格式如下

module.exports = {
    // ...
    module: {
        rules: [
            // ...
            {
                test: /\.(png|jpg|jpeg|gif)$/i,
                type: 'asset',
                generator: {
                    filename: 'assets/[hash][ext][query]'
                }
            }
        ]
    }
}

9.搭建开发环境

开发环境:配置 webpack-dev-server 快速开发应用程序
作用:启动 Web 服务,自动检测代码变化,热更新到网页
注意:dist 目录和打包内容是在内存里(更新快)
步骤:

  1. 下载 webpack-dev-server 软件包到当前项目
  2. 设置模式为开发模式,并配置自定义命令
  3. 使用 npm run dev 来启动开发服务器,试试热更新效果/实时更新

下载时在终端输入npm i webpack-dev-server --save-dev

// ...设置开发模式并配置自定义命令

module.exports = {
  // ...
  mode: 'development'
}
"scripts": {
  // ...
  "dev": "webpack serve --mode=development"
},

10打包模式的应用

方案1:webpack.config.js 配置导出函数,但是局限性大(只接受 2 种模式)

方案2:借助 cross-env (跨平台通用)包命令,设置参数区分环境

步骤:

方案3:配置不同的 webpack.config.js (适用多种模式差异性较大情况)

  1. 下载 cross-env 软件包到当前项目
  2. 配置自定义命令,传入参数名和值(会绑定到 process.env 对象下)
  3. 在 webpack.config.js 区分不同环境使用不同配置

下载时在终端输入npm i cross-env --save-dev

   module: {
       rules: [
         {
           test: /\.css$/i,
           // use: ['style-loader', "css-loader"],
           use: [process.env.NODE_ENV === 'development' ? 'style-loader' : MiniCssExtractPlugin.loader, "css-loader"]
         },
         {
           test: /\.less$/i,
           use: [
             // compiles Less to CSS
             process.env.NODE_ENV === 'development' ? 'style-loader' : MiniCssExtractPlugin.loader,
             'css-loader',
             'less-loader',
           ],
         }
       ],
     },

11.Webpack 开发环境调错 source map

  1. source map:可以准确追踪 error 和 warning 在原始代码的位置
  2. 问题:代码被压缩和混淆,无法正确定位源代码位置(行数和列数)
  3. 设置:webpack.config.js 配置 devtool 选项
  4. 注意:source map 适用于开发环境,不要在生产环境使用(防止被轻易查看源码位置)
// ...

module.exports = {
  // ...
  devtool: 'inline-source-map'
}

12.设置解析别名路径

原来路径如下:

import { checkPhone, checkCode } from '../src/utils/check.js'

配置解析别名:在 webpack.config.js 中设置

// ...

const config = {
  // ...
  resolve: {
    alias: {
      '@': path.resolve(__dirname, 'src')
    }
  }
}

这样以后引入目标模块写的路径就更简单了

import { checkPhone, checkCode } from '@/utils/check.js'

13.多页面打包

单页面:单个 html 文件,切换 DOM 的方式实现不同业务逻辑展示,后续 Vue/React 会学到
多页面:多个 html 文件,切换页面实现不同业务逻辑展示
需求:把黑马头条-数据管理平台-内容页面一起引入打包使用
步骤:

  1. 准备源码(html,css,js)放入相应位置,并改用模块化语法导出
  2. 下载 form-serialize 包并导入到核心代码中使用
  3. 配置 webpack.config.js 多入口和多页面的设置
// ...
const config = {
  entry: {
    '模块名1': path.resolve(__dirname, 'src/入口1.js'),
    '模块名2': path.resolve(__dirname, 'src/入口2.js'),
  },
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: './[name]/index.js'  
  }
  plugins: [
    new HtmlWebpackPlugin({
      template: './public/页面2.html', // 模板文件
      filename: './路径/index.html', // 输出文件
      chunks: ['模块名2']
    })
    new HtmlWebpackPlugin({
      template: './public/页面2.html', // 模板文件
      filename: './路径/index.html', // 输出文件
      chunks: ['模块名2']
    })
  ]
}

14.分割公共代码

需求:把 2 个以上页面引用的公共代码提取

配置 webpack.config.js 的 splitChunks 分割功能

// ...
const config = {
  // ...
  optimization: {
    // ...
    splitChunks: {
      chunks: 'all', // 所有模块动态非动态移入的都分割分析
      cacheGroups: { // 分隔组
        commons: { // 抽取公共模块
          minSize: 0, // 抽取的chunk最小大小字节
          minChunks: 2, // 最小引用数
          reuseExistingChunk: true, // 当前 chunk 包含已从主 bundle 中拆分出的模块,则它将被重用
          name(module, chunks, cacheGroupKey) { // 分离出模块文件名
            const allChunksNames = chunks.map((item) => item.name).join('~') // 模块名1~模块名2
            return `./js/${allChunksNames}` // 输出到 dist 目录下位置
          }
        }
      }
    }

15.代码总结演示

仅包含webpack.config.js

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin')
const webpack = require('webpack')

const config = {
  // 打包模式(development 开发模式-使用相关内置优化)
  // mode: 'development',
  // 入口
  // entry: path.resolve(__dirname, 'src/login/index.js'),
  entry: {
    'login': path.resolve(__dirname, 'src/login/index.js'),
    'content': path.resolve(__dirname, 'src/content/index.js'),
    'publish': path.resolve(__dirname, 'src/publish/index.js')
  },
  // 出口
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: './[name]/index.js',
    clean: true // 生成打包后内容之前,清空输出目录
  },
  // 插件(给 Webpack 提供更多功能)
  plugins: [
    new HtmlWebpackPlugin({
      template: path.resolve(__dirname, 'public/login.html'), // 模板文件
      filename: path.resolve(__dirname, 'dist/login/index.html'), // 输出文件
      useCdn: process.env.NODE_ENV === 'production', // 生产模式下使用 cdn 引入的地址
      chunks: ['login'] // 引入哪些打包后的模块(和 entry 的 key 一致)
    }),
    new HtmlWebpackPlugin({
      template: path.resolve(__dirname, 'public/content.html'), // 模板文件
      filename: path.resolve(__dirname, 'dist/content/index.html'), // 输出文件
      useCdn: process.env.NODE_ENV === 'production', // 生产模式下使用 cdn 引入的地址
      chunks: ['content']
    }),
    new HtmlWebpackPlugin({
      template: path.resolve(__dirname, 'public/publish.html'), // 模板文件
      filename: path.resolve(__dirname, 'dist/publish/index.html'), // 输出文件
      useCdn: process.env.NODE_ENV === 'production', // 生产模式下使用 cdn 引入的地址
      chunks: ['publish']
    }),
    new MiniCssExtractPlugin({
      filename: './[name]/index.css'
    }), // 生成 css 文件
    new webpack.DefinePlugin({
      'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV)
    })
  ],
  // 加载器(让 webpack 识别更多模块文件内容)
  module: {
    rules: [
      {
        test: /\.css$/i,
        // use: ['style-loader', "css-loader"],
        use: [process.env.NODE_ENV === 'development' ? 'style-loader' : MiniCssExtractPlugin.loader, "css-loader"]
      },
      {
        test: /\.less$/i,
        use: [
          // compiles Less to CSS
          process.env.NODE_ENV === 'development' ? 'style-loader' : MiniCssExtractPlugin.loader,
          'css-loader',
          'less-loader',
        ],
      },
      {
        test: /\.(png|jpg|jpeg|gif)$/i,
        type: 'asset',
        generator: {
          filename: 'assets/[hash][ext][query]'
        }
      }
    ],
  },
  // 优化
  optimization: {
    // 最小化
    minimizer: [
      // 在 webpack@5 中,你可以使用 `...` 语法来扩展现有的 minimizer(即 `terser-webpack-plugin`),将下一行取消注释(保证 js 代码还能压缩)
      `...`,
      new CssMinimizerPlugin(),
    ],
    splitChunks: {
      chunks: 'all', // 所有模块动态非动态移入的都分割分析
      cacheGroups: { // 分隔组
        commons: { // 抽取公共模块
          minSize: 0, // 抽取的chunk最小大小字节
          minChunks: 2, // 最小引用数
          reuseExistingChunk: true, // 当前 chunk 包含已从主 bundle 中拆分出的模块,则它将被重用
          name(module, chunks, cacheGroupKey) { // 分离出模块文件名
            const allChunksNames = chunks.map((item) => item.name).join('~') // 模块名1~模块名2
            return `./js/${allChunksNames}` // 输出到 dist 目录下位置
          }
        }
      }
    }
  },
  // 解析
  resolve: {
    // 别名
    alias: {
      '@': path.resolve(__dirname, 'src')
    }
  }
}
// 开发环境下使用 sourcemap 选项
if (process.env.NODE_ENV === 'development') {
  config.devtool = 'inline-source-map'
}
// 生产环境下使用相关配置
if (process.env.NODE_ENV === 'production') {
  // 外部扩展(让 webpack 防止 import 的包被打包进来)
  config.externals = {
    // key:import from 语句后面的字符串
    // value:留在原地的全局变量(最好和 cdn 在全局暴露的变量一致)
    'bootstrap/dist/css/bootstrap.min.css': 'bootstrap',
    'axios': 'axios',
    'form-serialize': 'serialize',
    '@wangeditor/editor': 'wangEditor'
  }
}

module.exports = config
Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐