webpack预备知识
webpack
是一个现代JavaScript
应用程序的静态模块打包器(module bundler)。当webpack
处理应用程序时,它会递归地构建一个依赖关系图(dependency graph),其中包含应用程序需要的每个模块,然后将所有这些模块打包成一个或多个bundle
。
webpack配置文件
webpack.config.js
webpack的核心概念
- 入口(entry)
- 输出(output)
- loader
webpack
开箱即用只支持JS
和JSON
两种文件类型,通过Loaders
去支持其它文件类型并且把它们转化成有效的模块,并且可以添加到依赖图中。本身是一个函数,接受源文件作为参数,返回转换的结果。(如:babel-boader、css-loader) - 插件(plugins)
插件⽤于bundle
⽂件的优化,资源管理和环境变量注⼊作⽤于整个构建过程。(如:CommonsChunkPlugin、CleanWebpackPlugin) - Mode
Mode
⽤来指定当前的构建环境是:production
、development
还是none
设置mode
可以使⽤webpack
内置的函数,默认值为production
有三个比较容易混淆的概念,bundle,chunk和module。
bundle
:打包最终生成的文件chunk
:每个chunk
是由多个module
组成,可以通过代码分割成多个chunk
。module
:webpack中
的模块
demos
下面我们通过一系列demo一步步学习和实践webpack
的基本使用。
demo01:入口和输出
webpack
根据配置文件中的entry
找到工程的入口文件,加载入口文件中定义的依赖进行打包,并将最终的文件输出到output
配置项定义的位置。
entry: './index.js',
output: {
filename: 'bundle.js',
path: __dirname + '/dist'
}
demo02:多入口
webpack
支持配置多个入口,并对不同入口进行命名。
entry: {
entry1: './entry1.js',
entry2: './entry2.js'
},
output: {
filename: '[name].bundle.js', // name即为入口名
path: __dirname + '/dist'
}
demo03:调整目录结构
在demo2
中,源文件和打包后的文件混杂在一起,看起来很乱。我们将源文件和输出文件分开,调整目录结构如下:
|-dist
|-src
|-content.js
|-index.js
|-package.json
|-webpack.config.js
demo04:自动生成html入口文件
当前我们在dist
目录下的index.html
文件手动引入所有资源,当打包规则渐趋复杂后,这一过程将变得难以维护。实际上,html
入口文件和其他输出文件一样,应该由webpack
生成,并自动添加依赖的资源;HtmlWebpackPlugin
插件可以完成这一自动化过程。
安装HtmlWebpackPlugin
插件 npm i html-webpack-plugin --save-dev
修改webpack.config.js
const path = require('path');
// 引入HtmlWebpackPlugin插件
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'bundle.js'
},
plugins: [
// 自动生成html文件到dist目录
new HtmlWebpackPlugin({
template: './src/index.html', // 源文件
filename: 'index.html' // 生成文件名
})
]
};
demo05:解析ECMASCript 6 和 React JSX
使用ES6
和react
进行开发,我们需要依赖babel
进行代码的转换。
解析
ES6
要借助babel-loader
增加ES6
的babel preset
配置(.babelrc
的配置文件-presets
和plugins
)
- step01: 安装
@babel/core
(解析es6)、@babel/preset-env
(解析es6)、babel-loader
(解析js文件) - step02: 安装语法:
npm i @babel/core @babel/preset-env babel-loader -D
.babelrc
文件的介绍:
{
"presets": [
"@babel/preset-env”
],
"plugins": [
"@babel/proposal-class-properties"
]
}
plugin
用来支持某个功能,presets
是多个plugin
的集合。就是这么简单
解析
react
要借助@babel/preset-react
- step01: 安装
react
、react-dom
(react)、preset-react
(解析react) - step02: 安装语法:
npm i react react-dom @babel/preset-react -D
添加loader
配置
module: {
rules: [{
test: /\.jsx?$/,
use: [{
loader: 'babel-loader',
options: {
presets: [
["react"],
["env"]
]
}
}],
exclude: /node_modules/
},
...
]
}
demo06:加载静态资源
作为一个module bundle
,webpack
可以将所有类型的资源处理为模块。不过,webpack
本身只理解javaScript
;对于非javascript
模块,需要使用相应的loader
进行转换。
**解析CSS、Less和Sass
作为一个
module bundle
,webpack
可以将所有类型的资源处理为模块。不过,webpack
本身只理解javaScript
;对于非javascript
模块,需要使用相应的loader
进行转换。
css-loader ⽤于加载.css
⽂件,并且转换成commonjs
对象
style-loader 将样式通过
安装语法:npm i style-loader css-loader -D
&& npm i less less-loader -D
添加loader
配置
{
test: /\.css$/,
use: [
'style-loader', //链式调用,从右向左,先解析css,再将解析好的css传递给style-loader
'css-loader',
'less-loader'
]
},
{
test: /\.less$/,
use: [
'style-loader',
'less-loader'
]
}
**解析图片和字体
file-loader
⽤于处理⽂件,也可以解析字体
安装语法: npm i file-loader -D
url-loader
也可以处理图⽚和字体,可以设置较⼩资源⾃动base64
,直接打包进js
文件中,不会再打包图片
安装语法:npm i file-loader -D
{
test: /\.(png|jpg|gif|jpeg)$/,
use: [
'file-loader',
]
},
{
test: /\.(png|jpg|gif|jpeg)$/,
use: [
{
loader: 'url-loader',
options: {
limit: 10240 //图片小于10k,打包时自动base64
}
}
]
},
{
test: /\.(woff|woff2|eot|ttf|otf)$/,
use: [
'file-loader',
]
}
demo07:source-map和dev-server
当我们使用webpack
打包后的文件时,代码的调试成为一件困难的事情;一旦代码出现错误和警告,我们很难追踪到它们的源代码中的具体位置。为了解决这一问题,可以使用source-map功能,将编译后的代码映射回原始代码
作用:通过
source map
定位到源代码
使用:开发环境开启,线上环境关闭(线上排查问题的时候可以将sourcemap上传到错误监控系统)
source map
关键字
eval
: 使用eval
包裹模块代码source map
: 产生.map
文件cheap
: 不包含列信息,只能看到行inline
: 将.map
作为DataURI
嵌入,不单独生成.map
文件(但是单个文件打包体积较大)module
: 包含loader
的sourcemap
案例:
// sourc-map
devtool: 'inline-source-map'
webpack-dev-server
提供了一个简单的web
服务,能够监听源代码的变化进行实时编译,并自动刷新浏览器页面
安装npm依赖 npm i webpack-dev-server --save-dev
修改webpack.config.js
,增加dev-server
配置项
devServer: {
contentBase: './dist' // webpack-dev-server可访问文件目录
}
为了方便,我们在package.json
添加一个npm脚本
"scripts": {
"start": "webpack-dev-server --open"
...
}
执行npm run start
,webpack-dev-server
会自动打开浏览器,加载dist
文件夹下的index.html
文件;修改源文件并保存,服务器将重新编译源文件并刷新页面
demo08:公共模块提取(提取页面公共资源)
1、基础库的分离
- 思路: 将
react
、react-dom
基础包通过cdn引入,不打入bundle
中 - 方法: 使用
html-webpack-externals-plugin
图片说明:
2、利用SplitChunksPlugin进行公共脚本分离
假设我们正在开发一个多页面应用,该应用包含主页(index
)和关于(about
)两个页面。很自然的,我们会针对两个页面分别设置入口和html
源文件:
entry: {
index: './src/index.js',
about: './src/about.js'
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].bundle.js'
}
plugins: [
new HtmlWebpackPlugin({
template: './src/index.html', // 源文件
filename: 'index.html' // 生成文件名
}),
new HtmlWebpackPlugin({
template: './src/about.html',
filename: 'about.html'
})
]
在这种多入口场景下,不同的入口文件往往会依赖一些共同的模块;这些重复的模块会被打包到各个bundle
,增大了输出文件的体积。webpack3
使用CommonsChunkPlugin
插件将公共的依赖模块提取到一个独立的bundle
,供多个页面重用(如通过缓存)。但是目前已废弃,webpack4
利用SplitChunksPlugin
进行公共包的分离
chunks
参数说明:
async
异步引入的库进行分离(默认,如import引入库)initial
同步引入的库进行分离all
所有引入的库进行分离(推荐)
module.exports = {
optimization: {
splitChunks: {
chunks: 'async',
minSize: 30000,
maxSize: 0,
minChunks: 1,
maxAsyncRequests: 5,
maxInitialRequests: 3,
automaticNameDelimiter: '~',
name: true,
cacheGroups: {
vendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10
}
}
}
}
};
- 案例一:利⽤
SplitChunksPlugin
分离基础包
test
: 匹配出需要分离的包
module.exports = {
optimization: {
splitChunks: {
cacheGroups: {
commons: {
test: /(react|react-dom)/,
name: 'vendors',
chunks: 'all'
}
}
}
}
};
- 案例二: 利⽤
SplitChunksPlugin
分离页面公共文件minChunks
: 设置最小引用次数为2次
minuSize
: 分离的包体积的大小
module.exports = {
optimization: {
splitChunks: {
minSize: 0,
cacheGroups: {
commons: {
name: 'commons',
chunks: 'all',
minChunks: 2
}
}
}
}
};
demo09:代码分割(CodeSplitting)
广义上说,设置多入口也属于代码分割的范畴,但这里我们特指代码的动态拆分,及动态加载。
对于大型web
应用尤其是单页面应用而言,按需加载/异步加载是非常必要的。有些代码往往只在特定场景下才会被使用到,将其分离出来按需进行加载,无疑可以有效减少主文件的大小。
代码分割的具体做法是定义一个分离点,在分离点中依赖的模块会被打包到一起,可以异步加载。一个分离点会产生一个打包文件。
适用的场景:
- 抽离相同代码到一个共享块
- 脚本懒加载,使得初始下载的代码更小
懒加载JS
脚本的方式:
CommonJS
:require.ensure
ES6
: 动态import
(目前还没有原生支持,需要babel
转换)
在分别介绍这两种方式之前,我们首先修改webpack.config.js
:
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'bundle.js',
chunkFilename: '[name].bundle.js' // 代码分割生成的代码块
}
这里我们增加了chunkFilename
配置,它决定非入口chunk
的名称。
1.import
(官方推荐方式)
src/component/App.js
...
onClick() {
let self = this;
// 模块懒加载
import(/* webpackChunkName: "detail" */ './Detail')
.then(Detail => {
ReactDOM.render(<Detail />, self.refs['intro']);
})
}
...
上述代码中,我们在组件内部的click
事件回调中,动态加载Detail
组件并在加载完成后插入当前组件。显然的,/* webpackChunkName: "detail" */
语句用来指定该分离点生成代码块的文件名。
如何使用动态import
?
使用import
方式需要注意的是,ES6 module
(import/export
语句)是静态的,无法用于按需加载,dynamic import
仍处于提案阶段,因此必须额外安装babel
的Syntax Dynamic Import
插件: npm install @babel/plugin-syntax-dynamic-import --save-dev
否则,你会看到这样的错误 SyntaxError: ‘import’ and ‘export’ may only appear at the top level
在.babelrc
文件中引入该plugins
"plugins": ["@babel/plugin-syntax-dynamic-import"],
...
}
关于
ES6 module
的静态加载,可以参考阮一峰的ES6教程
2.require.ensure
require.ensure(['./Detail'], function (require) {
var Detail = require('./Detail');
ReactDOM.render(<Detail />, self.refs['intro']);
}, 'detail');
需要注意,require.ensure
会保证所有的dependencies
项加载完毕后,再执行回调
demo10:数据mock
对于前后端分离开发来说,数据mock
是必不可少的一环。通常我们会在本地运行一个mock server
提供数据源,这时可以利用devServer
的proxy
设置将请求代理到mock server
devServer: {
proxy: {
"/api": "http://localhost:8080"
},
...
}
demo11:uglify和treeshaking和scope hoisting
Tree-shaking
原理
利用ES6
模块的特点:
- 只能作为模块顶层的语句出现
import
的模块名只能是字符串常量import binging
是immutable
的
代码擦除:uglify
阶段删除无用代码
使用:
mode: 'production', // 默认开启tree shaking
scope hoisting
原理
- 原理: 将所有模块的代码按照引用顺序放在一个函数作用域里,然后适当的重命名一些变量以防止变量名冲突
- 对⽐: 通过
scope hoisting
可以减少函数声明代码和内存开销
scope hoisting
使用 webpack mode
为production
默认开启 (webpack4)- 必须是
ES6
语法,CJS
不⽀持
webpack3
需加一行代码
plugins: [
new webpack.optimize.ModuleConcatenationPlugin()
]
demo12: webpack与ESLint集成
https://github.com/airbnb/javascript/tree/master/packages/eslint-config-airbnb
安装命令 eslint-config-airbnb 和 eslint-loader
npm i eslint-config-airbnb -D
npm i eslint eslint-plugin-import eslint-plugin-react eslint-plugin-react-hooks eslint-plugin-jsx-a11y -D
npm i eslint-loader -D
npm i babel-eslint -D
22年可以用eslint-webpack-plugin
代替
demo13: 写一个loader
// todo