5大核心概念
熟悉webpack的5大核心概念,是我们使用webpack的前提,这为后面的使用起指导作用。
- entry(入口)
webpack会以一个文件或多个文件作为打包入口,entry就是指示从哪个文件开始打包
- output(输出)
指示webpack将打包后的文件输出在哪个目录
- loader(加载器)
webpack本身只能处理js、json,其他资源需要loader完成
- plugins(插件)
扩展webpack的功能
- mode(模式)
- 开发模式:development
- 生产模式: production
基本配置
我们在项目的根目录下新建一个webpack.config.js配置文件,webpack会根据这个文件进行打包
const path = require('path') // node.js核心模块,专门用来处理路径问题
module.exports = {
//入口
entry: './src/main.js', //相对路径
//输出
output: {
//文件的输出路径
// __dirname node.js的变量, 代表当前文件的文件夹目录
path: path.resolve(__dirname,"dist"), //绝对路径
filename: 'main.js'
},
//加载器
module: {
rules: [
// loader的配置
],
},
//插件
plugins: [
// plugins的配置
],
//模式
mode: "development",
}
开发模式介绍
开发模式下我们主要做两件事:
- 编译代码,使得浏览器可以运行
- 代码质量检查,树立代码规范
处理样式资源
这里我们学习使用webpack处理Css、Less、Sass、Scss、Styl样式资源
css资源的处理
由于webpack本身并不能处理css等样式资源,所以我们要借助loader,webpack官网包含了loader的下载与配置。
安装
npm install --save-dev css-loader style-loader
在入口文件中引入css资源
import 'file.css';
在webpack.config.js中配置lodaer
module.exports = {
module: {
rules: [
{
test: /\.css$/, //检查.css结尾的文件
use: [ //执行顺序:从右到左(从下到上)
'style-loader', //将js中css通过创建style标签添加到html文件中生效
'css-loader' //将css资源编译成commonjs模块到js
]
}
]
}
}
less资源的处理
安装
npm install --save-dev less-loader less
配置
module.exports = {
...
module: {
rules: [{
test: /.less$/,
use: [{
loader: "style-loader" // creates style nodes from JS strings
}, {
loader: "css-loader" // translates CSS into CommonJS
}, {
loader: "less-loader" // compiles Less to CSS
}]
}]
}
};
处理图片资源
webpack5集成了打包图片的能力,只需要如下配置即可
{
test: /\.(png|jpe?g|gif|webp|svg)$/,
type: 'asset',
parser: {
dataUrlCondition: {
// 小于10kb的图片转base64
//优点: 减少请求数量 缺点:体积会更大
maxSize: 10 * 1024 // 10kb
}
}
}
修改输出文件目录
默认情况下,打包后的图片、入口文件等资源都在同一个文件夹下,非常混乱,我们可以单独为某一资源指定输出路径。以图片打包为例
{
test: /\.(png|jpe?g|gif|webp|svg)$/,
type: 'asset',
parser: {
dataUrlCondition: {
// 小于10kb的图片转base64
//优点: 减少请求数量 缺点:体积会更大
maxSize: 10 * 1024 // 10kb
}
},
generator: {
//输出图片的路径与名称
//[hash:10] hash值取前10位
filename: "static/images/[hash:10]/[ext]/[query]/"
}
}
自动清空上次打包内容
配置非常简单,只需要在output中添加clean: true
output: {
//文件的输出路径
// __dirname node.js的变量, 代表当前文件的文件夹目录
path: path.resolve(__dirname,"dist"), //绝对路径
// 入口文件打包输出文件名
filename: 'static/js/main.js',
// 自动清空上次打包的内容
// 原理: 在打包前,将path整个目录内容清空,再进行打包
clean: true,
},
ESLint代码规范检查
安装
npm install eslint eslint-webpack-plugin --save-dev
在webpack.config.js中添加eslint-webpack-plugin插件
const ESLintPlugin = require('eslint-webpack-plugin');
...
plugins: [
// plugins的配置
new ESLintPlugin({
//检查哪些文件
context: path.resolve(__dirname,"src")
}),
],
在根目录上新建.eslintrc.js文件
module.exports = {
//继承 Eslint 规则
extends: ["eslint:recommended"],
env: {
node: true, // 启用node中全局变量
browser: true, // 启用浏览器中全局变量
},
parserOptions: {
ecmaVersion: 6, // es6
sourceType: "module", // es6 module
},
rules: {
"no-var": 2, // 不能使用 var 定义变量
}
}
在根目录上新建.eslintignore文件,指明哪些文件不需要eslint扫描
babel
安装
npm install -D babel-loader @babel/core @babel/preset-env
在webpack.config.js的loader中配置
{
test: /\.js$/,
exclude: /(node_modules|bower_components)/, //排除某些文件不处理
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
}
}
在根目录下新建babel.config.js文件
module.exports = {
// 智能预设:能够编译ES6语法
presets: ["@babel/preset-env"],
}
处理HTML资源
我们在html中是手动引入入口文件的,但是如果有很多文件的话,那么就需要一个个的引入,并且文件在打包后文件名可能会变化,那有没有自动引入文件的方法呢?
HtmlWebpackPlugin
安装
npm install --save-dev html-webpack-plugin
配置
var HtmlWebpackPlugin = require('html-webpack-plugin');
...
plugins: [
new HtmlWebpackPlugin({
// 模板:以public/index.html文件创建新的htnl
// 新的html文件特点:1.结构和原来一致 2. 自动引入打包输出后的资源
template: path.resolve(__dirname,"public/index.html")
})
]
搭建开发服务器&自动化
每次修改代码或其他内容都需要重新打包,如果能自动打包的话,那就太方便了,所以我们需要搭建一个开发服务器,实现热更新,配置很简单。
安装
npm i webpack-dev-server -D
在webpack.config.js中配置
// 开发服务器:不会输出资源,打包后内容存放在内存中。
devServer: {
host: "localhost", //启动服务器域名
port: "3000", //启动服务器端口号
open: true, // 是否自动打开浏览器
},
CSS处理
提取css为单独的文件
CSS文件会被打包到js文件中,当js文件加载时,会动态创建style标签来生成样式,这样的话对于网站来说,会出现闪屏现象,因为浏览器会先加载html元素,再执行js。我们应该是单独的css文件,通过link标签来引入。
MiniCssExtractPlugin
这个插件会为每一个包含css的js文件单独创建一个css文件
安装
npm install --save-dev mini-css-extract-plugin
在webpack.config.js中配置
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
pluginsL: [
new MiniCssExtractPlugin({
filename: 'static/css/main.css', //指定输出位置
}),
]
将style-loader改为MiniCssExtractPlugin.loader,因为style-loader是js创建style标签引入样式,MiniCssExtractPlugin.loader是用打包时创建link标签引入
CSS兼容性处理
安装
npm install --save-dev postcss-loader postcss
在loader中配置
{
loader: "postcss-loader",
options: {
postcssOptions: {
plugins: [
[
"postcss-preset-env",
{
// Options
},
],
],
},
},
}
CSS压缩
安装
npm install css-minimizer-webpack-plugin --save-dev
在plugins
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");
plugins: [
new CssMinimizerPlugin(),
]
HTML、JS压缩
在生产环境下,默认进行了压缩,无需配置
Webpack高级配置
所谓高级配置其实就是进行webpack优化,让我们代码在编译/运行时性能更好。
我们会从以下角度进行优化:
- 提升开发体验
- 提升代码打包速度
- 减少代码体积
- 优化代码运行性能
提升开发体验-SourceMap
为什么
编译后的代码与源代码没有映射关系,如果代码运行报错,浏览器会直接定位到编译后的代码,并且出错的位置是不准确的,不易阅读,所以我们需要在编译代码与源代码间建立一个映射文件,将错误直接定位到源代码。
是什么
SourceMap(源代码映射)是一种在编译代码与源代码之间建立映射的方案
怎么用
- 开发模式下:cheap-module-source-map
- 优点:打包编译速度快,只包含行映射
- 缺点:不包含列映射
mode: "development",
devtool: "cheap-module-source-map",
- 生产环境下:source-map
- 优点:包含行、列映射
- 缺点:打包编译速度更慢
mode: "production",
devtool: "source-map"
提升打包速度
HotModuleReplacement
为什么
开发时我们修改了其中一个模块代码,Webpack默认会将所有模块全部重新打包编译,速度很慢。
所以我们需要做到修改某个模块代码,就只有这个模块代码需要重新打包编译,其他模块不变,这样打包速度就能很快。
是什么
HotModuleReplacement(HMR/热模块替换):在程序运行中,替换、添加或删除模块,而无需重新加载整个页面。
怎么用
- 基本配置
devServer: {
host: "localhost", //启动服务器域名
port: "3000", //启动服务器端口号
open: true, // 是否自动打开浏览器,
hot: true, //开启HMR (只能用于开发环境,生产环境不需要了)
},
此时css样式经过style-loader处理,已经具备HMR功能了。但是js还不行
所以我们可以在入口文件手动操作
import count from './js/count'
import sum from './js/sum'
import './css/index.css'
import './less/index.less'
console.log(count(2,2))
console.log(sum(1,2,3,4))
// 热模块替换
if(module.hot){
module.hot.accept('./js/count.js')
module.hot.accept('./js/sum.js')
}
上面这样写会很麻烦,所以实际开发我们会使用其他loader来解决。
比如:vue-loader,react-hot-loader
OneOf
为什么
打包时每个文件都会经过所有loader处理,虽然因为test正则原因实际上没有处理,但是都要经过一遍,比较慢。
是什么
顾名思义就是只能匹配上一个loader,剩下的就不匹配了。
怎么用
将loader包裹在oneOf对象中即可。
module: {
rules: [
// loader的配置
{
// 每个文件只能被其中一个loader配置处理
oneOf: [
{
test: /\.css$/, //检查.css结尾的文件
use: [ //执行顺序:从右到左(从下到上)
'style-loader', //将js中css通过创建style标签添加到html文件中生效
'css-loader' //将css资源编译成commonjs模块到js
]
}, {
test: /\.less$/,
use: [{
loader: "style-loader" // creates style nodes from JS strings
}, {
loader: "css-loader" // translates CSS into CommonJS
}, {
loader: "less-loader" // compiles Less to CSS
}]
}, {
test: /\.(png|jpe?g|gif|webp|svg)$/,
type: 'asset',
parser: {
dataUrlCondition: {
// 小于10kb的图片转base64
//优点: 减少请求数量 缺点:体积会更大
maxSize: 10 * 1024 // 10kb
}
},
generator: {
//输出图片的路径与名称
//[hash:10] hash值取前10位
filename: "static/images/[hash:10]/[ext]/[query]/"
}
}, {
test: /\.js$/,
exclude: /(node_modules|bower_components)/, //排除某些文件不处理
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
}
}
]
}
],
},
Include/Exclude
为什么
开发时我们需要使用第三方的库或插件,所有文件都下载到node_modules中了。而这些文件是不需要编译可以直接使用的。
所以我们在对js文件处理时,要排除node_modules下面的文件
是什么
- include
包含,只处理xxx文件
- exclude
排除,除了xxx文件以外其他文件都处理
怎么用
{
test: /\.js$/,
//exclude: /(node_modules|bower_components)/, //排除某些文件不处理
include: path.resolve(__dirname,"../src"),
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
}
Cache
为什么
每次打包时js文件都要经过Eslint检查和Babel编译,速度比较慢
我们可以缓存之前的Eslint检查和Babel编译结果,这样第二次打包时速度就会更快了
是什么
对Eslint检查和Babel编译结果进行缓存
怎么用
开启babel缓存
{
test: /\.js$/,
//exclude: /(node_modules|bower_components)/, //排除某些文件不处理
include: path.resolve(__dirname,"../src"),
use: {
loader: 'babel-loader',
options: {
//presets: ['@babel/preset-env'],
cacheDirectory: true, //开启babel缓存
cacheCompression: false,// 关闭缓存文件压缩
}
}
开启Eslint缓存
new ESLintPlugin({
//检查哪些文件
context: path.resolve(__dirname, "../src"),
exclude: "node_modules", //默认值
cache: true, //开启缓存
cacheLocation: path.resolve(__dirname,'../node_modules/.cache/eslintcache')
}),
多进程打包-Thread
为什么
当项目越来越大时,打包速度越来越慢,甚至于需要一个下午才能打包出来的代码。
我们想要继续提升打包速度,其实就是要提升js的打包速度,因为其他文件都比较少。
而对js文件处理主要就是eslint、babel、Terser三个工具,所以我们要提升他们的速度
我们可以开启多进程同时处理js文件,这样速度就比之前的单进程打包更快了。
是什么
多进程打包:开启电脑的多个进程同时干一件事,速度更快 需要注意:请尽在特别耗时的操作中使用,因为每个进程启动就有大约600ms左右开销
怎么用
我们启动进程的数量就是CPU的核数
- 如何获取CPU的核数,每个电脑都不一样
const os = require('os')
const threads = os.cpus().length // cpu核数
- 在loader中配置
{
loader: 'thread-loader', //开启多进程
options: {
works: threads // 进程数量
}
},
- 在eslint中配置
new ESLintPlugin({
//检查哪些文件
context: path.resolve(__dirname, "../src"),
exclude: "node_modules", //默认值
cache: true, //开启缓存
cacheLocation: path.resolve(__dirname, '../node_modules/.cache/eslintcache'),
threads, //开启多进程和设置进程数量
}),
- 在terser中配置
const TerserWebpackPlugin = require('terser-webpack-plugin')
new TerserWebpackPlugin({
parallel: threads,
})
减少代码体积
Tree Shaking
为什么
开发时我们定义了一些工具函数库,或者引用第三方工具函数库或组件库。
如果没有特殊处理的话我们打包时会引入整个库,但是实际上可能我们只用上极小部分的功能。
这样整个库都打包进来,体积就太大了。
是什么
Tree Shaking 是一个术语,通常用于描述移出javascript中没有使用上的代码。
注意:它依赖 ES Module
怎么用
webpack已经默认开启这个功能,无需其他配置
Babel
为什么
Babel为编译的每个文件都插入辅助代码,使代码体积过大!
Babel对一些公共方法使用了非常小的辅助代码,比如_extend。默认情况下会被添加到每一个需要它的文件中。
你可以将这些辅助代码作为一个独立模块,来避免重复引入。
是什么
@babel/plugin-transform-runtime: 禁用了Babel自动对每个文件的runtime注入,而是引入@babel/plugin-transform-runtime并且使所有辅助代码从这里引用。
怎么用
npm i @babel/plugin-transform-runtime -D
{
loader: 'babel-loader',
options: {
//presets: ['@babel/preset-env'],
cacheDirectory: true, //开启babel缓存
cacheCompression: false, // 关闭缓存文件压缩
plugins: ['@babel/plugin-transform-runtime'], //减少代码体积
}
}
Image Minimizer
为什么
开发项目中引用了较多图片,那么图片体积会比较大,将来请求速度比较慢。
我们可以对图片进行压缩,减少图片体积。
注意:如果项目中图片都是在线连接,那么就不需要了。本地图片才需要压缩。
是什么
image-minimizer-webpack-plugin 用来压缩图片的插件
怎么用
- 下载包
npm install image-minimizer-webpack-plugin imagemin --save-dev
- 无损压缩
npm install imagemin-gifsicle imagemin-jpegtran imagemin-optipng imagemin-svgo --save-dev
- 有损压缩
npm install imagemin-gifsicle imagemin-mozjpeg imagemin-pngquant imagemin-svgo --save-dev
优化代码运行性能
代码分割
为什么
打包代码时会将所有js文件打包到一个文件中,体积太大了。我们如果只要渲染首页,就应该只加载首页的js文件,其他文件不应该加载。
所以我们需要将打包生成的文件进行代码分割,生成多个js文件,渲染哪个页面就只加载某个js,这样加载的资源就少,速度就更快。
是什么
Preload/Prefetch
为什么
我们前面已经做了代码分割,同时会使用import动态导入语法来进行代码按需加载(我们也叫懒加载,比如路由懒加载就是这样实现的)。
但是加载速度还不够好,比如:用户点击按钮才加载这个资源的,如果资源体积很大,那么用户会感觉到明显的卡顿效果。
我们想在浏览器空闲时间,加载后续需要使用的资源。我们就需要用上Preload或Prefetch技术。
是什么
- Preload: 告诉浏览器立即加载资源
- Prefetch: 告诉浏览器在空闲时才开始加载资源
它们共同点:
- 都只会加载资源,并不执行
- 都有缓存。
它们区别:
- Preload加载优先级高,Prefetch加载优先级低。
- Preload只能加载当前页面需要使用的资源,Prefetch可以加载当前页面的资源,也可以加载下一个页面需要使用的资源。
总结:
- 当前页面优先级高的资源用 Preload 加载
- 下一个页面需要使用的资源用 Prefetch 加载
它们的问题:兼容性较差
PWA
为什么
开发 Web App项目,项目一旦处于网络离线情况,就没法访问了。
我们希望给项目提高离线体验。
是什么
渐进式网络应用程序(PWA): 是一种可以提供类似于native app(原生应用程序)体验的web app技术。
其中最重要的是,在离线时应用程序能继续运行功能。
内部通过Service Workers技术实现的,
怎么用
- 下载
npm install workbox-webpack-plugin --save-dev
- 在webpack.config.js中配置
const WorkboxPlugin = require('workbox-webpack-plugin');
new WorkboxPlugin.GenerateSW({
// these options encourage the ServiceWorkers to get in there fast
// and not allow any straggling "old" SWs to hang around
clientsClaim: true,
skipWaiting: true,
}),
- 在入口文件中添加
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/service-worker.js').then(registration => {
console.log('SW registered: ', registration);
}).catch(registrationError => {
console.log('SW registration failed: ', registrationError);
});
});
}
Loader原理
loader概念
帮助webpack将不同类型的文件转换为可识别的模块
loader执行顺序
- 分类
- pre: 前置loader
- normal: 普通loader
- inline: 内置loader
- post: 后置loader
- 执行顺序
- 4类loader的执行优先级:
pre > normal > inline > post - 相同优先级的loader执行顺序为: 从右到左,从下到上
开发一个loader
/*
loader就是一个函数
当webpack解析资源时,会调用相应的loader去处理
loader就会接收文件内容作为参数,返回内容出去
- content: 文件内容
- map: SourceMap
- meta: 别的loader传递的参数
*/
module.exports = function(content, map, meta){
console.log(content)
return content
}
loader 分类
1. 同步loader
同步loader有两种方式
module.exports = function (content){
return content
}
module.exports = function(content, map, meta){
console.log("同步loader")
/*
第一个参数:err 代表是否有错误
第二个参数:content 处理后的内容
第三个参数:source-map 继续传递source-map
第四个参数:meta 给下一个loader传递参数
*/
this.callback(null, content, map, meta)
}
2. 异步loader
异步loader会异步处理content,这里我们用一个延迟函数做演示。只有callback被调用后,content才会传递给下一个loader。
module.exports = function (content, map, meta) {
const callback = this.async()
setTimeout(() => {
console.log("异步loader")
callback(null, content, map, meta)
}, 1000);
}
3. raw loader
raw:未加工的,raw loader接收到的content是Buffer数据,这对处理图片等资源时很友好
// raw loader接收到content是Buffer数据
module.exports = function(content){
console.log(content)
return content
}
module.exports.raw = true;
4. pitch loader
开发loader
1.自定义clean-log-loader
clean-log-loader能自动清除js中的console.log语句
module.exports = function(content){
//清除文件中的console.log语句
return content.replace(/console\.log\(.*\);?/g,'')
}
2. 自定义banner.loader
const schema = require('./schema.json')
module.exports = function (content) {
//schema对options的验证规则
//schema符合JSON Schema的规则
const options = this.getOptions(schema)
const prefix = `
/*
* Author: ${options.author}
*/
`
return prefix + content
}
3. babel-loader
const schema = require("./schema.json")
const babel = require("@babel/core")
module.exports = function(content){
const options = this.getOptions(schema)
const callback = this.async()
//使用babel对代码进行编译
babel.transform(content, options, function (err, result) {
if(err) callback(err)
else callback(null,result.code)
})
}
Plugin
第一个plugin
- webpack加载webpack.config.js中所有配置, 此时就会new TestPlugin(), 执行插件的constructor
- webpack创建compiler对象
- 遍历所有plugins中的插件,调用插件的apply方法
- 执行剩下编译流程(触发各个hooks事件)
class TestPlugin {
constructor(){
console.log('TestPlugin constructor')
}
apply(compiler){
console.log('TestPlugin apply')
}
}
module.exports = TestPlugin