webpack 4.x 为例
安装
yarn add webpack webpack-cli -D
配置项
- 真正应用项目的webpack往往需要根据自己的业务来进行修改默认的配置,而不是一股脑的使用webpack默认的配置。这个时候就需要我们去了解webpack常用的配置项含义.
- 配置项所在的文件是webpack的配置文件,其运行在node的环境中,所以需要使用CommonJS规范来进行配置文件的编写。
mode
指定webpack的工作模式,生产环境还是开发环境还是最原始的环境。值为development 或者 production 或者none
const path = require('path');
module.exports = {
mode: 'development', //指定webpack的工作模式
entry: path.join(__dirname, 'src/index.js'),
output: {
filename: 'bundle.[hash].js',
path: path.join(__dirname, 'output')
}
}
entry
指定webpack打包的入口js文件。可以使用node提供的path模块来进行入口文件的路径指定,也可直接用相对路径的方式来进行入口文件路径的指定(注意采用相对路径的方式时,要将完整的路径写全,尤其是./不能缺少)
module.exports = {
entry: './src/main.js', // 相对路径方式
entry: path.join(__dirname, 'src/index.js'), // path模块指定路径方式
}
output
指定webpack打包完成之后,输出文件的路径位置以及输出文件名称。默认输出路径是根目录下的dist文件夹。可采用path模块路径的方式
const path = require('path');
module.exports = {
// entry: './src/index.js',
entry: path.join(__dirname, 'src/index.js'),
output: {
filename:'bundle.[hash].js', // 输出文件的名称
path:path.join(__dirname,'output'), // 输出文件的路径
publicPath: "",// 默认是空字符串,意思是网站的根目录
}
}
module
- 意思就是遇到的模块化代码都会走这个配置项进行处理。
- 值是一个对象,第一层键值对是rules,rules是规则,意思就是针对其他资源加载的规则配置,一个项目会有多种类型的文件,所以会有多个规则。所以rules是一个数组。
- 数组中的元素都是对象类型,对象类型必须设置两个属性,一个是test,一个是use。
- test值是一个正则表达式用于去匹配webpack打包过程中遇到的资源文件的路径。
- use属性值是指定要处理test匹配的类型文件专用的loader(加载器)
- use的值可以是一个数组,当处理同一类型的文件需要多个loader的时候,它就是一个数组。loader数组的执行顺序是从后往前的执行。
const path = require('path');
module.exports = {
mode: 'none', //指定webpack的工作模式
entry: path.join(__dirname, 'src/index.js'),
output: {
filename: 'bundle.[hash].js',
},
module: {
// 针对其他资源加载规则的配置,所以是一个数组
rules: [
{
test: /\.css$/, // 匹配到所有的css文件
use: 'css-loader' // 采用css-loader进行文件处理
}
]
}
}
loader
- 加载器也叫转换器,用于处理源代码中任何类型的资源。
- webpack的内部默认loader只能处理js文件,如果想处理js以外的类型文件,例如:css文件、图片文件等。就需要特定的loader来对文件进行转换。
- loader工作过程是接收原始资源数据作为参数经过加载器loader的处理,最终输出能够让webpack打包的JavaScript代码。
- loader分类:1、编译转换类的loader(将模块资源转换成js模块代码)2、文件操作类型的loader;(将源静态资源文件经过压缩后拷贝,然后导出静态资源文件的访问路径);3、代码检查类型的loader(检查代码的风格)
css-loader
style-loader
将css-loader转换得到的js模块代码,再次转换其结果最终以style标签的形式将样式挂载在html文件中的head标签内
file-loader
url-loader
- 将静态文件转换成代码的形式(Date URLs),没有物理文件的输出,就减少一次http请求。
- 小文件使用Data URLs的形式,减少http的请求次数
- 大文件单独提取存放,提高打包之后的js代码的加载速度
- url-loader在输出独立的大文件的时候,需要借助file-loader去输出
babel-loader
用于转换es6+的特性,同时需要babel的核心模块@babel/core与转换具体特性的集合插件@babel/preset-env
自己开发loader
- loader 其实就是一个函数,以原始资源为参数,以转换的资源结果为输出。所以loader的本质就是对原始资源的处理过程
- 简单处理md文件的loader处理
const marked = require('marked');
// source 就是原始资源
module.exports = (source) => {
// 对source的原始资源进行处理
const html = marked(source);
// 将处理的结果以js代码的形式导出
return `module.exports = ${JSON.stringify(html)}`
}
plugins
插件是webpack的另一个核心特性,其作用是用于增强webpack的自动化能力。是实现前端工程化的重要组成部分
例如:
- 依靠插件清除打包产生的dist目录
- 依靠插件直接将public文件夹拷贝至输出目录
- 依靠插件对loader转换之后的代码进行压缩,去除代码中的注释,debugger等等
clean-webpack-plugin
用于自动清除输出目录的插件
html-webpack-plugin
自动生成打包出来的js所用到的html插件,如果需要同时输出多个页面文件,那么就配置多个html-webpack-plugin的实例即可。
copy-webpack-plugin
- 将public文件夹的文件完全复制到输出目录中
- 该插件最好是只在生产模式下启用,目的是为了提高开发模式下的编译速度
{
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
template: './public/index.html',
publicPath: './', // html中引入的文件路径
}),
new CopyWebpackPlugin([
'public'
])
],
}
mini-css-extract-plugin
提取css到单个文件中,实现css的按需加载。其中使用MiniCssExtractPlugin.loader取代style-loader
optimize-css-assets-webpack-plugin
压缩提取出来的css文件,webpack内置的压缩插件(terser-webpack-plugin)仅仅只对js文件进行压缩。
- 注意:webpack推荐将所有压缩类的插件全部放在optimization配置项中的minimizer进行管理。直接放在plugins里面也是可以的
optimization:{
minimizer:[
new OptimizeCssAssetsWebpackPlugin(),
new TerserWebpackPlugin()
]
}
自己开发webpack插件
- 本质:在生命周期的钩子函数中挂载函数来实现扩展
- plugin必须是一个函数或者一个包含apply方法的对象。
- 以下是一个清除注释的自定义插件
class MyPlugin {
// webpack 启东时会自动调用apply方法,compiler参数会暴露生命周期钩子函数
apply(compiler) {
console.log('自定义插件启动了')
// 通过tap方法注册钩子函数
compiler.hooks.emit.tap('MyPlugin', compilation => {
// compilation 看成webpack打包的上下文
for (let name in compilation.assets) {
if (name.endsWith('.js')) {
// 拿到name对应的值
const content = compilation.assets[name].source();
const withoutAnnoation = content.replace(/\/\*\*+\*\*\//g, '')
compilation.assets[name] = {
source: () => withoutAnnoation,
size: () => withoutAnnoation.length
}
}
}
})
}
}
devServer
通过devServer参数来增加webpack的开发体验
自动编译
在webpack命令行启动的时候添加–watch参数 用于监听文件的变动,一旦文件发生变动,则会自动重新进行编译
yarn webpack --watch
webpack dev server
根据名字即可得知它提供了一个临时web服务的功能。同时集成了自动编译的功能
- 安装
yarn add webpack-dev-server -D
注意在安装的过程中可能会出现webpack webpack-cli 版本太高导致错误。根据错误网上搜索解决方案即可。
-
webpack-dev-server 为了提高效率输出内容并没有写入磁盘,而是写入到内存当中。
-
通过dev-server提供的代理API参数(proxy)可解决开发模式下的跨域问题
{
proxy: {
// 请求以/api开头的url,都会代理到配置的target接口中
// http://localhost:8000/api/users --→ https://api.github.com/api/users
'/api': {
target: 'https://api.github.com',
// 对请求的url进行重写
// http://localhost:8000/api/users --→ https://api.github.com/users
pathRewrite:{
'^/api':''
},
changeOrigin:true, // 不能使用localhost:8000作为主机名,所以要设置成true
}
}
}
HMR
- 产生背景:当webpack监听到文件中的代码发生变动的时候,webpack会重新打包编译,此时会造成浏览器的重新刷新,界面之前保存的任何状态会全部丢失。这种开发体验非常不好
- 作用:解决自动刷新导致的页面状态丢失
- 配置开启HMR的方式
- devServer中的hot设为true
- 引入webpack身上的HotModuleReplacementPlugin插件,放在plugins中使用
遇到的问题:只是这样配置的话可以实现更改css浏览器保留状态热更新的效果,但是更改js或者图片等模块的时候,浏览器仍然是会重新刷新,丢失状态。这并不是HMR不行,而是因为webpack需要我们自己手动处理模块热替换的逻辑。之所以CSS可以直接实现模块热替换的效果是因为在style-loader源代码里面已经进行了对样式的热替换处理
- HMR APIs
在入口文件利用HMR的api来对不同模块进行热替换处理。
- module.hot.accept ----- 注册某一个模块更新之后的处理函数
// accept接收两个参数,第一个参数是要进行热更新的js文件(填写完整的相对路径),第二个参数是目标js文件更新之后的处理函数。
module.hot.accept('./head',()=>{
console.log('head 模块更新了 要对其进行热替换处理')
})
问题:如果不用框架,使用原生js的话,那么HMR的使用将会变得异常麻烦,因为你需要根据不同的逻辑去编写不同的热更新处理代码,毫无疑问它会额外的增加开发成本。
- react框架 额外的处理方案
- webpack 5 已经实现了0配置,模块热替换。
- webpack 4 可以通过react-refresh插件来实现
完整dev-server 案例
devServer: {
contentBase: ['./public'], //静态资源的访问,其作用与copy-webpack-plugin作用一样,使用了contentBase以后开发模式下就可以不用CopyWebpackPlugin插件了,提高编译速度
open: true, // 自动打开浏览器
port: 8000, // 指定端口号
// hot: true, // 开启HMR,当代码出现错误的时候,会重新刷新浏览器,导致看不到代码错误信息
hotOnly:true, // // 开启HMR,当代码出现错误的时候,不会重新刷新浏览器,控制台可以查看代码错误信息
proxy: {
// 请求以/api开头的url,都会代理到配置的target接口中
// http://localhost:8000/api/users --→ https://api.github.com/api/users
'/api': {
target: 'https://api.github.com',
// 对请求的url进行重写
// http://localhost:8000/api/users --→ https://api.github.com/users
pathRewrite: {
'^/api': ''
},
changeOrigin: true, // 不能使用localhost:8000作为主机名,所以要设置成true
}
}
}
devtool
Source Map的介绍
- 中文译为源代码地图,映射了源代码与编译转换之后的代码之间的关系
- 用于解决源代码与生产环境下代码不一致,导致生产环境下代码出现错误不能准确定位的问题
配置source map
{
devtool:'source-map',
}
devtool的值
devtool的值决定了以哪种方式进行源代码的映射,每种方式的效率不同。映射效果越好,效率越低。
表格中build意思是初次构件的速度,rebuild意思是在watch模式下的重新构建速度,production意思是是否适合生产模式,quality意思是生成的source的质量。
- 生产模式下推荐 none
source map会暴露源代码,安全隐患较大 - 开发环境下推荐 cheap-module-eval-source-map
由于在使用react和vue的框架下,正常都会使用代码风格工具,所以错误只需要定位到行即可
webpack 优化
默认情况下 production模式下的打包是有一些默认的优化配置的,同时我们还可以自行去配置优化。
webpack DefinePlugin
为代码注入全局常量,这样在代码中可以不用引入而直接使用注入的全局常量。
new webpack.DefinePlugin({
API_URL: JSON.stringify("http://api.example.com")
})
Tree-shaking
- 从中式翻译上来看就是摇树的意思。生活中摇树会将树上的残枝落叶(残枝落叶已经从树身上脱离,有没有这些树都可活下来)摇下来。同样的,对于代码来说,一个项目中肯定有未引用的代码,通过Tree-shaking将这些未引用的代码去除掉,减少了打包之后的代码体积,为代码进行瘦身。
- Tree-shaking并不是配置文件中的某一个配置项,它是一组功能搭配使用后的优化效果
- 在production模式下会自动开启这个功能
- 其他模式手动启用tree-shaking的话,需要借助webpack中的optimization这个配置项
optimization: {
usedExports: true, // 标记代码中未被使用的代码
minimize: true, // 压缩并清除被被使用的代码
concatenateModules:true // 合并多个模块至一个函数中
},
以摇树为比喻:开启usedExports用来标记树上的残枝落叶,开启minimize会把残枝落叶从树上摇下来。
- concatenateModules属性
正常打包的过程中会将一个模块打包成一个函数,如果代码中有多个模块就会打包出多个函数。开启concatenateModules意思就是尽可能的将所有模块合并输出到一个函数中。从而来减少代码的体积,提高了代码的运行效率。
tree-shaking & babel-loader
两者同时使用的时候会造成tree-shaking失效。
- 原因:tree-shaking的前提就是ES Module,而babel-loader在转译ES的新特性的时候,可能会将ES Module 转换成CommonJS。例如使用babel-loader的时候还会使用@babel/preset-env插件,该插件就会将ES Module 转换成 CommonJS 。无需担心,该问题已经在新版的babel-loader已经修复掉。
- 对于旧版本的@babel/preset-env插件我们可以通过配置参数来禁止ES Module 转换成 CommonJS
{
presets: [
[require('@babel/preset-env'), {modules: false}] // modules设为false,意思是禁止ES Module转换,保证tree-shaking生效
]
}
webpack sideEffects
-
作用:标识代码是否有副作用 为代码提升可压缩的空间。
-
副作用:指的是模块执行时除了导出成员之外还做的其他事情
-
应用场景:一般在开发npm包的时候会用到该属性
-
在production模式下sideEffects会自动开启。
-
使用:在package.json文件中sideEffects属性设为false,意思是项目整体代码没有副作用,那么webpack在打包的时候会将没有用到的代码全部移除掉。如果sideEffects属性值是一个数组,那么数组中的内容就是副作用代码,webpack在打包的时候不会将数组中的文件删掉。
-
副作用代码案例:
案例一:
案例二:
在react项目中,jsx文件中通过import引入的css代码,全都属于副作用代码 -
注意事项
在optimization里面的sideEffects属性指的是webpack开启了sideEffects功能,而package.json文件中的sideEffects字段意思是标识整体项目是没有副作用的。
Code Splitting
- 产生背景:webpack在打包的时候会把所有代码打包在一起,造成bundle体积非常大。而应用在启动的时候,并不是所有的模块都要加载才能启动。这样会造成首次加载时间过长。这个时候按需加载应运而生。
- 实现方式:多入口打包 & 动态导入
多入口打包
- 一个页面对应一个入口文件,提取多个页面公共部分。此方式适合多页面应用打包
- 具体实现
- webpack的entry参数值是一个对象,该对象中一个属性就是一个入口,属性的key就是入口名称,属性值是入口文件的路径。
- 有多个入口文件就有多个输出文件。那么此时还要配置output属性值采用占位符的形式。
- 多入口文件肯定也会有多个html文件,此时plugins配置项中的HtmlWebpackPlugin也要配置多个
- 多入口文件有时会同时引用相同的文件,那么相同的文件就是公共模块,如果不提取,就会被打包两次。需要将公共部分单独打包到一个bundle中,供所有html文件引用。在optimization配置项中开启splitChunks功能即可实现提取公共部分
{
entry: {
index: path.join(__dirname, 'src/index.js'), // 第一个入口文件
about: path.join(__dirname, 'src/about.js') // 第二个入口文件
},
output: {
filename: '[name].bundle.js', // name 就是入口文件的名称
},
plugins: [
new HtmlWebpackPlugin({
template: './public/index.html',
publicPath: './', // html中引入的文件路径
filename: "index.html", // 输出的文件名
chunks: ['index'], // 指定html文件所引入的打包完成的js 模块
}),
new HtmlWebpackPlugin({
template: './public/about.html',
publicPath: './', // html中引入的文件路径
filename: "about.html", // 输出的文件名
chunks: ['about'] // 指定html文件所引入的打包完成的js 模块
}),
],
optimization: {
splitChunks: {
chunks: "all", // 将多入口文件中所有的公共模块都单独提取到一个bundle之中
}
}
}
动态导入
- 在需要用到某个模块的时候才去加载对应的模块,适合SPA应用
- 动态导入的模块都会被webpack提取到一个单独的bundle中
- 实现方式:通过import函数来实现模块的动态导入,代替以往在js文件头部一次性将所有的模块引入的方式
// import {test} from "./foot";
// import函数接收一个要进行动态导入模块的路径作为参数,返回一个promise对象
import('./foot').then(module => {
console.log(module) // module 就是 foot.js文件导出的内容
})
- 对于react和vue这种spa框架,在路由配置的地方就应该采用动态导入的方式实现按需加载
魔法注释
- 产生背景:通过动态导入的模块的名称就是一个序号,如果想重新命名,可以通过魔法注释的方式实现
- 实现方式:在import函数的参数位置添加行内注释
import(/* webpackChunkName: 'head' */'./head').then(({createEle}) => {
const dom = createEle();
document.body.appendChild(dom)
})
输出带有Hash的文件名
生产模式下推荐使用hash名,前后两次部署的前端静态文件,一旦静态文件的hash发生变化才去重新请求,若静态文件的hash没变化,则直接从缓存中取。
- [hash]
项目级的hash,一旦项目中的任何文件变动了,所有的 [hash]都会变化 - [chunkhash]
同一路的hash - [contenthash]
文件级别的hash,精确的去控制不同文件的hash值,推荐使用 - hash长度的指定
[contenthash:8]
webpack配置文件导出一个函数
module.exports = (env,argv)=> {
// env 是命令行中的环境参数
// argv 是命令行中的所有参数
// 根据env的不同可以修改config的具体配置项参数
const config = {
...webpack的参数
}
return config;
}
一个环境一个配置文件
- 此种方式适合大型项目。
- 一共需要三个文件,一个development环境配置文件,一个production环境配置文件,一个提取development与production 的公共配置文件。
- 使用webpack-merge进行配置的合并
webpack 工作原理
webpack会从配置文件中所配置的入口js文件开始解析打包代码, 根据入口文件中的import或require语句解析入口文件所依赖的资源模块,形成一个资源依赖树,webpack会递归这个依赖树找到每一个树节点所依赖的资源文件,再根据配置文件中的rules规则去找到模块对应的loader,通过loader去转换依赖的资源文件,loader转换的结果会放到bundle中。所以loader是webpack的核心。
webpack打包生成的js的运行原理
webpack打包完输出的js文件中是一个立即执行函数,该函数接收modules作为参数。该函数自调用的时候会传一个数组,数组中是相同的函数,每一个函数对应源代码中的module,以此来形成module的私有作用域。
注意:需要自己手动在浏览器进行代码的调试
命令行输入webpack执行了什么
当在命令行里面键入webpack的命令,按下Enter键之后,首先会到node_modules下面的.bin目录寻找webpack文件,该文件中代码量总共为149行。其中关键点就是在最后会调用其中的runCLi函数,根据函数内部的逻辑,可以得到紧接着又去找node_modules下面的webpack-cli文件夹下面bin目录中的cli.js文件。接下来我们要做的就是调试源代码。所谓的调试,其实就是依据代码中的函数的命名在结合实际webpack的执行结果,来推测源代码中到底在做什么。总结一句话就是,看函数,找调用,忽略判断条件,只去猜大体的执行意思。
对于cli.js文件来说一般只做两件事:1、处理options参数。2、将参数向后面的业务逻辑进行传递