Webpack4.0
- webpack4将 webpack 和 webpack-cli 分开了,需要安装cli后才能使用命令行工具
yarn add webpack webpack-cli --dev
- webpack4.0以后支持零配置打包,直接执行
yarn webpack
,默认将 src/index.js 作为入口文件,打包到 dist/main.js 中 - 工作模式
yarn webpack --mode development
开发模式,优化打包速度,不压缩,方便调试yarn webpack --mode production
生产模式,优化打包结果,压缩代码yarn webpack --mode none
原始模式,不对代码做任何处理
Webpack配置文件
/webpack.config.js,导出一个配置对象
const path = require('path')
module.exports = {
// 打包模式
mode: 'development',
// 入口文件,./ 不能省略
entry: './src/main.js',
// 输出目录
output: {
//输出文件的默认值是 ./dist/main.js
filename: 'bundle.js',
path: path.join(__dirname, 'dist'),
// bundle.js中的 __webpack_require__.p
// 使用html-webpack-plugin插件自动生成html的话无需指定
publicPath: 'dist/'
},
// 源代码地图
devtool: 'source-map',
// webpack-dev-server配置
devServer: {
// hot: true,
hotOnly: true
proxy: {}
},
module: {
//loader
rules: []
},
// 插件
plugins: []
}
打包结果运行原理
bundle.js
(function(modules){
function require(moduleId){
return require(0)
}
})([
(function(module, exports, require){
}),
(function(module, exports, require){})
])
- 使用立即执行函数封装代码,避免全局污染
- 缓存每个模块的输出值并自执行函数
- 处理模块间的相互引用
资源模块加载 Loader
- webpack 默认只能处理 js 文件,如果要处理非JS类型的文件,需要手动安装一些第三方加载器。
- Html加载器:html-loader
- css加载器:css-loader style-loader
- 文件资源加载器:file-loader url-loader
- 对小文件通过url-loader转换为 Data Url(base64的字符串)嵌入代码中
- 对大文件使用file-loader按照传统方式加载
- ES6语法编译:babel-loader @babel/core @babel/preset-env
- 配置 webpack.config.js 的 module 节点,这个对象上有一个 rules 属性,这个 rules 数组中存放了所有第三方文件的匹配和处理规则。
module.exports = {
module: {
rules: [
{
test: /.js$/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
}
},
{
test: /.css$/,
use: ['style-loader', 'css-loader']
},
{
test: /.png$/,
use: {
loader: 'url-loader',
options: {
limit: 10*1024 //10KB
}
}
},
{
test: /.html$/,
use: {
loader: 'html-loader',
options: {
attrs: ['img:src', 'a:href']
}
}
}
]
}
}
- use 表示用哪些模块来处理 test 所匹配到的文件,use 中相关loader模块的调用顺序是从后向前
- 通过options对loader添加配置选项
Webpack模块加载方式
- 遵循ESM标准的import声明
- 遵循CommonJS标准的require函数
- 遵循AMD标准的define函数和require函数
- 样式代码中的@import指令和url函数
- Html代码中图片标签的src属性
建议项目中统一使用同一种标准加载模块。
常用加载器分类
- 编译转换类 如:css-loader
- 文件操作类 如:file-loader
- 代码检查类 如:eslint-loader
Loader的工作原理
将加载的资源文件转换成标准JavaScript代码
Webpack插件机制 Plugin
增强webpack自动化能力
- 安装插件
yarn add xxx --dev
- 在 webpack.config.js 中导入
- 配置 plugins 属性(值是一个数组)
const {CleanWebpackPlugin} = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const CopyWebpackPlugin = require('copy-webpack-plugin')
module.exports = {
plugins: [
new CleanWebpackPlugin(),
// 可以传入一个配置对象,对生成的html进行配置
new HtmlWebpackPlugin({
title: 'Html Webpack Plugin',
meta: {
viewport: 'width=device-width'
},
// 指定一个模版文件,按照模版生成html
template: './src/index.html'
}),
// 同时输出多个页面文件,需要创建多个实例
new HtmlWebpackPlugin({
filename: 'about.html', //默认是index.html
}),
new CopyWebpackPlugin([
//需要复制的文件路径
'public'
])
]
}
常用插件
- clean-webpack-plugin 自动清除输出目录文件
- html-webpack-plugin 生成自动使用(所有)打包结果的Html
- copy-webpack-plugin 复制无需构建的静态文件
(文件数量过多时开发过程中不使用此插件,只有上线前打包时才会使用)
插件机制的工作原理
-
通过在webpack生命周期的钩子(Compiler Hooks)中挂载任务函数来实现。
-
一个插件就是一个函数或者一个包含apply函数的对象,一般定义为一个类型,使用时在plugins中构建一个实例来使用。
Demo: 开发一个插件,用来删除bundle.js中的注释
class MyPlugin {
// webpack启动时会自动执行apply方法
apply(compiler){
console.log('MyPlugin 启动')
//emit钩子:webpack往输出目录输出文件之前执行
compiler.hooks.emit.tap('Myplugin', compilation => {
// compilation 可以理解为此次打包的上下文
for (const name in compilation.assets) {
//资源文件名称
console.log(name)
//获取资源文件内容
console.log(compilation.assets[name].source())
if(name.endsWith('.js')){
//获取带有注释的js文件
const content = compilation.assets[name].source()
//去掉文件中的注释
const withoutComments = content.replace('/\/\*\*+\*\//g', '')
//覆盖当前资源文件
compilation.assets[name] = {
source: () => withoutComments,
size: () => withoutComments.length
}
}
}
})
}
}
增强webpack开发体验
- 通过 HTTP Server 运行
- 自动编译+自动刷新浏览器
- 提供 Source Map 支持
Webpack Dev Server
- 安装
yarn add webpack-dev-server --dev
- 在配置文件中配置webpack-dev-server
module.exports = {
devServer: {
//指定静态资源目录,值可以是一个字符串或一个数组
conentBase: './public',
//配置代理
proxy: {
'/api': {
//目标服务器
target: 'https://api.github.com',
//重写代理路径
pathRewrite: {
'^/api': ''
},
//不能使用localhost:8080 作为请求的主机名
changeOrigin: true
}
}
}
}
- 运行
yarn webpack-dev-server
或将启动命令写入Npm Scripts中- –open 自动打开浏览器
- –hot 模块热更新
- 打包的文件临时存放在内存中,而不是物理磁盘
- 在开发过程中,通过 devServer 的 contentBase 属性,可以指定静态资源目录
Source Map
解决源代码和打包后的运行代码不一致所带来的调试问题
- source map文件映射了转换过后的代码与源代码之间的关系;
- 转换后的文件需要引入source map文件才能使用
//# sourceMappingURL = xxx.map
- webpack 配置 source map 的不同模式,每种方式的效率和效果各不相同:
- devtool: ‘source-map’,生成source map文件,能定位错误代码的行和列
- devtool: ‘eval’,使用eval执行模块代码,不生成source map文件,只能定位源文件名称;
- eval-source-map:使用eval执行模块代码,生成source map文件,能定位错误代码的行和列;
- cheap-eval-source-map:阉割版,只定位到行;
- cheap-module-eval-source-map 模式:得到loader处理之前的(自己手写的)源代码
- inline-source-map:以dataurl的方式将source map嵌入到代码中
- hidden-source-map:生成source map文件,但不引入
- nosources-source-map:能定位错位代码位置,但在开发工具中看不到源代码
选择建议:
开发过程用 cheap-module-eval-source-map 模式;
生产环境用 none,或者用 nosources-source-map 模式。
Webpack HMR
- Hot Module Replacement,模块热替换/更新。
页面不刷新的情况下,及时更新模块代码,解决浏览器自动刷新带来的页面状态和用户输入丢失的问题。
- webpack-dev-server中集成了HMR,通过
--hot
参数 或 配置文件 开启。
const webpack = require('webpack')
module.exports = {
devServer: {
//hot: true
hotOnly: true
},
plugins: [
new webpack.HotModuleReplacementPlugin()
]
}
- 开启HMR后,css文件可以实现不刷新浏览器自动更新,其他模块需要手动处理模块更新后的逻辑。
在入口函数(如main.js)中通过 HMR APIs 处理引入的模块的HMR
// 注册模块更新后的处理函数 两个参数:1.模块路径 2.处理函数
module.hot.accept('./editor', () => {
console.log('模块已更新')
...
})
HMR使用注意事项
- 处理HMR的代码报错,页面还是会自动刷新—使用hotOnly
- 没有开启HMR,HMR API报错—先判断module.hot存在再使用
if(module.hot){
module.hot.accept()
}
- 与业务无关的处理HMR的代码,不会影响代码的正常运行
Webpack生产环境优化
开发环境更注重开发效率;
生产环境更注重运行效率;
不同的工作环境需要创建不同的webpack配置。
不同环境下的配置
在配置文件中添加判断条件,根据环境名参数(env)判断不同的工作环境,从而导出不同配置。适合中小型项目
const webpack = require('webpack')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const CopyWebpackPlugin = require('copy-webpack-plugin')
// 导出一个函数
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'
//禁用source map
config.devtool = false
//增加生产模式需要使用的插件
config.plugins = [
...config.plugins,
new CleanWebpackPlugin(),
new CopyWebpackPlugin(['public'])
]
}
return config
}
- 运行
yarn 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: 'Webpack Test',
template: './src/index.html'
})
]
}
- webpack.dev.js
const webpack = require('webpack')
const merge = require('webpack-merge')
const common = require('./webpack.commom.js')
module.exports = merge(common, {
mode: 'development',
devtool: 'cheap-eval-module-source-map',
devServer: {
hot: true,
contentBase: 'public'
},
plugins: [
new webpack.hot.HotModuleReplacementPlugin()
]
})
- webpack.prod.js
const merge = require('webpack-merge')
const {CleanWebpackPlugin} = require('clean-webpack-plugin')
const CopyWebpackPlugin = require('copy-webpack-plugin')
const common = require('./webpack.commom.js')
module.exports = merge(common, {
mode: 'production',
plugins: [
new CleanWebpackPlugin()
new CopyWebpackPlugin(['public'])
]
})
- 安装webpack-merge模块,
yarn add webpack-merge --dev
- 在不同环境的配置文件下合并对应的webpack配置
- 运行打包命令时要指定 config 参数,
yarn webpack --config webpack.dev.js
或者将命令定义在Npm Scripts中,然后运行yarn build
package.json
{
"scripts": {
"dev": "webpack --config webpack.dev.js",
"build": "webpack --config webpack.prod.js"
}
}
production模式下的优化功能
DefinePlugin
通过 process.env.NODE_ENV 为代码注入全局变量,
变量的值要求为JS代码片段
const webpack = require('webpack')
module.exports = {
entry: './src/main.js',
output: {
filename: 'bundle.js'
},
plugins: [
new webpack.DefinePlugin({
API_BASE_URL: JSON.stringfy('http://api.example.com')
})
]
}
在main.js中就使用变量API_BASE_URL
console.log(API_BASE_URL)
Tree Shaking
去掉代码中未引用的部分(dead-code)
- 在使用生产模式打包时Tree Shaking会自动开启
- 使用其他模式打包时也可以在 optimization 中手动开启
- Tree Shaking的前提是代码使用ESM规范
module.exports = {
mode: 'none',
entry: './src/index.html',
output: {
filename: 'bundle.js'
},
optimization: {
//只导出被使用的成员
usedExports: true,
//压缩代码
minimize: true,
//合并模块
concatenateModules: true,
//开启副作用功能
sideEffects: true
}
}
合并模块
用法:配置 optimization 的 concatenateModules 属性
作用:尽可能地将所有模块合并输出到一个函数中,提升运行效率,减小代码体积。
sideEffects
webpack4 新特性,一般用于npm包标记是否有副作用
副作用:模块执行时除了导出成员外所做的事情。
如果没有副作用,并且未被引用,打包时就可以被shaking掉。
- sideEffects在生产模式下也会自动开启
- 通过 optimization 的 sideEffects 开启,在 package.json 中的 sideEffects 字段标记
{
"sideEffects": [
"*.css",
]
}
代码分割/分包
避免bundle.js体积过大,将打包结果按照一定的规则分离到多个bundle中,根据运行需要按需加载
多入口打包
适用于多页面应用,一个页面对应一个打包入口。
module.exports = {
//多入口打包时入口配置为一个对象
entry: {
index: '.src/index/html',
about: '.src/about.html'
},
//动态输出文件名
output: '[name].bundle.js',
optmization: {
splitChunks: {
//提取所有公共模块到单独的bundle
chunks: 'all'
}
},
module: {
},
plugins: [
//html-webpack-plugin默认使用所有打包结果
//使用chunks属性为每个Html指定所使用的打包结果
new HtmlWebpackPlugin({
title: 'Index',
template: './src/index.html',
filename: 'index.html',
chunks: ['index']
}),
new HtmlWebpackPlugin({
title: 'About',
template: './src/about.html',
filename: 'about.html',
chunks: ['about']
})
]
}
提取公共模块:配置优化属性 optimization 将页面公共的部分提取出来,如公共的样式和请求api的部分。
动态导入(ESM)
动态导入的模块会被自动分包,实现按需加载。
- import方法返回一个promise对象,通过.then得到导入的模块对象
- 动态导入的模块打包后默认以序号命名(如:1.bundle.js)
- 可以通过行内注释为打包后的文件指定名称(打包后的文件名为:name.bundle.js)
import(/* webpackChunkName: name */'模块路径').then(module => {
。。。
})
MiniCssExtractPlugin
将css从打包结果中单独提取到css文件中,从而实现css的按需加载。
建议:超过150k的样式文件才需要单独提取
- 安装
yarn mini-css-extract-plugin
- 在配置文件中导入
- 在 plugins 中添加
- 修改样式加载器 style-loader
style-loader 是将样式通过style标签注入,使用MiniCssExtractPlugin的话需要使用 MiniCssExtractPlugin.loader 将样式以link的方式引入
OptimizeCssAssetsWebpackPlugin
webpack内部的压缩插件只针对js,其他资源文件需要额外的插件来压缩
- 安装 optimize-css-assets-webpack-plugin
- 导入
- 添加到plugins数组中
- 一般将这个插件添加到 optimization 的 minimizer 数组中,这样的话只有当 minimize 开启或者用生产模式打包时,该插件才会工作
- 配置了自定义压缩器 minimizer 后,内置的压缩器 terser-webpack-plugin 会被覆盖掉,需要手动添加回来以后js文件才会被压缩
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin')
const TerserWebpackPlugin = require('terser-webpack-plugin')
module.exports = {
mode: none,
entry: './src/index.js',
output: {
filename: '[name].bundle.js'
},
optimization: {
minimizer: [
new TerserWebpackPlugin(),
new OptimizeCssAssetsWebpackPlugin()
]
},
module: {
rules: [
{
test: /\.css$/,
use: [
// 'style.loader',
MiniCssExtractPlugin.loader,
'css-loader'
]
}
]
},
plugins: [
new MiniCssExtractPlugin()
]
}
输出文件名Hash
部署前端资源文件时一般会启用服务器的静态资源缓存,在设置filename时给文件名加上hash,就可以将缓存失效时间设置得很长
module.exports = {
entry: '.src/index.html',
output: {
filename: '[name]-[contenthash:8]'
}
}
- 项目级别的hash,任何地方发生改动,hash值都会发生变化
- chunkhash,同一路的文件发生改变,这一路文件(js和css)的hash值都会改变
- contenthash,文件级别,每个文件有不同的hash值
- :数字 可以指定hash值的位数