简介
webpack是一个模块打包机,它会从指定入口文件开始,递归的寻找JavaScript模块以及其他一些浏览器无法直接运行的扩展语言(Sass, TypeScript)等,将其打包成合适的格式以供浏览器使用。
它的作用有代码转换(利用各种loader将浏览器无法识别的语言转换成合适的格式),文件优化(比如说打包时压缩体积),代码分割,模块合并,自动刷新(热更新),代码校验,自动发布。
引用了网上的一张图来大致看一下webpack的运行机制:
webpack的一些基本配置
安装webpack以及默认配置
首先是安装webpack,在本地项目文件夹下npm init初始化之后,下载webpack以及webpack-cli:
npm init -y
npm i webpack webpack-cli -D
此时在文件夹下建立一个src文件夹,用于放置项目代码。webpack此时可以进行0配置打包,在命令行输入npx webpack
可以打包出一个dist文件夹,下面有一个main.js就是打包后的文件。这个打包后的文件内容,就是使用递归的方式解析src中的js模块,递归的方法名为__webpack_require__,它支持我们在浏览器中使用CommonJs规范。
默认打包的配置很弱,它只能识别js模块,在没有配置的情况下,webpack就相当于一个js模块打包机。当然我们不可能直接就0配置打包一个项目,下面我总结一下webpack中常用的一些基本配置。
webpack.config.js
webpack中默认的配置文件名为webpack.config.js,在根目录下建立一个名为webpack.config.js的文件,就可以在这个文件中写配置项。它的内容遵循CommonJs规范,webpack提供给我们修改这个文件名的一些方法:
(1)打包时输入命令npx webpack --config webpack.config.my.js
。
(2)为了不用每次都在命令行输出一串这么长的命令,在package.json中配置scripts,"build" : "webpack --config webpack.config.my.js"
。
这两种配置方法都可以修改默认配置文件名。先在webpack.config.my.js写一段基本的配置:
//webpack是用node写的
let path = require('path');
module.exports = {
mode: 'development', //模式 生产环境production 开发模式development
entry: './src/index.js', //入口
output: { //出口
filename: 'bundle.[hash:8].js', //打包后的文件名,[hash]每次打包生成新的文件
//__dirname以当前目录解析成绝对路径
path: path.resolve(__dirname, 'dist'), ///path字段只接受绝对路径,因此需要一个node模块来辅助配置 path.resolve把相对路径解析成绝对路径
publicPath: 'http://www.help.com'//公共路径,打包出的资源文件会带着这个公共路径。
}
}
Loader
loader帮助我们告诉浏览器遇到不能识别的模块应该怎么处理,前面我们说过webpack默认配置只是别js模块,那么解析图片、css、less这些模块就需要引入loader。
打包图片(file-loader,url-loader)
图片引入有三种方式:
- 在js中创建图片来引入,打包时要用到file-loader来解析图片地址加hash戳。
const logo = require('./01.jpg') //把图片引入,返回的结构是一个新的图片地址
const image = new Image();
console.log(logo); //用到file-loader 默认会在内部生成一张图片,到build目录下 把生成的图片的名字返回回来
image.src = logo;
document.body.appendChild(image)
配置如下:
- 使用file-loader:它解析静态资源,把原文件原封不动的拷贝一份放到打包后的文件夹下,并且把位置返回,上述logo的值就是该资源在打包后文件夹下的路径。一般可直接展示的文件用file-loader解析,比如图片,excel图表之类的。
{
test: /\.(png|jpe?g|gif)$/,
use: {
loader: 'file-loader',
options: {
name:[name].[ext],
outputPath: '/img/' //打包时另外生成一个img文件夹
}
}
},
- 使用url-loader:它是file-loader的加强版,我们在它的配置项中可以设置一个limit值。如果文件体积小于该值,则文件被编码成Base64字符串直接引用,而不打包成一个新文件。这样可以减少一次http请求。如果文件大于该值,它的功能就相当于file-loader。这种方式会使打包后的文件体积变大,因此两者性能需要权衡。
{
test: /\.(png|jpe?g|gif)$/,
use: {
loader: 'url-loader',
options: {
limit: 200 * 1024,//小于200k使用url-loader,大于200k使用file-loader
outputPath: '/img/'
}
}
},
- 在css中添加图片,打包时css-loader会将url('./01.jpg')解析为url(require('./01.jpg'))。
body {
background: red;
background: url('./01.jpg')
}
- 在html中通过图片的img标签引入图片,打包时需要用到html-withimg-loader来解析图片地址。
<img src="./01.jpg" alt="">
配置如下:
{
test: /\.html$/,
use: 'html-withimg-loader'
}
样式设置(css-loader,style-loader,less-loader)
打包CSS文件,我们需要用到两个loader,一个是style-loader,它负责处理css文件中的import、url()语法。style-loader以内联<style>
的形式将样式都写到模版html的<head>
头部中。打包LESS文件同样的套路,less-loader现将less转换成css。
配置如下:
module: {
rules: [
//loader的用法。字符串只用一个loader 多个loader需要用数组 loader的顺序 默认从右向左 从下往上执行
{
test: /\.css$/,
use: [{
loader: 'style-loader',
options: {
insertAt: 'top'//插在最上面,让自己写在模版html<style>标签中的样式优先级较高
}
}, 'css-loader']
},
{
test: /\.less$/,
use: [{
loader: 'style-loader',
options: {
insertAt: 'top'
}
}, 'css-loader', 'less-loader']
}
]
}
如下图所示,head标签下面的三个样式是分别在.css和.less文件中定义的样式,而<head>
标签上面的一个样式是在模版html中自己设定的。
CSS3为兼容自动加浏览器前缀(postcss-loader)
用一个autoprefixer包和一个postcss-loader自动添加浏览器前缀,且这个插件是会更新的,以前transform在需要加上webkit前缀,但Chrome支持后postcss-loader就不会再给这个属性加上前缀了。
npm i postcss-loader autoprefixer -D
//在根目录创建postcss.config.js文件
module.exports = {
plugins: [require('autoprefixer')]
}
//在rules css配置中新加入postcss-loader
module: {
rules: [
//loader的用法。字符串只用一个loader 多个loader需要用数组 loader的顺序 默认从右向左 从下往上执行
{
test: /\.css$/,
use: [{
loader: 'style-loader',
options: {
insertAt: 'top'//插在最上面,让自己写在模版html<style>标签中的样式优先级较高
}
}, 'css-loader']
}
]
}
Plugin
Plugin可以在webpack运行到某个阶段的时候,帮助我们做某些事情,类似于生命周期的概念。在某个时间点,需要某个机制完成一些事情。
生成模版html(HtmlWebpackPlugin插件)
在我们打包文件后,该插件会生成一个html模版,并且把打包后的其他文件在该模版中引用。生成的html模版的内容是我们可以自己定义的。
const HtmlWebpackPlugin = require('html-webpack-plugin');
plugins: [
new HtmlWebpackPlugin({
template: './src/index.html', //模版文件的位置
filename: 'index.html', //打包出来html文件的名称
minify: {
removeAttributeQuotes: true, // 去除双引号
collapseWhitespace: true, //变成一行
},
hash: true //添加一个hash戳
})
],
抽离CSS(MiniCssExtractPlugin插件)
我们打包时,把所有样式抽离出来生成一个CSS文件,在模版html文件中以link形式引入。
npm i mini-css-extract-plugin -D
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
plugins: [
new MiniCssExtractPlugin({
filename: 'main.css'
})
]
module: {
rules: [
{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader, //把style-loader换成MiniCssExtractPlugin.loader
'css-loader',
'postcss-loader'
]
},
{
test: /\.less$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader',
'less-loader',
'postcss-loader'
]
}
]
}
压缩CSS和JS(OptimizeCSSAssetsPlugin插件、UglifyJsPlugin插件)
进行到这一步会发现,生产模式下打包出来的main.css也没有被压缩,是因为用了MiniCssExtractPlugin这个插件不会压缩css,需要自己压缩。使用OptimizeCSSAssetsPlugin插件配置优化项,但是使用这个插件之后,css确实压缩了,但js又不会压缩了,因此还要用到UglifyJsPlugin再来压缩js。
npm i optimize-css-assets-webpack-plugin -D
npm i uglifyjs-webpack-plugin -D
const UglifyJsPlugin = require("uglifyjs-webpack-plugin"); //压缩js
const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin"); //压缩css
optimization: {
minimizer: [
new UglifyJsPlugin({
cache: true, //缓存
parallel: true, //并发压缩
sourceMap: true // set to true if you want JS source maps
}),
new OptimizeCSSAssetsPlugin({})
]
}
更新打包目录(CleanWebpackPlugin插件)
在没有使用该插件之前,每次打包上一版本的文件会遗留在dist文件夹下,需要我们手动删除。这个插件,在每次打包之前,先把之前的dist文件夹删除,打包生成新的dist目录。
npm install --save-dev clean-webpack-plugin
const {CleanWebpackPulgin} = require('clean-webpack-plugin')
plugins: [
new CleanWebpackPlugin()
]
SourceMap
源代码与打包后的代码的映射关系,帮助我们定位错误在源代码中的位置。在devtool字段中配置,推荐的配置如下:
- cheap :较快,只定位行,不定位列。
- moudle :定位引入的第三方模块的错误。
- eval :速度最快,包裹模块代码。
- source-map :生成map,内容是源代码和打包后的代码的映射。
devtool:"cheap-module-eval-source-map" //开发环境
devtool:"cheap-module-source-map" //线上生产环境
webpack-dev-server
一个提升开发效率的利器,每次改完代码都要重新打包一次,刷新浏览器非常的麻烦。用webpack-dev-server搭建一个服务器,使得我们不用真实的打包,而是在内存中打包,放置到devSever服务器上,以便我们在开发时调试测试整个项目。
下载webpack-dev-server:
npm i webpack-dev-server -D
之后,先在package.json中配置scripts,"dev" : "webpack-dev-server --config webpack.config.my.js"
。然后配置一下devServer中的一些配置项:
devServer: { //开发服务器的配置
port: 8889, //端口号
progress: true, //进度条
contentBase: './dist', //指定了服务器资源的根目录,但是在开发过程不会真实打包
compress: true, //启用 gzip 压缩
open: true //自动打开浏览器
},
mock
联调期间,前后端分离,直接获取数据会跨域,上线后我们使⽤用nginx转发,开发期间,webpack-dev-server就可以搞定这件事。
我们先启动服务器,mock一个接口:
const koa = require('koa')
const app = new koa()
const Router = require('koa-router')
const router = new Router()
router.get('/api/info', async (ctx, next) => {
ctx.body = {
username: 'zhunny',
message: 'hello mock'
}
ctx.status = 200
})
app.use(router.routes())
app.listen(3000)
此时在我们前端项目中请求该接口的数据,会存在跨域问题,我们在dev-server中配置服务器代理:
axios.get('http://localhost:3000/api/info').then(res=>{
console.log(res)
})
devServer: { //开发服务器的配置
port: 8889, //端口号
progress: true, //进度条
contentBase: './dist', //指定了服务器资源的根目录,但是在开发过程不会真实打包
compress: true, //启用 gzip 压缩
open: true, //自动打开浏览器
proxy: {
"/api": {
target: "http://localhost:3000"
}
}
},
之后修改请求的接口:
axios.get('/api/info').then(res=>{
console.log(res)
})
转换es6语法及校验
webpack本身可以处理ES6语法,但是有些浏览器对es6、es7或者是es8的语法还不能识别。出于兼容性的考虑,我们会使用Babel来将ES6转换成ES5语法。
- babel-loader: 是Babel于webpack通信的桥梁。
- @babel/core: Babel工具的核心代码库。
- @babel/preset-env: ES6语法的转换规则。
npm i babel-loader @babel/core @babel/preset-env -D
{
test: /\.js$/,
exclude: /node_modules/, //排除该文件夹下的内容
use: {
loader: 'babel-loader',
options: {
presets: [
'@babel/preset-env'
]
}
}
}
@babel/polyfill
当然只配置上述字段是不够的,到此步为止,一些es6、7、8新增的方法和类依然不能被识别。我们还需要下载@babel/polyfill
,它将es6、7、8中的语法特性打包放到浏览器中,相当于一个补丁包。
它的基本使用方法是在入口js文件中引用:import '@babel/polyfill'
。但是这种方法是引入整个补丁包,使得webpack打包后的体积变大。我们可以对这点进行优化。移除在js文件中引用的@babel/polyfill
,配置文件中添加useBuiltIns字段,对使用到的es6、7、8语法特性按需加载。
presets: [
[
'@babel/preset-env',
{
useBuiltIns: 'usage',//按需加载
corejs: 2 //指定core的版本
}
]
],
@babel/plugin-transform-runtime
当我们开发组件库、工具库这些场景时,在js文件中引用@babel/polyfill
就不合适了。因为@babel/polyfill
以全局变量的方式注入,会造成全局污染。上面用的按需加载usage的方法也不会造成全局污染,但是这个字段还在试验阶段。我们可以使用闭包方式@babel/plugin-transform-runtime
来代替。但是这种方式就不会对原型链上的某些方法进行转义,因此开发正常的业务场景就比较适合用polyfill,无所谓全局变量的影响,我们不需要担心某些原型链上的方法没有被转义。
npm i @babel/plugin-transform-runtime -D
npm i @babel/runtime -S
{
test: /\.js$/,
exclude: /node_modules/, //排除该文件夹下的内容
use: {
loader: 'babel-loader',
options: {
plugins: [
[
'@babel/plugin-transform-runtime',
{
absoluteRuntime:false,
corejs:2,
helpers:true,
regenerator:true,
useESMoudules:false
}
]
]
}
}
}
.babelrc
Babel配置可能内容较多,我们可以把options内容放到.babelrc中。
//.babelrc
{
"presets": [
[
"@babel/preset-env",
{
"useBuiltIns": "usage", //按需加载 实验性的功能
"corejs": 2
}
],
"@babel/preset-react"
]
}
一些提案语法的补丁包
一些es7的提案如class,则还需要用到@babel/plugin-proposal-class-properties,装饰器则需要用到@babel/plugin-proposal-decorators:
npm i @babel/plugin-proposal-class-properties -D
npm i @babel/plugin-proposal-decorators -D
{
test: /\.js$/,
use: {
loader: 'babel-loader',
options: {
presets: [
'@babel/preset-env'
],
plugins: [
['@babel/plugin-proposal-decorators', { "legacy": true }],
['@babel/plugin-proposal-class-properties', { "loose": true }],
['@babel/plugin-transform-runtime'] //generator
]
}
}
}
校验
js语法的校验用到了eslint以及eslint-loader,他的官网为https://eslint.org
,eslint在使用时需要配置一个.eslint.json的规则文件放在根目录,具体配置项见官网。
npm i eslint eslint-loader -D
{
test: /\.js$/,
use: {
loader: 'eslint-loader',
options: {
enforce: 'pre' //在普通loader之前执行
}
},
}
全局变量的引入
引入全局变量有三种方式,假如要在全局引入jquery库:
- 使用内联loader expose-loader将jquery暴露到window属性上。
import $ from 'jquery'
require(expose-loader)
console.log(window.$)
- 使用webpack.providePlugin插件将$注入到每个模块。
new webpack.providePlugin(
{$:'jquery'}
)
- 引入jquery的cdn。