一、webpack介绍
什么是构建工具:
构建工具让我们不用关心生产的代码,也不用关心代码如何在浏览器运行,只需要关心我们的开发怎样写的舒服就好。
1. 使用分类
关于webpack的使用,基本都围绕“配置”展开,而这些配置大致可划分为两类:
- 流程类:作用于流程中某个或若干个环节,直接影响打包效果的配置项
- 工具类:主流程之外,提供更多工程化能力的配置项
2. 流程类配置
- 输入
entry
- entry
- context
- 模块解析
require, import
- resolve
- externals
- 模块转译
module
- module
- 后处理
output
- optimization(代码分割)
- mode
- target
3. 配置总览
二、webpack使用(基础)
1. 使用步骤
- 初始化项目
npm init -y
- 安装依赖
webpack
,webpack-cli
- webpack: 核心依赖
- webpack-cli: 命令行工具
npm i webpack webpack-cli -D
- 在项目中创建
src
目录,在src
目录中创建index.js
文件
webpack
默认会打包src
目录下的index.js
文件,后面可以通过webpack.config.js
文件来修改默认的打包文件
- 构建
src/index.js
文件
打包后的文件会被打包到
dist
目录下
npx webpack
npx webpack --mode development # 指定开发环境
- 在
package.json
文件中添加scripts
字段
"scripts": {
"dev": "webpack",
"start": "webpack --mode development",
"build": "webpack --mode production"
}
2. 配置文件
webpack.config.js
:默认的配置文件(是在 node 中运行的,使用 commonjs 规范)
module.exports = {
// 配置打包模式:development 开发模式 production 生产模式
mode: 'development' // 和上面目录效果一样
}
3. entry
打包的入口文件,可以是一个字符串,也可以是一个数组,也可以是一个对象
module.exports = {
// 配置打包模式:development 开发模式 production 生产模式
mode: 'development',
// 配置入口文件(默认是src/index.js)
// entry: './src/index.js',
entry: {
index: ['./src/index.js', './src/main.js']
}
// entry: {
// index: './src/index.js',
// main: './src/main.js'
// },
}
- 字符串
{
"entry": "./src/index.js"
}
- 数组
将
index.js
和main.js
多个文件打包到一个文件中
{
index: ['./src/index.js', './src/main.js']
}
- 对象
每个文件对应一个入口文件,生成多个文件
dist/index.js
和dist/main.js
{
index: "./src/index.js",
main: "./src/main.js",
}
4. output
path
:打包后的文件路径(要求绝对路径)filename
:打包后的文件名clean
:打包前清空打包目录
{
output: {
// 打包后的文件名
// filename: '[name].js' // name 是占位符,会被 entry 中的 key 替换
filename: '[name]-[hash].js', // hash 是占位符,会被一个随机的字符串替换
// 打包后的文件路径,要求绝对路径
path: path.resolve(__dirname, 'dist'),
clean: true // 每次打包,自动清空打包目录
}
}
5. loader
5.1 loader概念
帮助webpack将不同类型的文件转换为webpack能够识别的模块
5.2 loader的执行顺序
(1)、分类
- pre:前置loader
- normal:普通loader(默认)
- inline:内联loader
- post:内置loader
(2)、执行顺序
- loader的优先级:
pre
>normal
>inline
>post
- 相同优先级的loader执行顺序为:从右到左,从下到上
例如:
// 此时loader执行顺序:loader3 -> loader2 -> loader1
module: {
rules: [
{
test: /\.js$/,
loader: "loader1",
},
{
test: /\.js$/,
loader: "loader2",
},
{
test: /\.js$/,
loader: "loader3",
},
],
},
// 此时loader执行顺序:loader1 -> loader2 -> loader3
module: {
rules: [
{
enforce: "pre",
test: /\.js$/,
loader: "loader1",
},
{
// 没有enforce就是normal
test: /\.js$/,
loader: "loader2",
},
{
enforce: "post",
test: /\.js$/,
loader: "loader3",
},
],
},
(3)、使用loader的方式
配置方式:在webpack.config.js
文件中指定loader(pre、normal、post loader)
- 配置方式:在
webpack.config.js
文件中指定loader(pre、normal、post loader) - 内联方式:在每个
import
语句中指定loader(inline loader)
(4)、inline loader(不建议写)
-
用法:
import Styles from 'style-loader!css-loader?modules!./style.css';
-
含义:
- 使用
css-loader
和style-loader
处理styles.css
文件 - 通过
!
将资源中的loader分开
inline loader
可以通过添加不同的前缀,跳过其他类型的loader
!
跳过normal loader
import Styles from '!style-loader!css-loader?modules!./style.css';
-!
跳过pre和normal loader
import Styles from '-!style-loader!css-loader?modules!./style.css';
!!
跳过pre、normal和post loader
import Styles from '!!style-loader!css-loader?modules!./style.css';
5.3 基本使用
loader
:用于对模块的源代码进行转换,可以将文件从不同的语言(如JavaScript
、CSS
、LESS
)转换为JavaScript
模块,以便在浏览器中使用只要进行源代码进行转换的都是
loader
例如:在 index.js
文件中引入 css
文件
import './style/index.css'
const h1 = document.createElement('h1')
h1.innerText = 'hello webpack'
document.body.appendChild(h1)
console.log('hello webpack')
执行 npx webpack
命令后,会报错,因为 webpack
只能打包 JavaScript
文件,不能打包 css
文件
ERROR in ./src/style/index.css 1:3
Module parse failed: Unexpected token (1:3)
You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file. See https://webpack.js.org/concepts#loaders
> h1 {
| color: #369;
| font-family: Arial, Helvetica, sans-serif;
@ ./src/index.js 1:0-26
上面的报错信息告诉我们,需要一个合适的 loader
来处理这个文件类型,目前没有配置 loader
来处理这个文件类型
- 解决步骤:
- 安装
css-loader
和style-loader
# `css-loader`:将 `css` 文件转换为 `JavaScript` 模块
# `style-loader`:将 `css` 文件插入到 `head` 标签中
npm i css-loader style-loader -D
- 在
webpack.config.js
文件中添加loader
配置 loader
是通过 module.rules
数组来配置的
module.exports = {
mode: 'development',
entry: './src/index.js',
output: {
filename: 'index.js',
path: path.resolve(__dirname, 'dist'),
clean: true
},
// 配置 loader
module: {
rules: [
{
test: /\.css$/i, // 正则表达式,匹配.css 结尾的文件
use: ['style-loader', 'css-loader'] // use 数组中的 loader 从右到左执行
},
{
test: /\.(jpg|png|svg)$/i, // 图片类型文件,webpack默认支持,使用 type: asset/resource 类型的 loader 来处理
type: 'asset/resource'
}
]
}
}
⚠️ 注意:use
数组中的 loader
从右到左执行,先执行 css-loader
,再执行 style-loader
总结:webpack 只能打包
JavaScript
文件,如果想要打包其他类型文件,需要添加对应文件的loader
图片资源:
type:
asset/resource
和asset
的区别:
- asset/resource:原封不动的图片 => 类似
file-loader(放入文件夹)
- asset:可以转base64,也可以原封不动输出 -> 类似
url-loader(转base64)+ file-loader
5.4 babel-loader的使用
babel
:是一个JavaScript
编译器,将ES6
代码转换为ES5
代码。可以将JavaScript
新特性转换可以兼容的旧版本的JavaScript
代码
babel主要作用:
- 转jsx
- 语法转换
- polyfill(主要是依赖core-js)
补充:
core-js
原理:去方法的原型上面找,如果没有找到,表示浏览器不支持,core-js
就手动写一个方法添加到原型上。
- 使用步骤:
- 安装
babel-loader
和@babel/core
和@babel/preset-env
babel-loader
:将babel
转换为webpack
可以识别的loader
@babel/core
:babel
的核心库
@babel/preset-env
:babel
的预设库,内置了一些常用的babel
插件
npm i babel-loader @babel/core @babel/preset-env -D
- 在
webpack.config.js
文件中添加loader
{
test: /\.m?js$/i,
exclude: /node_modules/, // 排除 node_modules 目录下的文件
use: {
loader: 'babel-loader', // 使用的 loader
options: {
presets: ['@babel/preset-env'] // 预设库
}
}
}
package.json
设置兼容性
{
"browserslist": {
"production": [">0.2%", "not dead", "not op_mini all"],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
// "browserslist": [
// "defaults" // 使用默认的浏览器列表
// ]
}
5.5 常见loader
5.6 自定义loader
- 使用步骤:
webpack.config.js
module: {
rules: [
{
test: /\.js$/,
use: [path.join(__dirname, './custom-loader')]
}
]
}
custom-loader.js
module.exports = function (source, sourceMap, data) {
// source: 为 loader 的输入
// data: 别的loader传递的数据
// 可能为文件内容,也可能是上一个 loader 处理的结果
console.log(source)
return source
}
5.7 同步loader
同步loader中不能执行异步操作
- webpack.config.js中
module: {
rules: [
{
test: /\.js$/,
use: ['./loader/sync-loader.js']
}
]
},
- loader/sync-loader.js
// 同步loader
// 写法1
module.exports = function(source) {
return source
}
// 写法2
module.exports = function (source, sourceMap, meta) {
/**
* 同步loader,必须调用callback
* 参数1:错误信息,有错误信息就传,没有就传null
* 参数2:loader处理后的内容
* 参数3:继续传递sourceMap,不然后续中断生成不了sourceMap
* 参数4:给下一个loader传递的参数
*/
this.callback(null, source, sourceMap, meta)
}
写法2在一些对前后loader关联多的时候用
5.8 异步loader
// 异步loader
module.exports = function (source, sourceMap, meta) {
// 调用 async 方法,返回值是 callback
const callback = this.async()
setTimeout(() => {
callback(null, source, sourceMap, meta)
}, 1000)
}
5.9 raw loader
row loader 接受到的 source 是Buffer(二进制数据),用于处理图片等资源
// 写法1
// row loader 接受到的 source 是Buffer(二进制数据)
module.exports = function (source) {
return source
}
module.exports.raw = true
// 写法2
function RawLoader(source) {
return source
}
RawLoader.raw = true
module.exports = RawLoader
5.10 pitch loader
需要向外暴露一个
pitch
方法,如果该方法return了一个值,那么就会中断后续的执行
module.exports = function (source, sourceMap, meta) {
console.log(source, 'source')
return source
}
module.exports.pitch = function (remainingRequest) {
// remainingRequest 剩下还需要处理的loader(以inline loader形式输出)
console.log('pitch', remainingRequest)
// return 'pitch-loader'
}
use: ['./loader/pitch-loader1.js', './loader/pitch-loader2.js', './loader/pitch-loader3.js']
normal loader:1 2 3
pitch:1 2 3
1、正常情况:先执行 pitch 1、2、3,再执行 loader 3、2、1
2、在pitch2中return了,那么先执行1、2,然后跳过pitch3、loader3、2,去执行loader1(因为loader1是pitch1的)
5.11 loader API
方法名 | 含义 | 用法 |
---|---|---|
this.async | 异步回调 loader。返回 this.callback | const callback = this.async() |
this.callback | 可以同步或者异步调用的并返回多个结果的函数 | this.callback(err, content, sourceMap?, meta?) |
this.getOptions(schema) | 获取 loader 的 options | this.getOptions(schema) |
this.emitFile | 产生一个文件 | this.emitFile(name, content, sourceMap) |
this.utils.contextify | 返回一个相对路径 | this.utils.contextify(context, request) |
this.utils.absolutify | 返回一个绝对路径 | this.utils.absolutify(context, request) |
文档:https://www.webpackjs.com/api/loaders/#the-loader-context
5.12 自定义loader:clean-log-loader
作用:去掉打包结果中所有的console.log语句
module.exports = function (source, sourceMap, meta) {
return source.replace(/console\.log\(.*\);?/g, '')
}
5.13 自定义loader:banner-loader
作用:给文件内容中添加作者信息
- webpack.config.js
{
test: /\.js$/,
use: [
{
loader: './loader/custom/banner-loader.js',
options: {
author: 'wifi歪f'
}
}
]
}
- loader/custom/banner-loader.js
通过
this.getOptions
可以获取到 loader 上下文对象。
const schema = require('./schema.json')
module.exports = function (source, sourceMap, meta) {
// this.getOptions 可以获取到 loader 上下文对象。
// schema:对options进行校验,要符合JSON Schema的规则
const options = this.getOptions(schema) // 不传参数,也能拿到数据,传了后schema会做类型校验
console.log(options);
const prefix = `
/**
* Author: ${options.author}
*/
`
return prefix + source
}
- loader/custom/schema.json
{
"type": "object",
"properties": { // 参数
"author": {
"type": "string"
}
},
"additionalProperties": false // 是否允许追加参数
}
例如:在webpack.config.js中追加参数
不符合JSON Schema规则,会报错
use: [
{
loader: './loader/custom/banner-loader.js',
options: {
author: 'wifi歪f',
age: 18 // 不符合JSON Schema规则
}
}
]
5.14 自定义loader:babel-loader
(1)、babel基础
- babel工具包:
- @babel-parser:将js代码解析为抽象语法树AST(解析)
- @babel/core:核心库(包含了解析、转换、生成的功能)
- @babel/generator:把转换后的抽象语法书(AST)生成目标代码(生成)
- @babel/code-frame
- @babel/runtime
- @babel/template
- @babel/traverse:是一个用于对抽象语法树(AST)进行递归遍历和更新的工具库,它可以通过访问和修改AST节点来实现代码转换。(转换)
@babel-parser -> @babel/traverse -> @babel/generator
- babel的使用
通过
transform
方法进行转换,options传入的是预设预设文档:https://www.babeljs.cn/docs/presets
import * as babel from "@babel/core";
// options传入的是预设
babel.transform(code, options, function(err, result) {
result; // result => { code, map, ast }
});
(2)、自定义babel-loader开发
- 安装依赖
npm i @babel/core @babel/preset-env -D
- webpack.config.js
use: [
{
loader: './loader/babel-loader/index.js',
options: {
presets: ['@babel/preset-env']
}
}
]
- loader/babel-loader/index.js
const schema = require('./schema.json')
const babel = require('@babel/core')
module.exports = function (source, sourceMap, meta) {
// 异步loader
const callback = this.async()
const options = this.getOptions(schema)
// 使用babel对代码进行编译转换
babel.transform(source, options, (err, result) => {
if (err) return callback(err)
else callback(null, result.code)
})
}
- loader/babel-loader/schema.json
{
"type": "object",
"properties": {
"presets": {
"type": "array"
}
},
"additionalProperties": true
}
5.15 自定义loader:file-loader
- 安装依赖
npm i loader-utils -D
- webpack.config.js
{
test: /\.(png|jpe?g)$/,
use: [
{
loader: './loader/file-loader/index.js'
}
],
type: 'javascript/auto' // 阻止webpack默认处理图片资源,只使用loader处理
}
- loader/file-loader/index.js
步骤参照的是:
type: assets
打包后的图片资源格式
// 处理图片、字体文件数据,都是buffer数据,需要使用 raw loader 才能处理
const loaderUtils = require('loader-utils')
module.exports = function (source) {
// 根据文件内容生成一个带hash值的文件名(使用第三方 loader-utils)
/**
* 参数1:this,loader的上下文对象
* 参数2:文件名模板,支持变量,如 [hash]、[ext]、[path]、[name]
* 参数3:传递的参数,{content: 文件内容}
*/
let filename = loaderUtils.interpolateName(this, '[hash].[ext]', {
content: source
})
filename = `images/${filename}`
// 将文件输出出去
this.emitFile(filename, source) // loader API:emitFile用于输出文件
// 返回文件路径
return `module.exports = "${filename}"`
}
module.exports.raw = true
6. plugin
plugin
:用来扩展webpack
的功能。
6.1 html-webpack-plugin
作用:自动生成
html
文件,自动引入打包后的js
文件
- 使用步骤:
- 安装
html-webpack-plugin
npm i html-webpack-plugin -D
- 在
webpack.config.js
文件中添加plugin
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
mode: 'development',
output: {
clean: true
},
plugins: [
// new HtmlWebpackPlugin(), // 自动生成 html 文件,自动引入打包后的 js 文件
new HtmlWebpackPlugin({
title: 'webpack app', // template的title会优先于这个title
template: './public/index.html' // 模板文件
})
]
}
6.2 mini-css-extract-plugin
作用:将 css 提取到单独的文件中,为每个包含 CSS 的 Js 文件创建一个 CSS 文件,需要配置loader(module)和plugins
- 使用步骤:
- 安装
mini-css-extract-plugin
npm install mini-css-extract-plugin -D
- 在
webpack.config.js
文件中添加loader
**注意:**使用了MiniCssExtractPlugin
后,需要去掉style-loader
,因为他会将css打包到js中去
module: {
rules: [
{
test: /\.css$/i,
// use: ['style-loader', 'css-loader']
use: [MiniCssExtractPlugin.loader, 'css-loader']
},
{
test: /\.less$/i,
// use: [ 'style-loader', 'css-loader', 'less-loader']
use: [MiniCssExtractPlugin.loader, 'css-loader', 'less-loader']
}
]
}
- 在
webpack.config.js
文件中添加plugins
plugins: [
new MiniCssExtractPlugin({
filename: 'static/css/main.css'
})
]
- 完整代码:
const HtmlWebpackPlugin = require('html-webpack-plugin')
const path = require('path')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
module.exports = {
mode: 'production',
optimization: {
usedExports: true
},
entry: './src/index.js',
output: {
filename: '[hash].js',
path: path.resolve(__dirname, 'dist'),
clean: true
},
module: {
rules: [
{
test: /\.css$/i,
// use: ['style-loader', 'css-loader']
use: [MiniCssExtractPlugin.loader, 'css-loader']
},
{
test: /\.less$/i,
// use: ['style-loader', 'css-loader', 'less-loader']
use: [MiniCssExtractPlugin.loader, 'css-loader', 'less-loader']
}
]
},
plugins: [
new MiniCssExtractPlugin({
filename: 'static/css/main.css'
}),
new HtmlWebpackPlugin({
title: 'webpack app',
template: './public/index.html'
})
],
devtool: 'source-map'
}
6.3 plugin工作原理
webpack 就像一条生产线,要经过一系列处理流程后才能将源文件转换成输出结果。 这条生产线上的每个处理流程的职责都是单一的,多个流程之间有存在依赖关系,只有完成当前处理后才能交给下一个流程去处理。
插件就像是一个插入到生产线中的一个功能,在特定的时机对生产线上的资源做处理。webpack 通过 Tapable 来组织这条复杂的生产线。 webpack 在运行过程中会广播事件,插件只需要监听它所关心的事件,就能加入到这条生产线中,去改变生产线的运作。
webpack 的事件流机制保证了插件的有序性,使得整个系统扩展性很好。
webpack 在编译代码过程中,会触发一系列 Tapable
钩子事件,插件所做的,就是找到相应的钩子,往上面挂上自己的任务,也就是注册事件,这样,当 webpack 构建的时候,插件注册的事件就会随着钩子的触发而执行了。
6.4 webpack内部的钩子
(1)、什么是钩子
钩子的本质就是:事件。为了方便我们直接介入和控制编译过程,webpack 把编译过程中触发的各类关键事件封装成事件接口暴露了出来。这些接口被很形象地称做:hooks
(钩子)。开发插件,离不开这些钩子。
(2)、Tapable
-
Tapable
为 webpack 提供了统一的插件接口(钩子)类型定义,它是 webpack 的核心功能库。 -
Tapable
还统一暴露了三个方法给插件,用于注入不同类型的自定义构建行为:
-
tap
:注册同步钩子。 -
tapAsync
:回调函数方式注册异步钩子。 -
tapPromise
:Promise 方式注册异步钩子。
6.5 plugin构建对象
(1)、Compiler(提供webpack的钩子)
compiler 对象中保存着完整的 Webpack 环境配置,每次启动 webpack 构建时它都是一个独一无二,仅仅会创建一次的对象。
这个对象会在首次启动 Webpack 时创建,我们可以通过 compiler 对象上访问到 Webapck 的主环境配置,比如 loader 、 plugin 等等配置信息。
它有以下主要属性:
compiler.options
可以访问本次启动 webpack 时候所有的配置文件,包括但不限于 loaders 、 entry 、 output 、 plugin 等等完整配置信息。compiler.inputFileSystem
和compiler.outputFileSystem
可以进行文件操作,相当于 Nodejs 中 fs。compiler.hooks
可以注册 tapable 的不同种类 Hook,从而可以在 compiler 生命周期中植入不同的逻辑。
compiler hooks 文档:https://www.webpackjs.com/api/compiler-hooks/
// tap为Tapable插件接口
compiler.hooks.someHook.tap('MyPlugin', (params) => {
/* ... */
});
(2)、Compilation(对资源的处理)
compilation 对象代表一次资源的构建,compilation 实例能够访问所有的模块和它们的依赖。
一个 compilation 对象会对构建依赖图中所有模块,进行编译。 在编译阶段,模块会被加载(load)、封存(seal)、优化(optimize)、 分块(chunk)、哈希(hash)和重新创建(restore)。
它有以下主要属性:
compilation.modules
可以访问所有模块,打包的每一个文件都是一个模块。compilation.chunks
chunk 即是多个 modules 组成而来的一个代码块。入口文件引入的资源组成一个 chunk,通过代码分割的模块又是另外的 chunk。compilation.assets
可以访问本次打包生成所有文件的结果。compilation.hooks
可以注册 tapable 的不同种类 Hook,用于在 compilation 编译模块阶段进行逻辑添加以及修改。
(3)、生命周期
Compiler:只会创建一次
Compilation:会创建多次
6.6 自定义插件
插件都是围绕着“钩子”展开,在编译的某个环节触发钩子,某种程度上可以理解为 —— 事件
- 时机:compiler.hooks.compilation
- 参数(上下文):compilation
- 交互:dependencyFactores.set
// custom-plugin.js
/**
* 1、webpack加载webpack.config.js配置文件,此时会new CustomPlugin(),执行插件的constructor
* 2、webpack创建compiler对象,
* 3、遍历所有plugins中的插件,此时会执行插件的apply方法
* 4、执行剩下的流程(触发各个hooks事件)
*/
class CustomPlugin {
constructor() {
console.log('触发了')
}
apply(compiler) {
// compiler.hooks.thisCompilation 当 webpack 开始构建的时候,会触发这个钩子,此时可以获取到 compilation 对象
// compilation对象 是每次创建的上下文实例
compiler.hooks.thisCompilation.tap('CustomPlugin', (compilation) => {
console.log(compilation)
})
}
}
module.exports = CustomPlugin
// webpack.config.js
const CustomPlugin = require('./custom-plugin.js')
export default {
plugins: [
new CustomPlugin()
]
}
6.7 注册hooks
hooks分类:
- Compiler的hooks
- Compilation的hooks
class TestPlugin {
constructor() {
console.log('TestPlugin')
}
apply(compiler) {
/**
* 由官方文档,environment是同步钩子,Tapable是tap注册
* compiler: webpack实例
* hooks: webpack实例上的钩子
* environment:钩子名
* tap:注册钩子(Tapable)
* tap的参数1: 插件名
* tap参数2: environment钩子回调函数,参数根据官方文档来定
*/
compiler.hooks.environment.tap('TestPlugin', () => {
console.log('environment')
})
// emit钩子(异步串型钩子AsyncParallelHook:可以做异步操作,但是必须按顺序执行)
compiler.hooks.emit.tap('TestPlugin', (compilation) => {
console.log('emit tap')
})
compiler.hooks.emit.tapAsync('TestPlugin', (compilation, callback) => {
setTimeout(() => {
console.log('emit tapAsync')
callback()
}, 1000)
})
compiler.hooks.emit.tapPromise('TestPlugin', (compilation) => {
return new Promise((resolve) => {
setTimeout(() => {
console.log('emit tapPromise')
resolve()
}, 1000)
})
})
// make钩子(异步并行钩子AsyncSeriesHook:一开始全部触发)
compiler.hooks.make.tapAsync('TestPlugin', (compilation, callback) => {
// 根据生命周期图所示,compilation钩子不能emit中触发,在需要在compilation hooks(钩子)触发前注册
compilation.hooks.seal.tap('TestPlugin', () => {
console.log('seal')
})
setTimeout(() => {
console.log('make tapAsync')
callback()
}, 1000)
})
}
}
module.exports = TestPlugin
输出:
TestPlugin
environment
make tapAsync
seal
emit tap
emit tapAsync
emit tapPromise
6.8 通过node调试查看compiler和compilation
- 在需要的地方添加debug
debugger
console.log(compiler)
debugger
console.log(compilation)
- package.json配置脚本
"debug": "node --inspect-brk ./node_modules/webpack-cli/bin/cli.js --mode development"
- 执行脚本后,再浏览器任意窗口打开控制台
点击绿色的图标
6.9 自定义插件:BannerWebpackPlugin
作用:给打包输出文件添加注释
步骤:
- 打包输出前添加注释:需要使用
compiler.hooks.emit
钩子, 它是打包输出前触发。 - 如何获取打包输出的资源?
compilation.assets
可以获取所有即将输出的资源文件。
实现:
- webpack.config.js
plugins: [
new BannerWebpackPlugin({
author: 'wifi歪f' // 会传入到插件的options中
})
]
- plugins/banner-plugins.js
class BannerWebpackPlugin {
constructor(options = {}) {
this.options = options
}
apply(compiler) {
compiler.hooks.emit.tapAsync(
'BannerWebpackPlugin',
(compilation, callback) => {
// 获取输出的资源
const assets = compilation.assets
// 过滤只保留js和css资源
const extensions = ['js', 'css']
const assetsArr = Object.keys(assets).filter((filepath) => {
const splitted = filepath.split('.')
const extension = splitted[splitted.length - 1] // 文件扩展名
return extensions.includes(extension)
})
const prefix = `/*
* Author: ${this.options.author}
* ${new Date().toLocaleString()}
*/`
// 遍历剩下资源,添加注释
assetsArr.forEach((asset) => {
// 获取资源内容
const source = compilation.assets[asset].source()
// 改变输出的资源
compilation.assets[asset] = {
// source:返回输出文件内容
source: () => prefix + source,
// 输出文件大小
size: () => (prefix + source).length
}
})
callback()
}
)
}
}
module.exports = BannerWebpackPlugin
6.10 自定义插件:CleanWebpackPlugin
作用:在 webpack 打包输出前将上次打包内容清空。
步骤:
-
如何在打包输出前执行?需要使用
compiler.hooks.emit
钩子, 它是打包输出前触发。 -
如何清空上次打包内容?
- 获取打包输出目录:通过 compiler 对象。
- 通过文件操作清空内容:通过
compiler.outputFileSystem
操作文件。
实现:
主要将
output.clean
关掉
class CleanWebpackPlugin {
apply(compiler) {
// 1. 注册钩子,在打包输出之前 emit
compiler.hooks.emit.tapAsync(
'CleanWebpackPlugin',
(compilation, callback) => {
// 2. 获取打包输出目录
const outputPath = compiler.options.output.path
// 3. 通过fs删除打包输出目录下的所有文件
const fs = compiler.outputFileSystem
this.removeFiles(fs, outputPath)
callback()
}
)
}
removeFiles(fs, filepath) {
// 删除打包目录下的所有资源,需要先将目录下的资源删除,再删除目录(不能直接删除这个目录)
// 1. 读取目录下的所有资源
const files = fs.readdirSync(filepath)
// 2. 遍历所有资源,删除
files.forEach((file) => {
// 拼接路径
const curPath = `${filepath}/${file}`
// 判断是否是目录
const stat = fs.statSync(curPath)
if (stat.isDirectory()) { // 是目录
// 递归删除
this.removeFiles(fs, curPath)
} else {
// 删除文件
fs.unlinkSync(curPath)
}
})
}
}
module.exports = CleanWebpackPlugin
6.11 自定义插件:AnalyzeWebpackPlugin
作用:分析 webpack 打包资源大小,并输出分析文件。
步骤:
compiler.hooks.emit
, 它是在打包输出前触发,我们需要分析资源大小同时添加上分析后的 md 文件。
实现:
class AnalyzeWebpackPlugin {
apply(compiler) {
compiler.hooks.emit.tap('AnalyzeWebpackPlugin', (compilation) => {
// 遍历所有文件,得到其大小
/**
* 将对象变成一个二维数组
* 对象:
* {
* key1: value1,
* key2: value2
* }
* 二维数组:
* [
* [key1, value1],
* [key2, value2]
* ]
*/
const assets = Object.entries(compilation.assets)
/**
* md表格语法格式
* | 资源名称 | 资源大小 |
* | --- | --- |
* | key1 | value1 |
*/
let content = `| 资源名称 | 资源大小 |
| --- | --- |`
assets.forEach(([filename, file]) => {
content = content + `\n| ${filename} | ${file.size()} |`
})
// 生成md文件
compilation.assets['analyze.md'] = {
source: () => content,
size: () => content.length
}
})
}
}
module.exports = AnalyzeWebpackPlugin
7. loader和plugin在运行时机上的区别
- loader 运行在打包文件之前上
- plugins 在整个编译周期都起作用
对于 loader,实质是一个转换器,将A文件进行编译形成B文件,操作的是文件,比如将 A.scSS 或 A.less 转变为 B.css,单纯的文件转换过程。
在 Webpack 运行的生命周期中会广播出许多事件,Plugin 可以监听这些事件,在合适的时机通过Webpack提供的API改变输出结果。
三、webpack使用(高级)
1. devServer 开发服务器
devServer
:是一个开发服务器,用于开发环境,它可以在内存中生成打包后的文件,不会在磁盘中生成打包后的文件,也不会在磁盘中生成source-map
文件,它可以在浏览器中访问打包后的文件,也可以在浏览器中访问source-map
文件
- 使用步骤:
- 安装
webpack-dev-server
npm i webpack-dev-server -D
- 运行命令
webpack serve
npx webpack serve
npx webpack serve --open --port 8080 # 自动打开浏览器,端口号为8080
也可以在 webpack.config.js
文件中添加 devServer
{
devServer: {
hot: true, // hmr热加载
port: 3000,
open: true
}
}
2. hmr
提升打包构建速度:hmr(热模块替换),在程序运行中,添加或删除模块,无需重新加载整个页面。
{
devServer: {
hot: true, // hmr热加载
port: 3000,
open: true
}
}
js文件一般是不支持热模块替换的,需要单独添加:
if (module.hot) {
// 只有添加了的文件才会实现热模块替换
module.hot.accept('./count.js')
module.hot.accept('./count2.js', () => {
console.log('accept')
})
}
3. source-map
source-map
:是一个映射文件,用于将打包后的文件映射到源代码文件,方便调试
- 使用步骤:
- 在
webpack.config.js
文件中添加devtool
- devtool
- cheap-module-source-map:只映射行,没有列映射
- source-map:既有行,又有列
- inline-source-map
- …
{
devtool: 'inline-source-map'
}
4. tree-shaking
tree-shaking—树摇,用于删除Dead Code,就是将引入后未使用的代码,进行删除过滤,不打包到最终产物里(只能对esmodule进行过滤,而commonjs不行)
对于esm的话也是有讲究的,默认导出是不会过滤的,只有按需导出,未使用的才会被过滤掉
- 使用步骤:开启tree-shaking
"mode": "production"
optimization.usedExports: true
export default {
mode: 'production',
optimization: {
usedExports: true
}
}
5. OneOf
OneOf
每个文件只能被其中一个loader处理,匹配上了第一个就不会去判断下面了
module: {
rules: [
{
oneOf: [
{
test: /\.css$/i,
use: [MiniCssExtractPlugin.loader, 'css-loader']
},
{
test: /\.less$/i,
use: [MiniCssExtractPlugin.loader, 'css-loader', 'less-loader']
}
]
}
]
},
6. Include-Exclude(包含、排除)
开发时用到的第三方库,所有的依赖文件都下载到node_modules中,而这些文件是编译后的,可以直接使用。所以在对js处理时,需要排除node_modules中的文件
- Include(包含):只处理某些文件
- Exclude(排除):不处理某些文件
{
test: /\.js$/,
use: [
{
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
}
],
exclude: /node_modules/, // 排除node_modules 文件夹下的文件
include: path.resolve(__dirname, 'src') // 只包含 src 文件夹下的文件
}
7. Cache(缓存)
打包时,js文件都要经过eslint和babel编译,速度比较慢,可以缓存之前的eslint缓存和babel编译结果,这样第二次打包速度就会更快
- Babel
{
test: /\.js$/,
use: [
{
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env'],
cacheDirectory: true, // 开启babel缓存
cacheCompression: false, // 关闭缓存文件的压缩
}
}
],
exclude: /node_modules/, // 排除node_modules 文件夹下的文件
include: path.resolve(__dirname, 'src') // 只包含 src 文件夹下的文件
}

这时候在node_modules下面会生成.chche
目录
- eslint
plugins: [
new ESLintPlugin({
caches: true,
cacheLocation: path.resolve(__dirname, 'node_modules/.cache/.eslintcache')
})
]
8. Thread(多进程打包)
- 安装依赖
npm i thread-loader -D
- 使用
terser-webpack-plugin
用于压缩js
const TerSerWebpackPlugin = require('terser-webpack-plugin') // webpack内置
const os = require('os')
// 获取cpu核数
const threads = os.cpus().length
// 放到需要的loader前
{
test: /\.js$/,
use: [
{
loader: 'thread-loader', // 开启多进程
options: {
works: threads // 进程数量
}
},
{
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env'],
cacheDirectory: true, // 开启babel缓存
cacheCompression: false, // 关闭缓存文件的压缩
}
}
],
exclude: /node_modules/, // 排除node_modules 文件夹下的文件
include: path.resolve(__dirname, 'src') // 只包含 src 文件夹下的文件
}
// 写法1: 在plugins
plugins: [
new TerSerWebpackPlugin({
parallel: threads // 开启多进程压缩
}),
]
// 写法2: optimization
plugins: [],
optimization: {
minimizer: [ // 需要压缩的内容
new TerSerWebpackPlugin({
parallel: threads // 开启多进程压缩
})
]
},
9. 优化代码运行性能 code split—optimization(代码分割)
code split
:代码分割
打包代码时,会将所有的js文件打包到一个文件中,体积会变得很大。需要将代码进行分割,生成多个js文件,渲染哪个页面,就加载某个js文件,这样就可以提升运行性能。
9.1 code split:多入口
entry: { // 多入口
app: './src/app.js',
main: './src/main.js'
},
9.2 code split:多入口提取公共模块
问题发现:
在9.1
中,我们对app和main两个文件进行了分别打包,同时在app和main中引入公共模块math.js
// app.js
import { sum } from "./math";
console.log('app.js');
console.log(sum(1,2,3), 'app');
// main.js
import { sum } from "./math";
console.log('main.js');
console.log(sum(1,2,3), 'main');
看打包产物:
- app的
(()=>{"use strict";console.log("app.js"),console.log([1,2,3].reduce(((o,e)=>o+e),0),"app")})();
- main的
(()=>{"use strict";console.log("main.js"),console.log([1,2,3].reduce(((o,e)=>o+e),0),"main")})();
上面我们可以看到:在app和main两个文件中都对公共模块进行了分别打包,造成了重复代码,我们希望对sum也进行单独拆分,让app和mian引入sum即可
使用:optimization.splitChunks
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
entry: { // 多入口
app: './src/app.js',
main: './src/main.js'
},
output: {
filename: '[name]-[hash].js',
path: path.resolve(__dirname, 'dist'),
clean: true
},
plugins: [
new HtmlWebpackPlugin({
template: './public/index.html'
})
],
optimization: {
// 代码分割配置
splitChunks: {
chunks: 'all', // 对所有模块都进行分割
// 默认配置
// minSize: 20000, // 模块超过20kb才进行分割
// minRemainingSize: 0, // 类似于miniSize,最后确保提取的文件大小不能为0
// minChunks: 1, // 模块至少使用1次才进行分割
// maxAsyncRequests: 30, // 按需加载时并行加载的最大个数
// maxInitialRequests: 30, // 入口js文件最大并行请求数
// enforceSizeThershold: 50000, // 超过50kb的模块强制进行分割(此时会忽略minRemainingSize、maxAsyncRequests、maxInitialRequests)
// cacheGroups: { // 组,哪些模块要打包到一个组
// defaultVendors: { // 组名,匹配所有模块,包括node_modules中的模块
// test: /[\\/]node_modules[\\/]/,
// priority: -10, // 优先级,越大越优先
// reuseExistingChunk: true // 如果该模块已经被提取过,就直接复用,而不是重新打包
// },
// default: { // 其他未匹配到的模块,打包到一个组中,组名为default
// minChunks: 2, // 至少被2个模块共享,这里minChunks权重更大
// priority: -20,
// reuseExistingChunk: true // 复用已打包的模块
// }
// },
// 修改配置
cacheGroups: {
// 其他没有写的配置会用上面的默认值
default: { // 其他未匹配到的模块,打包到一个组中,组名为default
minSize: 0, // 最小文件体积
minChunks: 2,
priority: -20,
reuseExistingChunk: true
}
}
}
}
}
9.3 code split:多入口按需加载(懒加载)
实现按需加载,动态导入模块配置:
发现问题:
当在app中引入count方法,当没有点击按钮,count文件也会被引入,我需要在点击按钮,动态引入count文件
import { sum } from "./math";
import { count } from "./count";
console.log('app.js');
console.log(sum(1,2,3), 'app');
document.querySelector('#btn').onclick = function () {
count(1,2)
}
实现:
import动态导入,特点:会将动态导入的文件单独打包,在需要的时候自动加载,打包后的文件名由webpack自动生成
import('./count').then(res => {
console.log(res.count(1,2), 'btn');
}).catch(err => {
console.log(err);
})
import { sum } from "./math";
// import { count } from "./count";
console.log('app.js');
console.log(sum(1,2,3), 'app');
document.querySelector('#btn').onclick = function () {
// import动态导入,特点:会将动态导入的文件单独打包,在需要的时候自动加载,打包后的文件名由webpack自动生成
import('./count').then(res => {
console.log(res.count(1,2), 'btn');
}).catch(err => {
console.log(err);
})
}
9.4 code split:单入口
开发时可能是单页面应用(SPA),只有一个入口(单入口),配置入下:
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
entry: './src/main.js', // 单入口
output: {
filename: 'js/[name]-[hash].js',
path: path.resolve(__dirname, 'dist'),
clean: true
},
plugins: [
new HtmlWebpackPlugin({
template: './public/index.html'
})
],
optimization: {
// 代码分割配置
splitChunks: {
chunks: 'all', // 对所有模块都进行分割
}
}
}
9.5 code split:给动态导入模块命名
动态导入的文件,文件名是随机的数字,要想给它命名,需要按照以下要求配置
/* webpackChunkName: 'xxx' */
:webpack魔法命名
- js中:
document.querySelector('#btn').onclick = function () {
// import动态导入,特点:会将动态导入的文件单独打包,在需要的时候自动加载,打包后的文件名由webpack自动生成
import(/* webpackChunkName: 'count' */'./count').then(res => {
console.log(res.count(1,2), 'btn');
}).catch(err => {
console.log(err);
})
}
- webpack.config.js(可以不写)
给打包输出的其他文件命名:chunkFilename
output: {
filename: 'js/[name].js',
path: path.resolve(__dirname, 'dist'),
clean: true,
chunkFilename: 'js/[name].chunk.js'
},
只写
/* webpackChunkName: 'xxx' */
,生成的名字为:xxx.js
写
/* webpackChunkName: 'xxx' */
和chunkFilename
,生成的名字为:xxx.chunk.js
9.6 code split:统一命名
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
module.exports = {
entry: './src/main.js', // 单入口
output: {
filename: 'js/[name].js',
path: path.resolve(__dirname, 'dist'),
clean: true,
// 打包输出的其他文件的文件名
chunkFilename: 'js/[name].chunk.js',
// 静态资源打包路径
assetModuleFilename: 'static/media/[name].[hash][ext]'
},
plugins: [
new HtmlWebpackPlugin({
template: './public/index.html'
}),
new MiniCssExtractPlugin({
filename: 'static/css/main.css',
chunkFilename: 'static/css/[name].chunk.css'
})
],
optimization: {
// 代码分割配置
splitChunks: {
chunks: 'all' // 对所有模块都进行分割
}
}
}
10. Preload和Prefetch
在前面做了代码分割(code split),也做了动态import导入(也叫懒加载),但是如果动态加载的文件很大,也会造成卡顿。可以通过浏览器空闲时间,加载后续使用的代码,就需要用上preload
和prefetch
技术
定义:
- preload:浏览器立即加载资源
- prefetch:在浏览器空闲时间加载资源
共同点:
- 都会加载资源,但是不会执行
- 都有缓存
区别:
- preload加载优先级高,prefetch加载优先级低
- preload只能加载当前页面需要使用的资源,prefetch可以加载当前页面资源,也可以加载下一个页面使用的资源
总结:
- 当前页面优先级高的资源用preload加载
- 下一个页面需要的资源用prefetch加载
使用1:
- preload
- 安装
npm i @vue/preload-webpack-plugin -D
- 使用
const PreloadWebpackPlugin = require('@vue/preload-webpack-plugin')
plugins: [
new PreloadWebpackPlugin({
rel: 'preload', // 以preload方式加载
as: 'script'
})
]
注意:main入口文件还是正常引入,只是其他资源会添加上
rel="preload"
<link
href="js/count.chunk.js"
rel="preload"
as="script"
/>
使用2:
webpackPreload: true
import(/* webpackChunkName: 'count', webpackPreload: true */'./count')
- prefetch
webpackPrefetch: true
import(/* webpackChunkName: 'count', webpackPrefetch: true */'./count').then(res => {
console.log(res.count(1,2), 'btn');
}).catch(err => {
console.log(err);
})
11. Network Cache
发生问题:
A模块使用了B模块的内容,当B模块文件的内容发生变化,打包后AB模块文件hash名都会变化(原因:A模块引入B,B的hash变化,所以导致A引入B的文件名变化,A的hash也会变化),当模块依赖过多,就会导致很多文件缓存实效
解决:
将A保存(引入)B文件的hash值(也就是引入的文件名路径)单独保存到一个文件,叫runtime运行时文件。当B模块变化,只会导致B模块和runtime文件发生变化,A文件名不变
plugins: [],
optimization: {
// 代码分割配置
splitChunks: {
chunks: 'all' // 对所有模块都进行分割
},
runtimeChunk: {
name: entrypoint => `runtime~${entrypoint.name}`
},
}
12. 解决js兼容性问题Core-js
之前我们使用babel
对js进行兼容性处理,其中使用@babel/preset-env
智能预设来处理兼容性问题,它可以将es6的一些语法进行编译转换,比如箭头函数、扩展运算符…,但是如果是async函数,promise对象,数组的一些方法(比如includes…),它没办法处理。
所以此时js代码仍然存在兼容性问题,core-js
就是专门来做es6以上api的polyfill
的
polyfill
又叫做垫片/补丁,用于提供原生不支持的功能
- babel和core-js的区别
- babel:处理新语法
- core-js:处理新api
用法:
- 全部引入
npm i core-js
// mian.js
import 'core-js'
- 按需引入
// main.js
import 'core-js/es/promise'
- 自动按需引入(通过babel)
webpack.config.js
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: [
{
loader: 'babel-loader', // 通过babel.config.js具体配置
}
],
},
]
}
babel.config.js
module.exports = {
presets: [
[
'@babel/preset-env',
{
useBuiltIns: 'usage', // 按需加载,自动引入
corejs: 3 // 指定core-js版本
}
]
]
}
为了效果明显,在package.json
中添加:
"browserslist": [ // 需要兼容ie11,效果更明显
"last 2 versions",
"not dead",
"ie 11"
]
就不需要单独/全部引入了
13. PWA 离线访问
pwa:是一种可以提供类似native app(原生应用)的web app技术,让它在离线的时候也能继续运行,内部通过Service Workers技术实现的
使用:
- 安装依赖
npm install workbox-webpack-plugin --save-dev
webpack.config.js
const WorkboxPlugin = require('workbox-webpack-plugin');
plugins: [
new WorkboxPlugin.GenerateSW({
// 这些选项帮助快速启用 ServiceWorkers
// 不允许遗留任何“旧的” ServiceWorkers
clientsClaim: true,
skipWaiting: true,
}),
],
- 需要在
main.js
中注册Service Worker
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)
})
})
}
14. @babel/plugin-transform-runtime 减少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'], // 减少代码体积
}
}
15. 总结
- 提升开发体验
- source-map:能够准确定位代码后代码的错误提示
- 提升打包构建速度
- hmr
- OneOf
- Include/Exclude
- Cache
- Thead
- 减少代码体积
- tree shaking
- @babel/plugin-transform-runtime:减少babel生成文件的体积
- 优化代码运行性能
- code-split
- preload/prefetch
- network cache
- core-js
- pwa
四、React-Cli
1、开发环境配置
1.1 基本配置
- 初始化
package.json
npm init -y
- 安装webpack依赖
npm i webpack webpack-cli webpack-dev-server -D
- 安装loader的对应依赖
- style-loader
- css-loader
- less
- less-loader
- sass
- sass-loader
- postcss-loader
- postcss-preset-env
- @babel/core
- babel-loader
- @babel/preset-env
- @babel/preset-react
npm i style-loader css-loader less less-loader sass sass-loader postcss-loader postcss-preset-env @babel/core babel-loader @babel/preset-env @babel/preset-react -D
- 安装plugins的对应依赖
- eslint
- eslint-webpack-plugin
- eslint-config-react-app
- html-webpack-plugin
npm i eslint eslint-webpack-plugin eslint-config-react-app html-webpack-plugin -D
- 新建并配置
webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const EslintWebpackPlugin = require('eslint-webpack-plugin');
const getStyleLoaders = (...pre) => {
return [
'style-loader',
'css-loader',
{
loader: 'postcss-loader',
options: {
postcssOptions: {
plugins: [
'postcss-preset-env' // 处理样式兼容性问题,需要配合package.json中的browserslist配置(来确定兼容性版本)
]
}
}
},
...pre
].filter(Boolean)
}
module.exports = {
entry: './src/main.js',
output: {
path: undefined, // 开发模式下不需要配置输出路径
filename: 'js/[name].js',
chunkFilename: 'js/[name].chunk.js',
// [hash:10]:取hash的前10位,[ext]:取文件扩展名
assetModuleFilename: 'assets/[hash:10][ext]' // 配置打包后的静态资源路径
},
module: {
rules: [
// 处理css
{
test: /\.css$/,
use: getStyleLoaders()
},
// 处理less
{
test: /\.less$/,
use: getStyleLoaders('less-loader')
},
// 处理sass
{
test: /\.s[ac]ss$/,
use: getStyleLoaders('sass-loader')
},
// 处理图片
{
test: /\.(png|jpe?g|gif|webp|svg)$/,
type: 'asset',
parser: {
dataUrlCondition: {
// 小于10kb的图片会被base64处理
maxSize: 10 * 1024
}
}
},
// 处理其他资源
{
test: /\.(woff2?|eot|ttf|otf)$/,
type: 'asset/resource',
generator: {
filename: 'font/[hash:10][ext]'
}
},
// 处理js、jsx
{
test: /\.jsx?$/,
include: path.resolve(__dirname, './src'),
use: [{
loader: 'babel-loader',
options: {
cacheDirectory: true, // 开启缓存
cacheCompression: false // 关闭缓存文件压缩
}
}]
}
]
},
plugins: [
// 配置html模板
new HtmlWebpackPlugin({
template: './public/index.html',
favicon: './public/favicon.svg'
}),
// eslint
new EslintWebpackPlugin({
context: path.resolve(__dirname, 'src'), // 检测哪些文件
exclude: 'node_modules',
cache: true, // 开启缓存
cacheLocation: path.resolve(__dirname, 'node_modules/.cache/.eslintcache') // 缓存路径
})
],
mode: 'development',
devtool: 'cheap-module-source-map',
resolve: {
alias: {
'@': path.resolve(__dirname, '../src')
},
extensions: ['.js', '.json', '.jsx', '.tsx', '.ts'] // 配置省略文件后缀
},
devServer: {
host: 'localhost',
port: '3000',
open: true,
hot: true,
historyApiFallback: true, // 解决前端刷新404问题
},
optimization: { // 配置代码分割
splitChunks: {
chunks: 'all'
},
runtimeChunk: { // 运行时代码
name: entrypoint => `runtime~${entrypoint.name}`
}
}
}
- 新建并配置
babel.config.js
module.exports = {
presets: [
['@babel/preset-env', { targets: { node: 'current' } }],
['@babel/preset-react', { runtime: 'automatic' }]
]
}
- 新建并配置
.eslintrc.js
module.exports = {
extends: ['react-app']
}
- 在
package.json
中新增browserslist
{
"dependencies": {
...
},
"browserslist": [
"> 1%",
"last 2 versions"
]
}
- 安装react相关依赖
npm i react react-dom react-router-dom
- 在public目录下新建index.html和放入小图标
需要给<div id="root"></div>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="root"></div>
</body>
</html>
- src下新建如下:
- src/main.js(不要写成jsx,因为webpack.config.js中写的entry是main.js)
- src/App.jsx
- src/index.scss
- src/views/home.jsx
- src/views/about.jsx
// main.js
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
import { BrowserRouter } from 'react-router-dom'
const root = ReactDOM.createRoot(document.getElementById('root'))
root.render(
<BrowserRouter>
<App />
</BrowserRouter>
)
// App.jsx
import React from 'react'
import './index.scss'
import { Routes, Route, Outlet, Navigate, NavLink } from 'react-router-dom'
import Home from './views/home'
import About from './views/about'
export default function App() {
return (
<>
<h1 className='h1-title'>Hello React-Cli</h1>
<NavLink to='/home'>Home</NavLink> |
<NavLink to='/about'>about</NavLink>
<hr />
<Routes>
<Route path='/' element={<Navigate to="/home" />} />
<Route
path='/home'
element={<Home />}
>
</Route>
<Route
path='/about'
element={<About />}
>
</Route>
</Routes>
<Outlet />
</>
)
}
// index.scss
.h1-title {
color: royalblue;
}
package.json
配置启动命令
"scripts": {
"dev": "webpack serve",
"dev2": "webpack serve --config webpack.config.js" // 也行
}
1.2 报错:
- Using
babel-preset-react-app
requires that you specifyNODE_ENV
orBABEL_ENV
environment variables. Valid values are “development”, “test”, and “production”. Instead, received: undefined.
原因:缺少环境变量
解决:安装
cross-env
安装依赖
npm i cross-env -D
将启动命令换成:
"scripts": {
"dev": "cross-env NODE_ENV=development webpack serve",
}
- [eslint] Error while loading rule ‘flowtype/define-flow-type’: context.getAllComments is not a function Occurred while linting xxx
原因:ESLint 插件或规则与当前使用的 ESLint 版本不兼容导致的
解决:安装
eslint-plugin-flowtype@latest
安装依赖
npm install eslint-plugin-flowtype@latest --save-dev
修改.eslintrc.js
module.exports = {
extends: 'react-app',
plugins: ['flowtype'],
rules: {
'flowtype/define-flow-type': 'off', // 禁用该规则
'flowtype/use-flow-type': 'off', // 禁用该规则
// 其他规则
}
};
1.3 优化配置
- hmr
js代码更新不会触发hmr,在开发react时,可以用它的插件来实现hmr
插件:react-refresh
1.4 效果:
2、生产环境配置
- 安装依赖
- mini-css-extract-plugin:提取css成单独的文件
- css-minimizer-webpack-plugin:压缩css
- terser-webpack-plugin:压缩js(webpack内置)
npm i mini-css-extract-plugin css-minimizer-webpack-plugin -D
- webpack.prod.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const EslintWebpackPlugin = require('eslint-webpack-plugin');
// 提取css成单独的文件
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
// 压缩css
const CssMinimizerWebpackPlugin = require('css-minimizer-webpack-plugin');
// 压缩js
const TerserWebpackPlugin = require('terser-webpack-plugin');
const getStyleLoaders = (...pre) => {
return [
MiniCssExtractPlugin.loader,
'css-loader',
{
loader: 'postcss-loader',
options: {
postcssOptions: {
plugins: [
'postcss-preset-env'
]
}
}
},
...pre
].filter(Boolean)
}
module.exports = {
entry: './src/main.js',
output: {
path: path.resolve(__dirname, './dist'),
filename: 'js/[name].js',
chunkFilename: 'js/[name].chunk.js',
assetModuleFilename: 'assets/[hash:10][ext]',
clean: true
},
module: {
rules: [
// 处理css
{
test: /\.css$/,
use: getStyleLoaders()
},
// 处理less
{
test: /\.less$/,
use: getStyleLoaders('less-loader')
},
// 处理sass
{
test: /\.s[ac]ss$/,
use: getStyleLoaders('sass-loader')
},
// 处理图片
{
test: /\.(png|jpe?g|gif|webp|svg)$/,
type: 'asset',
parser: {
dataUrlCondition: {
// 小于10kb的图片会被base64处理
maxSize: 10 * 1024
}
}
},
// 处理其他资源
{
test: /\.(woff2?|eot|ttf|otf)$/,
type: 'asset/resource',
generator: {
filename: 'font/[hash:10][ext]'
}
},
// 处理js、jsx
{
test: /\.jsx?$/,
include: path.resolve(__dirname, './src'),
use: [{
loader: 'babel-loader',
options: {
cacheDirectory: true, // 开启缓存
cacheCompression: false // 关闭缓存文件压缩
}
}],
}
]
},
plugins: [
// 配置html模板
new HtmlWebpackPlugin({
template: './public/index.html',
favicon: './public/favicon.svg'
}),
// eslint
new EslintWebpackPlugin({
context: path.resolve(__dirname, 'src'), // 检测哪些文件
exclude: 'node_modules',
cache: true, // 开启缓存
cacheLocation: path.resolve(__dirname, 'node_modules/.cache/.eslintcache') // 缓存路径
}),
// 提取css
new MiniCssExtractPlugin({
filename: 'css/[name]-[contenthash:10].css',
chunkFilename: 'css/[name]-[contenthash:10].chunk.css'
})
],
mode: 'production',
devtool: 'source-map',
resolve: {
alias: {
'@': path.resolve(__dirname, '../src')
},
extensions: ['.js', '.json', '.jsx', '.tsx', '.ts'] // 配置省略文件后缀
},
optimization: { // 配置代码分割
splitChunks: {
chunks: 'all'
},
runtimeChunk: { // 运行时代码
name: entrypoint => `runtime~${entrypoint.name}`
},
minimizer: [
// 压缩css
new CssMinimizerWebpackPlugin(),
// 压缩js
new TerserWebpackPlugin()
]
}
}
- 配置脚本
"scripts": {
"dev": "cross-env NODE_ENV=development webpack serve --config webpack.dev.js",
"build": "cross-env NODE_ENV=production webpack --config webpack.prod.js"
}
五、Vue-Cli
1、开发环境配置
1.1 基本配置
- 初始化package.json
npm init -y
- 安装webpack依赖
- webpack
- webpack-cli
- webpack-dev-server
- cross-env
npm i webpack webpack-cli webpack-dev-server cross-env -D
- 安装loader
- vue-style-loader(vue对样式的处理不采用style-loader,而是采用vue-style-loader)
- css-loader
- postcss-loader
- postcss-preset-env
- less
- less-loader
- sass
- sass-loader
- stylus
- stylus-loader
- vue-loader(用于编译.vue结尾的文件)
- vue-template-compiler(vue编译模版)
- @babel/core
- @babel/preset-env
- babel-loader
- @vue/cli-plugin-babel
- core-js
npm i vue-style-loader css-loader postcss-loader postcss-preset-env less less-loader sass sass-loader stylus stylus-loader vue-loader vue-template-compiler babel-loader @babel/core @babel/preset-env @vue/cli-plugin-babel core-js -D
- 安装plugin
- html-webpack-plugin
- eslint
- eslint-webpack-plugin
- eslint-plugin-vue
- @babel/eslint-parser
npm i html-webpack-plugin eslint eslint-webpack-plugin eslint-plugin-vue @babel/eslint-parser -D
- webpack.dev.js
⚠️注意:vue文件对样式的处理不采用style-loader,而是使用vue-style-loader
/** @type {import('webpack').Configuration} */
const path = require('path')
const { VueLoaderPlugin } = require('vue-loader')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const EslintWebpackPlugin = require('eslint-webpack-plugin')
const getStyleLoaders = (...pre) => {
return [
// vue文件对样式的处理不采用style-loader,而是使用vue-style-loader
'vue-style-loader',
'css-loader',
{
loader: 'postcss-loader',
options: {
postcssOptions: {
plugins: [
'postcss-preset-env' // 处理样式兼容性问题,需要配合package.json中的browserslist配置(来确定兼容性版本)
]
}
}
},
...pre
].filter(Boolean)
}
const config = {
entry: './src/main.js',
output: {
path: undefined,
filename: 'js/[name].js',
chunkFilename: 'js/[name].chunk.js',
assetModuleFilename: 'assets/[name].[hash:6][ext]'
},
module: {
rules: [
{
test: /\.css$/,
use: getStyleLoaders()
},
{
test: /\.less$/,
use: getStyleLoaders('less-loader')
},
{
test: /\.s[ac]ss$/,
use: getStyleLoaders('sass-loader')
},
{
test: /\.styl$/,
use: getStyleLoaders('stylus-loader')
},
// 处理图片
{
test: /\.(png|jpe?g|gif|webp|svg)$/,
type: 'asset',
parser: {
dataUrlCondition: {
// 小于10kb的图片会被base64处理
maxSize: 10 * 1024
}
}
},
// 处理其他资源
{
test: /\.(woff2?|eot|ttf|otf)$/,
type: 'asset/resource',
generator: {
filename: 'font/[hash:10][ext]'
}
},
{
test: /\.vue$/,
loader: 'vue-loader'
},
// 处理js
{
test: /\.js$/,
include: path.resolve(__dirname, './src'),
use: [
{
loader: 'babel-loader',
options: {
cacheDirectory: true, // 开启缓存
cacheCompression: false // 关闭缓存文件压缩
}
}
]
}
]
},
plugins: [
new VueLoaderPlugin(),
new HtmlWebpackPlugin({
template: './public/index.html',
favicon: './public/favicon.svg'
}),
new EslintWebpackPlugin({
context: path.resolve(__dirname, 'src'),
exclude: 'node_modules',
cache: true, // 开启缓存
cacheLocation: path.resolve(__dirname, 'node_modules/.cache/.eslintcache') // 缓存路径
}),
// 需要从vue-loader导入使用
new VueLoaderPlugin(),
],
mode: 'development',
devServer: {
host: 'localhost',
port: '3000',
open: true,
hot: true
},
resolve: {
alias: {
'@': path.resolve(__dirname, './src')
},
extensions: ['.js', '.json', '.vue'] // 配置省略文件后缀
},
devtool: 'cheap-source-map',
optimization: {
// 配置代码分割
splitChunks: {
chunks: 'all'
},
runtimeChunk: {
// 运行时代码
name: (entrypoint) => `runtime~${entrypoint.name}`
}
}
}
module.exports = config
- .eslintrc.js
module.exports = {
root: true,
env: {
node: true
},
extends: ['plugin:vue/vue3-essential', 'eslint:recommended'], // 在eslint-plugin-vue这个包中
parserOptions: {
parser: '@babel/eslint-parser'
}
}
- babel.config.js
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset',
[
'@babel/preset-env',
{
useBuiltIns: 'usage',
corejs: 3
}
]
]
}
- package.json
"scripts": {
"dev": "cross-env NODE_ENV=development webpack serve --config webpack.dev.js",
"build": "cross-env NODE_ENV=production webpack --config webpack.prod.js"
}
- 其余步骤就是vue部分了,略…
2、生产环境配置
- 安装依赖
- mini-css-extract-plugin
- css-minimizer-webpack-plugin
- terser-webpack-plugin(wbepack内置,如果没有需要安装)
- webpack.prod.js
⚠️注意:打包就不采用vue-style-loader,而是使用MiniCssExtractPlugin
/** @type {import('webpack').Configuration} */
const path = require('path')
const { VueLoaderPlugin } = require('vue-loader')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const EslintWebpackPlugin = require('eslint-webpack-plugin')
// 提取css成单独的文件
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
// 压缩css
const CssMinimizerWebpackPlugin = require('css-minimizer-webpack-plugin')
// 压缩js
const TerserWebpackPlugin = require('terser-webpack-plugin')
const getStyleLoaders = (...pre) => {
return [
// 打包就不采用vue-style-loader,而是使用MiniCssExtractPlugin
MiniCssExtractPlugin.loader,
'css-loader',
{
loader: 'postcss-loader',
options: {
postcssOptions: {
plugins: [
'postcss-preset-env' // 处理样式兼容性问题,需要配合package.json中的browserslist配置(来确定兼容性版本)
]
}
}
},
...pre
].filter(Boolean)
}
const config = {
entry: './src/main.js',
output: {
path: path.resolve(__dirname, './dist'),
filename: 'js/[name].js',
chunkFilename: 'js/[name].chunk.js',
assetModuleFilename: 'assets/[name].[hash:6][ext]',
clean: true
},
module: {
rules: [
{
test: /\.css$/,
use: getStyleLoaders()
},
{
test: /\.less$/,
use: getStyleLoaders('less-loader')
},
{
test: /\.s[ac]ss$/,
use: getStyleLoaders('sass-loader')
},
{
test: /\.styl$/,
use: getStyleLoaders('stylus-loader')
},
// 处理图片
{
test: /\.(png|jpe?g|gif|webp|svg)$/,
type: 'asset',
parser: {
dataUrlCondition: {
// 小于10kb的图片会被base64处理
maxSize: 10 * 1024
}
}
},
// 处理其他资源
{
test: /\.(woff2?|eot|ttf|otf)$/,
type: 'asset/resource',
generator: {
filename: 'font/[hash:10][ext]'
}
},
{
test: /\.vue$/,
loader: 'vue-loader'
},
// 处理js
{
test: /\.js$/,
include: path.resolve(__dirname, './src'),
use: [
{
loader: 'babel-loader',
options: {
cacheDirectory: true, // 开启缓存
cacheCompression: false // 关闭缓存文件压缩
}
}
]
}
]
},
plugins: [
new VueLoaderPlugin(),
new HtmlWebpackPlugin({
template: './public/index.html',
favicon: './public/favicon.svg'
}),
new EslintWebpackPlugin({
context: path.resolve(__dirname, 'src'),
exclude: 'node_modules',
cache: true, // 开启缓存
cacheLocation: path.resolve(__dirname, 'node_modules/.cache/.eslintcache') // 缓存路径
}),
new VueLoaderPlugin(),
// 提取css
new MiniCssExtractPlugin({
filename: 'css/[name]-[contenthash:10].css',
chunkFilename: 'css/[name]-[contenthash:10].chunk.css'
})
],
mode: 'production',
resolve: {
alias: {
'@': path.resolve(__dirname, './src')
},
extensions: ['.js', '.json', '.vue'] // 配置省略文件后缀
},
devtool: 'source-map',
optimization: {
// 配置代码分割
splitChunks: {
chunks: 'all'
},
runtimeChunk: {
// 运行时代码
name: (entrypoint) => `runtime~${entrypoint.name}`
},
minimizer: [
// 压缩css
new CssMinimizerWebpackPlugin(),
// 压缩js
new TerserWebpackPlugin()
]
}
}
module.exports = config