一、webpack基础
1. webpack的安装
注意:请先自行安装nodejs最新版的环境
- 全局安装webpack
npm i webpack webpack-cli -g
- 项目中安装webpack(推荐)
npm i webpack webpack-cli -D
2. webpack的使用
2.1 webpack-cli
npm 5.2以上的版本红提供了一个npx命令
npx想要解决的主要问题,就是调用项目内部安装的模板,原理就是在node_modules
下的.bin
目录中找到对应的命令执行
使用webpack命令:npx webpack
webpack4.0之后可以实现0配置打包构建,0配置的特点就是限制较多,无法自定义很多配置
开发中常用的还是使用webpack配置进行打包构建
2.2 webpack配置
webpack有四大核心概念
- 入口(entry):程序的入口js
- 输出(output):打包后存放的位置
- loader:用于对模块的源代码进行转换
- 插件(plugins):插件目的在于解决loader无法实现的其他事
- 配置 webpack.config.js
- 运行
npx webpack
const path = require('path')
// webpack遵循commonJS规范
module.exports = {
entry: './src/index.js',
output: {
// path.resolve()
// path.resolve(__dirname, './dist/')
// path.join(__dirname, './dist/')
path: path.resolve('./dist/'),
filename: 'bundle.js'
},
mode: 'development'
}
2.3 开发时自动编译工具
每次编译代码时,手动运行npm run build
就会变得很麻烦。
webpack中有几个不同的选项,可以帮助你在代码发生变化后自动编译代码:
- webpack's Watch Mode
- webpack-dev-server
- webpack-dev-middleware
多数场景下,可能需要使用webpack-dev-server
2.3.1 watch
在webpack
指令后面加上--watch
参数即可
2.3.2 webpack-dev-server(推荐)
- 安装
devServer
:
devServer
需要依赖webpack
,必须在项目依赖中安装webpack
npm i webpack-dev-server webpack -D
- index.html中修改
<script src="/bundle.js"></script>
- 运行:
npx webpack-dev-server
- 运行:
npx webpack-dev-server --hot --open --port 8090
- 配置:
package.json
的scripts:“dev”: "webpack-dev-server --hot --open --port 8090"
- 运行
npm run dev
devServer会在内存中生成一个打包好的bundle.js
,专供开发时使用,打包效率高,修改代码后会自动重新打包以及刷新浏览器,用户体验非常好
以上是cli的方式设置devServer的参数
还可以通过配置文件对devServer的参数进行修改:
- 修改
webpack.config.js
const path = require('path')
// webpack遵循commonJS规范
module.exports = {
entry: './src/index.js',
output: {
// path.resolve()
// path.resolve(__dirname, './dist/')
// path.join(__dirname, './dist/')
path: path.resolve('./dist/'),
filename: 'bundle.js'
},
mode: 'development',
devServer: {
open: true,
port: 3000,
compress: true,
hot: true,
contentBase: './src'
}
}
- 修改package.json的scripts:
"webpack": "webpack-dev-server"
- 运行
npm run dev
2.3.3 html插件
- 安装html-webpack-plugin 插件
npm i html-webpack-plugin -D
- 在
webpack.config.js
中的plugins
节点下配置
const HtmlWebpackPlugin = require('html-webpack-plugin')
plugins: [
new HtmlWebpackPlugin({
filename: 'index.html',
template: 'template.html'
})
]
功能:
- devServer时根据模板在express项目根目录下生成html文件(类似于devServer生成内存中的bundle.js)
- devServer时自动引入bundle.js
- 打包时会自动生成index.html
2.3.4 webpack-dev-middleware
- 安装
express
和webpack-dev-middleware
:
npm i expree webpack-dev-middleware -D
- 新建server.js
const express = require('express')
const webpack = require('webpack')
const webpackDevMiddleware = require('webpack-dev-middleware')
const config = require('./webpack.config')
const app = express()
const compiler = webpack(config)
app.use(webpackDevMiddleware(compiler, {
publicPath: '/'
}))
app.listen(3000, function() {
console.log('http://localhost:3000')
})
- 配置
package.json
中的scripts"server":"node server.js"
- 运行
npm run server
注意:如果要使用middleware,必须使用html-webpack-plugin
插件,否则html无法正确输出到express服务器根目录
2.3.5 小结
只有在开发时,才需要使用自动编译工具,例如:webpack-dev-server
项目上线时都会直接使用webpack进行打包构建,不需要使用这些自动编译工具
自动编译工具只是为了提高开发体验
2.4 处理css
- 安装
npm i css-loader style-loader -D
- 配置
webpack.config.js
module: {
rules: [
{
test: /\.css$/,
// webpack读取loader时,是从右往左的读取,会将css文件先交给最右侧的loader来执行
// loader的执行顺序是从右到左以管道的方式链式调用
// css-loader: 解析css文件
// style-loader: 将解析出来的结果,放到html中,使其生效
use: ['style-loader', 'css-loader']
}
]
}
2.5 处理less和sass
- 安装
npm i less less-loader node-sass sass-loader -D
- 配置
webpack.config.js
module: {
rules: [
{
test: /\.less$/,
use: ['style-loader', 'css-loader', 'less-loader']
},
{
test: /\.s(a|c)ss$/,
use: ['style-loader', 'css-loader', 'sass-loader']
}
]
}
2.6 处理图片和字体图标
-
npm i file-loader url-loader -D
url-loader 封装了file-loader,所以使用url-loader的时候一定要安装file-loader
{
test: /\.(jpg|jpeg|png|bmp|gif)$/,
// use: 'file-loader'
use: {
loader: 'url-loader',
options: {
// limit 如果图片大于5kb ,就以路径形式展示,小于的话就用base64格式展示
limit: 5 * 1024
}
}
},
{
test: /\.(woff|woff2|eot|svg|ttf)$/,
use: 'file-loader'
}
2.6.1 自定义打包图片目录和打包名称
{
test: /\.(jpg|jpeg|png|bmp|gif)$/,
// use: 'file-loader'
use: {
loader: 'url-loader',
options: {
limit: 5 * 1024,
outputPath: 'images',
// name: 图片的名称
// hash: 4 hash值保留4位数
// ext:保留文件原后缀
name: '[name]-[hash:4].[ext]'
}
}
}
2.7 babel-loader
npm i babel-loader @babel/core @babel/preset-env webpack -D
- 如果需要支持更高级的ES6语法,可以继续安装插件:
npm i @babel/plugin-proposal-class-properties -D
{
test: /\.js$/,
use: {
loader: 'babel-loader',
presets: ['@babel/env'],
plugins: ['@babel/plugin-proposal-class-properties']
},
exclude: /node_modules/
},
官方更建议的做法是在项目根目录下新建一个.babelrc
的babel配置文件
{
"presets": ["@babel/env"],
"plugins": ["@babel/plugin-proposal-class-properties"]
}
2.9 插件
2.9.1 clean-webpack-plugin
- 安装
npm i clean-webpack-plugin
- 引入插件
const CleanWebpackPlugin = require('clean-webpack-plugin')
- 使用插件
plugins: [
new HtmlWebpackPlugin({
filename: 'index.html',
template: 'template.html'
}),
new CleanWebpackPlugin()
]
2.9.2 cope-webpack-plugin
- 安装
npm i cope-webpack-plugin
- 引入
const CopyWebpckPlugin = require('copy-webpack-plugin')
- 使用
plugins: [
new HtmlWebpackPlugin({
filename: 'index.html',
template: 'template.html'
}),
new CleanWebpackPlugin(),
new CopyWebpckPlugin([
{
from: path.join(__dirname, 'assets')
to: 'assets'
}
])
]
2.10 BannerPlugin
这是一个webpack内置插件,用于给打包的js文件加上版权注释信息
- 引入webpack插件
const webpack = require('webpack')
- 创建插件对象
plugins: [
new HtmlWebpackPlugin({
filename: 'index.html',
template: 'template.html'
}),
new CleanWebpackPlugin(),
new CopyWebpckPlugin([
{
from: path.join(__dirname, 'assets')
to: 'assets'
}
]),
new webpack.BannerPlugin('李不言')
]
二、webpack高级配置
1. HTML中img标签的图片资源处理
- 安装
npm i html-withimg-loader -S
- 在
webpack.config.js
文件中添加loader
{
test: /\.(htm|html)/,
loader: 'html-withimg-loader'
}
2. 多页应用打包
- 在
webpack.config.js
中修改入口和出口配置
// 第一步:修改成多路口
entry: {
index: './src/index.js',
other: './src/other.js'
},
// 第二步:多入口无法对应一个固定的出口,所以filename为[name]变量
output: {
path: path.resolve('./dist/'),
filename: '[name].js'
},
// 第三步:如果用了html插件,需要手动配置多入口对应的html文件,将指定其对应的输出文件
plugins: [
new HtmlWebpackPlugin({
filename: 'index.html',
template: './src/index.html',
chunks: ['index']
}),
new HtmlWebpackPlugin({
filename: 'other.html',
template: './src/other.html',
chunks: ['other']
})
]
3. 第三方库的两种引用方式
- 通过
expose-loader
进行全局变量的注入 - 使用内置插件
webpack.ProvidePlugin
对每个模块的闭包空间,注入一个变量,自动加载模块,而不必到处import
或require
- expose-loader (将库引入到全局作用域)
- 安装
npm i expose-loader
- 配置
{
// require.resolve 用来获取模块的绝对路劲,所以这里的loader只会作用于jquery模块。并且只在bundle中找到它时,才能进行处理
test: require.resolve('jquery'),
use: {
loader: 'expose-loader',
options: '$'
}
}
- webpack.ProvidePlugin (将库自动加载到每个模块)
new webpack.ProvidePlugin({
$: 'jquery',
jQuery: 'jquery'
})
4. development / production不同配置文件打包
项目开发时一般需要使用两套配置文件,用于开发阶段打包(不压缩代码,不优化代码,增加效率)和上线阶段打包(压缩代码,优化代码,打包后直接上线使用)
抽取三个配置文件:
- webpack.base.js
- webpack.dev.js
- webpack.prod.js
步骤如下:
- 将开发环境和生产环境公用的配置放入base中,不同的配置各自放入prod或dev文件中(例如mode)
- 然后在dev和prod中使用
webpack-merge
把自己的配置与base的配置合并后导出 - 将package.json中的脚本参数进行修改,通过--config手动指定特定的配置文件
const merge = require('webpack-merge')
const baseConfig = require('./webpack.base')
module.exports = merge(baseConfig, {
mode: 'development',
devServer: {
open: true,
port: 3000,
compress: true,
hot: true,
// contentBase: './src'
}
})
5. 定义环境变量
除了区分不同的配置文件进行打包,还需要再开发时知道当前的环境是开发阶段或上线阶段,所以可以借助内置插件DefinePlugin来定义环境变量。最终可以实现开发阶段与上线阶段的api地址自动切换
- 引入webpack
const webpack = require('webpack')
- 创建插件对象,并定义环境变量
module.exports = merge(baseConfig, {
mode: 'production',
plugins: [
new webpack.DefinePlugin({
IS_DEV: 'true'
})
]
})
- 在src打包的代码环境下可以直接使用
6. 跨域问题及常用解决方案
目前解决跨域的主要方法有:
- jsonp(淘汰:利用script标签实现跨域,只能用于get请求)
- cors(主流:后端控制)
- http proxy
devServer: {
open: true,
port: 3000,
compress: true,
hot: true,
// contentBase: './src',
proxy: {
'/api': {
target: 'http://localhost:3000',
pathRewrite: {
'^/api': ''
}
}
}
},
三、webpack优化
1. production模块打包自带优化
-
tree shaking
tree shaking是一个术语,通常用于打包时移动JavaScript中的未引用的代码(dead-code),他依赖于ES6模块系统中import
和export
的静态结构特征
开发时引入一个模块后,如果只使用其中一个功能,上线打包时只会把用到的功能打包进bundle,其他没用到的功能都不会打包起来,可以实现最基础的优化 -
scope hoisting
scope hoisting的作用是将模块之间的关系进行结果推测,可以让webpack打包出来的代码文件更小、运行的更快
scope hoisting的实现原理其实很简单:分析出模块之间的依赖关系,尽可能的把打散的模块合并到一个函数中去,但前提是不能造成冗余代码
因此只有那些被引用了一次的模块才能被合并
由于scope hoisting需要分析出模块之间的依赖关系,因此源码必须采用ES6模块化语句,不然无法生效
原因和tree shaking一样 -
代码压缩
所有代码使用UglifyJsPlugin插件进行压缩、混淆
2. css优化
2.1 将css提取到独立文件中
mini-css-extract-plugin
是用于将css提取为独立的文件的插件,对每个包含css的js文件都会创建一个css文件,支持按需加载css和sourceMap
只能用在webpack4中,有如下优势
- 异步加载
- 不重复编译,性能更好
- 更容易使用
- 只针对css
使用方法:
- 安装
npm i -D mini-css-extract-plugin
- 在webpack配置文件中引入插件
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
- 创建插件对象,配置抽离的css文件名,支持placeholder语法
new MiniCssExtractPlugin({
fileName: '[name].css'
})
- 将原来配置的所有
style-loader
替换为MiniCssExtractPlugin.loader
rules: [
{
test: /\.css$/,
// webpack读取loader时,是从右往左的读取,会将css文件先交给最右侧的loader来执行
// loader的执行顺序是从右到左以管道的方式链式调用
// css-loader: 解析css文件
// style-loader: 将解析出来的结果,放到html中,使其生效
// use: ['style-loader', 'css-loader']
use: [MiniCssExtractPlugin.loader, 'css-loader']
},
{
test: /\.less$/,
// use: ['style-loader', 'css-loader', 'less-loader']
use: [MiniCssExtractPlugin.loader, 'css-loader', 'less-loader']
},
{
test: /\.s(a|c)ss$/,
// use: ['style-loader', 'css-loader', 'sass-loader']
use: [MiniCssExtractPlugin.loader, 'style-loader', 'css-loader', 'sass-loader']
}
]
2.2 自动添加css前缀
使用postcss
,需要用到postcss-loader
和autoprefixer
插件
- 安装
npm i -D postcss-loader autoprefixer
- 修改webpack配置文件中的loader,将
postcss-loader
防在css-loader
的右边,调用链从右到左
rules: [
{
test: /\.css$/,
// webpack读取loader时,是从右往左的读取,会将css文件先交给最右侧的loader来执行
// loader的执行顺序是从右到左以管道的方式链式调用
// css-loader: 解析css文件
// style-loader: 将解析出来的结果,放到html中,使其生效
// use: ['style-loader', 'css-loader']
use: [MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader']
},
{
test: /\.less$/,
// use: ['style-loader', 'css-loader', 'less-loader']
use: [MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader', 'less-loader']
},
{
test: /\.s(a|c)ss$/,
// use: ['style-loader', 'css-loader', 'sass-loader']
use: [MiniCssExtractPlugin.loader, 'style-loader', 'css-loader', 'postcss-loader', 'sass-loader']
}
]
- 项目根目录下添加
postcss
的配置文件:postcss.config.js
module.exports = {
plugins: [require('autoprefixer')]
}
2.3 开启css压缩
需要使用optimize-css-assets-webpack-plugin
插件来完成css压缩
但是由于配置css压缩时会覆盖掉webpack默认的优化配置,导致js代码无法压缩,所以还需要手动把JS代码压缩插件导入进来:terser-webpack-plugin
- 安装
npm i -D optimize-css-assets-webpack-plugin terser-webpack-plugin
- 导入插件
const TerserJSPlugin = require('terser-webpack-plugin')
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin')
- 在
webpack
配置文件中添加配置节点
optimization: {
minimizer: [new TerserJSPlugin({}), new OptimizeCSSAssetsPlugin({})]
}
tips: webpack4默认才有的JS压缩插件为:uglifyjs-webpack-plugin
, 在mini-css-extract-plugin
上一个版本中还推荐使用该插件,但新版的v0.6建议使用teser-webpack-plugin
来完成js代码压缩
3. js优化
Code Splitting 是webpack打包时用到的重要的优化特性之一,此特性能够把代码分离到不同的bundle中,然后按需加载或并行加载这些文件。代码分离可以用于获取更小的bundle,以及控制资源加载优先级,如果使用合理,会极大影响加载事件。
有三种常用的代码分离方法:
- 入口起点(entry points):使用
entry
配置手动地分离代码。 - 防止重复(prevent duplication):使用
SplitChunkPlugin
去重和分离chunk - 动态导入(dynamic imports):通过模块的内联函数调用来分离代码
3.1 手动配置多入口
- 配置多入口
module.exports = {
entry: {
index: './src/index.js',
other: './src/other.js'
},
output: {
// path.resolve()
// path.resolve(__dirname, './dist/')
// path.join(__dirname, './dist/')
path: path.resolve('./dist/'),
filename: '[name].js'
}
}
- 在mian.js和other.js 中都引入同一个模块,并使用其功能
main.js
import $ from 'jquery'
$(function(){('<div></div>').html('main').appendTo('body')})
other.js
import $ from 'jquery'
$(function(){('<div></div>').html('other').appendTo('body')})
- 修改package.json的脚本,添加一个使用dev配置文件进行打包的脚本(目的是不压缩代码检查打包的bundle时更方便)
这个方法会存在一些问题:
- 如果入口chunks之间包含重复模块,那些重复的模块都会被引入到各个bundle中
- 这种方法不够灵活,并且不能将核心应用程序逻辑进行动态拆分代码
3.2 抽取公共代码
tips:webpack v4以上使用的插件为 SplitChunksPlugin
,以前使用的CommonsChunkPlugin
已经被移除,最新版本的webpack只需要再配置文件中的optimization
节点下添加一个splitChunks
属性即可进行相关配置
- 修改webpack配置文件
optimization: {
splitChunks: {
chunks: 'all'
}
}
- 运行npm run dev-build
- 此时可以看到jQuery已经单独打包到了一个文件中
3.3 动态导入(懒加载)
webpack4默认允许import语法动态导入的,但是需要babel的插件支持,最新版本babel插件包为:@babel/plugin-syntax-dynamix-import
,以前老版本不是@babel
开头,已经无法使用,需要注意
动态导入最大的好处是实现了懒加载,用到哪个模块才会加载哪个模块,可以提高SPA应用程序的首屏加载速度,Vue、React、Angular框架的路由懒加载原理一样
- 安装
babel
插件
npm install -D @babel/plugin-syntax-dynamix-import
- 修改.babelrc配置文件,添加
@babel/plugin-syntax-dynamix-import
插件
{
"presets": ["@babel/env"],
"plugins": [
"@babel/plugin-proposal-class-properties",
"@babel/plugin-transform-runtime",
"@babel/plugin-syntax-dynamic-import"
]
}
- 将jQuery模块进行动态导入
function getComponent() {
return import('jquery').then({default: $}) => {
return $('<div></div>').html('main')
})
}
- 给按钮添加点击事件,点击后调用getComponent函数创建元素并添加到页面
window.onload = function() {
document.getElementById('btn').onclick = function() {
getComponent().then( (item) => {
item.appendTo('body')
})
}
}
3.4 SplitChunksPlugin配置参数
webpack4之后,使用SplitchunksPlugin插件替代了以前CommonsChunksPlugin
而SplitchunksPlugin的配置,只需要在webpack配置字文件中的optimization节点下的splitChunks进行修改即可,如果没有任何修改,则会使用默认配置
默认的SplitChunksPlugin配置适用于绝大多数用户
webpack会基于如下默认原则自动分割代码:
- 公共代码块或来自node_modules文件夹的组件模块
- 打包的代码块大小超过30k(最小化压缩之前)
- 按需加载代码块时,同时发送的请求最大数量不应该超过5
- 页面初始化时,同时发送的请求最大数量不超过3
默认配置如下:
module.exports = {
//...
optimization: {
splitChunks: {
chunks: 'async', // 默认值为async
minSize: 20000,
minRemainingSize: 0,
maxSize: 0,
minChunks: 1,
maxAsyncRequests: 30,
maxInitialRequests: 30,
automaticNameDelimiter: '~',
enforceSizeThreshold: 50000,
cacheGroups: {
defaultVendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10
},
default: {
minChunks: 2,
priority: -20,
reuseExistingChunk: true
}
}
}
}
};
4. 提高构建性能 --- noParse
在引入一些第三方模块时,例如jQuery、bootstrap等,我们知道其内部肯定不会依赖其他模块,因为最终我们用到的知识一个单独的js文件或者css文件
所以此时如果webpack再去解析他们的内部依赖关系,其实是非常浪费时间的,我们需要阻止webpack浪费精力去解析这些明知道没有依赖的库
可以在webpack配置文件的module节点下加上noParse,并配置正则来确定不需要解析依赖关系的模块
module: {
noParse: /jquery|bootstrap/
5. 提高构建性能 --- IgnorePlugin
在引入一些第三方模块时,例如moment,内部会做i18n国际化处理,所以会包含很多语言包,而语言包打包时会比较占用空间,如果我们项目只用到中文,或者少数语言,可以忽略掉所有的语言包,然后按需引入语言包,从而使构建效率更高,打包生成的文件更小
需要忽略第三方内部依赖的其他模块,只需要三步:
- 首先找到moment依赖的语言包是什么
- 使用ignorePlugin插件忽略其依赖
- 需要使用某些依赖时自动手动引入
五、webpack原理
1. 目标
- 了解webpack打包原理
- 了解webpack的loader原理
- 了解webpack的插件原理
- 了解ast抽象语法树的应用
- 了解tapable的原理
- 手写一个简单的webpack
2. 项目准备工作
- 新建一个项目
- 新建
bin
目录,将打包工具主程序放入其中
主程序的顶部应当有:#!/use/bin/env node
标志,指定程序执行环境为node - 在
package.json
中配置bin
脚本
{
"bin": "./bin/libuyan-park.js"
}
- 通过
npm link
链接到全局包中,供本地测试使用
网友评论