重拾 Webpack(上卷)
花了几天时间看了一本书《Webpack入门、进阶与调优》,之前看书评不错就去详细阅读了一遍,虽然感觉有些内容并非属于 webpack 而是不深不浅地介绍了一些在实战中的内容,但作为一本系统介绍 webpack 的解析书,确实写的比较清晰了,在这里归纳书中的前一部分的知识点,这也足以入门 webpack 了
初识Webpack
模块打包工具
- 模块打包工具的任务:解决模块之间的依赖,使其打包后能运行在浏览器上
- 模块打包工具的工作方式(主要分为以下两种)
- 将存在依赖关系的模块按照特定的规则合并为单个的 JS 文件,一次全部加载进入页面中
- 在页面初始时加载一个入口模块,其他模块异步地进行加载
- 有哪些模块打包工具?
- Webpack
- Parcel
- Rollup
- 为什么选择 Webpack ?
- 支持多种模块标准,如 AMD 规范 、Commonjs 规范、 ES6 模块规范 等等
- 完备的代码分割方案,通俗地说,就是首屏只加载必要的部分,不太重要的部分放到后面动态地加载
- 处理各种类型的资源,除了能处理 JavaScript 文件,还能处理样式、模板、甚至图片
- 庞大的社区支持
安装
- 注意:确保已经安装了 Node.js,并且该 Node 的版本要尽量新
- 初始化项目
- 新建
MyWebpack
文件夹,并输入:npm init -y
- 新建
- 安装 webpack:
- 我们采用局部安装的方式,输入:
注:npm install webpack webpack-cli --save-dev
webpack
是核心模块,webpack-cli
是命令行工具,在这里是需要的
- 我们采用局部安装的方式,输入:
- 检验安装
- 由于我们将 webpack 安装在了本地,所以这里无法使用 “webpack” 指令,
工程内部只能使用npx webpack <command>
的方式,所以我们输入以下命令检验版本:npx webpack -v npx webpack-cli -v
- 由于我们将 webpack 安装在了本地,所以这里无法使用 “webpack” 指令,
打包第一个应用
- 在根目录下添加以下几个文件:
- 新建
index.js
并输入:import addContent from './addContent' document.write('My first Webpack app <br/>') addContent()
- 新建
addContent.js
并输入:export default function(){ document.write('Hello World') }
- 新建
index.html
并输入:<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title></title> </head> <body> <script src="./dist/bundle.js"></script> </body> </html>
- 新建
- 在命令行中输入:
npx webpack --entry=./index.js --output-filename=bundle.js --mode=development
- 浏览器打开
index.html
即可看到内容 - 回顾刚才的命令:
entry
- 资源打包的入口,Webpack 将从这里开始进行模块依赖的查找,webpack 便知道了项目中包含
index.js
和addContent.js
两个模块,通过他们来生成产物
- 资源打包的入口,Webpack 将从这里开始进行模块依赖的查找,webpack 便知道了项目中包含
output-filename
- 打包后的文件名
mode=development
- 打包模式,Webpack 提供了 development、production、none 三种模式
- 当选择 development 和 production 模式时,它会自动添加适用于当前模式下的一系列配置,一般在开发环境下,我们选择 development 就行了
- 使用 npm scripts:
- 在
package.json
中添加一下命令:"scripts": { "build": "webpack --entry=./index.js --output-filename=bundle.js --mode=development" }
- 现在不需要像刚才那样输入冗长的命令,直接输入:
npm run build
- 在
- 使用默认目录配置
- 通常情况下,我们会设置两个目录,分别为源代码目录和资源输出目录
工程源代码放在/src
中,输出资源放在/dist
中 - Webpack 默认的源代码入口就是
src/index.js
,所以现在我们可以省略掉 entry 的配置,编辑package.json
如下:"scripts": { "build": "webpack --output-filename=bundle.js --mode=development" }
- 通常情况下,我们会设置两个目录,分别为源代码目录和资源输出目录
- 使用配置文件
- webpack 有非常多的配置项以及相应的命令行参数,我们可以通过以下命令查看:
npx webpack -h
- 新建
webpack.config.js
,输入如下:module.exports = { entry: './src/index.js', output: { filename: 'bundle.js', }, mode: 'development' }
- 现在我们可以去掉
package.json
中配置的打包参数了:"scripts": { "build": "webpack" }
- 输入
npm run build
即可重新打包
- webpack 有非常多的配置项以及相应的命令行参数,我们可以通过以下命令查看:
- webpack-dev-server
- 由于我们现在每次更新内容都需要重新打包一次,比较麻烦,我们可以使用 Webpack 社区提供的一个开发工具 ——
webpack-dev-server
- 安装工具
npm install webpack-dev-server --save-dev
- 在
package.json
中添加一项:"scripts": { "dev": "webpack-dev-server" }
- 最后,我们需要对
webpack.config.js
进行配置,如下:module.exports = { entry: './src/index.js', output: { filename: './bundle.js', }, mode: 'development', devServer: { publicPath: '/dist', }, }
webpack.config.js
的 devServer对象 是专门配置 webpack-dev-server 的,webpack-dev-server 主要工作就是接受浏览器的请求,然后将资源返回
当服务启动时,会先让 webpack 进行模块打包,当 webpack-dev-server 接收到 浏览器的资源请求时,它会首先进行 URL 地址校验,如果地址是资源服务地址(即配置中的 publicPath),那么就从 webpack 将打包结果返回给浏览器,否则直接从硬盘读取源文件并返回- 总结 webpack-dev-server 的职能:
- 令 webpack 进行模块打包,并处理打包结果的资源请求
- 作为 web server,处理静态资源文件请求
- 输入命令,并打开
http://localhost:8080/
:npm run dev
- 注意事项
- 直接用 webpack 开发和使用 webpack-dev-server 有一个很大的区别:前者每次都会生成 bundle.js,而后者只是将打包结果放在内存中,并没有实际写入 bundle.js 中,每次都是将内存中的打包结果返回给浏览器,可以通过删除 dist 目录来检验此区别
- 当然,还需说明的一点是,webpack-dev-server 中的很便捷的的特点就是
live-reloading
,来保持本地服务启动以及浏览器打开的状态
- 由于我们现在每次更新内容都需要重新打包一次,比较麻烦,我们可以使用 Webpack 社区提供的一个开发工具 ——
再谈模块打包
各种模块规范
- 此处不再叙述 CommonJs,ES6 Module 等规范标准以及它们之间的区别,详情清查:
模块打包原理
- 新建两个文件,内容分别如下:
index.js
const mod = require('./mod.js') const sum = mod.add(2,3) console.log('sum',sum)
mod.js
module.exports = { add: function(a, b){ return a + b; } }
- 打包之后的 JS 文件如下:
(function(modules) { var installedModules = {}; function __webpack_require__(moduleId) { if (installedModules[moduleId]) { return installedModules[moduleId].exports; } var module = installedModules[moduleId] = { i: moduleId, l: false, exports: {} }; modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); module.l = true; return module.exports; } __webpack_require__.m = modules; __webpack_require__.c = installedModules; __webpack_require__.d = function(exports, name, getter) { if (!__webpack_require__.o(exports, name)) { Object.defineProperty(exports, name, { enumerable: true, get: getter }); } }; __webpack_require__.r = function(exports) { if (typeof Symbol !== 'undefined' && Symbol.toStringTag) { Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); } Object.defineProperty(exports, '__esModule', { value: true }); }; __webpack_require__.t = function(value, mode) { if (mode & 1) value = __webpack_require__(value); if (mode & 8) return value; if ((mode & 4) && typeof value === 'object' && value && value.__esModule) return value; var ns = Object.create(null); __webpack_require__.r(ns); Object.defineProperty(ns, 'default', { enumerable: true, value: value }); if (mode & 2 && typeof value != 'string') for (var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key)); return ns; }; __webpack_require__.n = function(module) { var getter = module && module.__esModule ? function getDefault() { return module['default']; } : function getModuleExports() { return module; }; __webpack_require__.d(getter, 'a', getter); return getter; }; __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; __webpack_require__.p = ""; return __webpack_require__(__webpack_require__.s = "./index.js"); }) ({ "./index.js": (function(module, exports, __webpack_require__) { eval( "const mod = __webpack_require__(/*! ./mod.js */ \"./mod.js\")\r\nconst sum = mod.add(2,3)\r\nconsole.log('sum',sum)\r\n\n\n//# sourceURL=webpack:///./index.js?" ); }), "./mod.js": (function(module, exports) { eval( "module.exports = {\r\n\tadd: function(a, b){\r\n\t\treturn a + b;\r\n\t}\r\n}\n\n//# sourceURL=webpack:///./mod.js?" ); }) });
- 上述这个结果可以很清晰地展示它是如何将具有依赖关系的模块串联在一起的,此文件可以分为以下几个部分:
- 最外层立即执行匿名函数,用来包裹整个文件,并形成自己的作用域
- installedModules对象:每个模块只在第一次被加载的时候执行,然后导出的值就存在这个对象里面,当再次被加载的时候直接从里面取值,而不会重新执行
- webpack_require函数:对于模块加载的实现,在浏览器中可以通过
__webpack_require__(module.id)
来完成模块的导入 - module对象:工程中所有产生了依赖关系的文件都会以 key-value 的形式存放在这里
- key 可以理解为一个模块的 id,由数字或者很短的 hash 字符串组成
- value 是一个匿名函数包裹的模块实体,匿名函数的每个参数赋予了模块的导入和导出的功能
- 打包后的文件在浏览器中的执行过程:
- 最外层的匿名函数初始化浏览器的执行环境,为模块的加载和执行做准备工作,比如定义 installedModules 对象、webpack_require 函数等等
- 加载入口模块,每个打包后的文件都有一个入口模块,上述实例中,
index.js
是入口模块,浏览器即从入口模块开始执行 - 执行模块代码:
- 如果执行到了
module.exports
,则记录下模块的导出值 - 如果执行时遇到了
__webpack_require__
,则会暂时交出执行权,进入 webpack_require 函数体内加载其他模块的内容
- 如果执行到了
- 在 webpack_require 中判断即将加载的模块是否存在于 installedModules 中,如果存在则直接取值,否则返回上一步 —— 执行模块代码获取导出值
- 当所有依赖的模块均已执行完毕,则最后的执行权显然又会回到入口模块,当入口模块的代码执行结束,也就标致着整个模块打包过程结束
资源输入与输出
资源处理流程
- 在一切工作开始之前,我们需要指定一个或者多个 入口(entry) 来让 webpack 知晓应该从哪里开始打包,如果把各个模块的依赖关系比喻成一颗树,那么入口文件显然就是树根,如图:
- 这些存在依赖关系的模块,在打包时会被封装成一个 chunk,chunk 的字面意思是代码块,在 webpack 中,可以理解为被封装和抽象过后的一些模块,根据配置不同,webpack可能会形成一个或多个 chunk
- 由这个 chunk 得到的打包产物我们称为 bundle。
entry
、chunk
、bundle
的关系如下: - 在工程中可以定义多个入口,每个入口都会产生一个结果,比如我们有两个入口文件
index.js
和lib.js
,那么打包的结果就会生成dist/bundle.js
和dist/lib.js
,如图:
配置资源入口
- webpack 通过 context 和 entry 两个配置项来共同决定入口文件的路径,在配置时,实际上做了两件事:
- 确定入口模块的位置,告诉 webpack 从哪里开始打包
- 定义 chunk name ,如果该工程只有唯一入口,那么默认为 main,若有多个入口,那么分别定义对应的 chunk name。
context与entry
- context
- context 可以理解为资源入口的路径前缀,在配置时要求使用绝对路径的形式,比如下面两个例子:
// 指定路径为:<工程根路径>/src/home/index.js module.exports = { context: path.join(__dirname, './src'), entry: './home/index.js' }; // 等同于下面这种方式 module.exports = { context: path.join(__dirname, './src/home'), entry: './index.js' }
- 配置 context 的目的主要是让 entry 的编写更加简洁,这种作用在多入口的情况下尤其突出,此外,context 是可以省略的,则默认值为当前工程的根目录
- context 的配置形式只能为字符串
- context 可以理解为资源入口的路径前缀,在配置时要求使用绝对路径的形式,比如下面两个例子:
- entry
- 首先,entry 的配置形式可以有多种:字符串、数组、对象、函数,可以根据不同的需求场景来选择
- 字符串类型入口
- 直接传入路径
module.exports = { entry: './src/index.js', }
- 直接传入路径
- 数组类型入口:
- 传入一个数组的作用是将多个资源先合并,在打包时 webpack 会将数组中的最后一个元素作为实际的入口路径,如:
module.exports = { entry: ['babel-polyfill', './src/index.js'], }
- 以上配置等同于:
// webpack.config.js module.exports = { entry: './src/index.js', } // index.js import 'babel-polyfill'
- 传入一个数组的作用是将多个资源先合并,在打包时 webpack 会将数组中的最后一个元素作为实际的入口路径,如:
- 对象类型入口:
- 如果想要定义多个入口,则必须要使用对象地形式,对象的属性名(key)是 chunk name,属性值(value)是入口路径,如:
module.exports = { entry: { // chunk name 为 index,入口路径为 ./src/index.js index: './src/index.js', // chunk name 为 lib,入口路径为 ./src/lib.js lib: './src/lib.js', } }
- 当然,对象的属性值也可以为字符串或者数组,如:
module.exports = { index: ['babel-polyfill', './src/index.js'], lib: './src/lib.js' }
- 如果想要定义多个入口,则必须要使用对象地形式,对象的属性名(key)是 chunk name,属性值(value)是入口路径,如:
- 函数类型入口:
- 用函数定义入口时,只需要返回字符串、数组或者对象中的任何一种配置形式即可,如:
// 返回字符串型的入口 module.exports = { entry: () => './src/index.js', } // 返回对象型的入口 module.exports = { entry: () => ({ index: ['babel-polyfill', './src/index.js'], lib: './src/lib.js' }) }
- 使用函数的优势是我们可以在函数体内添加一些动态的逻辑来获取入口,而且,函数也支持返回一个 Promise 对象 来进行异步操作,如:
module.exports = { entry: () => new Promise((resolve) => { // 模拟异步操作 setTimeout(() => { resolve('./src/index.js'); }, 1000); }), };
- 用函数定义入口时,只需要返回字符串、数组或者对象中的任何一种配置形式即可,如:
- 注:使用字符串或数组定义单入口时,没有办法更改 chunk name,只能为默认的 “main”
使用对象来定义多入口时,则必须为每一个入口定义 chunk name
实例 —— 单页与多页
- 单页应用
- 对于 单页应用(SPA) 来说,一般定义单一入口即可:
module.exports = { entry: './src/index.js', }
- 这样做的好处是只会产生一个 JS 文件,依赖关系清晰,而弊端就是所有的模块都打包到一个文件中,可能会导致该输出文件体积过大,降低页面的渲染速度
在 webpack 的默认配置中,一个输出文件大于 250KB 时,会认为这个文件已经过大了,在打包时会发出警告
- 对于 单页应用(SPA) 来说,一般定义单一入口即可:
- 提取 vendor
- 假如工程产生的 JS 文件体积很大,那么一旦代码更新,输出文件也要响相应地更新,这对页面的性能影响是比较大的,我们可以通过 vendor 来解决这个问题
- 在 webpack 中,vendor 一般指的是工程所使用的库、框架等第三方模块集中打包产生的输出文件,如:
在上述的例子中,我们添加了一个新的 chunk name 作为 vendor 的入口,通过数组的形式将工程所需的第三方模块放了进去module.exports = { context: path.join(__dirname, './src'), entry: { index: './src/index.js', vendor: ['react', 'react-dom', 'react-router'], }, };
- 多页应用
- 我们希望每个页面都只加载各自必要的逻辑,而不是把所有的内容都打包到一个输出文件中,因此每个页面都需要有一个独立的输出文件,如:
module.exports = { entry: { pageA: './src/pageA.js', pageB: './src/pageB.js', // 提取 vendor 来对公共模块打包 vendor: ['react', 'react-dom'], } }
- 我们希望每个页面都只加载各自必要的逻辑,而不是把所有的内容都打包到一个输出文件中,因此每个页面都需要有一个独立的输出文件,如:
配置资源出口
- 所有与出口相关的配置都集中在 output 对象 中,此部分最好的学习当然是去查文档啦,给出链接:
- 原文档:Output
- 中文文档:输出(output)
本作品采用《CC 协议》,转载必须注明作者和本文链接