Webpack4.0 入门与进阶
前言
一、初识 Webpack
1、Webpack 是什么
- Webpack 官网地址:https://v4.webpack.docschina.org/concepts/
- webpack 是一个 JavaScript 应用程序的静态模块打包工具。当 webpack 处理应用程序时,它会在内部构建一个 依赖图(dependency graph),此依赖图会映射项目所需的每个模块,并生成一个或多个 bundle
2、Webpack 环境搭建
(1)Webpack 安装
- 安装 node.js:https://nodejs.org/zh-cn/
- 创建项目目录,并进入该目录
// 创建项目目录
mkdir webpack-demo
// 进入该项目目录
cd webpack-demo
- 初始化项目
// 初始化项目
npm init
{
"name": "webpack4.0",
"version": "1.0.0",
"description": "",
"private": true, // 表示是私有项目,不会发布到 npm 线上仓库
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "lgk",
"license": "ISC" // 如果想开源,可以改为 MIT
}
- 安装 Webpack
// 全局安装 Webpack (不推荐)
// 如果两个项目都要 webpack 打包,且两个项目的版本不一样,就会导致有一个运行不起来
npm install webpack webpack-cli -g
// 查看版本
webpack --version
// 卸载 webpack webpack-cli
npm uninstall webpack webpack-cli -g
// 局部安装 Webpack
npm install webpack webpack-cli --save-dev
npm install webpack webpack-cli -D
// 查看版本
npx webpack -v
// 不知道一个包的某个版本是否存在
npm info webpack
(2)Webpack 的配置文件
- webpack.config.js 文件
const path = require('path');
module.exports = {
mode: 'production', // 如果不配置,默认就是 production
// entry: './src/index.js', // 入口文件:项目从哪个文件开始打包
entry: {
main: './src/index.js'
},
output: {
filename: 'bundle.js', // 打包生成的文件名
// path 路径必须为绝对路径
path: path.resolve(__dirname, 'dist')
}
}
- package.json 文件
{
"name": "webpack4.0",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"bundle": "webpack"
},
"author": "",
"license": "ISC",
"devDependencies": {
"webpack-cli": "^4.8.0"
},
"dependencies": {
"webpack": "^5.54.0"
}
}
(3)运行 webpack 打包
- webpack-cli 的作用是使得可以执行如下代码
- 全局安装 webpack 后使用如下方式
webpack 入口文件
- 局部安装 webpack
npx webpack 入口文件
- npm scripts 方式:在 package.json 中 scripts 配置 “bundle”: “webpack” 后
npm run bundle
二、Webpack 核心知识
1、Loader
- Webpack 默认只能打包 .js 文件,如果想要打包其它,如:.png、.jpg、.txt 等文件,需要安装引入对应的“加载器” Loader
- Loader 是转换器,用来预处理除 JavaScript 之外的任何静态资源,方便 webpack 进行打包
- 安装 Loader
npm install vue-loader --save-dev
- 配置 Loader:同一个 rule 下,loader 的执行顺序是从后到前,从右到左
const path = require('path');
module.exports = {
mode: 'production', // 如果不配置,默认就是 production
// entry: './src/index.js', // 入口文件:项目从哪个文件开始打包
entry: {
main: './src/index.js'
},
output: {
filename: 'bundle.js', // 打包生成的文件名
// path 路径必须为绝对路径
path: path.resolve(__dirname, 'dist')
},
module: {
rules: [
{
test: /\.vue$/,
use: [{
loader: 'vue-loader',
options: {},
}]
},
]
}
}
(1)图片
- 安装 Loader
// file-loader
npm install file-loader --save-dev
// url-loader
npm install url-loader --save-dev
- 配置 Loader
const path = require('path');
module.exports = {
module: {
rules: [{
test: /\.(png|jpg|gif)$/,
use: [{
loader: 'file-loader',
options: {
outputPath: 'images/', // 打包到 dist/images 下
name: '[name].[ext]',
},
}]
},
{
test: /\.(png|jpg|gif)$/,
use: [{
loader: 'url-loader',
options: {
limit: 10240
},
}]
},
]
}
}
- file-loader 和 url-loader 的区别:
file-loader 返回的是图片的url
url-loader 可以通过 limit 属性对图片分情况处理,当图片小于 limit(单位: byte )大小时转 base64 ,大于 limit 时调用 file-loader 对图片进行处理
url-loader 封装了 file-loader,但 url-loader 并不依赖于 file-loader
(2)样式
- 安装 Loader
// style-loader
npm install style-loader --save-dev
// css-loader
npm install css-loader --save-dev
// sass-loader
npm install sass-loader --save-dev
// postcss-loader
npm install post-loader --save-dev
- 配置 Loader
const path = require('path');
module.exports = {
module: {
rules: [
{
test: /\.scss$/,
use: [{
loader: 'style-loader',
options: {},
},
{
loader: 'css-loader',
options: {
// 表示该loader引入之前还有引入两个loader
importLoaders: 2,
modules: true // css 模块化
}
},
{
loader: 'sass-loader',
options: {}
},
{
loader: 'postcss-loader', // 添加厂商前缀
options: {}
}]
}]
}
}
- 添加厂商前缀:使用 postcss-loader 时,安装 postcss-loader 的同时还需要安装插件:autoprefixer,并且在根目录下创建 postcss.config.js 文件并配置如下
npm install autoprefixer --save-dev
module.exports = {
plugins: [
require('autoprefixer')
]
}
- css 模块化:
(3)Webpack 打包字体文件
- 首先下载字体文件到项目
- 将 @font-face 中的 url 指向项目中的字体文件
- 安装配置 file-loader 的规则为字体文件的后缀即可
const path = require('path');
module.exports = {
module: {
rules: [{
test: /\.(eot|ttf|svg)$/,
use: [{
loader: 'file-loader',
options: {},
}]
},
]
}
}
2、Plugin
- Plugin 译为"插件",Plugin 作用是扩展 webpack 的功能,让 webpack 具有更多的灵活性。 在 Webpack 运行的生命周期中会广播出许多事件,Plugin 可以监听这些事件,在合适的时机通过 Webpack 提供的 API 改变输出结果
(1)html-webpack-plugin
- html-webpack-plugin 插件会以指定 html 文件为模版,在打包结束后,自动生成一个 html 文件,并把打包生成的 JS 自动引入到这个 html 文件中
- 安装插件
npm install html-webpack-plugin --save-dev
- 使用
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
mode: 'production', // 如果不配置,默认就是 production
// entry: './src/index.js', // 入口文件:项目从哪个文件开始打包
entry: {
main: './src/index.js'
},
output: {
filename: 'bundle.js', // 打包生成的文件名
filename: '[name].js', // 多入口时
// path 路径必须为绝对路径
path: path.resolve(__dirname, 'dist')
},
module: {
rules: [
{
test: /\.vue$/,
use: [{
loader: 'vue-loader',
options: {},
}]
},
]
},
plugins: [new HtmlWebpackPlugin({
template: 'src/index.html'
})]
}
(2)clean-webpack-plugin
- clean-webpack-plugin 插件会在打包运行前自动清空 dist 目录
- 安装插件
npm install clean-webpack-plugin --save-dev
- 使用
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
module.exports = {
mode: 'production', // 如果不配置,默认就是 production
// entry: './src/index.js', // 入口文件:项目从哪个文件开始打包
entry: {
main: './src/index.js'
},
output: {
filename: 'bundle.js', // 打包生成的文件名
// path 路径必须为绝对路径
path: path.resolve(__dirname, 'dist')
},
module: {
rules: [
{
test: /\.vue$/,
use: [{
loader: 'vue-loader',
options: {},
}]
},
]
},
plugins: [
new HtmlWebpackPlugin({
template: 'src/index.html'
}),
new HtmlWebpackPlugin(['dist'])
]
}
3、Entry 和 Output 的基础配置
- 如果 Output 不指定 filename,Entry 也不指定名称,默认生成 main.js 文件
- 如果 Output 不指定 filename,Entry 也指定名称,默认生成 Entry 指定名称的 .js 文件
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
module.exports = {
mode: 'production', // 如果不配置,默认就是 production
// entry: './src/index.js', // 入口文件:项目从哪个文件开始打包
entry: { // 多入口
main: './src/index.js',
sub: './src/index.js'
},
output: {
// publicPath: ‘’, // 会在 html 引入打包 js 文件前加该地址
// filename: 'bundle.js', // 单入口时打包生成的文件名
filename: '[name].js', // 多入口时的出口文件名与入口指定的相同
// path 路径必须为绝对路径
path: path.resolve(__dirname, 'dist')
},
module: {
rules: [
{
test: /\.vue$/,
use: [{
loader: 'vue-loader',
options: {},
}]
},
]
},
plugins: [
new HtmlWebpackPlugin({
template: 'src/index.html'
}),
new HtmlWebpackPlugin(['dist'])
]
}
4、SourceMap
- source-map 是一个映射关系,它可以知道打包后 dist 目录下的 .js 文件与源文件的对应关系,对应关系在 .js.map 文件
- 如果不使用 source-map,只能知道打包后的代码第几行出错,并不知道源代码的第几行出错;使用 source-map 后,可以知道打包后出错的代码对应源代码的位置
- 在 webpack.config.js 文件中配置
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
module.exports = {
mode: 'production', // 如果不配置,默认就是 production
// entry: './src/index.js', // 入口文件:项目从哪个文件开始打包
// development devtool: 'cheap-module-eval-source-map',
// production devtool: 'cheap-module-source-map',
entry: { // 多入口
main: './src/index.js',
sub: './src/index.js'
},
output: {
// publicPath: ‘’, // 会在 html 引入打包 js 文件前加该地址
// filename: 'bundle.js', // 单入口时打包生成的文件名
filename: '[name].js', // 多入口时的出口文件名与入口指定的相同
// path 路径必须为绝对路径
path: path.resolve(__dirname, 'dist')
},
module: {
rules: [
{
test: /\.vue$/,
use: [{
loader: 'vue-loader',
options: {},
}]
},
]
},
plugins: [
new HtmlWebpackPlugin({
template: 'src/index.html'
}),
new HtmlWebpackPlugin(['dist'])
]
}
- source-map 前加 inline:使用后不会单独生成 .map 文件了,而是变成 base64 字符串放在 .js 文件的末尾
- source-map 前加 cheap:加 cheap 后,表示在报错时只会显示到第几行,不会显示到第几列
- source-map 前加 module:加 module 表示不仅管项目业务代码的报错,还会管到第三方模块、loader等的错误
- eval:使用 eval 的方式和使用 source-map 的方式不同,是通过 eval 这种执行形式生成 source-map 的对应关系。eval 这种方式打包最快,但是对于项目量比较大时,显示的错误可能不太全面
- 在开发环境,devtool 使用 cheap-module-eval-source-map
- 在线上环境,devtool 使用 cheap-module-source-map
5、WebpackDevServer
- 安装 devServer
npm install webapck-dev-server --save-dev
- 配置 devServer
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
module.exports = {
mode: 'production', // 如果不配置,默认就是 production
// entry: './src/index.js', // 入口文件:项目从哪个文件开始打包
// development devtool: 'cheap-module-eval-source-map',
// production devtool: 'cheap-module-source-map',
entry: { // 多入口
main: './src/index.js',
sub: './src/index.js'
},
devServer: {
contentBase: './dist', // 服务器建在哪个目录下
open: true, // 表示在启动 webpack-dev-server 时,会自动打开浏览器,然后自动访问服务器的地址
// proxy: { // 跨域:当用户访问 localhost:8080/api 时,会自动转发到 localhost:3000 的地址
// '/api': 'http://localhost:3000'
// },
// port: 9000, // 默认是 8080 端口,如果要改的话可以自行设置
},
output: {
// publicPath: ‘’, // 会在 html 引入打包 js 文件前加该地址
// filename: 'bundle.js', // 单入口时打包生成的文件名
filename: '[name].js', // 多入口时的出口文件名与入口指定的相同
// path 路径必须为绝对路径
path: path.resolve(__dirname, 'dist')
},
module: {
rules: [
{
test: /\.vue$/,
use: [{
loader: 'vue-loader',
options: {},
}]
},
]
},
plugins: [
new HtmlWebpackPlugin({
template: 'src/index.html'
}),
new HtmlWebpackPlugin(['dist'])
]
}
- webpack:不会自动监听源代码的变化,每次都需要手动重新打包
- webpack --watch:当源代码发生变化,webpack 就能监听到打包文件发生变化,然后就会重新打包
- webpack-dev-server:不但会监听源代码的变化,还会自动的重新打开浏览器。webpack-dev-pack 打包后没有生成 dist 目录,打包生成的文件并不会放在 dist 目录下,而是放到电脑的内存中,这样可以有效的提升打包的速度
{
"name": "webpack4.0",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"bundle": "webpack",
"watch": "webpack --watch", // 当源代码发生变化,webpack 就能监听到打包文件发生变化,然后就会重新打包
"start": "webpack-dev-server"
},
"author": "",
"license": "ISC",
"devDependencies": {
"webpack": "^5.54.0",
"webpack-cli": "^4.8.0"
}
}
6、Hot Module Replacement(热模块替换)
- 热更新(HMR):它可以在应用运行的时候,不需要刷新页面,就可以直接替换、增删模块。
- 在 devServer 中配置 hot 值为 true,表示让 webpack-dev-server 开启 热更新。如果编译报错,会抛出错误,重新改成正确的,这个时候又会触发重新编译,整个浏览器会重新刷新!
- 在 devServer 中配置 hotOnly 值为 true,表示即使 HMR 的功能没有生效,也会让浏览器自动的重新刷新。如果编译报错,再改成正确的,重新编译,浏览器不会刷新!
- 引入 webpack 自带的插件 HotModuleReplacementPlugin
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const webpack = require('webpack')
module.exports = {
mode: 'development', // 如果不配置,默认就是 production
devtool: 'cheap-module-eval-source-map',
entry: { // 多入口
main: './src/index.js',
sub: './src/index.js'
},
devServer: {
contentBase: './dist', // 服务器建在哪个目录下
open: true, // 表示在启动 webpack-dev-server 时,会自动打开浏览器,然后自动访问服务器的地址
// proxy: { // 跨域:当用户访问 localhost:8080/api 时,会自动转发到 localhost:3000 的地址
// '/api': 'http://localhost:3000'
// },
// port: 9000, // 默认是 8080 端口,如果要改的话可以自行设置
hot: true, // 表示让 webpack-dev-server 开启 热更新
hotOnly: true // 表示即使 HMR 的功能没有生效,也会让浏览器自动的重新刷新
},
output: {
// publicPath: ‘’, // 会在 html 引入打包 js 文件前加该地址
// filename: 'bundle.js', // 单入口时打包生成的文件名
filename: '[name].js', // 多入口时的出口文件名与入口指定的相同
// path 路径必须为绝对路径
path: path.resolve(__dirname, 'dist')
},
module: {
rules: [
{
test: /\.vue$/,
use: [{
loader: 'vue-loader',
options: {},
}]
},
]
},
plugins: [
new HtmlWebpackPlugin({
template: 'src/index.html'
}),
new HtmlWebpackPlugin(['dist']),
new webpack.HotModuleReplacementPlugin()
]
}
- 对于更改 CSS 样式,css-loader 已经内置有监测 CSS 变化的 module.hot
- 对于一些 JS 代码,还需要手动编写监测 JS 更新的代码
// 例如:监测 number.js 模块的更新
if(module.hot) {
module.hot.accept('./number', () => {
// 监测到 number.js 中代码发生改变后处理的业务
})
}
7、Babel 处理 ES6 语法
- 有些老版本的浏览器不支持 ES6 的语法,所以需要将 ES6 转为 ES5
- 安装 babel-loader
// babel-loader 只是将 webpack 和 Babel 打通,实际上 babel-loader 并不会将 ES6 转为 ES5
npm install --save-dev babel-loader @babel/core
// 所以还需要借助其他模块做语法转换
npm install --save-dev @babel/preset-env
// 对于一些ES6 新增的像 map 等函数,preset-env 还是不够,所以需要补充
npm install --save @babel/polyfill
// 安装好 polyfill 之后只需要在所有需要转换的代码之前引入即可
// import "@babel/polyfill"
- 使用 babel-loader
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const webpack = require('webpack')
module.exports = {
mode: 'development', // 如果不配置,默认就是 production
devtool: 'cheap-module-eval-source-map',
entry: { // 多入口
main: './src/index.js',
sub: './src/index.js'
},
devServer: {
contentBase: './dist', // 服务器建在哪个目录下
open: true, // 表示在启动 webpack-dev-server 时,会自动打开浏览器,然后自动访问服务器的地址
},
output: {
// publicPath: ‘’, // 会在 html 引入打包 js 文件前加该地址
// filename: 'bundle.js', // 单入口时打包生成的文件名
filename: '[name].js', // 多入口时的出口文件名与入口指定的相同
// path 路径必须为绝对路径
path: path.resolve(__dirname, 'dist')
},
module: {
rules: [
{
test: /\.vue$/,
use: [{
loader: 'vue-loader',
options: {},
}]
},
{
test: /\.js$/,
exclude: /node_modules/,
loader: "babel-loader",
options: {
presets: [["@babel/preset-env", {
useBuiltIns: 'usage', // 按需打包 polyfill
targets: {
chrome: "67" // 对于已经支持ES6的浏览器,就不会转为ES5了
}
}]]
}
},
]
},
plugins: [
new HtmlWebpackPlugin({
template: 'src/index.html'
}),
new HtmlWebpackPlugin(['dist']),
new webpack.HotModuleReplacementPlugin()
]
}
- 可以将 babel-loader 中 options 的代码摘离出来放到 .babelrc 文件中,然后就可以不用在每个文件头部用 import @babel/polyfill 引入了
{
presets: [
"@babel/preset-env", {
useBuiltIns: 'usage', // 按需打包 polyfill
targets: {
chrome: "67" // 对于已经支持ES6的浏览器,就不会转为ES5了
}
},
"@babel/preset-react"
]
}
8、Webpack 实现对 React 框架代码的打包
三、Webpack 进阶
1、Tree Shaking
- Tree Shaking 指的是当引入一个模块时,不引入该模块所有的代码,只引入需要的代码,Tree Shaking 只支持 ES Module 的引入方式
- 配置 Tree Shaking 需要在 webpack.config.js 文件中设置如下代码(开发环境,生产环境默认已经配置)
module.exports = {
mode: 'development',
devtool: 'cheap-module-eval-source-map',
devServer: {
contentBase: './dist',
open: true,
hot: true,
hotOnly: true,
},
entry: {
main: './src/index.js'
},
output: {
filename: '[name].js',
path: path.resolve(__dirname, 'dist')
},
module: {
// loader
rules: [{
test: /\.js$/,
exclude: /node_module/,
loader: 'babel-loader'
}]
},
plugins: [
new HtmlWebpackPlugin({
template: 'src/index.html'
})
],
optimization: {
usedExports: true
}
}
- 在 package.json 文件中配置 sideEffects,如果某些文件不需要 Tree Shaking,可以在 sideEffects 中配置
{
"name": "test",
"sideEffects": false,
// "sideEffects": ["*.css"]
"version": "1.0.0",
//等等
}
2、Development 和 Production 模式的区分打包
- 开发环境:source-map 比较全面,可以在开发时快速定位问题;代码不希望压缩,可以明显看到打包生成的内容;
- 生产环境:source-map 较为简洁;代码压缩,提高打包构建速度;
webpack.dev.js 文件:
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const webpack = require('webpack')
module.exports = {
mode: 'development', // 如果不配置,默认就是 production
devtool: 'cheap-module-eval-source-map',
entry: { // 多入口
main: './src/index.js'
},
devServer: {
contentBase: './dist', // 服务器建在哪个目录下
open: true, // 表示在启动 webpack-dev-server 时,会自动打开浏览器,然后自动访问服务器的地址
port: 8080,
hot: true,
hotOnly: true
},
output: {
// publicPath: ‘’, // 会在 html 引入打包 js 文件前加该地址
// filename: 'bundle.js', // 单入口时打包生成的文件名
filename: '[name].js', // 多入口时的出口文件名与入口指定的相同
// path 路径必须为绝对路径
path: path.resolve(__dirname, 'dist')
},
module: {
rules: [{
test: /\.(eot|ttf|svg)$/,
use: [{
loader: 'file-loader',
options: {
outputPath: 'images/', // 打包到 dist/images 下
name: '[name].[ext]',
},
}]
},
{
test: /\.(png|jpg|gif)$/,
use: [{
loader: 'url-loader',
options: {
limit: 10240
},
}]
},
{
test: /\.css$/,
use: [
'style-loader',
'css-loader',
'postcss-loader'
]
},
{
test: /\.scss$/,
use: [{
loader: 'style-loader',
options: {},
},
{
loader: 'css-loader',
options: {
// 表示该loader引入之前还有引入两个loader
importLoaders: 2,
modules: true // css 模块化
}
},
{
loader: 'sass-loader',
options: {}
},
{
loader: 'postcss-loader', // 添加厂商前缀
options: {}
}]
},
{
test: /\.js$/,
exclude: /node_modules/,
loader: "babel-loader",
options: {}
},
]
},
plugins: [
new HtmlWebpackPlugin({
template: 'src/index.html'
}),
new HtmlWebpackPlugin(['dist']),
new webpack.HotModuleReplacementPlugin()
],
optimization: {
usedExports: true
}
}
webpack.prod.js 文件:
- 生产环境不需要 devServer
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
module.exports = {
mode: 'production', // 如果不配置,默认就是 production
devtool: 'cheap-module-source-map',
entry: { // 多入口
main: './src/index.js'
},
output: {
// publicPath: ‘’, // 会在 html 引入打包 js 文件前加该地址
// filename: 'bundle.js', // 单入口时打包生成的文件名
filename: '[name].js', // 多入口时的出口文件名与入口指定的相同
// path 路径必须为绝对路径
path: path.resolve(__dirname, 'dist')
},
module: {
rules: [{
test: /\.(eot|ttf|svg)$/,
use: [{
loader: 'file-loader',
options: {
outputPath: 'images/', // 打包到 dist/images 下
name: '[name].[ext]',
},
}]
},
{
test: /\.(png|jpg|gif)$/,
use: [{
loader: 'url-loader',
options: {
limit: 10240
},
}]
},
{
test: /\.css$/,
use: [
'style-loader',
'css-loader',
'postcss-loader'
]
},
{
test: /\.scss$/,
use: [{
loader: 'style-loader',
options: {},
},
{
loader: 'css-loader',
options: {
// 表示该loader引入之前还有引入两个loader
importLoaders: 2,
modules: true // css 模块化
}
},
{
loader: 'sass-loader',
options: {}
},
{
loader: 'postcss-loader', // 添加厂商前缀
options: {}
}]
},
{
test: /\.vue$/,
use: [{
loader: 'vue-loader',
options: {},
}]
},
{
test: /\.js$/,
exclude: /node_modules/,
loader: "babel-loader",
options: {}
},
]
},
plugins: [
new HtmlWebpackPlugin({
template: 'src/index.html'
}),
new HtmlWebpackPlugin(['dist'])
]
}
package.json 文件:
{
"name": "webpack4.0",
"sideEffects": false,
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"dev": "webpack-dev-server --config webpack.dev.js",
"build": "webpack --config webpack.prod.js",
"test": "webpack --config webpack.dev.js"
},
"author": "",
"license": "ISC",
"devDependencies": {
},
"dependencies": {
}
}
抽取公共配置 webpack.common.js 文件:
- 合并配置文件需要安装第三方模块
npm install webpack-merge --save-dev
- webpack.dev.js
const webpack = require('webpack');
const merge = require('webapck-merge');
const commonConfig = require('./webpack.common.js');
const devConfig = {
mode: 'development',
devtool: 'cheap-module-eval-source-map',
devServer: {
contentBase: './dist', // 服务器所在目录
open: true, // 开启服务
port: 8080, // 设置端口
hot: true, // 开启热更新
// hotOnly: true // 开启后编译出错不会自动刷新
},
plugins: [
new webpack.HotModuleReplacementPlugin()
]
}
module.exports = merge(commonConfig, devConfig)
- webpack.prod.js
const merge = require('webapck-merge');
const commonConfig = require('./webpack.common.js');
const prodConfig = {
mode: 'production',
devtool: 'cheap-module-source-map'
}
module.exports = merge(commonConfig, prodConfig)
- webpack.common.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
module.exports = {
entry: { // 多入口
main: './src/index.js'
},
output: {
// path 路径必须为绝对路径
path: path.resolve(__dirname, 'dist')
},
module: {
rules: [{
test: /\.(eot|ttf|svg)$/,
use: [{
loader: 'file-loader',
options: {
outputPath: 'images/', // 打包到 dist/images 下
name: '[name].[ext]',
},
}]
},
{
test: /\.(png|jpg|gif)$/,
use: [{
loader: 'url-loader',
options: {
limit: 10240
},
}]
},
{
test: /\.css$/,
use: [
'style-loader',
'css-loader',
'postcss-loader'
]
},
{
test: /\.scss$/,
use: [{
loader: 'style-loader',
options: {},
},
{
loader: 'css-loader',
options: {
// 表示该loader引入之前还有引入两个loader
importLoaders: 2,
modules: true // css 模块化
}
},
{
loader: 'sass-loader',
options: {}
},
{
loader: 'postcss-loader', // 添加厂商前缀
options: {}
}]
},
{
test: /\.vue$/,
use: [{
loader: 'vue-loader',
options: {},
}]
},
{
test: /\.js$/,
exclude: /node_modules/,
loader: "babel-loader",
options: {}
},
]
},
plugins: [
new HtmlWebpackPlugin({
template: 'src/index.html'
}),
new HtmlWebpackPlugin(['dist'])
],
optimization: {
useExports: true
}
}
3、Code Splitting(代码分割)
- lodash 是一个功能集合,里面提供了很多工具方法
npm install lodash --save
// 引入lodash: import _ from 'lodash';
不进行代码分割存在的问题:
- 当引入第三方库和业务逻辑代码放在一起打包时,打包文件会很大,加载时间会很长
- 当每次业务逻辑发生变化时,又要重新加载这个体积很大的包,会耗时较长
普通解决方案:
- 可以把 lodash 等第三方库各自放在新建的 js 文件中引入,然后挂载在 window 对象上全局使用,同时在 entry 中配置入口指向该文件,这样就可以把第三方库与业务逻辑分开引入
// lodash.js 文件
import _ from 'lodash';
window._ = _;
entry: {
main: './src/index.js',
lodash: './src/lodash.js',
}
- 浏览器可以并行加载文件,这样浏览器就能分开加载第三方库文件和业务逻辑文件
- 当业务逻辑发生变化时,只需要加载业务文件即可,第三方库在缓存里就有
webpack 解决方案:
- Code Splitting 对代码进行拆分,可以让代码执行的性能更高
- Code Splitting 本质上和 webpack 没有关系,在没有 webpack 之前,可以像上面一样手动拆分代码
- 在 webpack4.0 提供 splitChunksPlugin 插件与 webpack 捆绑,不用安装就能使用进行代码分割
- webpack 中实现代码分割有两种方式:同步代码 和 异步代码
同步代码分割:
- webpack 自带 splitChunksPlugin 可以实现自动分割,只需要在 webpack.common.js 中做 optimization 的配置即可
optmization: {
splitChunksPlugins: {
chunks: 'all',
cacheGroups: {
vendors: false,
default: false
}
}
}
异步代码分割:
- 指的是使用 import() 引入的异步组件或模块代码
- 无需做任何配置,会自动进行异步代码分割,放置到新的文件中
- 在异步加载时可以使用魔法注释指定生成的异步文件名
// 例如 lodash 的引入时
function getComponent() {
return import(/* webpackChunkName: "lodash" */ 'lodash').then(({default: _}) => {
// lodash 使用逻辑
})
}
// 动态异步获取 若是不支持,可以安装如下插件
npm install @babel/plugin-syntax-dynamic-import --save-dev
// 然后在 .babelrc 文件中配置,然后 Babel 就能转以这种实验性质的语法
plugins: ["@babel/plugin-syntax-dynamic-import"]
splitChunksPlugin 配置参数:
- 如果将 splitChunks 设置为空对象,则会使用默认配置,如下
optimization: {
splitChunks: {
chunks: 'async', // all:对异步和同步都分割 async:只对异步分割
minSize: 30000, // 如果打包后包大于30000字节(30Kb)就会做代码分割
maxSize: 0, // 如果分割后还大于maxSize,会尝试再次进行分割(maxSize 一般不设置)
minChunks: 1, // 当一个模块至少使用 minChunk 次才会代码分割
maxAsyncRequests: 5, // 同时加载模块数最多是 5,如果超过就不分割
maxInitialRequests: 3, // 入口文件做分割,最多生成 3 个 js 文件,超过则不再分割
automaticNameDelimiter: '~', // 使用~作为下面的cacheGroups组名与文件名的连接符生成分割后的文件名
name: true, // 打包后的名字使用cacheGroups里的名字有效
cacheGroups: { // 打包同步代码时,若符合上面的条件则会到这,执行完成后才会进行分割
vendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10, // 值越大,这个组的优先级越高
filename: 'vendors.js'
},
default: {
minChunks: 2,
priority: -20, // -20 < -10,default 的优先级小于 vendors
reuseExistingChunk: true, // 如果一个模块被打包过了,再打包时就忽略该模块,直接使用
filename: common.js
}
}
}
}
output 中 filename 与 chunkFilename 的区别:
- filename 是入口文件对应生成的输出文件名
- chunkFilename 是入口文件间接引入生成的输出文件名
4、Webpack 懒加载(Lazy Loading),Preloading/Prefetching
- 懒加载:就是异步的加载某个模块,当对象需要用到的时候再去加载
- webpack 本质上与懒加载关系不大,只不过 webpack 能识别 import() 语法,然后对引入的模块进行代码分割而已
- webpack 使用 Prefetching 和 Preloading 实现懒加载,使用的方式是在使用 import() 异步加载某个模块时,对加载的模块加上魔法注释,注释内容为:webpackPrefetch: true 或者 webpackPreload: true
import(/* webpackPrefetch: true */ 'lodash')
- Prefetching/Preloading 实现懒加载主要区别是:prefetching 的代码会等到主要的 js 代码加载完,浏览器空闲时才会去加载(例如首页中的登录注册模块),preloading 代码会与主要的 js 代码一起加载
5、CSS 文件的代码分割
- webpack 官网提供的 MiniCssExtractPlugin 插件对 CSS 进行代码分割,该插件暂时不支持热更新,一般用于生产环境,用在开发环境会降低开发效率
npm install --save-dev mini-css-extract-plugin
- 在 webpack.config.js 中配置
- 如果要使用该插件,还需要对 Loader 进行配置,之前最后一步是使用 style-loader 把 CSS 样式挂在 HTML 页面上,现在要将 style-loader 换成 MiniCssExtractPlugin.loader,同时修改 webpack.common.js 和 webpack.dev.js 中 Module 关于 sass 和 css 的代码
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const merge = require('webapck-merge');
const commonConfig = require('./webpack.common.js');
const prodConfig = {
mode: 'production',
devtool: 'cheap-module-source-map',
module: {
rules: [
{
test: /\.scss$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader',
'sass-loader',
'postcss-loader'
]
},
{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader',
'postcss-loader'
]
}
]
},
plugins: [
new MiniCssExtractPlugin({
filename: '[name].css',
chunkFilename: '[name].chunk.css'
})
]
}
module.exports = merge(commonConfig, prodConfig)
- 然后在 package.json 中将 sideEffects 的值设置为 ["*.css"]
CSS 代码压缩:
- CSS 代码压缩可以使用 OptimizeCssAssetsPlugin 插件
npm install --save-dev optimize-css-assets-plugin
- 在生产环境配置引入使用
optimization: {
minimizer: [
new OptimizeCssAssetsPlugin({}) // 别忘了加空对象
]
}
6、Webpack 与浏览器缓存
- 在开发环境,由于有热更新,每次对源代码修改都会重新打包,重新生成输出文件
- 在生产环境,每次修改源代码生成的输出文件都同名,由于浏览器有缓存,对于同名的输出文件会直接使用缓存的内容,就会导致源代码的更新不生效
- 所以在配置生产环境 output 的 filename 和 chunkFilename 时,需要加在 [contenthash]
// webpack.prod.js
output: {
filename: '[name].[contenthash].js',
chunkFilename: '[name].[contenthash].js'
}
- 对于较老版本的 webpack4.0 ,对同一源代码再次打包,有可能生成的 hash 值不同,解决这个问题的方法是在 optimization 中 runtimeChunk
optimization: {
runtimeChunk: {
name: 'runtime'
},
usedExports: true,
splitChunks: {
chunks: 'all'
}
}