创建项目
mkdir webpack-project
cd webpack-project
npm init -y
项目准备
安装webpack@4.43.0和webpack-cli@3.3.12版本依赖存放到开发环境下
yarn add webpack@4.43.0 webpack-cli@3.3.12 -D
新建.npmrc文件指定安装源registry="https://registry.npm.taobao.org/",这样就不用每次安装项目的时候切换源了
新建webpack.config.js配置项
module.exports = {
entry, // 入口 string || array || object
output, // 出口 object {path, filename}
mode, // 打包模式 string
module, // 各种处理chunk的loader object {rules:[{test, use: string||array||array[object{loader,options}]}]}
plugins // 插件
}
构建webpack,在package.json文件中scripts字段中写入脚本dev:'webpack'打包构建输出bundle文件
兼容各种浏览器前缀css样式,需要在package.json文件中新建browserslist字段为数组例如["last 2 version", "> 1%"]
样式处理
1、css文件处理
需要两个loader:css-loader、style-loader
# 安装:
yarn add css-loader style-loader -D
# 配置
module: {
rules: [
{
test: /\.css$/,
use: ['style-loader', 'css-loader']
}
]
}
loader的处理顺序为从后往前,如上述rules中的loader先处理css-loader把css文件转为序列化(string)chunk,然后再由style-loader处理创建style标签把代码块放进去,这样的话样式就生效了
2、同样less、sass文件处理也是一样的步骤
- less需要三个loader:style-loader、css-loader、less-loader,需要额外安装yarn add less less-loader@7.3.0 -D
- scss也需要三个loader:style-loader、css-loader、sass-loader,需要额外安装yarn add node-sass sass-loader -D
3、postcss至于css相当于babel至于js,跟webpack一样是工具,其功能一是把css处理成js可以操作的抽象语法树AST,其二就是调用插件来处理AST并得到结果
- 自动浏览器前缀:autoprefixer
- css压缩等cssnano
- 根目录下创建postcss.config.js文件,写入配置,使用方式和less-loader一样
module.exports = {
plugins: [require("autoprefixer"), require("cssnano")]
};
4、样式文件分离
经过loader的处理,css最后会被打包到js中,运行时会动态的插入head中,但是一般在生产环境中会把css文件分离出来(有利于客户端缓存、并行加载及减少js包的大小),这时候用到插件mini-css-extract-plugin,会以link的方式插入到html中
const minicss = require("mini-css-extract-plugin");
module.exports = {
...
module:{
rules:[
{
test: /\.less$/,
use: [
// "style-loader", // 创建style标签方式
minicss.loader, // 以link方式插入html中
"css-loader",
]
}
]
},
plugins: [
new minicss({
filename: "[name].css"
})
]
}
5、图片字体文件处理
file-loader和url-loader都可以用来处理本地的资源文件,如图片、字体、音频等。不过url-loader可以指定文件的大小小于限定一般为1024*33KB,转为base64URL,不会输出真实的文件,可以减少网络请求
use: [
{
loader: 'url-loader', // 配置url-loader即可,内部会自动调用file-loader
options: {
name: '[name].[ext]', //输出名字占位符[name],[contenthash],[chunkhash]等
limit: 1024*3, // 一般3KB以下图片转为base64URL
...
}
}
]
使用图片的时候,file-loader有outputPath和publicPath两个参数极为重要
module.exports = {
...
module: {
rules: [
{
test: /\.(png|jpe?g|gif|webp)$/,
use: {
loader: "file-loader",
options: {
name: "[name].[ext]",
outputPath: "images/", // 图片的输出位置,也就是存放位置
publicPath: "../images/" // 图片的引用位置,css中如何引用图片
// 图片的路径+图片的名称 ../images/ + logo.png
}
}
},
]
}
}
devtool配置选项
devtool选项为cheap-module-eval-source-map开启开发模式的代码映射,帮助更快的查找错误来源位置
devServer配置选项
devServer选项设置跟webpack-dev-server开启本地服务有关
yarn add webpack-dev-server@3.11.0 -D
devServer: {
contentBase: path.join(__dirname, "dist"), // 表示服务器从哪个目录去查找内容文件(即页面文件,比如HTML)
port: 8081, // 端口
open: true, // 自动打开浏览器
proxy: { // 代理
"/api": {
target: "http://localhost:9092/"
}
}
}
当开启webpack-dev-server时,会自动接管webpack构建,并且会把打包文件保存到本地内存中,这样读取更快,修改文件保存时会浏览器会自动刷新——热更新
HMR:热模块替换(Hot Module Replacement)
当我们修改文件的时候,浏览器不刷新,内容自动变化,这就是热更新
- CSS模块HMR
devServer: {
contentBase: path.join(__dirname, "dist"),
port: 8081, // 端口
open: true, // 自动打开浏览器
hot: true, // 开启热替换-CSS
}
注意不支持chunkhash和contenthash
- JS模块HMR-HotModuleReplacementPlugin插件
devServer: {
contentBase: path.join(__dirname, "dist"),
port: 8081, // 端口
open: true, // 自动打开浏览器
hot: true, // 开启热替换
hotOnly: true, // 关闭浏览器的自动刷新
}
注意:需要使⽤module.hot.accept来观察模块更新,本质上就是监控目录下文件,先删除文件再生成文件这样就不影响其他模块
插件优化
1、自动生成html文件,html-webpack-plugin会在打包结束后,⾃动⽣成⼀个html⽂件,并把打包⽣成的js模块引⼊到该html,一般使用两个配置项template和filename
中
yarn add html-webpack-plugin@4 -D
const path = require('path')
const htmlwebpackplugin = require('html-webpack-plugin')
module.exports = {
...
plugins: [
new htmlwebpackplugin({
template: './src/index.html',
filename: '[name].html'
})
]
}
2、自动清除打包文件clean-webpack-plugin
yarn add clean-webpack-plugin -D
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
...
plugins: [
new CleanWebpackPlugin()
]
如何编写一个loader
-
loader本质上就是一个函数,不可以是箭头函数
这是因为要使用到上下文的this,调用callback等API,必须是一个声明式函数,该函数接收一个参数是源码 -
loader必须有返回值,string或buffer
-
参数是source源代码,对其进行加工处理返回源码
-
this.callbackAPI返回多个信息 -
this.asyncAPI处理异步逻辑 -
this queryAPI 处理配置文件中options参数
案例1-创建一个替换源码中字符串的loader
// src/index.js
console.log('hello webpack');
// src/myLoader/replace-loader.js
module.exports = function (source){
return source.replace('webpack', 'kkb')
}
在配置文件中使用自定义loader
module.exports = {
...
// 这个选项是解析除了node_modules中其他文件夹,这样就不用每次手动path.resolve路径了,只需要写名字即可
resolveLoader: {
modules: ["node_modules", "./myLoaders"]
},
module: {
rules: [
{
test: /\.js$/,
use: [
loader: 'replace-loader',
options: { // 这个选项传参给this.query API
name: '开课吧'
}
]
}
]
}
}
指纹策略
webpack中有三种指纹策略,分别是hash、chunkhash、contenthash
-
hash:作用范围最大,内容有更新就会发生变化,一般取6位
[hash:6]注意:整个工程不管单页面入口还是多页面入口,只要有文件内容更新hash就会发生变化
-
chunkhash:作用范围是一个chunks,在多页面入口中常用到,也是一般取6位
[chunkhash:6]注意:同属一个chunks下的css文件生成打包文件也是chunkhash命名,css更新整个chunks下的hash都变化
-
contenthash:自身内容更新,hash才会更新,一般取6位
[contenthash:6]注意:例如index.js中有index.css,css命名[contenthash:6],js命名[chunkhash:6],则css改变,js中hash变化;反之,js改变,css中hash不变
总结:单页面用hash,多页面用chunkhash,资源文件用contenthash
index chunks
index.js chunk [chunkhash:6]
index.css [contenthash:6]
list chunks [hash:6]
list.js chunk
多页面打包方案
两个问题:一是如何动态生成entry;二是如何遍历entry对象,动态实例化htmlwebpackplugin
文件目录如下:
根据格式,每个目录下都有index.js文件就默认为一个入口目录
完整代码:
/**
* 多页面打包方案
* 多入口entry
*
* key chunks
* 循环遍历对象,动态的实例化htmlwebpackplugin
*/
/**
* webpack配置项
*/
const path = require("path");
const htmlwebpackplugin = require("html-webpack-plugin");
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
const minicss = require("mini-css-extract-plugin");
const glob = require("glob");
const setMPA = () => {
const entry = {};
const htmlwebpackplugins = [];
// 页面路径 glob通配符
const entryFiles = glob.sync(path.join(__dirname, "./src/*/index.js"));
// console.log(entryFiles);
entryFiles.forEach((item, index) => {
const entryFile = item;
const match = entryFile.match(/src\/(.*)\/index\.js/);
// console.log(match);
const pageName = match[1];
entry[pageName] = item;
htmlwebpackplugins.push(
new htmlwebpackplugin({
template: path.join(__dirname, `src/${pageName}/index.html`),
filename: `${pageName}.html`,
chunks: [pageName]
})
);
});
//返回entry和htmlwebpackplugins
return {
entry,
htmlwebpackplugins
};
};
const { entry, htmlwebpackplugins } = setMPA();
module.exports = {
entry,
output: {
path: path.resolve(__dirname + "/mpa"),
filename: "[name]-[chunkhash:6].js"
},
mode: "development",
module: {
rules: [
{
test: /\.(png|jpe?g|gif|webp)$/,
use: {
// loader: "file-loader",
loader: "url-loader", // url-loader就是file-loader的变体,内部会自动调用file-loader
options: {
name: "[name].[ext]",
outputPath: "images/", // 图片的输出位置,也就是存放位置
publicPath: "../images/", // 图片的引用位置,css中如何引用图片
// 图片的路径+图片的名称 ../images/ + logo.png
limit: 1024 * 3 // 3KB,小于3kb使用url-loader,否则使用file-loader
}
}
},
{
test: /\.woff2$/,
use: {
loader: "file-loader",
options: {
name: "[name].[ext]",
outputPath: "css/",
publicPath: "./"
}
}
},
{
test: /\.less$/,
use: [minicss.loader, "css-loader", "less-loader"]
}
]
},
plugins: [
new minicss({
filename: "css/[name]-[contenthash:6].css"
}),
new CleanWebpackPlugin(),
...htmlwebpackplugins
]
};
babel处理js模块
babel是Javascript编译器,能够将ES6代码转换成ES5代码,并且可以兼容各个浏览器
babel在执行编译的过程中,首先会从项目的根目录下的.babelrcJSON文件中读取配置,没有会从配置文件中的loader的options选项中读取配置
- 兼容低版本浏览器
- 支持其他技术栈,如Vue,React,TypeScript
- ES6+语法如const、let
- ES6新特性如promise
babel就是个工具,不干活,干活的是preset(即一组预先设定的插件)选项有以下四种:
- @babel/preset-env:把ES6+语法转为ES5
- @babel/prest-flow:处理flow语法
- @babel/preset-react:处理JSX
- @babel/preset-typescript:处理TypeScript
安装
yarn add @babel/core @babel/preset-env babel-loader -D
babel-loader是webpack与babel的通信桥梁,不会做把ES6转成ES5的工作,这部分工作需要用到@babel/preset-env来做
// webpack.config.js
{
test:/\.js$/,
exclude: /node_modules/,
use: {
loader: "babel-loader",
options: {
presets: ["@babel/preset-env"]
}
}
}
通过上面的几步还不够,默认的Babel只支持let、const等一些基础的ES6语法转换,如Promise等新特性转换则需要借助@babel/polyfill,把ES6+的新特性都装过来,来弥补低版本浏览器中缺失的特性
@babel/polyfill
yarn add @babel/polyfill -S // 需要安装到生产环境中
按需加载,减少冗余
在index.js中第一行加入import "@babel/polyfill"会发现打包后的体积大很多,这是因为polyfill默认把所有的特性注入进来
实现按需加载的方式如下:
// webpack.config.js
options: {
presets: [
["@babel/preset-env",
{
targets: { // 新版本浏览器都支持ES6+新特性
edge: "17",
firefox: "60",
chorme: "67"
},
corejs: 2, // 新版本需要指定核心版本库 3x只是比2多包含新特性数量
useBuiltIns: "usage" // 按需引入
}]
]
}
useBuiltIns选项是babel7的新功能,这个选项有三个参数可用:①entry: 需要在 webpack 的⼊⼝⽂件⾥ import "@babel/polyfill" ⼀次。 babel会根据你的使⽤情况导⼊垫⽚,没有使⽤的功能不会被导⼊相应的垫⽚。 ②usage: 不需要 import ,全⾃动检测,但是要安装 @babel/polyfill 。 ③false: 如果你 import "@babel/polyfill" ,它不会排除掉没有使⽤的垫⽚,程序体积会庞⼤。(不推荐)
如何编写一个plugin
webpack在编译代码过程中有生命周期,每个周期对应不同的打包阶段module和assets
plugin本质上是一个class类,实现emit阶段资源列表里生成一个txt文档的插件
class textwebpackplugin {
constructor(options) {}
// 注册钩子
apply(compiler) {
compiler.hooks.emit.tapAsync("textwebpackplugin", (compliation, cb) => {
compliation.assets["new.txt"] = {
source: function () {
// 资源的内容
return "hello 自定义plugins";
},
size: function () {
// 资源的大小
return 1024;
}
};
cb();
});
}
}
module.exports = textwebpackplugin;
webpack的打包流程
- 拿到配置,初始化工作,最终配置
- 实例化一个compile类,注册插件,对应的生命周期绑定相应的事件
- 执行编译,compile.run
- compile(构建阶段)->compilation(bundle资源被加工成什么样子)
- 递归处理所有的依赖模块生成chunk
- 把chunk输出到output指定的位置
打包公共库
如果我们打包的目的是生成一个供别人使用的库,开源的,那么就需要用到output.library来指定公共库的名称
output.libraryTarget指定打包库的规范,通常有var、this、commonjs、umd等等
output.libraryExport指定导出默认如函数或者变量等
module.exports = {
output: {
library: 'myLib', // 支持占位符[name]
libraryTarget: 'umd', // 指定规范
libraryExport: "default" // 指定导出
}
}
性能优化
优化的目的:
- 优化开发体验
- 优化输出质量
影响webpack构建速度的有两个大户:一是loader和plugin方面的构建过程;二是压缩,要优化构建过程,可以从减少查找过程、多线程、提前编译和Cache缓存多个角度来优化
减少查找过程
1、优化loader查找范围
可以从test include exclude三个配置项来缩小loader的处理范围
// 推荐使用include
// string
include: path.resolve(__dirname, "./src"),
// array
include: [
path.resolve(__dirname, 'app/styles'),
path.resolve(__dirname, 'vendor/styles')
]
注意:exclude优先级要高于include和test
2、优化resolve.modules配置
用于配置webpack去哪些目录下寻找第三方模块,默认是['node_modules'],在当前目录下的node_modules里去找,没有往上级的node_modules里找,递归处理
如果我们第三方模块都安装在项目根目录下,可以指定这个路径
module.exports = {
resolve: {
modules: [path.resolve(__dirname, "./node_modules")]
}
}
3、优化resolve.alias配置
通过别名来将原导入路径映射成一个新的导入路径,直接指定文件,避免耗时
module.exports = {
resolve: {
alias: {
"@": path.join(__dirname, "./src")
}
}
}
多线程
由于运行在Node.js之上的Webpack是单线程模型的,所以Webpack需要处理的事情要一件一件做,不能多件事一起做,我们需要Webpack能同一时间处理多个任务,发挥多核CPU电脑的威力
thread-loader是针对loader进行优化的,会将loader放置在一个worker池里进行,以达到多线程构建
module.exports = {
module: {
rules: [
{
test: /\.js$/,
include: path.resolve('src'),
use: [
'thread-loader',
// 你的高开销的loader放置在此如babel-loader
]
}
]
}
}
Cache缓存
提升构建速度的另一个大杀器就是使用缓存
Webpack中打包的核心是Javascript文件的打包,使用的babel-loader,其实很多时候打包时间长都是babel-loader执行慢导致的,其会产生一些运行期间重复的公共文件,造成代码体积的冗余,同时也会减慢编译的速度
babel-loader提供cacheDirectory配置给Babel编译设定给定的缓存目录
rules: [
{
test: /\.js$/,
loader: 'babel-loader',
options: {
cacheDirectory: true
}
}
]
压缩速度的优化
相当于构建过程而言,压缩只有生产环境打包时才会做,通过terser-webpack-plugin开启多线程和缓存
const TerserPlugin = require('terser-webpack-plugin');
module.exports = {
optimization: {
minimizer: [
new TerserPlugin({
cache: true, // 开启缓存
parallel: true // 多线程
})
]
}
}
压缩体积的优化
主要分为以下几个方面:
1、css压缩,使用optimize-css-assets-webpack-plugin和cssnano
在Webpack中,css-loader已经集成了cssnano,我们还可以使用optimize-css-assets-webpack-plugin来自定义cssnano的规则
const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin");
new OptimizeCSSAssetsPlugin({
cssProcessor: require("cssnano"), // 这⾥制定了引擎,不指定默认也是 cssnano
cssProcessorOptions: {
discardComments: { removeAll: true }
}
})
2、html压缩
使用html-webpack-plugin
new htmlwebpackplugin({
title: "Webpack React",
template: "./src/index.html",
minify: {
removeComments: true, // 移除HTML中的注释
collapseWhitespace: true, // 删除空白符与换行符
minifyCSS: true // 压缩内联css
}
})
3、js压缩
在mode=production下,Webpack会自动压缩代码,也可以使用terser-webpack-plugin自定义压缩
tree-shaking:擦除无用的js和css代码
tree shaking的原理是利用ES模块化引入静态分析流程,在编译时就正确知道加载了哪些模块,从而判断哪些模块未被加载或变量未被引用,再进行删除即可
摇树,清除无用的CSS和JS(Dead Code)
-
代码不会被执行,不可到达
-
代码执行的结果不会被用到
-
代码只会影响死变量(只写不读)
-
JS只支持import和export方式!!!
清除无用的css方式:需要安装glob-all purify-css purifycss-webpack 清除无用的js方式:开启optimization.usedExports为true
code-splitting:代码分割
optimization: {
splitChunks: {
chunks: 'all' // 所有的chunks代码公共部分分离出来称为一个单独的文件
}
}
HardSourceWebpackPlugin 使用HardSourceWebpackPlugin缩短构建时间,其是一个非常强大的插件,可以大大的缩短构建时间,webpack5已经内置插件
const HardSourceWebpackPlugin = require('hard-source-webpack-plugin')
module.exports = {
plugins: [new HardSourceWebpackPlugin()]
}