为什么一直没写一篇webpack的总结呢?
因为webpack涉及到的东西实在太多,又及其杂碎,我不想写一篇大而广却无实际意义的文章,也无法用一篇文章来涵盖webpack所有的知识点。所以今天的目的不在于全面的讲解webpack,而是罗列出一些本人在实际项目中所是用到的知识点,总结出来加深记忆和供以后参考。
什么是webpack?
webpack是一种前端资源构建工具,是一个现代 JavaScript 应用程序的静态模块打包器(module bundler)。
- 前端资源构建工具:前端资源是指浏览器不认识的 web 资源, 比如 sass、less、ts,包括 js 里的高级语法。这些资源要能够在浏览器中正常工作,必须一一经过编译处理,将原本浏览器不能识别的规范和各种各样的静态文件进行分析,压缩,合并,打包,最后生成浏览器支持的代码。而 webpack 就是可以集成这些编译工具的一个总的构建工具。
- 静态模块打包器:静态模块就是 web 开发过程中的各种资源文件,webpack 根据引用关系,递归地构建一个依赖关系图,然后利用这个关系图将所有静态模块打包成一个或多个 bundle 输出。
webpack核心概念
- entry
entry指示 webpack 应该使用哪个模块来作为构建其内部依赖图的开始,是webpack 配置模块的入口。webpack 开始构建时会从 entry 的文件开始递归解析出所有依赖的模块,最后输出到称之为 bundles的文件中。
entry有三种类型,分别为:
string: 单入口,打包形成一个chunk,输出一个buldle文件。chunk的名称默认是main.js(output的filename不写时);
module.exports = {
entry: './src/index.js'
};
array:单入口数组语法,传入一个数组的作用是将多个资源预先合并,所有入口文件最终只会形成一个chunk,输出出去只有一个bundle文件。chunk的名称默认是main.js(output的filename不写时);
module.exports = {
entry: ['./src/index.js', 'sub.js']
};
object :多入口,有几个入口文件就形成几个chunk,输出几个bundle文件。此时chunk的名称就是对象key值(home,sub1)。
module.exports = {
entry: {
index: './src/index.js',
sub: ['./src/sub1.js', './src/sub2.js'],
}
};
分离第三方库(vendor)
webpack所构建的单页应用具有依赖关系清晰的优势,但它同时也有相应的弊端,即所有模块都打包到一起,当应用的规模上升到一定程度之后会导致产生的资源体积过大,降低用户的页面渲染速度。在Webpack默认配置中,当一个bundle大于250kB时(压缩前)会认为这个bundle已经过大了,在打包时会发生警告。
为了解决这个问题,就需要提取第三方库(vendor),在Webpack中vendor一般指的是工程所使用的库、框架等第三方模块集中打包而产生的bundle。
module.exports = {
entry: {
index: './src/index.js',
vendors: ['react','react-dom','react-router'],
}
};
上面例子中我们并没有为vendor设置入口路径,Webpack要如何打包呢?
这时我们可以使用CommonsChunkPlugin(在Webpack 4之后CommonsChunkPlugin已被废弃,可以采用optimization.splitChunks)将app与vendor这两个chunk中的公共模块提取出来。
通过这样的配置,index.js产生的bundle将只包含业务模块,其依赖的第三方模块将会被抽取出来生成一个新的bundle,这也就达到了我们提取vendor的目标。
- output
output告诉 webpack 在哪里输出它所创建的 bundles,以及如何命名这些文件。是 Webpack 配置输出文件的文件名、路径等信息的选项。即使存在多个入口,也只需要配置一个 output。
module.exports = {
entry: './src/index.js',
output: {
filename: 'js/bundle.js',
path: _dirname + '/dist', //Webpack 4之后path可以省略
}
};
path指定资源输出的位置,要求值必须为绝对路径,在Webpack 4之后,output.path已经默认为dist目录。除非我们需要更改它,否则不必单独配置。
filename的作用是控制输出资源的文件名,其形式为字符串。filename可以不仅仅是bundle的名字,还可以是一个相对路径,如果路径中的目录不存在,Webpack会在输出资源时创建该目录。
上面的例子为entry为单入口时,可以配置将一个单独的 bundle.js 文件输出到项目下的/dist目录中。那么在多入口的场景中,我们需要对产生的每个bundle指定不同的名字,使用占位符(substitutions)来确保每个文件具有唯一的名称。
module.exports = {
entry: {
index: './src/index.js',
sub: ['./src/sub1.js', './src/sub2.js'],
}
output: {
filename: '[name].js',
path: __dirname + '/dist/assets' //指定打包后的bundle放在/dist/assets目录下
}
};
filename中的[name]
会被替换为chunk name即index和sub。因此最后会写入到硬盘:./dist/index.js, ./dist/sub.js。
这里的[name]
除了可以指代chunk name以外,还有其他几种模板变量可以用于filename的配置中,用于控制客户端缓存:
- [hash]:指代Webpack此次打包所有资源生成的hash
- [chunkhash]:指代当前chunk内容的hash
- [id]:指代当前chunk的id
- [query]:指代filename配置项中的query
[hash]
和[chunkhash]
都与chunk内容直接相关,如果在filename中使用,当chunk的内容改变时,可以同时引起资源文件名的更改,从而使用户在下一次请求资源文件时会立即下载新的版本而不会使用本地缓存。
[query]
也可以起到类似的效果,但它与chunk内容无关,要由开发者手动指定。
output还有一个十分重要的配置项:publicPath,用来指定资源的请求位置
import Img from './img.jpg';
function component() {
//...
var img = new Image();
myyebo.src = Img //请求url
//...
}
module.exports = {
entry: {
index: './src/index.js',
vendors: ['react','react-dom','react-router'],
}
output: {
filename: '[name].[chunkhash].js',
publicPath: './dist/static/img/'
}
};
上面的例子中原本图片请求的地址是./img.jpg,而在配置上加上publicPath后,实际路径就变成了了./dist/static/img/img.jpg,这样就能从打包后的资源中获取图片了。
- loader
一个Web工程通常会包含HTML、JS、CSS、模板、图片、字体等多种类型的静态资源,并且这些资源之间都存在着某种联系。由于webpack 自身只理解 JavaScript,需要使用 loader将所有类型的文件转换为 webpack 能够处理的有效模块,然后让webpack对其进行打包。
module.exports = {
module: {
rules: [
{ test: /\.txt$/, use: 'raw-loader' },
{
test: /\.css$/,
use:[
{
loader: 'style-loader',
},
{
loader: 'css-loader',
}
]
},
{
test: /\.(scss|sass)$/,
use: ['style-loader', 'css-loader', 'sass-loader']
}
]
}
};
loader 支持链式传递,通过配置可以看出,针对每个文件类型,loader是支持以数组的形式配置多个的,因此当Webpack在转换该文件类型的时候,会按顺序链式调用每一个loader,前一个loader返回的内容会作为下一个loader的入参。在最后一个 loader,返回 webpack 所预期的 JavaScript。
loader 需要单独安装(例如:npm install --save-dev css-loader),并且需要在webpack.comfig.js中的modules配置项下进行配置,Loaders的配置包括以下几方面:
-
test
:接收一个正则表达式或者一个元素为正则表达式的数组,只有正则匹配上的模块才会使用这条规则。用于标识出应该被对应的 loader 进行转换的某个或某些文件; -
use
:接收一个字符串或数组,数组包含该规则所使用的loader。表示进行转换时应该使用哪个 loader; -
include/exclude
: 手动添加必须处理的文件(文件夹)或屏蔽不需要处理的文件(文件夹); -
options
: 为loaders提供额外的设置选项。
- plugins
插件是用来拓展Webpack功能的,它们会在整个构建过程中生效,执行相关的任务。目的在于解决loader无法实现的事,loaders是在打包构建过程中用来处理源文件的(JSX,Scss,Less..),一次处理一个,插件并不直接操作单个文件,它直接对整个构建过程起作用。
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
plugins: [
new HtmlWebpackPlugin({template: './src/index.html'})
]
};
- 插件的作用范围:从上面的描述可以看出,plugins作用于从打包优化和压缩,一直到重新定义环境中的变量。插件接口功能极其强大,可以用来处理各种各样的任务。
- 插件需要通过npm进行安装,
require()
加载,接着将它添加到plugins
数组中。多数插件可以通过选项option
自定义 - 由于插件可以携带参数/选项,你必须在 webpack 配置中向 plugins 属性传入 new 实例
几个常用插件:
- HtmlWebpackPlugin:自动生成index.html且自动引用打包后的js;
- CleanWebpackPlugin:清理dist文件夹。webpack会生成文件,然后将这些文件放置在dist文件夹中,但是webpack无法追踪到哪些文件是实际在项目中用到的。通常,在每次构建前清理dist文件夹,是比较推荐的做法;
- HotModuleReplacementPlugin:热更新,设置方法:devServer配置项中添加 hot:true 参数。HotModuleReplacementPlugin是webpack模块自带的,所以引入webpack后,在plugins配置项中直接使用即可。
const webpack = require('webpack');
module.exports = {
devServer: {
hot: true,
...
},
plugins: [
new webpack.HotModuleReplacementPlugin() // 热更新插件
]
}
- mode(webpack4新增)
通过选择 development 或 production 之中的一个来设置 mode 参数,就可以启用相应模式下的 webpack 内置的优化。例如:production模式下会进行tree shaking(去除无用代码)和uglifyjs(代码压缩混淆)
module.exports = {
mode: 'production'
};
// 也可从cli参数中传递:webpack --mode=production
webpack执行流程
- 读取webpack的配置参数;
- 启动webpack,创建Compiler对象并开始解析项目;
- 从入口文件(entry)开始递归解析entry依赖的所有module。每找到一个module, 就会根据配置的loader去找相应的转换规则,对module进行转换后再解析当前module所依赖的module,形成依赖关系树。这些模块会以entry为分组,一个entry和所有相依赖的module也就是一个chunk,最后webpack会把所有chunk转换成bundle输出;
- 整个过程中webpack会通过发布订阅模式,向外抛出一些hooks,而webpack的插件即可通过监听这些关键的事件节点,执行插件任务进而达到干预输出结果的目的。
其中文件的解析与构建(第三步)是一个比较复杂的过程,在webpack源码中主要依赖于compiler
和compilation
两个核心对象实现。
-
compiler
对象是一个全局单例,他负责把控整个webpack打包的构建流程。 -
compilation
对象是每一次构建的上下文对象,它包含了当次构建所需要的所有信息,每次热更新和重新构建,compiler
都会重新生成一个新的compilation
对象,负责此次更新的构建过程。
构建本地服务
webpack-dev-server是Webpack提供了一个可选的本地开发服务器,这个本地服务器基于node.js构建,它是一个单独的组件,具体功能:
- 打包;
- 开启一个服务器;
- 实时的自动编译代码,只要我们对源代码做了保存(ctrl+s)。
tips:webpack-dev-server打包过后生成的bundle.js,不在我们配置的./dist目录下。而是一个暂存在内存中并且被认为是当前网站根目录下(这就需要注意一下将引入的包的目录改成根目录);
webpack-dev-server有两个核心配置:
- devServer配置项
contentBase:该配置项指定了服务器资源的根目录,如果不配置contentBase的话,那么contentBase默认是当前执行的目录,一般是项目的根目录;
port:指定了开启服务器的端口号,默认为8080;
host:配置 DevServer的服务器监听地址,默认为 127.0.0.1;
headers:该配置项可以在HTTP响应中注入一些HTTP响应头;
historyApiFallback:该配置项属性是用来应对返回404页面时定向跳转到特定页面的。一般是应用在单页应用,比如在访问路由时候,访问不到该路由的时候。通过该配置项,设置属性值为true的时候,会自动跳转到 index.html下,当然我们也可以手动通过正则来匹配路由;
// 跳到index.html页面
historyApiFallback: true
// 使用正则来匹配路由
historyApiFallback: {
rewrites: [
{ from: /^\/user/, to: '/user.html' },
{ from: /^\/home/, to: '/home.html' }
]
}
hot:前面已经讲过的热更新,需要配合plugin一起使用;
proxy : 该配置来解决跨域的问题,有时候我们使用webpack在本地启动服务器的时候,由于我们使用的访问的域名是 http://localhost:8081 这样的,但是我们服务端的接口是其他的;
// 假设服务端接口域名为:http://news.baidu.com
proxy: {
'/api': {
target: 'http://news.baidu.com', // 目标接口的域名
// secure: true, // https 的时候 使用该参数
changeOrigin: true, // 是否跨域
pathRewrite: {
'^/api' : '' // 重写路径
}
}
}
inline:设置为true,当源文件改变时会自动刷新页面;
open:该属性用于DevServer启动且第一次构建完成时,自动使用我们的系统默认浏览器去打开网页。也可以通过cli参数中设置(webpack-dev-sever --open);
compress:配置是否启用 gzip 压缩,boolean 类型,默认为 false;
overlay:该属性是用来在编译出错的时候,在浏览器页面上显示错误。该属性值默认为false,需要的话,设置该参数为true。
- sourceMap调试配置
sourceMap是一项将编译、打包、压缩后的代码映射回源代码的技术,由于打包压缩后的代码并没有阅读性可言,一旦在开发中报错或者遇到问题,直接在混淆代码中debug问题会带来非常糟糕的体验,sourceMap可以帮助我们快速定位到源代码的位置,提高我们的开发效率。sourceMap其实并不是Webpack特有的功能,而是Webpack支持sourceMap,像JQuery也支持souceMap。
module.exports = {
devtool: 'source-map' // 会生成对于调试的完整的.map文件,但同时也会减慢打包速度
}
优化webpack配置
关于webpack的优化这里不再做详细讲解,仅列出几个常见的优化项:
- 分离代码:可通过配置多入口(entry),提取公共代码(optimization.splitChunks),分离css(MiniCssExtractPlugin),合理的配置mode参数与devtool参数等实现;
- 缩小文件的搜索范围,可通过配置include、exclude、alias实现;
- 处理图片,依赖于url-loader通过配置options来限制只有小于1kb(可更加需要自行设置)的图片才转为base64;
- 压缩代码:optimization.minimize
- 离线缓存:webpack-mainfast
webpack版本迭代
从本来开始接触webpack至今已经经历了2次版本迭代,这里主要说下webpack4的变化,webpack5的新特性后续会继续补充。
- v3与v4的区别
- 新增mode配置,用于设置当前环境,有两个值:production、development ;
- CommonChunksPlugin和UglifyJsPlugin这两个plugin已经从webpack4中移除。可使用optimization.splitChunks进行模块划分(提取公用代码),optimization.minimize进行代码压缩;
- webpack4使用MiniCssExtractPlugin取代ExtractTextWebpackPlugin来实现css分离;
- v4与v5的区别