1、我的webpack学习之路(webpack初步认知和webpack基础打包)
2、我的webpack学习之路(webpack基础开发配置)
3、我的webpack学习之路(webpack配置详解)
4、关于我的学习笔记
主要分为两个方面来处理:
开发环境性能优化:
生产环境性能优化:
在以前学习了webpack-dev-server,用它来实现代码修改热更新,但是有一个问题是,当我们只修改了其中一个模块时(某个css\某个js),就会将所有的模块全部重新构建,影响构建速度,所以要用到HMR
HMR: hot module replacement 热模块替换/模块热替换
作用:一个模块发生变化,只会重新打包这一个模块(而不是打包所有模块)极大提升构建速度
实现HMR其实很简单,只需要在devServer中添加hot:true即可
// webpack.config.js
devServer: {
...
hot: true, // 开启HMR功能
}
问题:
1、修改样式文件时,HMR正常使用,只重新构建了修改过的css文件,因为style-loader内部实现了HMR功能~
2、js文件:发现js文件默认不适用HMR功能
解决方法:需要修改js代码,添加支持HMR功能的代码
import print from './print.js'
// 会全局寻找module这个对象,查看hot热更新(HMR)是否启用 ---> 让HMR功能代码生效
if(module.hot) {
module.hot.accept('./print.js', function{
// 方法会监听print.js文件的变化,一旦发生变化,其他模块不会打包构建,会执行后面的回调函数
print()
})
}
注意:HMR功能对js处理,只能处理非入口js文件的其他文件(入口文件改变,则必然全都重新构建)
3、HTML文件:也是默认不支持HMR功能的,但会导致htmlwe文件不能热更新了~~
解决办法:修改entry入口,将html文件引入
entry: ['./src/js/index.js', './src/index.html'],
思考:html文件需不需要HMR呢?
html文件对应来讲只有一个html文件,在看下HMR的作用,一个模块发生变化,只会重新打包这一个模块。不像js文件有很多个模块,其中一个变其他不变。html文件变化,只能变化着一个文件,所以没必要使用HMR功能
source-map: 一种提供源代码到构建后代码映射技术(如果构建后代码出错,通过映射可以追踪源代码错误)
配置方法是在webpack.config.js中增加一个配置devtool:'source-map’
devtool: 'source-map'
如上配置,就配置好了一个调试优化,执行webpack命令后,会发现和build.js同一目录下多了一个map 文件,这个就是所谓的source-map文件,提供源代码到构建后代码映射关系
devtool:有几个参数: [ inline- |hidden -| eval- ] [ nosources- ] [ cheap - [ module - ] ] source-map
参数 | map文件生产方式 | 介绍 |
---|---|---|
source-map | 外部 | 错误代码的准确信息 和 源代码的错误位置 |
inline-source-map | 内联 | 只生成一个内联source-map;错误代码的准确信息 和 源代码的错误位置 |
hidden-source-map | 外部 | 错误代码的错误原因,但是没有错误位置; 不能追踪到源代码错误,只能提示到构建后代码的错误位置 |
eval-source-map | 内联 | 每个文件狗生成对应的source-map,都在eval;错误代码的准确信息 和 源代码的错误位置 |
nosources-source-map | 外部 | 错误代码的准确信息,但是没有任何源代码信息 |
cheap-source-map | 外部 | 错误代码的准确信息 和 源代码的错误位置;只能精确到行 |
cheap-module-source-map | 外部 | 错误代码的准确信息 和 源代码的错误位置;module会将loader的source-map加入 |
内联:sorce-map文件是和build.js合二为一
外部:在build.js同级生产map文件
内联 和 外部的区别: 1、外部生成了文件,内联没有。2、内联构建速度更快
开发环境:速度快一点,调试更友好
速度快(eval> inline>cheap>...)
eval-cheap-souce-map
eval-source -map
调试友好
source-map
cheap-moudle-sourec-map
cheap-sourec-map
开发环境最好的选择:---> eval-source-map / eval-source-cheap-moudle-map
生产环境:源代码要不要隐藏?调试要不要更友好
内联会让代码体积非常大,所以生产环境不用内联
nosources-source-map 全部隐藏
hidden-source-map 只隐藏源代码,会提示构建后代码错误
生产环境最好的选择:---> source-map / cheap-moudle-sourec-map
首先配置是很简单,如下
module: {
rules: [
{
oneOf: [
// 对应loader配置
]
}
]
}
为什么使用oneOf?
在我原先写loader的时候,在rules中有非常多的loader规则,一个文件要被所有的loader都过一遍,有些loader处理不了,有些loader则会被命中,这样就不太好。配置oneOf能让loader处理性能更好(oneOf主要是提升构建速度,是文件不会被多个loader反复都过一遍)
oneOf的意思:以下loader只会匹配一个
注意:不能有两项配置,处理同一类型的文件,如下,js类型文件有两种配置,只会生效一个loader配置。
解决办法:将eslint-loader提取出去,如下。这样rules内的两个loader都会执行,配置了enforce: ‘pre’,也会优先执行
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/, // 排除node_modules,
loader: 'eslint-loader',
// 设置优先执行
enforce: 'pre',
options: {
fix: true
}
},
{
oneOf: [
//{
// test: /\.js$/,
// exclude: /node_modules/, // 排除node_modules,
// loader: 'eslint-loader',
// 设置优先执行
// enforce: 'pre',
// options: {
// fix: true
//}
//},
// js兼容处理..
{
test: /\.js$/,
exclude: /node_modules/,
loader: 'babel-loader',
options: {
presets: [
[
'@babel/preset-env',
{
// 按需加载
useBuiltIns: 'usage',
corejs: {
version: 3},
targets: {
chrome: '60'
}
}
]
]
}
}
]
}
]
}
从两点出发来设置缓存:1、babel缓存;2、整体资源缓存
1、babel缓存是什么意思呢?
我们写代码的时候,永远是js代码是最多的,结构和样式没有什么办法做更好的处理,为什么对babel处理,因为babel对js进行编译处理,编译成浏览器能识别的语言(js兼容性处理)。假设100个js模块,只改了1个js文件,不可能将全部文件重新编译处理,应该是不变的,类似前面的HMR功能,一个某块变,其他模块不变。生产环境先不能是使用webpack-dev-server所以不能使用HMR功能。配置如下,只需要在babel-loader中options添加一个熟悉
{
test: /\.js$/,
exclude: /node_modules/,
loader: 'babel-loader',
options: {
// 开启babel缓存
// 第二次构建时,才会读取缓存
cacheDirectory: true,
presets: [
[
'@babel/preset-env',
{
// 按需加载
useBuiltIns: 'usage',
corejs: {
version: 3},
targets: {
chrome: '60'
}
}
]
]
}
}
2、文件资源缓存
怎么查看缓存呢?首先先写一个简单的服务器代码server.js
// server,js
/*
服务器代码
通过node server.js运行
访问服务器地址
http://loacalhost:3000
*/
const express = require('express')
const app = express()
app.use(express.static('build', {
maxAge: 1000 * 3600}))
app.listen(3000)
当我修改了代码是,访问的还是上一次缓存的内容,修改的代码没有生效
可以通过文件名添加版本号(hash值)的方式来解决这个问题(如果资源名称没有变就走缓存,如果变了就会重新请求资源文件)
hash:每次webpack构建时会生成一个唯一的hash值
...
module.exports = {
entry: './src/js/index.js',
output: {
filename: 'js/build.[hash:10].js',
path: resolve('build')
},
module: {
...
},
plugins: [
new MiniCssExtractPlugin({
filename: 'css/build.[hash:10].css'
}),
...
],
mode: 'production'
}
如上配置,将输出的文件名添加10位的hash值,但是这样会有一个问题:因为css文件和js文件是共享的webpack打包生成的hash值,一旦发生变化css文件和js文件是一起变的,修改一个都会变,缓存就失效了。webpack又引入了另一个hash值chunkhash
chunkhash:根据chunk生成的hash值,如果打包来源同一个chunk,那么hash值就一样
...
module.exports = {
entry: './src/js/index.js',
output: {
filename: 'js/build.[chunkhash:10].js',
path: resolve('build')
},
module: {
...
},
plugins: [
new MiniCssExtractPlugin({
filename: 'css/build.[chunkhash:10].css'
}),
...
],
mode: 'production'
}
这里打包会发现js和css的hash值还是一样的,因为css是在js中被引进来的,所以同属于一个chunk。因为这些都被引入到同一个入口文件,所有根据入口文件引入的都会生成一个chunk。
所以最后的解决方法就是通过contenthash来解决
contenthash: 根据文件的内容来生成hash值。不同的hash值一定不一样
...
module.exports = {
entry: './src/js/index.js',
output: {
filename: 'js/build.[contenthash:10].js',
path: resolve('build')
},
module: {
...
},
plugins: [
new MiniCssExtractPlugin({
filename: 'css/build.[contenthash:10].css'
}),
...
],
mode: 'production'
}
这样只需要控制hash值的变化来控制资源更新
babel缓存:让第二次打包构建速度更快
文件资源缓存:让代码上线运行缓存更好用
tree shaking: 去除无用代码
使用前提:
1、必须使用es6模块化
// test.js
export function a(x, y) {
return x * y;
}
export function b(x, y) {
return x - y;
}
// index.js
import {
a } from './test'
console.log(a(2,3))
2、开启production环境
mode: 'production'
作用:减少代码体积
满足以上条件后重新构建打包,在打包过程中会自动启动tree shaking。打包完成后查看build.js会发现test.js内b方法的相关内容没有了
使用的时候还要注意一个小问题,在不同版本tree shaking会有差异,会无意之间将css文件当做为经引用的代码干掉
模拟测试,在package.json中添加**“sideEffects”: false**
“sideEffects”: false : 所有代码都是没有副作用的代码(都可以进行tree shaking)
再构建一次,会发现没有css资源,这样写会把css资源干掉
解决办法: “sideEffects”: ["*.css"]
/*
tree shaking: 去除无用代码
前提:1、必须使用es6模块化。2、开启production环境
作用:减少代码体积
在package.json中配置
"sideEffects": false 所有代码都没有副作用(都可以进行 tree shaking)
问题:可能会吧css / @babel/polyfill (副作用)文件干掉
"sideEffects": ["*.css","*.less"],这里标记不被tree shaking的文件
*/
code spli:将打包输出的一个文件(chunk)分割成多个文件
webpack代码分割大致分为三种方式:1、定义多入口文件。2、使用optimization配置插件。3、通过js
代码让某个文件被单独打包成一个chunk
1、定义多入口文件
entry以往的使用是写一个入口文件地址
entry: './src/index.js'
如上就是配置单入口,倘若在index.js文件中引入其他文件,那么最终打包只会生产一个文件
// index.js
import test from './test'
console.log('hello index')
通过entry配置多入口文件
entry: {
main: './src/index.js',
test: './src/test.js'
}
如上配置多入口文件,在运行打包时,会看到打包出了两个文件,同时也不需要在入口文件中引入test.js文件
打包后的两个文件都是build.hash.js比较难区分,可以同配置output来设置文件名
output: {
// 设置[name],取文件名
filename: 'js/[name].[contenthash:10].js',
path: resolve('build')
}
2、使用optimization配置插件
作用:可以将node_modules中的代码单独打包成一个chunk输出;自动分析多入口chunk中,有没有公共的文件,如果有会打包成单独一个chunk,不会重复打包
plugin: [...],
optimization: {
splitChunks:{
chunks: 'all'
}
}
3、通过写js代码的方式打包
如果entry只设置了一个入口,在单入口文件中通写入js代码(import)的方式引导打包
//index.js
import('./test') // 这里返回的是promise对象
.then((res) => {
// res为test导出对象结果
console.log('导入成功')
})
.catch(() => {
console.log('导入失败')
})
如上配置,运行打包后,也会各自生产两个打包文件,还要一个命名问题,test打包生成的文件名是根据打包顺序命名的可能为123.hash.js。可以通过import内添加注释,设置文件名
import(/* webpackChunkName: 'test' */'./test')
如上配置运行打包后,打包文件就会是名为test的文件
懒加载和预加载通过js代码实现,与webpack配置无关,与代码分割(code split)有一定的相似之处
正常情况下
// index.js
console.log('index.js文件被加载')
import {
a} from './test.js'
console.log(a)
export function a(x, y) {
return x * y;
}
console.log('test.js被加载')
懒加载: 当文件需要使用时才加载
<button id="btn"> 点击 button>
//index.js
console.log('index.js文件被加载')
document.getElementById('btn').onclick = function() {
import(/* webpackChunkName: 'test' */'./test').then(({
a}) => {
console.log(a(2,3))
})
}
在运行打包后,可以看到被index,test被打包成两个chunk,类似代码分割,只有在点击事件生效时,才会加载test文件,重复点击时,也只会加载一次,方法同样事项
预加载:会在使用之前,提前加载js文件
加上一个配置webpackPrefetch
//index.js
console.log('index.js文件被加载')
document.getElementById('btn').onclick = function() {
import(/* webpackChunkName: 'test',webpackPrefetch: true */'./test').then(({
a}) => {
console.log(a(2,3))
})
}
打包后打开html文件,会发现点击事件没有生效,但是已经加载了,点击事件生效后,从缓存中拿test文件使用,不会重新加载
正常加载可以认为是并行加载(同一时间加载多个文件)
预加载:prefetch: 等其他资源加载完毕,浏览器空闲了,在偷偷加载。(预加载存在很大兼容问题,最好只在高版本或者移动端使用)
PWA: 渐进式网络开发应用(离线可访问)使用serviceworker实现
webpack实现PWA需要借助一个插件workbox-webpack-plugin
npm安装后在webpack中配置
const WorkboxWebpackPlugin = require('workbox-webpack-plugin')
...
Plugins: [
...
new WorkboxWebpackPlugin.GenerateSW({
/*
1.帮助serviceworker快速启动
2.删除旧的serviceworker
生成一个serviceworker 配置文件 ~
*/
clientsClaim: true,
skipWaiting: true
})
]
webpack文件配置好后,配置入口文件注册serviceWorker
//index.js
// 应为serviceworker有兼容性问题,以下是简单兼容性处理,是否支持serviceworker
if('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/service-worker.js')
.then(() => {
console.log('sw注册成功')
})
.catch(() => {
console.log('sw注册失败')
})
})
}
注意:eslint 不认识window、navigator 全局变量
解决: 需要修改package.json 中eslintConfig配置
“env”: {
“browser”: true // 支持浏览器端全局变量
}
sw代码必须运行在服务器上:两种运行服务方法
—> nodejs
—>
npm i server -g
serve -s build 启动一个服务器,将build目录下所有资源作为静态资源暴露出去
如上就完成了pwa的简单配置
js主线程是单线程的,它同一时间只能干一件事,事情比较多,就要排队等前一个任务结束,在继续;所以通过多进程来优化打包速度
首先需要下载一个loader thread-loader
thread-loader放在某一个loader的后面,就会对其开启多进程打包
thread-loader一般是给babel-loader使用,使用方式如下
//webpack.config.js
{
test: /\.js$/,
exclude: /node_modules/,
use: [
// 开启多进程打包(babel工作的时候就会开启多进程)
/*
开启多进程打包是有利有弊的(合理使用)
进程启动大概600ms,进程通信也有开销(时间)
只有工作消耗时间比较长,才需要多进程
一般来说js代码比较多,消耗时间比较长
启动进程数(cpu核数-1)
*/
//'thread-loader',
//如下可做调整
{
loader: 'thread-loader',
options: {
workers: 2 // 进程2个
}
},
{
loader: 'babel-loader',
options: {
presets: [
[
'@babel/preset-env',
{
// 按需加载
useBuiltIns: 'usage',
corejs: {
version: 3},
targets: {
chrome: '60' }
}
]
],
// 开启babel缓存
// 第二次构建时,会读取之前的缓存
cacheDirectory: true
}
]
}
}
作用:防止将某一些包打包到我们最终输出的bundle中
假设通过cdn链接引入jQuery,可以通过externals禁止,不会被打包了
//webpack.config.js
mode: 'production',
externals: {
// 拒绝jqeruy包被打包进来
// 忽略库名: 'npm包名'
jquery: 'jQuery'
}
类似externals,会指示webpack那些库是不参与打包的,不同的是dll会单独对某些库进行单独打包,将多个库打包成一个chunk
node_modules内的某些库比较大,正常打包的话会被打包成一个文件,这样文件体积增大。通过dll将这些库单独拆开,打包成不同的chunk,更有利于性能优化
dll打包后能,webpack运行打包后不会重复打包第三方依赖库,提高效率
首先定义一个webpack.dll.js
//webpack.dll.js
/*
使用dll技术,对某些库(第三方库:jq,react,vue)进行单独打包
当你运行 webpack 的时候,默认查找 webpack.config.js 配置文件
需求: 运行 webpack.dll.js 文件
--> webpack --config webpack.dll.js
*/
const path = require('path')
const webpack = require('webpack')
function resolve(dir) {
return path.join(__dirname,dir)
}
module.exports = {
entry: {
// 最终打包生成 [name] ---> jquery
// ['jquery',...] ---> 要打包的库是jquery
jquery: ['jquery']
},
output: {
filename: '[name].js',
path: resolve('dll'),
library: '[name]_[hash]' // 打包的库里面向外暴露出去的内容叫什么名字
},
plugins:[
// 打包生成 manifest.json ----> 提供和jquery的映射关系(通过这个映射关系知道jquery不需要打包)
new webpack.DllPlugin({
name: '[name]_[hash]', // 映射库的暴露的内容名称
path: resolve('dll/manifest.json') // 输出的名称
})
],
mode: 'production'
}
其次配置webpack.config.js,告诉它那些包不参与打包
// webpack.config.js
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const webpack = require('webpack')
const AddAssetHtmlWebpackPlugin = require('add-asset-html-webpack-plugin')
function resolve(dir) {
return path.join(__dirname,dir)
}
....
plugins: [
// 处理html结构
new HtmlWebpackPlugin({
template: './src/index.html'
}),
// 告诉webpack那些库不参与打包,同时使用时的名称也要变
new webpack.DllReferencePlugin({
manifest: resolve('dll/manifest.json')
}),
// 将某个文件打包输出出去,并在html中自动引入该资源
new AddAssetHtmlWebpackPlugin({
filepath: resolve('dll/jquery.js')
})
],
"devDependencies": {
"@babel/core": "^7.12.3",
"@babel/polyfill": "^7.12.1",
"@babel/preset-env": "^7.12.1",
"add-asset-html-webpack-plugin": "^3.1.3",
"babel-loader": "^8.1.0",
"core-js": "^3.6.5",
"css-loader": "^5.0.0",
"eslint": "^7.12.1",
"eslint-config-airbnb-base": "^14.2.0",
"eslint-loader": "^4.0.2",
"eslint-plugin-import": "^2.22.1",
"file-loader": "^6.2.0",
"html-loader": "^1.3.2",
"html-webpack-plugin": "^4.5.0",
"jquery": "^3.5.1",
"less": "^3.12.2",
"less-loader": "^7.0.2",
"mini-css-extract-plugin": "^1.2.1",
"optimize-css-assets-webpack-plugin": "^5.0.4",
"postcss-loader": "^4.0.4",
"postcss-preset-env": "^6.7.0",
"style-loader": "^2.0.0",
"url-loader": "^4.1.1",
"webpack": "^4.44.2",
"webpack-cli": "^3.3.12",
"webpack-dev-server": "^3.11.0",
"workbox-webpack-plugin": "^5.1.4"
},