一、模块打包工具
1.1、模块打包工具 - 概要
1.2、Webpack 快速上手
//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
<body>
<script type="module" src="src/index.js"></script>
</body>
cnpm init --yes
cnpm i webpack webpack-cli -D
yarn webpack --version
yarn webpack
//index.html
<body>
<script src="dist/main.js"></script>
</body>
//package.json{ "scripts": { "build": "webpack" }}
1.3、Webpack 配置文件
//webpack.config.jsconst path = require('path')//通过导出对象的属性完成相应的配置选项module.exports = { entry: './src/mian.js', //指定 webpack 打包的输入文件路径(相对路径情况的 ./ 是不能省略的) output: { filename: 'main.js', //设置输出文件的名称 path: path.join(__dirname, 'dist'), //设置输出文件的路径 => 该路径必须为绝对路径 }, //指定 webpack 输出文件的属性 => 该属性的值为一个对象}
1.4、Webpack 工作模式
yarn webpack
//webpack.config.jsmodule.exports = { mode: 'development', //设置 Webpack 的工作模式}
1.5、Webpack 资源模块加载
//main.cssbody{ margin: 0 auto; padding: 0 20px; max-width: 800px; background: #0f0;}
1.5.1、修改 webpack.config.js
//webpack.config.js/*amend - */entry: './src/main.js'/*amend + */entry: './src/main.css'/*amend end */
1.5.2、运行 webpack 打包命令
yarn webpack
1.5.3、解析 CSS 文件, 需要安装 css-loader
cnpm i css-loader -D
1.5.4、在 webpack.config.js 中添加对应的配置
//webpack.config.jsmodule.exports = { /*amend + */ module: { rules: [ { text: /.css$/, //正则表达式, 匹配打包过程的文件路径 use: 'css-loader' //指定匹配到的文件打包所需要的loader } ]//针对于其他资源模块打包规则的配置 } /*amend end */}
1.5.6、重新运行 webpack 重新打包项目, 并启动项目
yarn webpack
1.5.7、这里还需安装一个 style-cloader
cnpm i style-loader -D
1.5.8、修改 webpack.config.js
//webpack.config.jsmodule: { rules: [ { test: /.css$/, /*amend */ use: [ 'style-loader', 'css-loader' ]//这里可以接受一个数组, 数组中加载多个 loader, 执行时是 从后往前 执行 } ]}
1.5.9、总结
1.6、Webpack 导入资源模块
1.6.1、编辑 webpack.config.js
//webpack.config.jsmodule.exports = { /*amend - */ entry: './src/main.css' /*amend + */ entry: './src/main.js' /*amend end */}
1.6.2、编辑 src/main.js
//main.jsimport createHeading from './heading.js'/*amend + */import './main.css'/*amend end */
1.6.3、重新打包
yarn webpack
1.6.4、运行该项目
1.6.5、为 heading 添加样式
/* heading.css */.heading{ color: #fff; background: #00f; padding: 20px; margin: 0 atuo;}
//heading.js/*amend + */import './heading.css'/*amend end */.....element.textContent = 'hello world'/*amend + */element.classList.add('heading')/*amend end */> 重新打包项目```sheyarn webpack
1.7、Webpack 文件资源加载器
import './main.css'/*amend + */import icon from './icon.png'/*amend end */......//这里需要接收该图片模块的默认导出, 该模块的默认导出就是打包后图片的资源, 有了资源路径后, 就可以使用该路径/*amend + */const img = new Image()img.src = icondocument.body.append(img)
cnpm i file-loader -D
module: { rules: [ ....., { test: /.png$/, use: 'file-loader' } ]}
//webpack.config.js.....output: { filename: 'main.js', path: path.join(__dirname, 'dist'), /*amend + */ publicPath: 'dist/' //告诉 Webpack 打包后文件的位置 => 默认为空,表示网站的根目录, 这里的 / 不能省略}
1.8、Webpack Data URLs 与 url-loader
cnpm i url-loader -D
....module: { rules: [ ..., { test: /.png$/, use: 'url-loader' } ]}
....module: { rules: [ .... { test: /.png$|.jpg$/, use: { loader: 'url-loader', options: {// 为 url-loader 添加配置选项 limit: 10 * 1024 //url-loader 转化的最大的文件体积, 超过该设置的, 任然交给 file-loader 处理; } } } ]}
1.9、Webpack 常用加载器分类
1..9.1、编译转换类:
1.9.2、文件操作类
1.9.3、代码检查类
1.10、Webpack 处理 ES2015
cnpm i babel-loade @babel/core @babel/preset-env -D
....module: { rules: [ /*amend + */ { test: /.js$/, use:{ loader: 'babel-loader', options: { presets: ['@babel/preset-env'] } } } /*amend end */ .... ]}
**总结: **
- webpack 默认只是打包工具, 不会处理代码中的 ES6 及更高版本的新特性, 如果需要处理新特性, 可以通过为 JS 代码配置加载器来实现
- 加载器可以用来编译转化代码
1.11、Webpack 模块加载方式
1.11.1、遵循 ES Modules 标准的 import 声明
1.11.2、遵循 CommonJs 标准的 require 函数
1.11.3、遵循 AMD 标准的 define 函数和 require 函数
1.11.4、一些 独立的加载器, 在工作时也会处理所加载资源中的导入的模块(Loader 加载的非 JS 也会出发资源资源加载)
1.11.4.1、样式代码中的 url 函数加载资源
body{ min-height: 100vh; background: #f4f8fh url(background.png); background-size: cover;}
1.11.4.2、样式文件中的 import 指令加载资源
body{ margin: 0; padding: 0;}
@imprt url(reset.css);body{ .....}
1.11.4.3、HTML 代码中的 img 的 src 属性
<footer> <img src="better.png" width="256" /></footer>
import './main.css'import footerHtml from './footer.html'// html 文件默认会将 html 代码作为字符串导出, 需要接受用参数来接受document.write(footerHtml)
cnpm i html-loader -D
.....{ test: /.html$/, use: { loader: 'html-loader' }}
<footer> .... <a href="better.png">downPng</a></footer>
....{ test: /.html$/, use: { loader: 'html-loader', options: { attrs: ['img: src', 'a: href']//默认当中只用 'img: src' } }}
总结: 几乎代码中所有需要引用到的资源, 都会被 Webpack 找出来, 然后根据 webpack.config.js , 交个不同的 loader 处理, 将处理的结果整体打包到输出目录;
1.12、Webpack 核心工作原理
1.13、Webpack 开发一个Loader(Webpack Loader 的工作原理)
//main.jsimport about from './about.md'console.log(about)
//about.md#关于我我是 lcy668 一个奋斗的前端工程师
<!DOCTYPE html><html lang="zh-CN"><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>Document</title></head><body> <script src="dist/bundle.js"></script></body></html>
//webpack.config.jsconst path = require('path')module.exports = { mode: 'none', entry: './src/main.js', output: { filename: 'bundle.js', path: path.join(__dirname, 'dist'), publicPath: 'dist/' }}
Loader 介绍
//markdown-loader.jsmodule.exports = source => { //通过 source 接收输入, 通过 返回值输出 console.log(source); return 'hello ~'}
//webpack.config.js...../*amend + */module: { rules: [ { test: /.md$/, use: './markdown-laoder' //这里不仅仅只可以使用模块的名称, 还可以使用模块的文件路径 } ]}
第一种解决方案: 修改返回结果
//markdown-loader.jsmodule.exports = source => { console.log(source) return 'console.log("hello ~")'}
cnpm i marked -D
//markdown-laoder.jsconst marked = require('marked')module.exports = source => { //console.log(source) //return 'console.log("hello ~")' const html = marked(source)//现在的结果是一段 Html 代码, 如果直接返回 html 会报错; //正确的做法就是将 html 代码转化为一段 JS 代码, 这里, 希望把这段 html 转化为字符串作为当前模块的导出; return `module.exports = ${JSON.stringify(html)}`}
第二种解决方案: 返回一个 html 字符串, 交给下一个 loader 处理
return html
cnpm i html-loader -D
//webpack.config.js.....relues: [ { test: /.md$/, use: [ 'html-loader', './markdown-loader' ] }]
总结: Loader 负责资源文件从输入到输出的转换, 对于同一个资源可以依次使用多个 Loader
1.14、Webpack 插件机制
Webpack 常用插件: clean-webpack-plugin
安装 clean-webpack-plugin
cnpm i clean-webpack-plugin -D
编辑 webpack.config.js
//webpack.config.jsconst { CleanWebpackPlugin } = require('clean-webpack-plugin')......., //绝大多数插件导出的都是一个类型plugins: [ new CleanWebpackPlugin()]
运行 webpack 打包命令 => 之前的文件就不会存在了;
Webpack 常用插件 html-webpack-plugin
安装 html-webpack-plugin
cnpm i html-webpack-plugin -D
修改 webpack.config.js
/*amend + *///html-webpack-plugin 默认导出的就是插件的类型, 不需要解构内部的成员;const HtmlWebpackPlugin = require('html-webpack-plugin')/*amend end */....,plugins: [ ....., new HtmlWebpackPlugin()]
html-webpack-plugin 选项
//webpack.config.js....., plugins: [ new CleanWebpackPlugin(), new HtmlWebpackPlugin({ //通过传入对象参数修改 HTML 文件 title: 'WebpackPluginHtml', //设置 html 文件的title, mate: {//已对象的形式设置 html 中的源数据标签 viewport: 'width=device-width, initial-scale=1.0' } }) ]
通过 Webpack 命令再次打包项目
<!-- 为 index.html 文件生成模板文件 --><!DOCTYPE html><html landg="zh-CN"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta http-equiv="X-UA-Compatible" content="ie=Edge" /> <title><%= htmlWebpackPlugin.options.title %></title> </head> <body> <h1> <%= htmlWebpackPlugin.options.csaData %> </h1> </body></html>
配置 webpack.config.js
....., plugins: [ new CleanWebpackPlugin(), new HtmlWebpackPlugin({ title: 'WebpackPluginTitle', csaData: '测试数据', template: './src/indexTemplate.html' }) ]
通过 webpack 重新打包项目
html-webpack-plugin 多实例
编辑 webpack.config.js
....., plugins: [ new CleanWebpackPlugin(), //用于生成 index.html new HtmlWebpackPlugin({ title: 'WebpackPluginTitle', csaData: '测试数据', template: './src/indexTemplate.html' }), //用于生成 about.html new HtmlWebpackPlugin({ filename: 'about.html' //指定输出的文件名,默认值为 index.html }) ]
通过 webpack 重新打包项目
Webpack 常用插件: copy-webpack-plugin & 总结
安装 copy-webpack-plugin
cnpm i copy-webpack-plugin -D
配置 webpack.config.js
const CopyWebpackPlugin = require('copy-webpack-plugin')......, plugins: [ ......, //该类型的构造函数要求出入一个数组: 用于指定需要拷贝的文件路径, 可以是一个通配符、目录、文件的相对路径 new CopyWebpackPlugin({ patterns: [ { from: "./public", to: "./" } ] }) ]
总结: 至此, 我们了解了几个 Webpack 常用的插件, 这几个插件一般的项目中都可以用到; 当你需要其他的插件时, 可以提取需求的关键词去 GitHub 上搜索 => 虽然每个插件的作用不尽相同,但是他们的用法都是类似的
1.15、 Webpack 开发一个插件
开发一个插件
编辑 webpack.config.js => 开发插件
const ......;//定义一个类class MyPlugin { //定义一个 apply 方法: 该方法会在 Webpack 启动时自动被调用 => 接受一个 compiler 对象参数, 该参数就是 Webpack 工作过程最核心的一个对象, 里面包含了此次构建的所有配置信息, 我们也是通过该对象去注册钩子函数 //=>这里的需求是该插件可以用来清除 Webpack在打包生成生成的 JS 代码中那些没有必要的注释 => bundle.js 当中去除了这些注释之后,就可以更加容易阅读; //有了需求之后, 需要明确该插件的执行时机, 也就是要把这个插件挂载在哪个钩子上面; 需求是删除 bundle.js 中没有必要的注释, 意思就是需要 bundle.js 明确之后才可以实施相应的动作; //在官网中查找 API => 找到 emit 钩子, 根据文档提示, 发现该钩子在 Webpack 即将要玩输出目录输出文件时执行, 非常符合需求; apply (compiler) { console.log('myPlugin 启动') //通过 compiler.hooks 访问到 emit 钩子函数 => 通过 tap 方法注册一个钩子函数: 该函数接受两个参数 => 第一个是插件的名称, 第二个是需要挂载在插件上的函数 compiler.hooks.emit.tap('MyPlugin', compilation => { // compilation: 可以理解为此次打包的上下文, 所有打包过程中产生的结果都会放到该对象中 // compilation.assets: 获取即将写入目录当中的资源文件信息, 是一个对象 for (const name in compilation.assets) { console.log(name) // 通过 name 获取输出的文件名称 //通过 compilation.assets[name].source() 获取输出文件内容 console.log(compilation.assets[name].source()) if (name.endsWith('.js')) { const contents = compilation.assets[name].source() const widthoutComments = contents.replace(/\/\*\*+\*\//g, '') compilation.assets[name] = { source: () => widthoutComments,//重新赋值输出文件内容 size: () => widthoutComments.length //用来范围新内容的大小, 该方法是 Webpack 内部要求的必需的方法 } } } }) } }....,plugins: [ ....., new Myplugin()]
通过开发一个插件的机制: 插件是通过在生命周期的钩子中挂载函数实现扩展, 如果你要深入了解插件机制, 你需要去理解 Webpack 底层的原理
1.16、Webpack 开发体验问题
Webpack 实现自动编译
启动 webpack 命令打包时, 在命令行后面添加 --watch 参数
yarn webpack --watch
Webpack 实现自动刷新浏览器
全局范围安装 BrowserSync
cnpm i browser-sync -g
通过 --watch 模式启动编译服务 => 通过 browser-sync 启动 HTTP 服务并同时监听 dist 目录下的文件变化
browser-sync dist --files "**/*"
Webpack Dev Server
安装 Webpack Dev Server
cnpm i webpack-dev-server -D
运行 webpack-dev-server 命令
yarn webpack-dev-server
通过 --open 命令行参数自动开启浏览器
yarn webpack-dev-server --open
**高版本webpack (v4)及以后版本将不支持 webpack-dev-server, 解决方案: 在 package.json 的 scripts 标签中添加命令: **
//package.json.....,"scripts": { ....., "devServer": "webpack server --open"//我的命令行}
Webpack Dev Server 静态资源访问
//webpack.config.js....,//添加 devServer 属性 => 专门为 Webpack Dev Server 指定相应的配置 devServer: { contentBase: './public', //指定额外的静态资源路径: 该属性值可以是个字符串, 也可以是个数组(可以配置一个或者多个路径) }
注释掉 copy-webpack-plugin
//webpack.config.js...., //开发阶段最好不要使用 copy-webpack-plugin // new CopyWebpackPlugin()
再次执行 webpack-dev-server
yarn webpack-dev-server
Webpack Dev Server 代理 API 服务
目标: 将 Github API 代理到开发服务器
配置 webpack.config.js
....., devServer: { contentBase: './public', proxy: {//专门添加代理服务配置, 对象内的每一个属性就是一个代理规则的配置 '/api': { //http://localhost:8080/api/users -> https://api.github.com/api/users: 请求的路径是什么, 代理的路径地址是会完全一致的 => 而实际的地址为: https://api.github.com/users => 对代理路径的 /api 需要通过重写的方式去掉 target: 'https://api.github.com', //代理的目标路径 pathRewrite: { '^/api': ''//规则: 将以 /api 为开头的路径替换为 '' }, //实现代理路径的重写 changeOrigin: true //原因: 默认代理服务器会以实际在浏览器中请求的主机名(localhost:8080) 作为代理请求的主机名 -> 在浏览器端对代理后的地址发起请求, 请求背后还是会去请求到 github 上, 该请求默认的主机名是本地的 localhost => 服务器是需要通过主机名判断请求的来源, 服务器就会将资源发送到来源地址(主机名) => localhost:8080 对于服务器肯定是不认识的 => 所以这里需要修改, 当该属性为 true 时, 请求会以实际代理主机名去发起请求 => 这时主机名就会保持 api.github.com 的原有状态 } //属性的名称就是需要被代理的请求路径前缀 -> 请求以哪个地址开始, 就会触发代理请求 => 属性值: 是为属性名称所匹配的到的代理规则配置 } }
重新运行 webpack-dev-server
yarn webpack-dev-server
1.17、Source Map
下载 jquery, 查看 jquery.xxxx.min.map
//运行代码........, //# sourceMappingURL = xxxxxx
Source Map 解决了源代码与运行代码不一致说产生的问题
Webpack 配置 Source Map
修改 webpack 配置文件
//webpack.config.js//原始代码 ......,//devtool: 用来配置开发过程中的辅助工具 -> Source Map 相关的配置在该属性里配置devtool: 'source-map'
运行 webpack 命令打包项目
yarn webpack
通过 serve 工具将打包结果运行起来
serve dist
Webpack 配置 Source Map - eval 模式下的 Source Map
Source Map: eval 模式
修改 webpack 配置文件
devtool: 'eval'
通过 webpack 重新打包项目
yarn webpack
通过 serve 运行打包结果 dist
serve dist
Webpack 不同 devtool 之间的差异
分别编写 heading.js、main.js
//heading.jsexport default () => { console.log('Heading ~') const element = document.createElement('h2') element.textContent = 'Hello World' element.classList.add('heading') element.addEventListener('click', () => { alert('Hello Webpack') }) return element}
//main.jsimport createHeading from './heading.js'const heading = createHeading()document.body.append(heading)console.log('main.js running')//故意放置一个错误console.log11('main.js error')
安装 webpack, babel-loader, html-webpack-plugin
cnpm i webpack webpack-cli babel-loader @babel/core @babel/preset-env html-webpack-plugin -D
编辑 webpack 配置文件
//webpack.config.jsconst HtmlWebpackPlugin = require('html-webpack-plugin')//Source Map 模式数组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']//Webpack 的配置对象可以是数组, 数组中的每个元素都是一个单独的打包配置 -> 可以在一次的打包过程中同时执行多个打包任务module.exports = allModes.map(item => { return { devtool: item, mode: 'none', entry: './src/main.js', output: { filename: `js/${itme}.js` }, module: { rules: [ { test: /\.js$/, use: { loader: 'babel-loader', //配置 babel-loader 的目的: 在待会的对比中能够辨别其中一类模式的差异 options: { presets: ['@babel/preset-env'] } } } ] }, plugins: [ new HtmlWebpackPlugin({ filename: `${item}.html` }) ] }})
通过 webpack 命令打包项目
yarn webpack
通过 serve 运行打包结果
serve dist
Webpack 对比不同 devtool 模式
eval 模式
eval-source-map 模式
eval-cheap-source-map 模式
eval-cheap-module-source-map 模式
带有 module 的模式, 解析出来的源代码不会经过 ES6 加工的, 是真正的源代码; 不带 module 的模式是经过 ES6 加工后的结果;
cheap-source-map 模式
inline-source-map 模式
Hidden-source-map 模式
nosources-source-map 模式
选择合适的 Source Map
理解不同模式的差异, 是为了在不同环境中快速选择合适的模式
1.18、Webpack 自动刷新的问题
Webpack HMR 介绍
开启 HMR
-
需要在运行 webpack-dev-server 命令时添加 --hot 命令行参数;
yarn webpack-dev-server --hot
-
过配置文件开启: 配置 webpack.config.js 中 devServer 的 hot 属性为 true => 载入 Webpack内置插件: HotModuleReplacementPlugin
//配置 webpack.config.js 开启 HMRconst webpack = require('webpack')//原 webpack.config.js 配置信息: .....,devServer: { hot: true},//原 webpack.config.js 配置信息: .....,plugins: [ ....., new webpack.HotModuleReplacementPlugin()]
执行 webpack-dev-server 命令 启动服务器
yarn webpack-dev-server --open
HMR 的疑问
HMR APIs
使用 HMR APIs 手动更新模块并替换: => 编写 main.js
//mian.js//在 main.js 中 加载了其他的模块, 因为在 main.js 导入了其他模块, 当其他模块更新后就必须替换其他模块;.....,//HMR APIs 为 module 对象提供了 hot 对象;// hot 对象: HMR API 的核心对象, 提供 accept 方法 -> 用于注册模块更新后的处理函数 => 第一参数: 依赖模块的路径; -> 第二个参数: 依赖路径更新后的处理函数;module.hot.accept('./editor.js', () => { console.log('editor 模块更新了, 需要手动处理热替换逻辑')})
通过 webpack-dev-server 启动项目
yarn webpack-dev-server --open
修改 editor 模块
//editor.js....,console.log(111)
手动处理 JS 模块热替换
修改 main.js
import createEditor from './editor'import background from './better.png'import './global.css'const editor = createEditor()document.body.appendChild(editor)const img = new Image()img.src = backgrounddocument.body.appendChild(img)let lastEditor = editeormodule.hot.accept('./editor.js', () => { document.body.removeChild(editor) const newEditor = createEditor() document.body.appendChild(newEditor) lastEditor = newEditor})
修改 editor 模块
编辑 main.js
.....,let lastEditor = editormodule.hot.accept('./editor.js', () => { const editorValue = lastEditor.innerHTML document.body.removeChild(editor) const newEditor = createEditor() newEditor.innerHTML = editorValue document.body.appendChild(newEditor) lastEditor = newEditor})
Webpack 处理图片模块热替换
编辑 main.js
import createEditor from './editor'import background from './better.png'import './global.css'const editor = createEditor()document.body.appendChild(editor)const img = new Image()img.src = backgrounddocument.body.appendChild(img)// 以下用于处理 HMR, 与业务代码无关// editor 模块 JS 热替换逻辑let lastEditor = editeormodule.hot.accept('./editor.js', () => { document.body.removeChild(editor) const newEditor = createEditor() document.body.appendChild(newEditor) lastEditor = newEditor})// 图片模块 热替换module.hot.accept('./better.png', () => { img.src = background})
以上就是想应用 针对与两种不同类型资源的热替换的逻辑, 可能你会觉得麻烦, 因为你要写一些额外的代码, 甚至有人会觉得不得不用; => 个人想法: 利大于弊, 对于一个长期开发的项目, 这点额外的工作并不算什么, 如果你能为自己的代码设计规律, 就可以实现一些通用的设计方案; => 如果你使用的是框架开发的话, 使用 HMR 将会十分简单, 因为大部分框架都有成熟的 HMR 方案
HMR 注意事项
Q1: 处理 HMR 的代码报错会导致自动刷新
修改 webpack.config.js
.....,devServer: { hotOnly: true //hot: true => hotOnly: true}
重新启动 webpack-dev-server
Q2: 没启用HMR 的情况下, HMR API 报错
//main.js.....,// 以下用于处理 HMR, 与业务代码无关if (module.hot) { .....,//HMR 逻辑代码}
Q3: 代码中多了一些与业务无关的代码
1.19、Webpack生产环境优化
不同环境下的配置
2.一个环境对应一个配置文件
配置文件根据环境不同导出不同配置
修改 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')// Webpack 的配置文件还支持导出一个函数, 在函数中返回配置对象 => 接受两个参数: env -> 通过 CLI 传递的环境名参数; argv -> 运行 CLI 过程中传递的所有参数;module.exports = (env, argv) => { const config = {.....}//将开发模式的配置文件放在 config 中 if ('production' == env) {//生产环境的 env 就是 production config.mode = 'production' config.devtool = false config.plugins = [ ...config.plugins, new CleanWebpackPlugin(), new CopyWebpackPlugin(['public']) ] } return config}
不传递任务命令行参数 直接运行 webpack
yarn webpack
传递命令行参数 --env production 运用 webpack
yarn webpack --env production
不同环境对应不同配置文件
在项目的根目录下新建 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: 'projTitle', template: './src/index.ejs' }), new webpack.HotModuleReplacementPlugin() ]}
在根目录下新建 webpack.dev.js
在根目录下新建 webpack.prod.js
const common = require('./webpack.common')const merge = require('webpack-merge')const { CleanWebpackPlugin } = require('clean-webpack-plugin')const CopyWebpaclPlugin = require('copy-webpack-plugin')//通过 Object.assign() 方法将公共配置对象复制到这里的配置对象中, 并且可以用文件中的一些对象覆盖公共配置对象中的配置; => Object.assign() 方法是完全覆盖前一个对象中的同名属性, 这样的特点对于值类型的属性覆盖是没有问题的, 当时配置中的像 plugins 这样的数组是希望在原有的基础上添加两个插件, Object.assign() 方法就不适合与这里了;// webpack-merge 是专门来解决合并问题的 => 安装 webpack-merge => cnpm i webpack-merge -Dmodule.exports = merge(common, { mode: 'production', plugins: [ new CleanWebpackPlugin(), new CopyWebpackPlugin(['public']) ]})
运行命令行进行打包
yarn webpack --config webpack.prod.js
Webpack DefinePlugin
编辑 webpack.config.js
const webpack = require('webpack')module.exports = { mode: 'none', entry: './src/main.js', output: { filename: 'bundle.js' }, plugins: [ new webpack.DefinePlugin({ //构造函数接受一个对象, 该对象的每一个键值都会被注入到代码中 API_BASE_URL: 'https://api.example.com' //为代码注入 API 服务地址 Ex: https://api.example.com }) ]}
编辑main.js
//main.jsconsole.log(API_BASE_URL)
运行 webpack 打包
yarn webpack
Webpack 体验 Tree Shaking
//components.jsexport const Button = () => { return document.createElement('button') console.log('dead-code')//该段代码是在 return 后, 属于未引用代码}export const Link = () => { return document.createElement('a')}export const Heading = level => { return document.createElement('h' + level)}
//index.jsimport { Button } from './components'//这里只导入了 components 中的 Button 成员 就导致了 components 中的 Link 和 Heading 成员未被引用 对于打包后的结果就是冗余的document.body.appendChild(Button())
运行 webpack 以 production 模式打包
yarn webpack --mode production
Webpack 使用 Tree Shaking
运行 webpack 以 none 模式打包
yarn webpack
编辑 webpack.config.js
module.exports = { mode: 'none', entry: './src/index.js', output: { filename: 'bundle.js' }, optimization: { //该属性: 集中配置 Webpack 的优化功能 usedExports: true, //在输出结果中 只导出那些外部使用了的成员 }}
重新运行 webpack 以 none 模式打包
yarn webpack
编辑 webpack.config.js
....., optimization: { usedExports: true, minimize: true }
Webpack 合并模块
编辑 webpack.config.js
....., optimization: { usedExports: true, concatenateModules: true, //minimize: true 为了看到 concatenateModules 的作用 先关闭 minimize }
运行 webpack 打包项目
Tree-shaking & Babel
运行 webpack 打包
yarn webpack
编辑 webpack.config.js
...., module: { relues: [ { test: /\.js$/, use: { loader: 'babel-loader', options: { presets: [ //注意这里是 二维数组: 第一个成员 -> 所使用的的 preset 的名称 第二个成员 -> 给该 preset 定义的配置对象 ['@babel/preset-env', { modules: 'commonjs'/* 默认该属性的值为 auto(根据环境判断是否开启 ESM 插件) */ }] ] } } } ] },....,
最新版本的 babel-loader 并不会造成 Tree-shaking 失效 将 webpack.config.js 中 babel-loader 的 presets 的modules 设置为 false => Tree-shaking 就不会失效
Webpack sideEffects
sideEffects 特性介绍
//button.jsexport default () => { return document.createElement('button') console.log('dead-code')}
//heading.jsexport default level => { return document.createElement('h' + level)}
//link.jsexport default () => { return document.createElement('a')}
//components/index.jsexport { default as Button } from './button'export { default as Link } from './link'export { default as Heading } from './heading'
//src/index.jsimport { Button } from './components'document.body.appendChild(Button())
编辑 webpack.config.js
module.exports = { mode: 'none', entyr: './src/index.js', output: { filename: 'bundle.js' }, optimization: { sideEffects: true }}
编辑 package.json
{ ....., "sideEffects": false //标识当前 package.json 所影响的项目中的所有代码都没有副作用 这些没有用到的模块没有副作用 -> 就会被移除掉}
sideEffects 注意
//extend.js//为 Number 的原型添加一个扩展方法Number.prototype.pad = function (size) { // 将数字转化为字符串 let result = this + '' // 在数字前补充指定个数的 0 whild (result.length < size) { result = '0' + result } return result}
编辑 src/index.js
import { Button } from './components'import './extend'consoke.log(8.pad(3))document.body.appendChild(Button())
运行 webpack 打包
编辑 package.json
.....,"sideEffects": [ "./src/extend.js", "*.css"]
1.20、Webpack 代码分割
Webpack 多入口打包
// src/index.js: 负责实现 index 页面上所有的功能import fetchApi from './fetch'import './global.css'import './index.css'const mainElement = document.querySelector('.main')fetchApi('/posts').then(data => { data.forEach(item => { const aritcle = document.createElement('article') article.className = 'post' const h2 = document.createElement('h2') h2.textContent = item.title article.appendChild(h2) const paragraph = document.createElement('p') paragraph.textContent = item.body article.appendChild(paragraph) mainElement.appendChild(article) })})
// src/album.js: 负责实现相册页面的所有功能import fetchApi from './fetch'import './global.css'import './album.css'const mainElement = document.querySelector('.main')fetchApi('/photos?albumId=1').then(data => { data.forEach(item => { const section = document.createElement('section') section.className = 'photo' const img = document.createElement('img') img.src = item.thumbnailUrl section.appendChild(img) const h2 = document.createElement('h2') h2.textContent = item.title section.aooendChild('h2') mainElement.appendChild(section) })})
// src/fetch.js: 公共模块 -> 负责用来提供请求 API 的方法export default endpoint => { return fetch(`https://jsonplaceholder.typicode.com${endpoint}`) .then(response => response.json())}
编辑 webpack.config.js
const { CleanWebpackPlugin } = require('clean-webpack-plugin')const HtmlWebpackPlugin = require('html-webpack-plugin')module.exports = { mode: 'none', entry: './src/index.js', output: { filename: 'bundle.js' }, module: { rules: [ { test: /\.css$/, use: [ 'style-loader', 'css-loader' ] } ] }, plugins: [ new CleanWebpackPlugin(), new HtmlWebpackPlugin({ title: 'Multi Entry', template: './src/index.html', filename: 'index.html' }), new HtmlWebpackPlugin({ title: 'Multi Entry', template: './src/album.html', filename: 'album.html' }) ]}
编辑 webpack.config.js
....., entry: { //该对象中一个键值对就是一个打包入口 -> 键就是入口的名称 值就是入口对应的文件路径 index: './src/index.js', album: './src/album.js' }, output: { filename: '[name].bundle.js' //[name] 会被替换为入口的名称 }
修改 webpack.config.js
// html-webpack-plugin 插件默认会输出一个自动注入所有打包结果的 html -> 如果要指定输出 html 文件所引用的 bundle -> 使用 chunks 属性设置 每一个打包入口会形成一个独立的 chunks ....., plugins: [ new CleanWebpackPlugin(), new HtmlWebpackPlugin({ title: 'Multi Entry', template: './src/index.html', filename: 'index.html', chunks: ['index'] }), new HtmlWebpackPlugin({ title: 'Multi Entry', template: './src/album.html', filename: 'album.html', chunks: ['album'] }) ]
Webpack 提取公共模块
编辑 webpack.config.js
....., optimization: { splitChunks: { chunks: 'all' //将所有的公共模块都提取到单独的 bundle 中 } }....,
Webpack 动态导入
// src/index.jsimport posts from './posts/posts'import albom from './album/album'const render = () => { const hash = window.location.hash || '#posts' const mainElement = document.querySelector('.main') mainElement.innerHTML = '' if ('#posts' === hash) { mainElement.appendChild(posts()) } else if ('#album' === hash) { mainElement.appendChild(album()) }}render()window.addEventListener('hashchange', render)
修改 src/index.js
// import posts from './posts/posts'// import albom from './album/album'// 动态导入使用的是 ES Modules 的动态导入 -> 在需要导入的地方通过 import() 函数导入指定的路径 -> 该方法返回的是 Promise -> 在该 Promise 的 then 方法中就可以拿到模块对象 moduleconst render = () => { const hash = window.location.hash || '#posts' const mainElement = document.querySelector('.main') mainElement.innerHTML = '' if ('#posts' === hash) { // mainElement.appendChild(posts()) // 由于使用的是默认导出 -> 所以需要结构 -> 结果完成后 创建页面的元素 import('./posts/posts').then(({ default: posts }) => { mainElement.appendChild(posts()) }) } else if ('#album' === hash) { //mainElement.appendChild(album()) import('./album/album').then(({ default: album }) => { mainElement.appendChild(album()) }) }}render()window.addEventListener('hashchange', render)
Webpack 魔法注释
编辑 src/index.js
// import posts from './posts/posts'// import albom from './album/album'// 动态导入使用的是 ES Modules 的动态导入 -> 在需要导入的地方通过 import() 函数导入指定的路径 -> 该方法返回的是 Promise -> 在该 Promise 的 then 方法中就可以拿到模块对象 moduleconst render = () => { const hash = window.location.hash || '#posts' const mainElement = document.querySelector('.main') mainElement.innerHTML = '' if ('#posts' === hash) { // mainElement.appendChild(posts()) // 由于使用的是默认导出 -> 所以需要结构 -> 结果完成后 创建页面的元素 // 使用 webpack 魔法注释: 在调用 import() 函数时添加一个行内注释参数 -> 该注释有特定的格式: webpackChunkName: 分包名称 import(/* webpackChunkName: 'posts' */, './posts/posts').then(({ default: posts }) => { mainElement.appendChild(posts()) }) } else if ('#album' === hash) { //mainElement.appendChild(album()) import(/* webpackChunkName: album */, './album/album').then(({ default: album }) => { mainElement.appendChild(album()) }) }}render()window.addEventListener('hashchange', render)
Webpack MiniCssExtractPlugin
通过命令行安装 mini-css-extract-plugin
cnpm i mini-css-extract-plugin -D
编辑 webpack.config.js
....,const MiniCssExtractPlugin = require('mini-css-extract-plugin')....., plugins: [ ......, new MiniCssExtractPlugin() ]
编辑 webpack.config.js
......,const MiniCssExtractPlugin = require('mini-css-extract-plugin')......, module: { rules: [ { test: /\.css$/, use: [ // style-loader, //将样式文件通过 style 标签注入页面 MiniCssExtractPlugin.loader, 'css-loader' ] } ] }......, plugins: [ ......, new MiniCssExtractPlugin() ]
Webpack OptimizeCssAssetsWebpackPlugin
通过命令行安装 optimize-css-assets-webpack-plugin
cnpm i optimize-css-assets-webpack-plugin -D
编辑 webpack.config.js
......,const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin')......, plugins: [ ......, new OptimizeCssAssetsWebpackPlugin() ]
编辑 webpack.config.js
......,const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin')......, optimization: { minimizer: [ new OptimizeCssAssetsWebpackPlugin() ] } plugins: [ ......, // new OptimizeCssAssetsWebpackPlugin() ]
通过命令行安装 terser-webpack-plugin
cnpm i terser-webpack-plugin -D
将 terser-webpack-plugin 插件手动添加到 minimizer 数组中
// webpack.config.js......,const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin')const TerserWebpackPlugin = require('terser-webpack-plugin')......, optimization: { minimizer: [ new TerserWebpackPlugin(), new OptimizeCssAssetsWebpackPlugin() ] } plugins: [ ......, // new OptimizeCssAssetsWebpackPlugin() ]
Webpack 输出文件名 Hash
-
最普通的 Hash: ‘[name]-[hash]’
//webpack.config.jsmodule.exports = { ......, output: { filename: '[name]-[hask].bundle.js' }, ......, plugins: [ ......, new MIniCssExtractPlugin({ filename: '[name]-[hash].bundle.js' }) ]}
运行 webpack 打包: yarn webpack
修改项目中的任何一个文件代码 -> 重新打包
-
chunkhash
//webpack.config.jsmodule.exports = { ......, output: { filename: '[name]-[chunkhash].bundle.js' }, ......, plugins: [ ......, new MIniCssExtractPlugin({ filename: '[name]-[chunkhash].bundle.js' }) ]}
运行 webpack 打包: yarn webpack
修改 index.js -> 重新打包
修改 posts.js -> 重新打包
-
contenthash
//webpack.config.jsmodule.exports = { ......, output: { filename: '[name]-[contenthash].bundle.js' }, ......, plugins: [ ......, new MIniCssExtractPlugin({ filename: '[name]-[contenthash].bundle.js' }) ]}
修改 index.js -> 重新打包
修改 posts.css -> 重现打包
修改 hash 的长度
//webpack.config.jsmodule.exports = { ......, output: { filename: '[name]-[contenthash:8].bundle.js' }, ......, plugins: [ ......, new MiniCssExtractPlugin({ filename: '[name]-[contenthash:8].bundle.js' }) ]}
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!