笔记来源:拉勾教育 大前端高薪训练营
阅读建议:内容较多,建议通过左侧导航栏进行阅读
模块化打包工具解决的是前端整体的模块化,并不单指 JavaScript 模块化。
产生原因
webpack,常用的模块打包器(Module bundler),可以将零散的JavaScript 代码打包到一个 JS 文件中。
对于那些存在环境兼容问题的代码,可以在打包过程中通过模块加载器(Loader)对其进行编译转换。
webpack,具有代码拆分(Code Splitting)的能力,可以将所有的代码都按照需要进行打包,可以实现渐进式的打包方式。
开发应用程序,使用 Webpack。
1,准备三个文件,本次使用 http-server 构建服务器,也可以采用其他的
代码示例如下(heading.js):
export default () => {
const element = document.createElement('h2')
element.textContent = 'hello world'
element.addEventListener('click', () => {
alert('Hello webpack')
})
return element
}
代码示例如下(index.js):
import createHeading from './heading.js'
const heading = createHeading()
document.body.append(heading)
代码示例如下(index.html):
<script type="module" src="./src/index.js">script>
2,webpack 是一个 NPM 工具模块,需要初始化包管理文件 package.json
$ yarn init --yes # or npm init -y
3,安装 webpack核心模块 ,以及对应的 webpack-cli模块
$ yarn add webpack webpack-cli --dev # or npm i webpack webpack-cli --save-dev
4,查看 webpack 是否安装成功
$ yarn webpack --version
运行结果,如下图所示:
5,使用 webpack ,进行打包
$ yarn webpack
运行结果,如下图所示:
6,在 package.json 中,使用 NPM Scripts 对打包命令进行包装
配置代码如下:
{
"scripts": {
"build": "webpack"
}
}
7,将 index.html 中引入的 js,进行修改
代码示例如下(index.html):
<script src="dist/main.js">script>
webpack 4.0+ 支持按照约定的内容,进行打包,即 src/index.js
(默认入口文件) 打包到 dist/main.js
在项目根目录添加 webpack.config.js 配置文件。
$ yarn webpack --mode development
$ yarn webpack --mode production
$ yarn webpack --mode none
指定 webpack 打包的入口文件
配置代码如下(webpack.config.js):
module.exports = {
entry: './src/main.js' // 相对路径时,./ 不能省略
}
设置输出文件的位置
配置代码如下(webpack.config.js):
const path = require('path')
module.exports = {
output: {
filename: 'bundle.js', // 设置输出文件的名称
// 指定输出文件所在的目录,必须是绝对路径
// 利用 node 的 path 模块,组合生成绝对路径
path: path.join(__dirname, 'output')
}
}
配置打包的工作模式 值为:development | production | none
配置代码如下(webpack.config.js):
module.exports = {
mode: 'development',
}
导入资源模块
JavaScript 驱动整个前端应用的业务,因此需要把 打包入口 设置为 js 文件,它也相当于是 运行入口。在 js 代码中 通过 import 导入其他资源文件。
1,在 main.js 中引入 css 文件
代码示例如下(main.js):
import createHeading from './heading.js'
import './main.css'
const heading = createHeading()
document.body.append(heading)
2,在 webpack.config.js 中设置入口文件
配置代码如下(webpack.config.js):
module.exports = {
entry: './src/main.js' // 相对路径时,./ 不能省略
}
页面效果,如下图所示:
一般需要根据代码的需要动态导入资源,因为需要资源的不是应用,而是此时正在编写的代码。
优势
1)逻辑合理,JS 确实需要这些资源文件;
2)确保上线资源不缺失,都是必要的。
webapck 进行打包过程中,会使用 loader 进行编译转换。下面,来看一下都有那些编码方式会触发对应的 loader ,并最终将其打包为 资源模块?
webpack 兼容多种模块化标准,但是建议不要混合使用标准
。
1,遵循 ES Modules 标准的 import 声明
语法代码如下:
import ... from 'module_path'
2,遵循 CommonJs 标准的 require 函数
语法代码如下:
require('module_path')
//通过 require 函数载入 ES Modules,对于 ES Modules 的默认导出
// 需要通过导入require 结果的default 属性进行获取
const xxx = require('module_path').default
3,遵循 AMD 标准的 define 函数 和 require 函数
语法代码如下:
define(['module_path', 'module_path', ...], ( mod1, mod2) => { })
require(['module_path', 'module_path', ...], ( mod1, mod2) => { })
@import url('./reset.css');
body {
background-color: url('./u0.png');
}
<footer>
<img src="./u0.png">
<a:href>a:href>
footer>
import footerHtml from './footer.html'
document.write(footerHtml)
Loader (加载器) 是 webpack 的核心特性,借助于 Loader 就可以加载任何类型的资源,从而实现资源模块加载的功能。
webpack 内部内置的 loader 只能对 JS 文件进行打包,其他的资源文件需要引入其他的 loader 进行处理,最终都会转换成 js 模块。
编译转换器,会把加载到的资源模块转换为 JavaScript 代码,如 css-loader。
css-loader,用来对 css 文件进行转换的加载器,将资源文件转换成 js 代码。
基本使用
1,安装 loader 模块
$ yarn add css-loader --dev # or npm i css-loader --save-dev
2,在 css 文件中,书写 body 的样式
样式代码如下(main.css):
body {
margin: 0 auto;
padding: 0 20px;
max-width: 800px;
background-color: #000000;
}
3,在 webpack.config.js 中进行配置
配置代码如下(webpack.config.js):
module.exports = {
entry: './src/main.css',
module: {
rules: [ // rules 数组,是指针对其他资源模块的加载规则的配置
{
test: /.css$/, // 正则表达式,用来匹配在打包过程中遇到的文件路径
use: 'css-loader' // 用来指定匹配到的文件,需要去使用的 loader
}
]
}
}
页面效果,如下图所示:
可以看到,此时页面中并没有对应的样式。这是因为还需要 style-loader 将转换后的结果追加到页面中。
style-loader,用来把 css-loader 转换后的结果,通过 style 标签的形式追加到页面中。
基本使用
1,安装 loader 模块
$ yarn add style-loader --dev # or npm i style-loader --save-dev
2,修改 webpack.config.js 中的配置
配置代码如下(webpack.config.js):
module.exports = {
entry: './src/main.css',
module: {
rules: [ // rules数组,是指针对其他资源模块的加载规则的配置
{
test: /.css$/, // 正则表达式,用来匹配在打包过程中遇到的文件路径
// 用来指定匹配到的文件,需要去使用的 loader
use: [ // 若配置多个loader,执行顺序是 从后往前 的
'style-loader',
'css-loader' // 先将 css 代码转换成 js 模块
]
}
]
}
}
babel-loader,用来将 ES6+ 的新特性,转换为 ES5,需要 @babel/core
核心模块,以及用于去完成具体特性转换插件的集合 @babel/preset-env
。
基本使用
1,安装 loader 模块
$ yarn add babel-loader @babel/core @babel/preset-env --dev
2,在 webpack.config.js 中添加配置规则
配置代码如下(webpack.config.js):
module.exports = {
module: {
rules: [
{ // 通过 babel-loader 取代默认的加载器,处理代码中的新特性
test: /.js$/,
use: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
},
]
}
}
总结
1)Webpack 只是 打包工具;
2)加载器可以用来编译转换代码。
html-loader,用来处理 HTML 中制定的标签属性。
基本使用
1,安装 loader 模块
$ yarn add html-loader --dev
2,在 HTML 中编写代码
代码示例如下(footer.html):
<footer>
<img src="./u0.png">
<a:href>a:href>
footer>
3,在 JS 文件中进行导入
代码示例如下(main.js):
import footerHtml from './footer.html'
document.write(footerHtml)
4,在 webpack.config.js 中,添加配置
module.exports = {
module: {
// 针对其他资源模块的加载规则的配置
rules: [
{
test: /.html$/,
use: {
loader: 'html-loader',
options: {
// 指定哪个标签属性组合应该被此 loader 处理
// 这个属性默认只有 'img:src'
attrs: ['img:src', 'a:href']
}
}
}
]
}
}
文件操作类加载器,会把加载到的资源模块拷贝到输出的目录,同时会将文件的访问路径向外导出,如 file-loader。
file-loader,文件资源加载器,这里主要指图片、字体等资源。
基本使用
1,安装 loader 模块
$ yarn add file-loader --dev # or npm i file-loader --save-dev
2,在 main.js 引入图片
代码示例如下(main.js):
import createHeading from './heading.js'
import './main.css'
import u from './u0.png'
const heading = createHeading()
document.body.append(heading)
const img = new Image()
img.src = u
document.body.append(img)
3,在 webpack.config.js 中添加配置规则
配置代码如下(webpack.config.js):
module.exports = {
output: {
filename: 'main.js',
path: path.join(__dirname, 'dist'),
publicPath: 'dist/' // 设置网站的根目录 / 不能省略,默认为 ''
},
module: {
rules: [
{ // 配置图片规则
test: /.png$/,
use: 'file-loader'
}
]
}
}
url-loader,用来将资源(图片、字体等)文件转换为 Data URL 的形式,Data URLs 是一种特殊的 url 协议,url 可以直接去表示文件内容的方式,也就是说,这种 URL 中的文本就已经包含了文件内容。
在使用过程中,不会再去发送任何的 HTTP请求。如果要将图片、字体等二进制的文件进行编译时,会将文件的内容进行 base64 编码,然后以 base64编码 过后的结果(一个字符串)去表示文件的内容。
url-loader,适合转换体积比较小的文件资源。
基本使用
1,安装 loader 模块
$ yarn add url-loader --dev # or npm i url-loader --save-dev
2,在 webpack.config.js 中进行配置
配置代码如下(webpack.config.js):
module.exports = {
module: {
rules: [
{
test: /.png$/,
use: {
loader: 'url-loader',
options: { // 设置配置选项
limit: 10 * 1024 // 10 KB 单位字节,只匹配 10 KB 以下的
}
}
}
]
}
}
最佳实践
1)小文件使用 Data URLs,转换为 Data URLs 嵌入代码中,减少请求次数;
2)大文件单独提取存放,提高加载速度。
注意
在对 url-loader 适合的文件大小进行限制后,需要同时安装 file-loader,因为超出限制的文件,会去查找 file-loader 进行转换。如果不安装 file-loader,将会因为找不到,而引起程序报错。
代码检查类加载器,会对所加载到的资源文件(一般指代码)去进行校验。目的是为了统一代码的风格,从而提高代码质量,这种加载器一般不会修改生产环境的代码,如 eslint-loader。
希望得到的结果是 markdown 转换过后的字符串。
1,新建一个名为 markdown-loader 的项目,并创建 src 文件夹;
2,在 src 文件夹下,新建 xxx.md 文件,测试内容随便写;
3,在 src 文件夹下,新建 main.js 入口文件,并引入 xxx.md 文件
代码示例如下(main.js):
import about from './about.md'
console.log(about);
4,在根目录下,新建 index.html,页面展示的入口文件;
5,初始化包管理文件 package.json
$ yarn init --yes # or npm init -y
6,安装 webpack 模块,以及其依赖命令模块 webpack-cli
$ yarn add webpack webpack-cli --dev # or npm i webpack webpack-cli --save-dev
7,在根目录下,新建 markdown-loader.js,即 md 文件的转换 loader
8,在根目录下,新建 webpack.config.js,并设置对 md文件的规则配置
配置代码如下(webpack.config.js):
const path = require('path')
module.exports = {
mode: 'none',
entry: './src/main.js',
output: {
filename: 'bundle.js',
path: path.join(__dirname, 'dist'),
publicPath: 'dist/'
},
module: {
rules: [
{ // 用来转换的 loader 既可以是一个模块,也可以是一个 js 文件
test: /.md$/,
use: './markdown-loader'
}
]
}
}
markdown-loader.js 文件需求,输入:就是每次加载到的资源文件;输出:就是此次加工后的结果。
1,在 markdown-loader.js 中进行配置
配置代码如下(markdown-loader.js):
// 通过 source 参数,接收输入
module.exports = source => {
console.log(source);
return `console.log('hello ~')`
}
可以看到,使用的 js 的代码标准进行了输出字符串,这是因为 loader 要求输出结果必须是一段标准的 JavaScript代码。
2,若要解析 md 文件,则需要安装 md 文件的解析模块
$ yarn add marked --dev # or npm i marked --save-dev
3,在 markdown-loader.js 中,使用 marked 模块解析 md 文件,并将结果以模块形式导出
配置代码如下(markdown-loader.js):
const marked = require('marked') // 导入 md 文件的解析模块
module.exports = source => {
// 解析 source, 返回值是一串html字符串,即转换后的结果
const html = marked(source)
// 将其结果进行导出,需要输出 JavaScript 代码
return `module.exports = ${JSON.stringify(html)}`
// or
// return `export default ${JSON.stringify(html)}`
}
4,返回 html字符串,交给下一个 loader 处理
配置代码如下(markdown-loader.js):
const marked = require('marked') // 导入 md 文件的解析模块
module.exports = source => {
// 解析 source, 返回值是一串html字符串,即转换后的结果
const html = marked(source)
// 返回 html字符串,交给下一个 loader 处理
return html
}
5,安装用于处理 html 加载的 loader
$ yarn add html-loader --dev # or npm i html-loader --save-dev
6,修改 webpack.config.js 配置文件
配置代码如下(webpack.config.js):
const path = require('path')
module.exports = {
mode: 'none',
entry: './src/main.js',
output: {
filename: 'bundle.js',
path: path.join(__dirname, 'dist'),
publicPath: 'dist/'
},
module: {
rules: [
{
test: /.md$/,
use: [
'html-loader',
'./markdown-loader' // 按照 从后往前 的顺序进行执行
]
}
]
}
}
webpack plugins 可以增强 webpack 自动化能力,可以解决除了资源加载以外的其他自动化工作,例如:打包之前,自动清除 dist 目录;拷贝静态文件至输出目录;压缩输出代码等。
clean-webpack-plugin,自动清除输出目录插件。
基本使用
1,安装插件模块
$ yarn add clean-webpack-plugin --dev
2,在 webpack.config.js 中,添加插件配置
配置代码如下(webpack.config.js):
// 导入 clean-webpack-plugin 插件,这个插件导出一个 CleanWebpackPlugin 成员
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
module.exports = {
// ...
plugins: [ // plugins 专门用来配置插件的属性
// 一般插件导出的都是一个类型,通过这个类型创建实例
// 然后将这个实例,放入到 plugins 数组中
new CleanWebpackPlugin()
]
}
注意
plugins 属性,是一个数组,它与 module 属于同级。
html-webpack-plugin,自动生成 HTML 插件,不需要在手动书写 html 入口文件。
基本使用
1,安装插件模块
$ yarn add html-webpack-plugin --dev
2,在 webpack.config.js 中,导入插件,并进行配置
配置代码如下(webpack.config.js):
const path = require('path')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
// html-webpack-plugin 默认导出的就是一个类型,无需解构其内部成员
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
mode: 'none',
entry: './src/main.js',
output: {
filename: 'bundle.js',
path: path.join(__dirname, 'dist'),
// publicPath: 'dist/' // 自动生成 html 时,不需配置
},
plugins: [
new CleanWebpackPlugin(),
// 添加 HtmlWebpackPlugin 实例对象
// 自定义输出文件内容:给构造函数传入对象参数,指定配置选项
new HtmlWebpackPlugin({ // 用于生成 index.html
title: 'Webpack Plugin Sample', // 设置 HTML 的标题
meta: { // 设置页面中的 元数据标签
viewport: 'width=device-width'
},
// 指定模板文件,使生成的HTML文件根据模板文件进行生成
template: './src/index.html'
}),
// 同时输出多个页面文件, 即可以添加多个 HtmlWebpackPlugin 实例
// 每一个 HtmlWebpackPlugin 实例,就是用来生成一个 HTML文件的
new HtmlWebpackPlugin({ // 用于生成 about.html
filename: 'about.html' // 指定输出的文件名
})
]
}
copy-webpack-plugin,将不需要编译转换的 静态资源文件 拷贝到输出目录,一般存放在 public 目录下。
基本使用
1,安装插件模块
$ yarn add copy-webpack-plugin --dev
2,在 webpack.config.js 中,导入插件,并进行配置
配置代码如下(webpack.config.js):
const path = require('path')
const CopyWebpackPlugin = require('copy-webpack-plugin')
module.exports = {
// ...
plugins: [
// ...
// 传入 数组参数,用于指定需要去拷贝的文件路径
new CopyWebpackPlugin([
// 'public/**' // 可以是一个目录,通配符,文件路径
'public' // 表示会将 public 目录下所有文件全部拷贝到输出目录
])
]
}
Plugin 是通过 在webpack 生命周期的钩子中挂载函数实现扩展的。Webpack 要求,插件必须是一个函数或者是一个包含 apply 方法的对象,最后将其挂载到 钩子上。
删除 bundle.js 中生成的注释。
在 webpack.config.js 中,进行插件的创建和使用
配置代码如下(webpack.config.js):
class MyPlugin { // 定义一个插件类型
/**
* 在 webpack 启动时,自动被调用
* @param {*} compiler
* compiler 对象参数,是 webpack 工作过程中最核心的一个对象
* compiler 对象参数,包含了此次构建的所有配置信息
* 通过 compiler 对象注册钩子函数
*/
apply(compiler) {
console.log('MyPlugin 启动');
/**
* 通过 compiler.hooks 可以访问到钩子
* 通过 tap 方法去注册钩子函数
* tap方法,接收两个参数:
* -- 第一个参数:插件的名称
* -- 第二个参数:需要挂载到钩子上的函数
*/
compiler.hooks.emit.tap('MyPlugin', compilation => {
// compilation 对象 => 可以理解为此次打包的上下文
// 所有打包的结果,都会放到 compilation 对象当中
for (const name in compilation) {
// 对象中的键(属性名),代表每一个资源文件的名称
// 判断只对 js文件 进行处理
if (name.endsWith('.js')) {
// assets 获取即将写入目录当中的资源文件信息
// source() 拿到对应的资源文件的内容
const contents = compilation.assets[name].source()
// 使用正则替换代码中的注释
const withoutComments = contents.replace(/\/*\**\*\//g, '')
// 将结果 覆盖到原有的文件中
compilation.assets[name] = {
source: () => withoutComments, // 返回新内容
size: () => withoutComments.length // 返回内容的大小,必须方法
}
}
}
})
}
}
module.exports = {
// ...
plugins: [
// ...
// 应用 MyPlugin 插件
new MyPlugin()
]
}
1,直接在命令行启动 watch
工作模式,监听文件变化,自动重新打包。
$ yarn webpack --watch
2,在 package.json 中,配置 NPM Scripts
配置代码如下(package.json):
{
"scripts": {
"build": "webpack --watch"
}
}
可以看到,开启 watch 工作模式以后,打包命令会一直处于工作状态,当文件修改后,会自动进行打包,直到手动结束 cli 命令。
使用 BrowserSync 启动热更新开发 Web服务器,实现自动刷新浏览器
1,全局安装 BrowserSync 模块
$ yarn global add browser-sync # or npm i browser-sync -g
2,使用命令启动 web服务器
$ browser-sync dist --files "**/*"
缺点
1)操作上太麻烦了;
2)效率上降低了。
Webpack Dev Server,是 Webpack 官方推出的开发工具,提供用于开发的 HTTP Server,集成了 自动编译 和 自动刷新浏览器 等功能。
本次安装 Webpack Dev Server 版本,要求 webpack 4 版本以下。webpack 5+ 版本已经集成了 Webpack Dev Server 开发工具,无需再进行安装,直接使用 webpack server
即可开启服务。
注意
Webpack Dev Server 的版本,要比 Webpack 的版本低一级,否则无法使用。
1,安装 开发工具
$ yarn add webpack-dev-server --dev
2,运行命令,使用 --open 自动唤醒浏览器,并打开运行地址
$ yarn webpack-dev-server --open
执行说明
1)运行时,内部自动使用 webpack 进行打包;
2)启动 HTTP Server,自动运行打包结果;
3)自动监听代码变化,自动立即重新打包;
4)打包结果不会写入到磁盘中,打包结果会暂时存放在内存中;
5)HTTP Server 会从内存中读取这些文件,然后发送给浏览器。
优势
Webpack Dev Server,会减少很多磁盘读写操作,从而大大提高构建效率。
问题
Dev Server 默认只会 serve 打包输出文件,即只要是 webpack 打包输出的文件,都可以直接被访问。其他静态资源如果也需要 serve,则需要去告知 webpack。
1,配置静态资源访问
配置代码如下(webpack.config.js):
const path = require('path')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
// const CopyWebpackPlugin = require('copy-webpack-plugin')
module.exports = {
// 专门为 webpack-dev-server 指定的配置选项
devServer: {
// 额外为开发服务器指定查找静态资源目录,可以是字符串或数组,配置一个或多个
contentBase: ['./public']
},
plugins: [
new CleanWebpackPlugin(),
// 用于生成 index.html
new HtmlWebpackPlugin({
title: 'Webpack Tutorials',
meta: {
viewport: 'width=device-width'
},
template: './src/index.html'
}),
// 开发阶段最好不要使用这个插件
// 一般放在上线前的最后一次打包使用
// 开发阶段需要频繁的serve, 多次使用插件会影响运行效率
// new CopyWebpackPlugin(['public'])
]
}
2,开发阶段接口跨域问题,需要配置 代理API
配置代码如下(webpack.config.js):
module.exports = {
// 专门为 webpack-dev-server 指定的配置选项, 开发阶段的配置
devServer: {
// 额外为开发服务器指定查找静态资源目录,可以是字符串或数组,配置一个或多个
contentBase: ['./public'],
// proxy属性,用来添加代理服务配置
proxy: {
/**
* 每一个属性就是一个代理规则的配置
* 属性名: 需要被代理的请求路径前缀,
* 即请求以哪一个地址开始,就会走对应的代理请求
* 属性值: 为这个前缀所匹配到的代理规则配置
*/
'/api': {
// http://localhost:8080/api/users => https://api.github.com/api/users
target: 'https://api.github.com', // 代理目标
// http://localhost:8080/api/users => https://api.github.com/users
// 如果代理目标地址中没有‘/api’,则需要重写代理目标地址
pathRewrite: {
'^/api': '' // 以正则的形式进行匹配,以 ^ 开头
},
// 不能使用 localhost:8080 作为请求 GitHub 的主机名
// 设置改变主机名
changeOrigin: true
}
}
}
}
Source map,源代码地图,用来映射转换过后的代码与源代码之间的关系。一段转换过后的代码,通过转换过程中生成的 Source Map 文件,可以逆向得到源代码。主要用来在开发阶段进行调试和定位错误。
基本使用
1,引用 Source Map 的注释
//# sourceMapppingURL=jquery-3.4.3.min.map
在浏览器中,打开开发人员工具,开发人员工具加载到的 js文件最后存在上面的注释,会自动去请求 Source Map 文件,然后根据文件的内容逆向解析出对应的源代码,以便于调试。又因为存在映射关系,所以源代码中如果出现错误,就会很容易定位到源代码中错误的位置。
2,在 webpack.config.js中,配置 Source Map
配置代码如下(webpack.config.js):
// 其余代码省略
module.exports = {
// 配置开发过程中的辅助工具,
// 也就是与 Source Map 相关的一些功能配置
devtool: 'source-map',
}
3,运行打包命令,查看 bundle.js 底部是否存在 Source Map 的注释
$ yarn webpack
查看 bundle.js 底部,如下图所示:
Webpack 目前支持 12 种不同的方式,每种方式所生成的 Source Map 效果,以及生成 Source Map 的速度都是不一样的。效果最好的,生成速度越慢;反之,生成最快的,生成的 Source Map 效果不好,几乎没有。
12 种方式对比,如下图所示:
eval() 函数,用来运行字符串中的 JavaScript 代码。默认情况下,运行的代码会运行在一个临时的虚拟机环境中,可以通过 sourceURL 来声明这段代码所属的文件路径。
基本使用
1,在浏览器控制台,进行测试
eval('console.log(123)//# sourceURL=./foo/bar.js')
运行示例,如下图所示:
2,在 webpack.config.js 中,进行配置
配置代码如下(webpack.config.js):
// 其余代码省略
module.exports = {
// 配置开发过程中的辅助工具,
// 也就是与 Source Map 相关的一些功能配置
devtool: 'eval',
}
浏览器定位错误,如下图所示:
在 eval 模式下,会将每个模块的js 代码都放到 eval() 函数中执行,在执行的字符串最后通过 sourceURL 的方式去说明所对应的文件路径,这样浏览器通过 eval 在执行代码时,就会知道代码所对应的源代码是哪一个文件,从而去定位错误所出现的文件。
总结
eval 模式下,不会生成 Source Map 文件,因此构建速度最快,但是其效果比较简单,只能知道源代码文件的名称,而不知道具体的行列信息。
一次打包过程中,同时生成所有模式下的不同结果。
基本使用
配置代码如下(webpack.config.js):
const HtmlWebpackPlugin = require('html-webpack-plugin')
const allModes = [
'eval',
'cheap-eval-source-map',
'cheap-module-eval-source-map',
'eval-source-map',
'cheap-source-map',
'cheap-module-source-map',
'inline-cheap-source-map',
'inline-cheap-module-source-map',
'source-map',
'inline-source-map',
'hidden-source-map',
'nosources-source-map'
]
module.exports = allModes.map(item => {
return {
devtool: item,
mode: 'none',
entry: './src/main.js',
output: {
filename: `js/${item}.js`
},
module: {
rules: [
{
test: /\.js$/,
use: {
// 辨别其中一类模式的差异
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
}
}
]
},
plugins: [
// 为每一个打包任务生成一个 HTML文件
new HtmlWebpackPlugin({
filename: `${item}.html`
})
]
}
})
2,使用 http-server 开启服务器
$ http-server dist
运行结果,如下图所示:
3,通过查看每一个 HTML 文件,得出各个模式的对比结果。
1)带有 module 的模式,不会经过 loader 的编译转换,生成的 Source Map 就是项目的源代码。而不带有 module 的模式,则是经过 babel 转换后的代码。
2)hidden-source-map,一般用在生成第三方包的时候,需要生成 Source Map 文件,但又不想在 包 中引入。
3)nosources-source-map,用在生产环境中,保护源代码不被暴露。
选择合适的 Source Map
下面是建议选择模式,应根据具体情况具体选择。
开发模式,选择cheap-module-eval-source-map
1)代码每行不会超过 80 个字符;
2)代码经过 Loader 转换过后的差异较大;
3)首次打包速度慢无所谓,重写打包相对较快。
生产模式,选择 none
1)Source Map 会暴露源代码;
2)调试是开发阶段的事情。
生产模式,方便定位错误,选择 nosource-source-map
1)不会向外暴露源代码;
2)出现错误时,可以找到源代码对应的位置。
HMR(Hot Module Replacement),模块热替换,又叫模块热更新,他是 Webpack 中最强大的功能之一,它能够实现在应用运行过程中实时替换某个模块,而应用运行状态不会改变。HMR 极大程度的提高了开发者的工作效率,因此很受欢迎。
HMR ,已经集成在 webpack-dev-server 中,无需再单独安装模块。
开启 HMR
1,直接在命令中,使用 --hot
开启热更新
$ yarn webpack-dev-server --hot
2,在 webpack.config.js 中,进行配置开启热更新
配置代码如下(webpack.config.js):
const webpack = require('webpack')
module.exports = {
mode: 'development',
devServer: {
hot: true
},
plugins: [
// 载入 webpack 的内置插件
new webpack.HotModuleReplacementPlugin()
]
}
Webpack 中的 HMR 并不可以开箱即用,他需要手动处理 JS 模块热替换逻辑。
HMR APIs
在 main.js 中,使用 HMR APIs 手动处理热替换
代码示例如下(main.js):
// module.hot 对象,是 HMR API 的核心对象
/**
* accept(),用于注册模块更新过后的处理函数
* 第一个参数,指的是 依赖模块的路径
* 第二个参数,指的是 依赖更新过后的处理函数
*/
module.hot.accept('./editor', () => {
// 热替换逻辑
})
注意事项
1,处理 HMR 的代码报错会导致自动刷新
配置代码如下(webpack.config.js):
const webpack = require('webpack')
module.exports = {
devServer: {
hotOnly: true // 只使用 HMR,不会 fallback 到 live reloading
},
plugins: [
// 载入 webpack 的内置插件
new webpack.HotModuleReplacementPlugin()
]
}
2,启用 HMR 的情况下,HMR API 报错
代码示例如下(main.js):
if (module.hot) { // 先判断这个对象是否存在,再进行任务的注册
module.hot.accept('./editor', () => {
// 热替换逻辑
})
}
3,业务中添加的处理代码,会在打包过后自动移除。
配置文件根据环境不同导出不同配置
在 webpack.config.js 中,进行导出配置
配置代码如下(webpack.config.js):
const webpack = require('webpack')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const CopyWebpackPlugin = require('copy-webpack-plugin')
/**
* 导出一个函数,在这个函数中设置所需的配置对象
* 接收两个参数,
* 第一个参数 env:即 通过 cli 传递的环境名参数
* 第二个参数 argv: 指 运行 cli 过程中传递的所有参数
*/
module.exports = (env, argv) => {
// 设置默认模式:开发模式
const config = {
mode: 'development',
entry: './src/main.js',
output: {
filename: 'js/bundle.js'
},
devtool: 'cheap-eval-module-source-map',
devServer: {
hot: true,
contentBase: 'public'
},
module: {
rules: [
{
test: /\.css$/,
use: [
'style-loader',
'css-loader'
]
},
{
test: /\.(png|jpe?g|gif)$/,
use: {
loader: 'file-loader',
options: {
outputPath: 'img',
name: '[name].[ext]'
}
}
}
]
},
plugins: [
new HtmlWebpackPlugin({
title: 'Webpack Tutorial',
template: './src/index.html'
}),
new webpack.HotModuleReplacementPlugin()
]
}
// 生产模式
if (env === 'production') {
config.mode = 'production'
config.devtool = false // 禁用 Source Map
config.plugins = [
...config.plugins,
new CleanWebpackPlugin(),
new CopyWebpackPlugin(['public'])
]
}
return config
}
启动开发模式,默认模式为 开发模式,无需指定工作模式
$ yarn webpack
启动生产模式,使用 --env
指定工作模式
$ yarn webpack --env production
注意
只适用中小型项目的配置,大型项目不适合。
不同环境对应不同配置文件
1,新建 webpack.common.js,用来存放 公共 配置信息
配置代码如下(webpack.common.js):
const HtmlWebpackPlugin = require('html-webpack-plugin')
// 公共配置
module.exports = {
entry: './src/main.js',
output: {
filename: 'js/bundle.js'
},
module: {
rules: [
{
test: /\.css$/,
use: [
'style-loader',
'css-loader'
]
},
{
test: /\.(png|jpe?g|gif)$/,
use: {
loader: 'file-loader',
options: {
outputPath: 'img',
name: '[name].[ext]'
}
}
}
]
},
plugins: [
new HtmlWebpackPlugin({
title: 'Webpack Tutorial',
template: './src/index.html'
})
]
}
2,安装 webpack-merge 模块, 合并 webpack 配置
$ yarn add webpack-merge --dev
3,新建 webpack.prod.js ,用来存放 生产模式 配置信息
配置代码如下(webpack.prod.js):
// 导入 webpack-merge
const merge = require('webpack-merge')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const CopyWebpackPlugin = require('copy-webpack-plugin')
const common = require('./webpack.common')
// webpack-merge模块 导出 merge(), 合并 webpack 配置
module.exports = merge(common, {
mode: 'production',
plugins: [
new CleanWebpackPlugin(),
new CopyWebpackPlugin(['public'])
]
})
4,新建 webpack.dev.js ,用来存放 开发模式 配置信息
配置代码如下(webpack.dev.js):
const webpack = require('webpack')
const merge = require('webpack-merge')
const common = require('./webpack.common')
module.exports = merge(common, {
mode: 'development',
devtool: 'cheap-eval-module-source-map',
devServer: {
hot: true,
contentBase: 'public'
},
plugins: [
new webpack.HotModuleReplacementPlugin()
]
})
5,运行时,可以直接使用 --config
参数指定配置文件
$ yarn webpack --config webpack.prod.js
6,或者 在 package.json 中,配置 NPM Scripts
配置代码如下(package.json):
{
"scripts": {
"dev": "webpack --config webpack.dev.js",
"build": "webpack --config webpack.prod.js"
}
}
DefinePlugin,为代码注入全局成员。在 production 模式下,默认启用,会为代码中注入一个 process.env.NODE_ENV
的常量。一般通过这个成员去判断当前的运行环境,从而去执行对应的操作。
基本使用
在 webpack.config.js 中,进行配置
配置代码如下(webpack.config.js):
const webpack = require('webpack')
module.exports = {
// ...
plugins: [
// DefinePlugin是 webpack 的内置插件
// 构造函数接收一个对象,对象中的每一个键值都会被注入到代码中
new webpack.DefinePlugin({
// 值要求的是一个 JS代码片段
API_BASE_URL: JSON.stringify('https://api.example.com')
})
]
}
Tree-shaking,摇掉 代码中未引用代码(dead-code)。在启动生产模式时,自动检测出未引用的代码,并自动将未引用代码移除,从而减少冗余代码。
Tree-shaking 并不是指某个配置选项,它是一组功能搭配使用后的优化效果,在 production 模式下自动开启。
基本使用
不使用 production 模式,其他模式下开启 Tree-shaking。
1,在 webpack.config.js 中,进行配置
配置代码如下(webpack.config.js):
module.exports = { // 其他代码省略
mode: 'none',
// 集中配置 webpack 内部的优化功能
optimization: {
// 模块只导出被使用的成员
usedExports: true,
// 尽可能将所有模块合并并输出到到一个函数中
concatenateModules: true,
// 压缩输出结果
minimize: true
}
}
Tree-shaking 前提是 ES Modules,也就是说,由 webpack 打包的代码必须使用 ES Modules。
2,最新的 babel-loader 默认关闭 ESM 转换,可以在 webpack.config.js 中,手动设置关闭
配置代码如下(webpack.config.js):
module.exports = {
mode: 'none',
module: {
rules: [
{
test: /\.js$/,
use: {
loader: 'babel-loader',
options: {
presets: [
// 如果 Babel 加载模块时已经转换了 ESM,则会导致 Tree Shaking 失效
// ['@babel/preset-env', { modules: 'commonjs' }]
// 配置为 false,确保 preset-env 内部不会开启 ES Module 转换的插件
// ['@babel/preset-env', { modules: false }]
// 使用默认配置:auto,这样 babel-loader 会自动关闭 ESM 转换
['@babel/preset-env', { modules: 'auto' }]
]
}
}
}
]
},
optimization: {
// 模块只导出被使用的成员
usedExports: true,
// 尽可能合并每一个模块到一个函数中
// concatenateModules: true,
// 压缩输出结果
// minimize: true
}
}
sideEffects,webpack 4 中新增的特性,它允许通过配置的方式去标识代码是否有副作用,从而为 Tree-shaking 提供更大的压缩空间,在 production 模式下自动开启。
副作用,是指模块执行时除了导出成员之外所做的事情。
sideEffects,一般用于 npm 包标记是否有副作用。
确保你的代码真的没有副作用,否则会误删掉那些有副作用的代码。
1,在 webpack.config.js 中,开启sideEffects特性
配置代码如下(webpack.config.js):
module.exports = {
mode: 'none',
optimization: {
// 手动开启特性
sideEffects: true,
// 模块只导出被使用的成员
// usedExports: true,
// 尽可能合并每一个模块到一个函数中
// concatenateModules: true,
// 压缩输出结果
// minimize: true,
}
}
检查 package.json 中是否有 sideEffects 的标识,以此来判断这个模块是否有副作用。
2,设置所有模块都没有副作用,模块中没有被用到的代码就不会再被打包,则将会被移除掉。
配置代码如下(package.json):
{
"sideEffects": false
}
3,设置某些模块具有副作用,即 将这些模块会被打包进输出结果,不会被移除掉。
配置代码如下(package.json):
{
"sideEffects": [
"./src/extend.js",
"*.css"
]
}
Code Splitting,代码分包/代码分割,在应用中按需加载模块,从而提高应用的响应速度和运行效率。
多入口打包,一般适用于多页应用程序,即 一个页面对应一个打包入口,公共部分单独提取。
基本使用
1,在 webpack.config.js 中,进行配置
配置代码如下(webpack.config.js):
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
mode: 'none',
/**
* entry 属性值的形式:
* 字符串:设置一个打包入口
* 数 组: 将多个文件打包进一个文件中
* 对 象: 设置多个打包入口,分别生成对应的打包文件
* 一个属性对应一个打包入口,
* 属性名为入口名称,属性值为入口所对应的文件路径
*/
entry: {
index: './src/index.js',
album: './src/album.js'
},
output: {
// 多个入口,就意味着生成多个打包文件
// 使用 [name] 占位符,动态输出文件名
// [name] 最终会被替换成 入口的名称
filename: '[name].bundle.js'
},
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
title: 'Multi Entry',
template: './src/index.html',
filename: 'index.html',
// 指定每个 HTML 文件所使用的 bundle文件
// 每个打包入口会形成独立的 chunks
chunks: ['index']
}),
new HtmlWebpackPlugin({
title: 'Multi Entry',
template: './src/album.html',
filename: 'album.html',
chunks: ['album']
})
]
}
2,在 webpack.config.js 中,配置属性,使其在打包时自动提取公共模块
配置代码如下(webpack.config.js):
// 其余部分省略
module.exports = {
optimization: {
splitChunks: {
// 自动提取所有公共模块到单独 bundle
chunks: 'all'
}
},
}
按需加载,需要用到某个模块时,再加载这个模块,可以极大的节省带宽和流量。动态导入的模块会被自动提取到对应的 bundle 中,从而实现分包。相对多入口打包,更加灵活。
基本使用
在需要导入的地方,使用 ES Modules 的动态导入
代码示例如下(index.js):
if (hash === '#posts') {
// 魔法注释:命名 bundle 的名称,相同的名称会打包到一个 bundle 中
// /* webpackChunkName: 'components' */'
import(/* webpackChunkName: 'components' */'./posts/posts').then(({ default: posts }) => {
mainElement.appendChild(posts())
})
} else if (hash === '#album') {
import(/* webpackChunkName: 'components' */'./album/album').then(({ default: album }) => {
mainElement.appendChild(album())
})
}
MiniCssExtractPlugin,将 CSS 代码从打包结果中提取出来的插件,通过这个插件,可以实现 CSS 的按需加载。
基本使用
1,安装插件模块
$ yarn add mini-css-extract-plugin --dev
2,在 webpack.config.js 中,进行导入和配置
配置代码如下(webpack.config.js):
// 导入 mini-css-extract-plugin 插件
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
module.exports = {
module: {
rules: [
{
test: /\.css$/,
use: [
'style-loader', // 将样式通过 style 标签注入到页面中
// 实现样式文件通过 link 标签的方式注入
MiniCssExtractPlugin.loader,
'css-loader'
]
}
]
},
plugins: [
// 创建 MiniCssExtractPlugin 插件实例,自动提取 CSS 到单个文件中
new MiniCssExtractPlugin()
]
}
使用建议
当样式代码所占内存较小时,不建议生成单个文件,此时减少请求次数,可能效果更好。
webpack 内置的压缩插件,只针对 JS 代码,对于其他的资源文件,都要使用对应的压缩插件进行压缩。
OptimizeCssAssetsWebpackPlugin,压缩输出的 CSS 文件。
基本使用
1,安装插件模块
$ yarn add optimize-css-assets-webpack-plugin --dev # css 压缩插件
$ yarn add terser-webpack-plugin --dev # webpack 内置的 JS 压缩插件
2,在 webpack.config.js 中,进行导入和配置
配置代码如下(webpack.config.js):
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
// 导入 mini-css-extract-plugin 插件
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
// 导入 optimize-css-assets-webpack-plugin
const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin')
// 导入 webpack 内置的 JS 压缩插件
const TerserWebpackPlugin = require('terser-webpack-plugin')
module.exports = {
optimization: {
// 只有在 minimizer 特性开启时,才会运行其内部的插件
// 生产模式时,minimizer 特性自动开启,即下面插件才会工作
// 配置 minimizer 时,webpack 内部的压缩插件就会失效,需要手动添加
minimizer: [
new TerserWebpackPlugin(),
new OptimizeCssAssetsWebpackPlugin()
]
}
}
文件名使用 Hash 的原因?
一旦资源文件发生改变,文件名称也可以跟着改变。对于客户端而言,全新的文件名,就是全新的请求,则将不会存在缓存问题。也就是说,把服务端的缓存策略时间设置的非常长,就不用担心文件更新过后的问题。因此,生产模式下,文件名使用 Hash。
三种 Hash
1,[hash] 属于 项目级别,即项目中的任何一个地方变化,都会导致打包时,Hash值 全部改变
配置代码如下(webpack.config.js):
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
module.exports = {
output: {
filename: '[name]-[hash].bundle.js'
},
plugins: [
new MiniCssExtractPlugin({
filename: '[name]-[hash].bundle.css'
})
]
}
2,[chunkhash] 属于 chunk 级别,即在打包中,只要是同一路的打包,chunkhash 就是相同的。相比于 [hash],控制较精确一点。
配置代码如下(webpack.config.js):
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
module.exports = {
output: {
filename: '[name]-[chunkhash].bundle.js'
},
plugins: [
new MiniCssExtractPlugin({
filename: '[name]-[chunkhash].bundle.css'
})
]
}
3,[contenthash] 属于 文件级别,根据输出文件的内容输出 hash值,即不同的文件就有不同的hash值,最适合解决缓存问题,通过 :number
指定hash的长度。
配置代码如下(webpack.config.js):
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
module.exports = {
output: {
filename: '[name]-[contenthash:8].bundle.js'
},
plugins: [
new MiniCssExtractPlugin({
filename: '[name]-[contenthash].bundle.css'
})
]
}
Rollup,可以将项目中散落的代码打包成整块的代码,从而使得这些划分的模块,更好的运行在浏览器环境或者 NodeJs 中。相对于 Webpack 来说,Rollup 更为小巧,它仅仅是一款 ES Modules 的打包器,是一个提供充分利用 ES Modules 各项特性的高效打包器。
开发一个框架或者类库,使用 Rollup。
Rollup打包时,会默认开启 Tree-shaking。
基本使用
$ yarn add rollup--dev
$ yarn rollup
$ yarn rollup ./src/index.js --format iife --file dist/bundle.js
在项目根目录下,新建 rollup.config.js 配置文件。
基本使用
1,安装 Rollup 模块
$ yarn add rollup --dev
2,新建 rollup.config.js 配置文件,进行打包信息的配置
配置代码如下(rollup.config.js):
// 导出一个配置对象
export default {
input: 'src/index.js', // 指定打包的入口文件
// 指定输出文件的配置信息
output: {
file: 'dist/bundle.js', // 指定输出文件的文件名
format: 'iife' // 指定输出文件的格式,自执行函数
}
}
3,运行命令,通过 --config
表明使用项目中的配置文件,默认不会读取配置文件,默认读取的配置文件名称:rollup.config.js
$ yarn rollup --config # [配置文件名称]
插件是 Rollup 唯一扩展途径。
rollup-plugin-json,用来在代码中导入 json 文件。
基本使用
1,安装插件模块
$ yarn add rollup-plugin-json --dev
2,在 rollup.config.js 中,进行配置
配置代码如下(rollup.config.js):
// 默认导出插件函数
import json from 'rollup-plugin-json'
export default {
// plugins 属性,存放的是插件函数的调用结果
plugins: [
json()
]
}
rollup-plugin-node-resolve,用来加载 NPM 模块。也就是说,可以在代码中,直接使用模块名称导入模块。
基本使用
1,安装插件模块
$ yarn add rollup-plugin-node-resolve --dev
2,在 rollup.config.js 中,进行配置
配置代码如下(rollup.config.js):
import resolve from 'rollup-plugin-node-resolve'
export default {
plugins: [
resolve()
]
}
rollup-plugin-commonjs,用来加载 CommonJS 模块。
基本使用
1,安装插件模块
$ yarn add rollup-plugin-commonjs --dev
2,在 rollup.config.js 中,进行配置
配置代码如下(rollup.config.js):
import commonjs from 'rollup-plugin-commonjs'
export default {
plugins: [
commonjs()
]
}
Code Splitting,代码分包/代码分割。由于 Rollup 遵循 ES Modules,因此他可以使用 ES Modules 的动态导入实现。
基本使用
1,在 JS 文件中,使用 动态导入 进行模块的加载
代码示例如下(index.js):
// 返回 Promise 对象
import('./logger').then(({ log }) => {
log('code splitting~')
})
2,修改 rollup.config.js 配置文件
配置代码如下(rollup.config.js):
export default {
input: 'src/index.js',
output: {
// file: 'dist/bundle.js', // 只能指定一个文件
// format: 'iife'
dir: 'dist', // 需要输出多个文件
// iife 自执行函数,会把所有的模块放到同一个函数中,没有引导代码,无法实现代码拆分。
format: 'amd' // 浏览器环境中,遵循 AMD 标准
}
}
类似于 webpack 中的多入口打包,配置多个入口文件,生成多个输出文件,并在打包过程中,自动将公共部分单独提取。
基本使用
1,在 rollup.config.js 中,配置多个入口文件,类似 webpack
配置代码如下(rollup.config.js):
export default {
// input: ['src/index.js', 'src/album.js'],
input: {
foo: 'src/index.js',
bar: 'src/album.js'
},
// 多入口打包,会提取公共模块,会实行代码拆分
output: {
dir: 'dist',
format: 'amd'
}
}
2,手动创建 index.html ,在其中使用打包后的 JS 文件
代码示例如下(index.html):
<body>
<script src="https://unpkg.com/[email protected]/require.js" data-main="foo.js">script>
body>
优点
缺点
Parcel,是一款完全 零配置 的前端应用打包器。相对于 Webpack 来说,构建速度要快,因为他的内部采用多进程。
Parcel 官方建议,使用 HTML 文件作为打包入口。
1,安装 Parcel 打包器
$ yarn add parcel-bundler --dev
2,在 src 目录下,新建 index.html 文件,此文件将作为打包的入口文件
代码示例如下(index.html):
<body>
<script src="main.js">script>
body>
3,在 src 目录下,新建 main.js 和 foo.js 文件,作为测试文件
代码示例如下(main.js):
import foo from './foo.js'
foo.bar()
代码示例如下(foo.js):
export default {
bar: () => {
console.log('hello parcel~');
}
}
4,开发环境,打包命令,将 HTML 文件作为打包入口,同时开启 Web 服务器
$ yarn parcel src/index.html
5,生成环境,打包命令
$ yarn parcel build src/index.html
在运行打包命令后,不仅会生成对应的打包文件,还会自动开启一个 web 服务器。
启动的web服务器,会自动监听代码的变化,从而实现自动编译,浏览器自动刷新。也就是说,不管做什么,都不用担心配置等,Parcel 会自动去实现。
测试用例
1,HMR(自动热替换),在 main.js 编写 热替换 逻辑
代码示例如下(main.js):
if (module.hot) {
// accept() 只接收一个回调函数参数,
// 作用:当前模块更新,或者其所依赖的所有模块更新过后,会自动执行
module.hot.accept(() => {
// 热替换逻辑
})
}
2,自动安装依赖
代码示例如下(main.js):
import $ from 'jquery'
$(document.body).append('Hello Parcel
')
通过测试,可以知道,在引入 jquery 之后,会自动下载 jquery 的依赖包。
3,动态导入
代码示例如下(main.js):
import('jquery').then($ => {
$(document.body).append('Hello Parcel
')
})