前端工程化的发展(3):打包工具时代(Webpack/Rollup/Parcel)

83 阅读18分钟

2015 前后 打包工具时代(Webpack / Rollup / Parcel)

初步认识

问题背景(为什么要有打包工具?)

Grunt/Gulp 的“任务流”阶段,虽然能自动执行压缩、合并,但依然存在严重问题:

  1. 模块化需求爆炸
    • ES6 Module(2015)被正式提出。
    • Node.js 已经用 CommonJS 模块,前端也想要模块化。
    • 浏览器却不支持(那时还不能直接 <script type="module">)。
      👉 必须有工具来处理 依赖关系
  2. 依赖复杂
    • 一个文件可能 import N 个依赖,每个依赖又有更多依赖。
    • 手工合并不现实,需要 自动构建依赖图(Dependency Graph)
  3. 性能问题
    • 项目越来越大,加载几十上百个 JS 文件 → 网络请求太多。
    • 需要合并成少量 bundle 文件,减少请求。
  4. 前端全面“工程化”
    • Vue/React/Angular 兴起,需要单文件组件(.vue.jsx)。
    • 需要 预处理器(Sass、TypeScript、Babel)。
    • Gulp/Grunt 只是流水线,不会“理解”这些复杂结构。

👉 总结:
前端不只是要自动化,还要“打包” —— 把所有资源(JS/CSS/图片/字体)视为模块,统一管理和输出。


解决方案:打包工具的出现

1. Webpack(2014~2015 崛起)
  • 核心思想
    一切皆模块(JS、CSS、图片、字体...)。
    通过构建 依赖图(Dependency Graph),输出少量 bundle。
  • 关键能力
    • Loader:处理各种非 JS 资源(.css.ts.vue)。
    • Plugin:扩展功能(压缩、优化、代码拆分)。
    • HMR(热更新):只刷新改动部分。
    • Code Splitting:按需加载,避免单文件过大。

👉 Webpack 成为前端的“工业级工厂”,几乎统治了 2015~2020 年。


2. Rollup(2015)
  • 定位:专注 库/框架打包,不是应用。
  • 特点
    • 天生支持 ES Module(tree-shaking 优化比 Webpack 更干净)。
    • 输出格式灵活(ESM、CJS、UMD),适合发布 npm 包。
    • 配置简单,但缺少 Webpack 的“全家桶”功能。

👉 Vue、React、D3.js 等库都用 Rollup 打包。


3. Parcel(2017)
  • 特点:零配置上手快,自动识别依赖。
  • 亮点
    • 内置 HMR、自动拆分、内置 Babel/PostCSS。
    • 不需要复杂的 webpack.config.js。
  • 局限:灵活度比 Webpack 低,在社区热度逐渐被 Vite 抢走。

打包工具解决了什么?

  1. 模块化统一
    • 把 CommonJS、AMD、ESM 都能处理。
    • 输出浏览器能直接运行的 bundle。
  2. 减少请求
    • 合并 JS/CSS → 减少 HTTP 请求,提高性能。
  3. 工程能力提升
    • TypeScript → JS
    • Sass/Less → CSS
    • Vue/React 单文件组件 → 普通 JS/CSS/HTML
      👉 所有非原生资源都能被“编译成浏览器能跑的东西”。
  4. 优化生产环境
    • Tree-shaking:删除无用代码。
    • Code Splitting:按需加载。
    • 压缩(Terser、cssnano)。
    • Hash 命名:缓存优化。

阶段总结

  • Grunt/Gulp(2012~2014)
    自动化任务执行器(压缩、合并、刷新)。
    👉 解决“体力活”,但不懂模块化。
  • Webpack/Rollup/Parcel(2015~)
    打包工具,构建依赖图,把一切都当模块处理。
    👉 解决“模块化 + 工程化”,成为现代前端的核心。

总结:
打包工具让前端真正进入“工业化生产”时代。
它们不再只是“工人(任务流)”,而是“工厂(依赖图 + 全流程处理)”。


一切皆模块

前置思考

什么是“ 一切皆模块(JS、CSS、图片、字体...)”?模块化不是用于解决资源复用和避免全局污染的吗?这模块指的是什么?什么可以被称之为模块?怎么做到一切皆模块的?


最初的“模块”概念

最早在 JavaScript 语言层面,模块指的是 一块独立的代码,目的是解决两个问题:

  1. 复用:把常用逻辑抽出来,可以被不同文件引用。
  2. 隔离作用域:避免全局变量污染。

例子:

// math.js
export function add(a, b) { return a + b }

// main.js
import { add } from './math.js'
console.log(add(2, 3))

这里的模块就是 JavaScript 文件


工程化后的“模块”扩展

后来,随着前端应用越来越复杂,问题出现了:

  • JS 文件不仅依赖其他 JS,还依赖 CSS、图片、字体、JSON 配置 等。
  • 如果每种资源都要用不同方式引入,管理混乱。

于是,Webpack、Rollup 这样的工具提出:
👉 既然模块的本质就是“一个独立、可被引用的资源单元”,那么为什么只限于 JS 呢?

于是扩展出:

  • JS 模块:逻辑功能。
  • CSS 模块:样式规则。
  • 图片模块:图像资源(被 import 时转成 URL 或 Base64)。
  • 字体模块:字形文件(同样转成 URL)。
  • JSON 模块:配置数据。

所以,在工程化语境下,“模块”指的是:
任何可以被依赖、被引用,并在最终构建产物里有确定表现的资源单元。


怎么做到“一切皆模块”的?

① 在构建工具出现之前
  • 浏览器原生只能识别三种资源:
    1. HTML → 用 <link> 加 CSS、用 <script> 加 JS。
    2. CSS → 用 url(...) 引入图片或字体。
    3. JS → 只能自己写逻辑,没“模块”概念(早期只有全局变量)。

👉 在这个阶段,模块只局限于 JS 的逻辑文件,其他资源只是“外链文件”。
CSS、图片、字体,它们在浏览器眼中就是“文件”,不是“模块”。


② 构建工具带来的变化

Webpack、Rollup、Vite 出现后,它们引入了一个关键机制:

“统一依赖图(Dependency Graph)”

  • 它把 所有 import/require 的东西都视为依赖节点,不管是 .js.css.png
  • 只要构建工具能“理解并处理”它,就能纳入依赖图。

📌 举例:

import './style.css'   // 被 CSS loader 转译成 JS 注入 <style>
import logo from './logo.png' // 被 file loader 转换为 URL
import data from './data.json' // 直接变成 JS 对象

这些原本浏览器“不懂”的 import,经过 Loader/Plugin 转换 → 最终都变成 JS 能理解的内容(URL、对象、字符串…),从而能和 JS 一起纳入 同一个依赖体系


③ 所以,“模块”定义发生了扩展
  • 过去:模块 = JS 文件(语言级别限制)。
  • 现在:模块 = 能被依赖、能被构建工具解析的资源单元

换句话说:
“模块”不再是语言层面的概念,而是工程化层面的概念。


④ 为什么以前不行,现在可以?

变化点就在于:

  1. 浏览器本身没这个能力 → 它只认 <link><script><img>
  2. 构建工具做了“预处理” → 在最终送到浏览器之前,把所有非 JS 资源转成浏览器能懂的东西。
    • CSS → 被转成 JS 注入 <style>
    • 图片/字体 → 被转成 URL 或 base64,再由 JS/HTML 使用。
    • JSON → 被转成 JS 对象。

因此,现在你能在代码里 import 图片,但这是因为 构建工具帮你打了“翻译”补丁


⑤ 核心变化

变化点 = 模块的定义从“语言层面”扩展到了“构建工具层面”。
过去,浏览器只认 JS 模块(如何认识JS模块可回顾模块化的发展);现在,借助构建工具,任何文件都能被“翻译成 JS 世界能理解的依赖”,因此都能叫“模块”。


非JS类型具体如何模块化

前置思考

前面讲到了“一切皆模块”这个概念,但是在细节上依然不够清晰。JS的模块化,逻辑上比较容易理解,是按需引入JS,然后避免不同JS文件的相互干扰,但是像CSS、图片、JSON等资源,又是怎么模块化的?


① CSS 模块化

过去的问题

  • CSS 是全局作用域的:
    • 一个 .btn {} 写在 A.css,可能影响 B 页面。
    • 没有“按需加载”机制,要么写在一个大 CSS 文件,要么每个页面自己 <link>

模块化之后(借助构建工具 + 规范)

  • 按需引入
import './Button.css'   // 只在用到 Button 组件时才会被打包
  • 作用域隔离(比如 CSS Modules):
/* Button.module.css */
.btn { color: red; }
import styles from './Button.module.css'
<button className={styles.btn}>OK</button>

→ 工具会把 .btn 编译成 .btn_xxxHash,避免全局污染。

👉 总结:CSS 模块化解决了“作用域污染 + 无法按需引入”的问题。


② 图片/字体 模块化

过去的问题

  • 图片、字体只能写死路径:
<img src="/static/logo.png">
  • 难以做缓存、版本管理(换图片后用户浏览器可能还用旧缓存)。

模块化之后

  • 可以直接在 JS 中 import
import logo from './logo.png'
<img src={logo}>
  • 构建工具会:
    • 给文件加 hash:logo.8f3ad.png(缓存更新)。
    • 小图片转成 base64,避免额外请求。
    • 大图片单独拎出来按需加载。

👉 总结:图片/字体模块化解决了“缓存管理 + 按需使用 + 依赖统一”的问题。


③ JSON 模块化

过去的问题

  • 想在前端用 JSON,要么写死在 JS 里,要么 AJAX 请求额外文件。

模块化之后

  • 直接 import:
import data from './config.json'
console.log(data.apiBaseUrl)
  • 构建工具会把 JSON 编译成:
export default { "apiBaseUrl": "https://xxx" }

👉 总结:JSON 模块化解决了“本地配置/数据文件能直接作为依赖使用”的问题。


统一的本质

无论是 JS、CSS、图片还是 JSON,模块化的本质都是:

  1. 按需引入 → 依赖图里有才打包。
  2. 作用域隔离 → 不影响其他模块。
  3. 可被构建工具管理 → 能做缓存优化、代码分割、压缩优化。

也就是说,模块化不是某种文件的天然属性,而是“被工具接管后,文件被纳入统一依赖体系”


Node.js入门

什么是Node.js?

  1. Node.js 是一个独立的 JavaScript 运行环境,能独立执行 JS 代码,因为这个特点,它可以用来编写服务器后端的应用程序
  2. Node.js 作用除了编写后端应用程序,也可以对前端代码进行压缩,转译,整合等等,提高前端开发和运行效率
  3. Node.js 基于Chrome V8 引擎封装,独立执行 JS 代码,但是语法和浏览器环境的 V8 有所不同,没有 document 和 window 但是都支持 ECMAScript 标准的代码语法
  4. 想要得到 Node.js 需要把这个软件安装到电脑,在素材里有安装程序(window 和 mac 环境的)参考 PPT 默认下一步安装即可

Node.js 没有图形化界面,需要使用 cmd 终端命令行(利用一些命令来操控电脑执行某些程序软件)输入,node -v 检查是否安装成功

  1. node -v
  2. 需求:新建 index.js 文件,编写打印代码和 for 循环打印 3 个 6
/**
  * 目标:编写 js 代码,用 node 命令执行
  * 终端作用:敲击命令,调用对应程序执行
  * 终端打开:目标文件->右键->在集成终端中打开
  * 命令:node xxx.js (注意路径)
  */
 console.log('Hello, World')
 for (let i = 0; i < 3; i++) {
   console.log(6)
 }
  1. Node.js 执行目标 JS 文件,需要使用 node xxx.js 命令来执行(我们可以借助 VSCode 集成终端使用,好处:可以快速切换到目标 JS 文件所在终端目录,利用相对路径找到要执行的目标 JS 文件

fs模块-读写文件

  1. 模块:类似插件,封装了方法和属性供我们使用
  2. fs 模块:封装了与本机文件系统进行交互的,方法和属性
  3. fs 模块使用语法如下:

加载 fs 模块,得到 fs 对象

-  const fs = require('fs')
- 写入文件内容语法:
fs.writeFile('文件路径', '写入内容', err => {
   // 写入后的回调函数
 })
- 读取文件内容的语法:
fs.readFile('文件路径', (err, data) => {
   // 读取后的回调函数
   // data 是文件内容的 Buffer 数据流
 })
  1. 需求:向 test.txt 文件写入内容并读取打印
/**
  * 目标:使用 fs 模块,读写文件内容
  * 语法:
  * 1. 引入 fs 模块
  * 2. 调用 writeFile 写入内容
  * 3. 调用 readFile  读取内容
  */
// 1. 引入 fs 模块
const fs = require('fs')
// 2. 调用 writeFile 写入内容
// 注意:建议写入字符串内容,会覆盖目标文件所有内容
fs.writeFile('./text.txt', '欢迎使用 fs 模块读写文件内容', err => {
  if (err) console.log(err)
  else console.log('写入成功')
})
// 3. 调用 readFile  读取内容
fs.readFile('./text.txt', (err, data) => {
  if (err) console.log(err)
  else console.log(data.toString()) // 把 Buffer 数据流转成字符串类型
})

path模块-路径处理

  1. 为何需要路径处理?
  • 在 Node.js 待执行的 JS 代码中要用绝对路径,这是因为Node.js 执行 JS 代码时,代码中的路径都是以终端所在文件夹出发查找相对路径,而不是以我们认为的从代码本身出发,会遇到问题,所以在 Node.js 要执行的代码中,访问其他文件,建议使用绝对路径
const fs = require('fs')
console.log(__dirname) // D:\备课代码\2_node_3天\Node_代码\Day01_Node.js入门\代码\03

// 1. 加载 path 模块
const path = require('path')
// 2. 使用 path.join() 来拼接路径
const pathStr = path.join(__dirname, '..', 'text.txt')
console.log(pathStr)

fs.readFile(pathStr, (err, data) => {
  if (err) console.log(err)
  else console.log(data.toString())
})
  1. path.join() 方法有什么用?
  • 按照所在本机系统的分隔符作为定界符来链接你传入的路径
  1. __dirname模块内置变量的值是多少?
  • 动态获取当前文件所在文件夹的绝对路径

文件压缩的原理

html文件压缩
  1. 前端工程化:前端代码压缩,整合,转译,测试,自动部署等等工具的集成统称,为了提高前端开发项目的效率
  2. 需求:把准备好的 html 文件里的回车符(\r)和换行符(\n)去掉进行压缩,写入到新 html 中
  3. 步骤:
    1. 读取源 html 文件内容
    2. 正则替换字符串
    3. 写入到新的 html 文件中,并运行查看是否能正常打开网页
  4. 代码如下:
/**
  * 目标一:压缩 html 里代码
  * 需求:把 public/index.html 里的,回车/换行符去掉,写入到 dist/index.html 中
  *  1.1 读取 public/index.html 内容
  *  1.2 使用正则替换内容字符串里的,回车符\r 换行符\n
  *  1.3 确认后,写入到 dist/index.html 内
  */
const fs = require('fs')
const path = require('path')
// 1.1 读取 public/index.html 内容
fs.readFile(path.join(__dirname, 'public', 'index.html'), (err, data) => {
  const htmlStr = data.toString()
  // 1.2 使用正则替换内容字符串里的,回车符\r 换行符\n
  const resultStr = htmlStr.replace(/[\r\n]/g, '')
  // 1.3 确认后,写入到 dist/index.html 内
  fs.writeFile(path.join(__dirname, 'dist', 'index.html'), resultStr, err => {
    if (err) console.log(err)
    else console.log('压缩成功')
  })
})

js文件压缩
  1. 需求:把准备好的 JS 文件代码的回车符,换行符,打印语句去掉,并插入到之前 html 内容之后
  2. 步骤:
    1. 读取 js 文件内容
    2. 正则表达式替换回车符,换行符,打印语句为空字符串
    3. 拼接 html 代码和 js 代码,写入到新的 html 文件中
  3. 代码如下:
/**
  * 目标二:压缩 js 里代码,并整合到 html 中一起运行
  *  2.1 读取 public/index.js 内容
  *  2.2 使用正则替换内容字符串里的,回车符\r 换行符\n 打印语句console.log('xxx');
  *  2.3 确认后,拼接 html 内容写入到 dist/index.html 内
  */
const fs = require('fs')
const path = require('path')
fs.readFile(path.join(__dirname, 'public', 'index.html'), (err, data) => {
  const htmlStr = data.toString()
  const resultStr = htmlStr.replace(/[\r\n]/g, '')

  // 2.1 读取 public/index.js 内容
  fs.readFile(path.join(__dirname, 'public', 'index.js'), (err, data) => {
    const jsStr = data.toString()
    // 2.2 使用正则替换内容字符串里的,回车符\r 换行符\n 打印语句console.log('xxx');
    const jsResultStr = jsStr.replace(/[\r\n]/g, '').replace(/console.log\('.+?'\);/g, '')
    const result = `<script>${jsResultStr}</script>`
    console.log(result)

    // 2.3 确认后,拼接 html 内容写入到 dist/index.html 内
    fs.writeFile(path.join(__dirname, 'dist', 'index.html'), resultStr + result, err => {
      if (err) console.log(err)
      else console.log('压缩成功')
    })
  })


})

基于http模块创建Web服务

基本实现
  1. 需求:引入 http 模块,使用相关语法,创建 Web 服务程序,响应返回给请求方一句提示 ‘hello,world’
  2. 步骤:
    1. 引入 http 模块,创建 Web 服务对象
    2. 监听 request 请求事件,对本次请求,做一些响应处理
    3. 启动 Web 服务监听对应端口号
    4. 运行本服务在终端进程中,用浏览器发起请求
  3. 注意:本机的域名叫做 localhost
  4. 代码如下:
/**
  * 目标:使用 http 模块,创建 Web 服务
  * Web服务:一个程序,用于提供网上信息浏览服务
  * 步骤:
  *  1. 引入 http 模块,创建 Web 服务对象
  *  2. 监听 request 事件,对本次请求,做一些响应处理
  *  3. 启动 Web 服务监听对应端口号
  *  4. 运行本服务在终端,用浏览器访问 http://localhost:3000/ 发起请求(localhost 是本机域名)
  * 注意:终端里启动了服务,如果想要终止按 ctrl c 停止即可
  */
// 1. 引入 http 模块,创建 Web 服务对象
const http = require('http')
const server = http.createServer()
// 2. 监听 request 事件,对本次请求,做一些响应处理
server.on('request', (req, res) => {
  res.end('hello, world') // 一次请求只能对应一次响应
})
// 3. 启动 Web 服务监听对应端口号
server.listen(3000, () => {
  console.log('Web 服务启动了')
})

设置支持中文字符
  1. 需求:让 Web 服务返回中文字符,浏览器能正确加载解析

  2. 步骤:给 Web 服务程序添加响应头,设置内容类型和正确的编码格式,重启 Web 服务测试访问即可

res.setHeader('Content-Type', 'text/html;charset=utf-8')
  1. 编码:编码是信息从一种形式或格式转换为另一种形式的过程,指的把文字在计算机里的二进制数据,用什么形式展示出来

  2. utf-8编码:是一种关系映射表,也叫 utf-8 编码表,可以把中文,英文等等很多字符准确的展示出来


提供数据接口
案例1——后端代码工作过程
  1. 需求:基于 Web 服务,开发提供省份列表数据的接口,了解下后端的代码工作过程
  2. 步骤:
    1. 基于 http 模块,创建 Web 服务
    2. 使用 req.url 获取请求资源路径,并读取 province.json 理论省份数据返回给请求方
    3. 其他路径,暂时返回不存在的提示
    4. 运行 Web 服务,用浏览器发起请求测试,看是否可以获取到省份列表数据
  3. 代码如下:
/**
  * 目标:基于 Web 服务,开发-省份列表数据接口
  * 步骤:
  *  1. 创建 Web 服务
  *  2. 使用 req.url 获取请求的资源路径,读取 json 文件数据返回
  *  3. 其他请求的路径,暂时返回不存在的提示
  *  4. 运行 Web 服务,用浏览器请求地址查看效果
  */
const fs = require('fs')
const path = require('path')
// 1. 创建 Web 服务
const http = require('http')
const server = http.createServer()
server.on('request', (req, res) => {
  // 2. 使用 req.url 获取请求的资源路径,读取 json 文件数据返回
  if (req.url === '/api/province') {
    fs.readFile(path.join(__dirname, 'data/province.json'), (err, data) => {
      res.setHeader('Content-Type', 'application/json;charset=utf-8')
      res.end(data.toString())
    })
  } else {
    // 3. 其他请求的路径,暂时返回不存在的提示
    res.setHeader('Content-Type', 'text/html;charset=utf-8')
    res.end('你要访问的资源路径不存在')
  }
})
server.listen(3000, () => {
  console.log('Web 服务启动了')
})

案例2——查询参数如何传递给后端
  1. 需求:基于刚刚的 Web 服务,开发提供城市列表数据的接口,了解下后端代码的工作过程
  2. 步骤:
    1. 判断 req.url 资源路径+查询字符串,路径前缀匹配 /api/city
    2. 借助 querystring 模块的方法,格式化查询字符串
    3. 读取 city.json 城市数据,匹配省份名字下属城市列表
    4. 返回城市列表,启动 Web 服务测试
  3. 代码如下:
/**
  * 目标:基于 Web 服务,开发-城市列表数据接口
  * 步骤:
  *  1. 判断 req.url 资源路径+查询字符串,路径前缀匹配/api/city
  *  2. 借助 querystring 模块的方法,格式化查询参数字符串
  *  3. 读取 city.json 城市数据,匹配省份名字下属城市列表
  *  4. 返回城市列表,启动 Web 服务测试
  */
const qs = require('querystring')
const fs = require('fs')
const path = require('path')
const http = require('http')
const server = http.createServer()
server.on('request', (req, res) => {
  // 省份列表接口
  if (req.url === '/api/province') {
    fs.readFile(path.join(__dirname, 'data/province.json'), (err, data) => {
      res.setHeader('Content-Type', 'application/json;charset=utf-8')
      res.end(data.toString())
    })
    // 1. 判断 req.url 资源路径+查询字符串,路径前缀匹配/api/city
  } else if (req.url.startsWith('/api/city')) {
    // 城市列表接口
    // 2. 借助 querystring 模块的方法,格式化查询参数字符串
    // req.url: '/api/city?pname=辽宁省'
    // 以?分隔符分割,拿到'pname=辽宁省'查询参数字符串
    const str = req.url.split('?')[1]
    // 把查询参数字符串 转成 JS 对象结构
    const query = qs.parse(str)
    // 获取前端发来的省份名字
    const pname = query.pname
    // 3. 读取 city.json 城市数据,匹配省份名字下属城市列表
    fs.readFile(path.join(__dirname, 'data/city.json'), (err, data) => {
      // 把 JSON 文件内对象格式字符串,转成对象结构
      const obj = JSON.parse(data.toString())
      // 省份名字作为 key,去obj对象里取到对应城市列表 value 值
      const cityList = obj[pname]
      // 4. 返回城市列表,启动 Web 服务测试
      // 响应的是 JSON 字符串内容
      res.setHeader('Content-Type', 'application/json;charset=utf-8')
      res.end(JSON.stringify(cityList))
    })
  } else {
    res.setHeader('Content-Type', 'text/html;charset=utf-8')
    res.end('你要访问的资源路径不存在')
  }
})
server.listen(3000, () => {
  console.log('Web 服务启动了')
})

提供网页资源
  1. 需求:基于 Web 服务,开发提供网页资源的功能,了解下后端的代码工作过程
  2. 步骤:
    1. 基于 http 模块,创建 Web 服务
    2. 使用 req.url 获取请求资源路径为 /index.html 的时候,读取 index.html 文件内容字符串返回给请求方
    3. 其他路径,暂时返回不存在的提示
    4. 运行 Web 服务,用浏览器发起请求
  3. 代码如下:
/**
  * 目标:编写 web 服务,监听请求的是 /index.html 路径的时候,返回 dist/index.html 时钟案例页面内容
  * 步骤:
  *  1. 基于 http 模块,创建 Web 服务
  *  2. 使用 req.url 获取请求资源路径,并读取 index.html 里字符串内容返回给请求方
  *  3. 其他路径,暂时返回不存在提示
  *  4. 运行 Web 服务,用浏览器发起请求
  */
const fs = require('fs')
const path = require('path')
// 1. 基于 http 模块,创建 Web 服务
const http = require('http')
const server = http.createServer()
server.on('request', (req, res) => {
  // 2. 使用 req.url 获取请求资源路径,并读取 index.html 里字符串内容返回给请求方
  if (req.url === '/index.html') {
    fs.readFile(path.join(__dirname, 'dist/index.html'), (err, data) => {
      res.setHeader('Content-Type', 'text/html;charset=utf-8')
      res.end(data.toString())
    })
  } else {
    // 3. 其他路径,暂时返回不存在提示
    res.setHeader('Content-Type', 'text/html;charset=utf-8')
    res.end('你要访问的资源路径不存在')
  }
})
server.listen(8080, () => {
  console.log('Web 服务启动了')
})

基于Express搭建Web服务

  1. Express定义链接: 基于 Node.js 平台,快速,开放,极简的 Web 开发框架
  2. 概念:使用 express 本地软件包,快速搭建 Web 服务(基于 http 模块)
  3. 功能:开发 Web 服务,提供数据接口,提供网页资源供浏览器使用
  4. 需求:基于 express 编写 Web 服务,对 get 请求方法和 / 路径监听,有人请求返回一段提示字符串
  5. 使用:
    1. 下载 express 本地软件包到项目中
    2. 导入 express 创建 Web 服务对象
    3. 监听请求方法和请求路径,返回一段提示字符串
    4. 对其他请求方法和请求路径,默认返回 404 提示
    5. 监听端口号,启动 Web 服务,在浏览器请求测试
  6. 代码如下:
/**
  * 目标:基于 express 本地软件包,开发 Web 服务,返回资源给请求方
  */
// 1. 下载 express 软件包
// 2. 导入并创建 Web 服务对象
const express = require('express')
const server = express()

// 3. 监听请求的方法和请求的资源路径
server.get('/', (req, res) => {
  res.send('你好,欢迎使用 Express')
})

// 4. 监听任意请求的方法和请求的资源路径
server.all('*', (req, res) => {
  res.status(404)
  res.send('你要访问的资源路径不存在')
})

// 5. 监听端口号,启动 Web 服务
server.listen(3000, () => {
  console.log('Web 服务已启动')
})
提供数据接口
  1. 需求:基于 express,开发提供省份列表数据的接口

  2. 步骤:监听 get 请求方法的 /api/province 路径,并读取 province.json 里省份数据返回给请求方

  3. 核心代码:

// 监听 get 请求方法,监听资源路径 /api/province,就读取 province.json 省份数据返回
server.get('/api/province', (req, res) => {
  fs.readFile(path.join(__dirname, 'data/province.json'), (err, data) => {
    res.send(data.toString())
  })
})

跨域问题

什么是跨域?
  1. 跨域:从一个源的文档/脚本,加载另一个源的资源产生了跨域
  2. 例如:网页文档打开时所在源和 AJAX 访问的源(协议,域名,端口)有一个不同,就发生了跨域访问
  3. 网页出现跨域访问时,会在浏览器控制里报错如下:

Access to XMLHttpRequest 意思为:XHR 链接出了问题,从一个源(http://localhost:5500 跨域访问 http://localhost:3000

  1. 需求:在 LiveServer 的 Web 服务启动网页,用 AJAX 访问本机 Web 服务提供的省份列表接口,体验下跨域问题,index.html 代码如下:
// 目标:请求本机 Web 服务提供的省份列表数据
 axios({
   url: 'http://localhost:3000/api/province',
   // method: 'get'
 }).then(res => {
   console.log(res)
 }).catch(err => {
   console.log(err)
 })

解决方案1-CORS
  1. 目标:前后端分离的项目,前端和后端不在一个源,还要保证数据通信

  2. 解决:采用 CORS (跨域资源共享),一种基于 HTTP 头的机制,该机制通过允许服务器标示除了它自己以外的其他源(域、协议或端口),来访问加载服务器上的资源

  3. 思路:

  • 服务器端:设置 Access-Control-Allow-Origin 响应头字段,允许除了它自己以外的源来访问自己的资源
  • 前端:正常发起 AJAX 请求,无需额外操作
  1. 步骤:

    1. 下载 cors 本地软件包

    2. 导入 cors 函数

    3. 使用 server.use() 给 Web 服务添加插件功能

    4. 把 cors 函数调用传入给 Web 服务,启动测试

  2. 设置响应头代码

// 2. 导入 cors 函数
const cors = require('cors')
// 3. 使用 server.use() 给 Web 服务添加插件功能
server.use(cors())

解决方案2-同源访问
  1. 目标:开发环境用 cors,上线部署关闭 cors,并采用同源访问方式
  2. 做法:让后端 Web 服务即可以提供数据接口,也可以返回网页资源
  3. 好处:安全,后端的接口不允许非同源来访问
  4. 代码:express 设置暴露 public 文件夹作为静态资源目录,供浏览器直接访问,可以访问里面的 html 网页
// 暴露指定的文件夹,让前端可以直接拼接路径和资源名字来访问
 server.use(express.static(path.join(__dirname, 'public')))
总结
为何要解决跨域
  • 因为前后端分离的项目,不在同一个源去开发项目,需要保证数据之间通信
CORS 如何实现跨域的?只适用于什么阶段的项目?
  • 让后端开启 CORS 跨域资源共享,在响应头设置 Access-Control-Allow-Origin: '*'
  • CORS只适用于本地开发阶段项目
项目上线,如何解决跨域问题?
  • 把前端项目和后端项目部署到同一个源下访问

Webpack时期的开发环境 vs 生产环境

为什么要区分环境?

Webpack(2015 前后崛起) 的语境下,前端已经进入了 模块化 + 打包工具化 的时代,项目规模越来越大,前端开发和上线部署的需求差异明显:

  • 开发环境(Dev):快速迭代,调试友好。
  • 生产环境(Prod):体积最小、性能最佳、加载最快、安全可靠。

👉 换句话说,同一份代码,在不同场景下需要不同的构建策略,于是 Webpack 引入了 mode 概念(development / production)。


Webpack 的两套模式

1. 开发环境(Development)

目标:提升开发体验,快速反馈。
常见特性:

  • 源码调试友好devtool: 'cheap-module-source-map' 生成 Source Map,方便定位错误到源码。
  • 无压缩、无优化:不做 Uglify、Tree-shaking,保证构建速度。
  • 模块热替换(HMR):只更新改动的模块,页面状态不丢失。
  • DevServer 本地服务:起一个 Node.js 服务器,支持 Live Reload / API 代理。

👉 开发环境优先考虑“快”,哪怕打包结果大点、不优化都没关系。


2. 生产环境(Production)

目标:产出最优上线资源。
常见特性:

  • 代码压缩:UglifyJS / Terser 压缩 JS。
  • Tree-shaking:移除未使用的代码。
  • 代码分割(Code Splitting):按需加载,减少首屏体积。
  • 长效缓存优化:输出 bundle.[contenthash].js,结合浏览器缓存策略。
  • 静态资源优化:CSS 提取(MiniCssExtractPlugin)、图片压缩。
  • 环境变量替换process.env.NODE_ENV 被替换成 "production",让代码走优化逻辑。

👉 生产环境优先考虑“性能”,哪怕构建慢一些也值得。


为什么需要这种区分?

我们可以从 痛点演化 来看:

  1. 早期(无区分)
    Grunt/Gulp 时代基本只有一份构建脚本,想要调试就注释掉压缩插件,上线再手动开启,非常麻烦。
  2. Webpack 设计
    官方直接定义了 mode: 'development' | 'production',帮开发者内置了两套“最佳实践”:
    • 开发:快 → Source Map、无压缩。
    • 生产:优 → 压缩、Tree-shaking、缓存。
  3. 工程化思想
    区分环境意味着:同一个源码,可以按不同目标产出多份优化后的产物,适配“开发-调试”和“用户-体验”的双重需求。

总结(核心对比)

维度DevelopmentProduction
构建速度快,几秒钟慢,可能几十秒
体积大,不压缩小,压缩/拆分/优化
调试体验有 Source Map,错误直指源码Source Map 精简甚至关闭,调试不友好
热更新支持 HMR不支持 HMR
缓存不关注缓存使用 contenthash
长效缓存
插件侧重点DevServer, HMR压缩、优化、提取 CSS

一句话总结:
👉 开发环境是为了“开发者体验”生产环境是为了“用户体验”


为什么库和框架还需要打包

从代码实现的角度看

1. 源码不是最终能直接使用的形式

大多数库/框架的源码并不是「浏览器 / Node 能直接跑」的:

  • 用 TypeScript 写的 → 需要转成 JS。
  • 用 ES6+ 写的 → 需要转成兼容的 ES5(兼容旧环境)。
  • 用 JSX/TSX 写的(React/Vue) → 需要转译成 JS 函数调用。
  • 用模板语法写的(Vue SFC、Angular 模板) → 需要编译成 JS 渲染函数。

👉 也就是说,源码本身无法直接被消费,必须打包成「可执行的 JS」


2. 模块系统不统一,需要兼容

现实情况是:不同的运行环境,支持的模块规范不一样:

  • Node.js (老版本) → CommonJS (require)
  • 现代浏览器 → ES Module (import)
  • 老浏览器 → 不支持模块,需要 UMD/全局变量

如果库作者只写一份源码(比如 ESModule),直接发出去,很多用户的项目可能无法用。
所以库需要打包成 多份产物(CJS / ESM / UMD),确保「无论在 Node、Webpack、Vite 还是 <script> 标签里」都能用。


3. 内部依赖需要处理

库往往依赖其他第三方库:

  • 有些依赖需要 内联打进产物(比如一个很小的工具函数,用户不需要单独装)。
  • 有些依赖需要 标记为 external(外部依赖,比如 React,不能强行打进 Vue 产物)。

👉 这些依赖关系必须在打包时处理清楚,否则使用者会遇到「重复依赖」「版本冲突」等问题。


4. Tree Shaking 友好性

比如 lodash 源码可能写成一大坨工具函数:

// 源码(简化)
export function cloneDeep(...) { ... }
export function debounce(...) { ... }
export function throttle(...) { ... }

如果不打包处理,最终用户 import 一个函数,可能会整个文件都进 bundle,导致冗余。
库打包时(借助 Rollup 等)会把模块边界优化,使得 Tree Shaking 能生效,用户只会引入自己用到的部分。


5. 统一压缩 & 产物优化

源码里可能有:

  • 注释、调试信息、测试代码
  • 多余的空格、未优化的表达式

这些在「库发布」时必须清理掉,否则使用者项目里会白白变大。
所以库打包时会:

  • 压缩 JS(Terser/esbuild)
  • 去掉无用代码
  • 产出一个「最小可用体积」的版本

从使用者的角度看

早期(jQuery、Zepto、Underscore.js 那个年代),库都是直接发布一个 .js 文件,用户 <script> 引入即可。但随着 模块化标准 的发展,情况复杂了:

  • 不同的模块规范并存
    • Node.js 世界:CommonJS(require()
    • 浏览器 AMD/UMD:RequireJS 等
    • 现代浏览器 / 规范:ESM(import/export

👉 如果一个库只提供一种格式(比如只支持 ES Modules),那么:

  • 旧的 Node 环境就无法直接使用。
  • 打包工具(Webpack、Rollup)可能不兼容。

因此库需要通过打包,产出多种“构建产物”(CJS、ESM、UMD),以便不同环境都能使用。


实际场景

以几个实际例子说明:

  • React / Vue
    • 提供多种构建产物:cjs.jsesm.jsvue.global.js,用户可以:
      • Node 环境直接 require('vue')
      • 现代打包工具 import { ref } from 'vue'
      • <script src="vue.global.js"> 直接在浏览器跑
  • Lodash
    • 发布了 lodash(打包好的版本)
    • 也有 lodash-es(纯 ES Module 版本),方便 Tree-shaking
  • Axios
    • 提供 UMD 构建 → <script src="axios.min.js"> 可直接用
    • 也支持 CommonJS / ESM,引入更灵活

👉 可以看到,打包的根本原因就是:库要适配各种运行环境,保证可用性、性能和体积控制


总结

应用打包是为了性能优化(减少请求、缩小体积),库打包是为了分发与兼容性(多环境适配、Tree-shaking、依赖管理、减少使用者负担)。


应用 vs 库打包对比表

对比维度应用打包(App Bundle)库打包(Library Bundle)
主要目的提升运行性能(减少请求数、缩小体积、加快加载)提升分发兼容性(适配不同环境、减少用户负担)
目标用户最终用户(直接在浏览器跑应用)开发者(安装库 / 框架作为依赖)
入口形式单一入口(index.js
/ main.js
/ index.html
多构建产物(CJS、ESM、UMD 等)
对 Tree-shaking 的依赖有,但更多依赖打包工具整体优化极其重要,用户只想用库的一部分功能
资源整合强调合并压缩:JS、CSS、图片、字体等都打到一起,减少 HTTP 请求一般保持模块粒度,避免冗余,但保证能被 Tree-shaking
依赖处理把所有依赖都打进去(应用必须自给自足)常把依赖标记为 external,避免用户重复打包(如 React 不会内置 Vue)
输出产物通常是一个或少数几个 bundle(适配生产环境)多格式产物:xxx.cjs.js
xxx.esm.js
xxx.umd.js
代码优化重点压缩、拆分(code-splitting)、懒加载、缓存优化保持源码清晰、模块边界明确,方便 Tree-shaking
典型工具Webpack、Vite、ParcelRollup(最常见)、也可用 esbuild / tsup 等
典型案例淘宝前端应用、B 站网页、后台管理系统Vue、React、Axios、Lodash

总结

  • 应用打包:关心最终页面能否 流畅
  • 库打包:关心库能否在 各种环境里顺利被使用,同时保持 轻量和可按需引入

Webpack工具深入

什么是WebPack?

  1. Webpack 是一个静态模块打包工具,从入口构建依赖图,打包有关的模块,最后用于展示你的内容
  2. 静态模块:编写代码过程中的,html,css, js,图片等固定内容的文件
  3. 打包过程,注意:只有和入口有直接/间接引入关系的模块,才会被打包
  4. Webpack 的作用:把静态模块内容,压缩,这个和,转译等(前端工程化)
    • 把 less/sass 转成 css 代码
    • 把 ES6+ 降级成 ES5 等
    • 支持多种模块文件类型,多种模块标准语法
  5. 为何不学 vite?

现在很多项目还是基于 Webpack 来进行构建的,所以还是要掌握 Webpack 的使用

  1. 体验 Webpack 打包 2 个 JS 文件内容
  2. 需求:封装 utils 包,校验用户名和密码长度,在 index.js 中使用,使用 Webpack 打包
  3. 步骤:

新建项目文件夹 Webpack_study,初始化包环境,得到 package.json 文件

1. <font style="color:rgb(51, 51, 51);"> npm init -y</font>
2. <font style="color:rgb(51, 51, 51);">新建 src 源代码文件夹(书写代码)包括 utils/check.js 封装用户名和密码长度函数,引入到 src/index.js 进行使用</font>
    * <font style="color:rgb(51, 51, 51);">src/utils/check.js</font>
/**
  * 目标:封装检验用户名和密码长度的函数
  */
export const checkUserName = uname => {
  return uname.length >= 8
}
export const checkPassWord = pwd => {
  return pwd.length >= 6
}

export default {
  checkUserName,
  checkPassWord
}
    * <font style="color:rgb(51, 51, 51);">src/index.js</font>
/**
  * 目标:引入工具函数使用
  */
import { checkUserName, checkPassWord } from './utils/check.js'
const unameResult = checkUserName('itheima007')
const pwdResult = checkPassWord('7654321')
console.log(unameResult, pwdResult)

下载 webpack webpack-cli 到项目(版本独立)

3. <font style="color:rgb(51, 51, 51);"> npm i webpack webpack-cli --save-dev</font>

注意:虽然 webpack 是全局软件包,封装的是命令工具,但是为了保证项目之间版本分别独立,所以这个比较特殊,下载到某个项目环境下,但是需要把 webpack 命令配置到 package.json 的 scripts 自定义命令,作为局部命令使用

项目中运行工具命令,采用自定义命令的方式(局部命令)

4. <font style="color:rgb(51, 51, 51);"> npm run build</font>

npm run 自定义命令名字

注意:实际上在终端运行的是 build 右侧的具体命名

5. <font style="color:rgb(51, 51, 51);">自动产生 dist 分发文件夹(压缩和优化后,用于最终运行的代码)</font>

Webpack 打包入口和出口

Webpack 默认入口和出口是src/index.js 和 dist/main.js,那么如何修改呢?

  1. Webpack 配置:影响 Webpack 打包过程
  2. 步骤:
    1. 项目根目录,新建 Webpack.config.js 配置文件
    2. 导出配置对象,配置入口,出口文件路径
const path = require('path')
 
 module.exports = {
   entry: path.resolve(__dirname, 'src/main.js'), // 入口
   output: { // 出口
     path: path.resolve(__dirname, 'dist'),
     filename: 'app.js',
     clean: true // 先清空 dist,然后再输出最新内容
   }
 }
3. <font style="color:rgb(51, 51, 51);">重新打包观察</font>

打包案例

原始打包
  1. 需求:点击注册按钮,判断用户名和密码长度是否符合要求
  2. 步骤:
    1. 新建 public/index.html 准备网页模板(方便查找标签和后期自动生成 html 文件做准备)
    2. 核心 JS 代码写在 src/main.js 打包入口文件
/**
  * 目标:点击注册,检验用户名和密码长度
  */
document.querySelector('.login-btn').addEventListener('click', () => {
  const username = document.querySelector('.username').value
  const password = document.querySelector('.password').value

  if (!checkUserName(username)) {
    alert('用户名长度要大于等于8位')
    return
  } else if (!checkPassWord(password)) {
    alert('密码长度要求大于等于6位')
    return
  }

  console.log('用户名和密码长度符合要求')
})
3. <font style="color:rgb(51, 51, 51);">运行自定义命令,让 Webpack 打包 JS 代码</font>
4. <font style="color:rgb(51, 51, 51);">手动复制 index.html 到 dist 下,手动引入打包后的 JS 代码文件,运行 dist/index.html 在浏览器查看效果</font>

引入插件自动生成 html 文件
  1. 插件 html-webpack-plugin 作用:在 Webpack 打包时生成 html 文件,并引入其他打包后的资源
  2. 步骤:

下载 html-webpack-plugin 本地软件包到项目中

1. <font style="color:rgb(51, 51, 51);"> npm i html-webpack-plugin --save-dev</font>
2. <font style="color:rgb(51, 51, 51);">配置 webpack.config.js 让 Webpack 拥有插件功能</font>
// ...
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  // ...
  plugins: [ // 插件列表
    new HtmlWebpackPlugin({
      template: path.resolve(__dirname, 'public/index.html') // 以指定的 html 文件作为生成模板
    })
  ]
};
3. <font style="color:rgb(51, 51, 51);">指定以 public/index.html 为模板复制到 dist/index.html,并自动引入其他打包后资源</font>

3. 运行打包命令,观察打包后 dist 文件夹下内容并运行查看效果


打包css代码
  1. 注意:Webpack 默认只识别 JS 和 JSON 文件内容,所以想要让 Webpack 识别更多不同内容,需要使用加载器
  2. 介绍需要的 2 个加载器来辅助 Webpack 才能打包 css 代码
  3. 步骤:
    1. 准备 css 文件引入到 src/mian.js 中(压缩转译处理等)
/**
  * 目标:引入 css 文件内容
  */
 import './css/index.css'

注意:这里只是引入代码内容让 Webpack 处理,不需定义变量接收在 JS 代码中继续使用,所以没有定义变量接收

下载 css-loader 和 style-loader 本地软件包

2. <font style="color:rgb(51, 51, 51);"> npm i css-loader style-loader --save-dev</font>
3. <font style="color:rgb(51, 51, 51);">配置 webpack.config.js 让 Webpack 拥有该加载器功能</font>
// ...

module.exports = {
  // ...
  module: { // 加载器
    rules: [ // 规则列表
      {
        test: /\.css$/i, // 匹配 .css 结尾的文件
        use: ['style-loader', 'css-loader'], // 使用从后到前的加载器来解析 css 代码和插入到 DOM
      }
    ]
  }
};
4. <font style="color:rgb(51, 51, 51);">打包后运行 dist/index.html 观察效果,看看准备好的样式是否作用在网页上</font>

打包less代码
  1. 加载器 less-loader:把 less 代码编译为 css 代码,还需要依赖 less 软件包
  2. 步骤:
    1. 准备 less 样式引入到 src/main.js 中
/**
  * 目标:引入 less 文件内容
  */
 import '@/less/index.less'

下载 less 和 less-loader 本地软件包

2. <font style="color:rgb(51, 51, 51);"> npm i less less-loader --save-dev</font>
3. <font style="color:rgb(51, 51, 51);">配置 webpack.config.js 让 Webpack 拥有功能</font>
// ...

module.exports = {
  // ...
  module: { // 加载器
    rules: [ // 规则列表
      // ...
      {
        test: /\.less$/i,
        use: [
          // compiles Less to CSS
          'style-loader',
          'css-loader',
          'less-loader',
        ],
      },
    ]
  }
};
4. <font style="color:rgb(51, 51, 51);">打包后运行 dist/index.html 观察效果</font>

打包图片
  1. 资源模块:Webpack 内置了资源模块的打包,无需下载额外 loader
  2. 步骤:
    1. 准备图片素材到 src/assets 中(存放资源模块 - 图片/字体图标等)
    2. 在 index.less 中给 body 添加背景图
body{
   background: url(../assets/background.png) no-repeat center center;
 }
3. <font style="color:rgb(51, 51, 51);">在 main.js 中给 img 标签添加 logo 图片</font>
/**
  * 目标:要给 img 标签设置一个 logo 图片
  * 注意:再赋予给 img 的 src 属性图片的时候,需要把图片数据对象引入过来
  */
import imgObj from '@/assets/logo.png'
document.querySelector('.logo-img').src = imgObj
4. <font style="color:rgb(51, 51, 51);">配置 webpack.config.js 让 Webpack 拥有打包图片功能</font>
// ...

module.exports = {
  // ...
  module: { // 加载器
    rules: [ // 规则列表
      // ...
      { // 针对资源模块(图片,字体文件,图标文件等)处理
        test: /\.(png|jpg|jpeg|gif)$/i,
        type: 'asset', // 根据文件大小(8KB)小于:把文件转成 base64 打包进 js 文件中(减少网络请求次数)大于:文件复制到输出的目录下
        generator: { // 输出文件时,路径+名字
          filename: 'assets/[hash][ext]'
        }
      }
    ]
  }
};
5. <font style="color:rgb(51, 51, 51);">打包后运行 dist/index.html 观察效果</font>

3. 注意: - 小于 8KB 文件会被转成 data URI(base64 字符串)打包进 JS 文件中(好处:可以减少网络请求次数,缺点:增加 30% 体积) - 大于 8KB 文件会被复制到 dist 下,自动替换使用代码的图片名字


集成 babel 编译器

  1. babel 定义:是一个 JavaScript 语法编译器,将采用 ECMAScript 2015+ 语法编写的代码转换为向后兼容的 JavaScript 语法,以便能够运行在当前和旧版本的浏览器或其他环境中
  2. babel-loader:让 Webpack 可以使用 babel 转译 JavaScript 代码
  3. 步骤:
    1. 编写一段映射数组元素,每个数值 + 1 的代码(要求用箭头函数)
/**
  * 目标:让 Webpack + Babel 编译降级 JS 语法
  */
const arr = [1, 2, 3]
const result = arr.map(val => val + 1)
console.log(result)

下载 babel babel-loader core 本地软件包

2. <font style="color:rgb(51, 51, 51);"> npm i babel-loader @babel/core @babel/preset-env -D</font>
3. <font style="color:rgb(51, 51, 51);">配置 webpack.config.js 让 Webpack 拥有功能</font>
// ...

module.exports = {
  // ...
  module: { // 加载器
    rules: [ // 规则列表
      // ...
      {
        test: /\.m?js$/,
        exclude: /(node_modules|bower_components)/, // 排除指定目录里的 js (不进行编译降级)
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-env'] // 预设规则
          }
        }
      }
    ],
  }
};
4. <font style="color:rgb(51, 51, 51);">打包运行 dist/index.html 观察效果</font>

4. 各个软件包的作用表格:

模块作用
@babel/coreJs 编译器,分析代码
@babel/preset-envbabel 预设,规则
babel-loader让 webpack 翻译 js 代码

Webpack 开发服务器

  1. 每次改动代码,都要重新打包,很麻烦,所以这里给项目集成 webpack-dev-server 开发服务器
  2. 作用:启动 Web 服务,打包输出源码在内存,并会自动检测代码变化热更新到网页
  3. 步骤;

下载 webpack-dev-server 软件包到当前项目

1. <font style="color:rgb(51, 51, 51);"> npm i webpack-dev-server --save-dev</font>
2. <font style="color:rgb(51, 51, 51);">配置自定义命令,并设置打包的模式为开发模式</font>
"scripts": {
   // ...
   "dev": "webpack serve --mode=development"
 },
3. <font style="color:rgb(51, 51, 51);">使用 npm run dev 来启动开发服务器,访问提示的域名+端口号,在浏览器访问打包后的项目网页,修改代码后试试热更新效果</font>

在 js / css 文件中修改代码保存后,会实时反馈到浏览器


打包模式

  1. 打包模式:告知 Webpack 使用相应模式的内置优化
  2. 分类:
模式名称模式名字特点
开发模式development调试代码,实时加载,模块热替换等
生产模式production压缩代码,资源优化,更轻量等
  1. 如何设置影响 Webpack呢?
    • 方式1:在 webpack.config.js 配置文件设置 mode 选项
// ...
 
 module.exports = {
   // ...
   mode: 'production'
 }
- <font style="color:rgb(51, 51, 51);">方式2:在 package.json 命令行设置 mode 参数</font>
"scripts": {
   "build": "webpack --mode=production",
   "dev": "webpack serve --mode=development"
 },
  1. 注意:命令行设置的优先级高于配置文件中的,推荐用命令行设置

开发环境引入source map精准定位到报错源码位置

  1. source map:可以准确追踪 error 和 warning 在原始代码的位置
  2. 问题:代码被压缩和混淆,无法正确定位源代码位置(行数和列数)
  3. 设置:webpack.config.js 配置 devtool 选项
// ...
 
 module.exports = {
   // ...
   devtool: 'inline-source-map'
 }

inline-source-map 选项:把源码的位置信息一起打包在 JS 文件内

  1. 注意:source map 适用于开发环境,不要在生产环境使用(防止被轻易查看源码位置)

设置解析别名路径

  1. 解析别名:配置模块如何解析,创建 import 或 require 的别名,来确保模块引入变得更简单
  2. 例如:

原来路径如下:

1. <font style="color:rgb(51, 51, 51);"> </font>`<font style="color:rgb(51, 51, 51);">import { checkUsername,  checkPassword } from '../src/utils/check.js'</font>`
2. <font style="color:rgb(51, 51, 51);">配置解析别名:在 webpack.config.js 中设置</font>
// ...

module.exports = {
  // ...
  resolve: {
    alias: {
      MyUtils: path.resolve(__dirname, 'src/utils'),
      '@': path.resolve(__dirname, 'src')
    }
  }
}
3. <font style="color:rgb(51, 51, 51);">这样我们以后,引入目标模块写的路径就更简单了</font>
import { checkUsername,  checkPassword } from 'MyUtils/check.js'
import { checkUsername,  checkPassword } from '@/utils/check.js'
  1. 修改代码的路径后,重新打包观察效果是否正常!

问题回顾

  • Webpack 有什么用?
    • 压缩,转译,整合,打包我们的静态模块
  • Webpack 怎么用?
    • 初始化环境,编写代码,安装 Webpack 软件包,配置自定义命令,打包体验查看结果
  • 如何运行 package.json 里的自定义命令?
    • npm run 自定义命令
  • Webpack 默认入口和出口?
    • src/index.js 和 dist/main.js
  • 如何影响 Webpack 打包过程?
    • 查文档,新建配置文件和配置属性
  • Webpack 打包后的前端代码是如何运行的?
    • 手动引入到 html 文件中,再交给浏览器运行
  • html-webpack-plugin 插件怎么用?
    • 找到插件文档,下载到项目中,配置到 Webpack 的配置文件中即可使用
  • 加载器的作用是什么?
    • 让 Webpack 识别更多的代码内容类型
  • Webpack 支持 less 代码打包需要哪 2 个软件包?
    • 需要 less less-loader 这 2 个软件包
  • 资源模块指的是什么?
    • 图片,字体文件等等
  • babel 编译器的作用?
    • 把 ECMAScript2015+ 语法向后转换,兼容低版本浏览器
  • npm 下载的包如何作用在前端项目上?
    • 被 Webpack 打包处理后,再引入到 html 文件中运行
  • webpack-dev-server 的作用?
    • 启动 Webpack 开发服务器,会启动一个 Web 服务,实时检测代码变化重新打包,并快速反应最新效果到浏览器页面上
  • 开发模式和生产模式的区别?
    • 开发模式注重代码热替换更快,让开发调试代码更便捷,生产模式注重项目体积更小,更轻量,适配不同的浏览器环境
  • 为何打包后,在控制台无法准确定位到源码的位置信息?
    • 因为 Webpack 把代码压缩和混淆了
  • 路径中的 '@' 符号代表什么意思?
    • 看在 webpack 配置中的别名路径是什么,就会在打包时替换成哪个路径使用