针对webpack,是大家(前端开发)在日常的开发中都会遇见的,通过书写的方式输出,学习到的关于前端工程化的小知识点的总结和学习,形成自己的知识体系
概念
webpack官网定义:
webpack 是一个现代 JavaScript 应用程序的静态模块打包器(module bundler)。当 webpack 处理应用程序时,它会递归地构建一个依赖关系图(dependency graph),其中包含应用程序需要的每个模块,然后将所有这些模块打包成一个或多个 bundle。
在开始了解webpack配置前,首先需要理解四个核心概念:
- 入口(entry):webpack 应该使用哪个模块,来作为构建其内部依赖图的开始。
- 输出(output):webpack 在哪里输出它所创建的 bundles,以及如何命名这些文件。
- loader:能够去处理那些非 JavaScript 文件(webpack 自身只理解 JavaScript)。
- 插件(plugins):用于执行范围更广的任务。
安装及构建
// npm 安装
npm install webpack webpack-cli -g
// yarn 安装
yarn global add webpack webpack-cli
安装好后,可以在不适用配置文件的方法,直接对文件进行打包:webpack
在这里我们会看见一个WARNING
的信息,这是因为没有设置mode
,我们只需要加一个参数-p
即可:
webpack -p index
这样默认会生成一个dist
文件夹,里面有个main.js
文件:
有了入口文件我们还需要通过命令行定义一下输入路径dist/bundle.js
:
webpack -p index.js -o dist/bundle.js
webpack 配置文件
命令行的打包构建方式仅限于简单的项目,如果在生产中,项目复杂,多个入口,我们就不可能每次打包都输入一连串的入口文件地址,也难以记住;因此一般项目中都使用配置文件来进行打包;配置文件的命令方式如下:
webpack [--config webpack.config.js]
配置文件默认的名称就是webpack.config.js
,一个项目中经常会有多套配置文件,我们可以针对不同环境配置不同的额文件,通过--config
来进行更换:
// 开发环境
webpack --config webpack.config.dev.js
// 生产环境
webpack --config webpack.config.prod.js
多种配置类型
config
配置文件通过module.exports
导出一个配置对象:
// webpack.config.js
const path = require('path')
const resolve = function (dir) {
return path.resolve(__dirname, dir)
}
module.exports = {
entry: {
app: resolve('../index.js')
},
output: {
filename: '[name].[hash:8].js',
path: resolve('../dist')
},
}
除了导出为对象,还可以导出为一个函数,函数中会带入命令行中传入的环境变量等参数,这样可以更方便的对环境变量进行配置;比如我们可以通过ENV
来区分不同环境:
const path = require('path')
const resolve = function (dir) {
return path.resolve(__dirname, dir)
}
module.exports = function(ENV, argv) {
return {
// 其他配置
entry: resolve('../index.js'),
output: {}
}
}
还可以导出为一个Promise,用于异步加载配置,比如可以动态加载入口文件:
entry: () => './demo'
或
entry: () => new Promise((resolve) => resolve(['./demo', './demo2']))
入口
正如在上面提到的,入口是整个依赖关系的起点入口;我们常用的单入口配置是一个页面的入口:
module.exports = {
entry: resolve('../index.js')
}
但是我们项目中可能不止一个模块,因此需要将多个依赖文件一起注入,这时就需要用到数组了:
module.exports = {
entry: [
'@babel/polyfill',
resolve('../index.js')
]
}
如果我们项目中有多个入口起点,则就需要用到对象形式了:
// webpack 就会构建两个不同的依赖关系
module.exports = {
entry: {
app: resolve('../index.js'),
share: resolve('../share.js')
}
}
输出
output
选项用来控制webpack如何输入编译后的文件模块;虽然可以有多个entry
,但是只能配置一个output
:
module.exports = {
entry: resolve('../index.js'),
output: {
filename: 'index.js',
path: resolve('../dist')
},
}
这里我们配置了一个单入口,输出也就是index.js
;但是如果存在多入口的模式就行不通了,webpack会提示Conflict: Multiple chunks emit assets to the same filename
,即多个文件资源有相同的文件名称;webpack提供了占位符
来确保每一个输出的文件都有唯一的名称:
module.exports = {
entry: {
app: resolve('../index.js'),
share: resolve('../index.js'),
},
output: {
filename: '[name].bundle.js',
path: resolve('../dist')
},
}
这样webpack打包出来的文件就会按照入口文件的名称来进行分别打包生成三个不同的bundle文件;还有以下不同的占位符字符串:
占位符 | 描述 |
---|---|
[hash] | 模块标识符(module identifier)的 hash |
[chunkhash] | chunk 内容的 hash |
[name] | 模块名称 |
[id] | 模块标识符 |
[query] | 模块的 query,例如,文件名 ? 后面的字符串 |
在这里引入module
、chunk
和bundle
的概念,上面代码中也经常会看到有这两个名词的出现,那么他们三者到底有什么区别呢?首先我们发现module
是经常出现在我们的代码中,比如module.exports
;而chunk
经常和entry
一起出现,bundle
总是和output
一起出现。
- module:我们写的源码,无论是commonjs还是amdjs,都可以理解为一个个的module
- chunk:当我们写的module源文件传到webpack进行打包时,webpack会根据文件引用关系生成chunk文件, webpack 会对这些chunk文件进行一些操作
- bundle:webpack处理好chunk文件后,最后会输出bundle文件,这个bundle文件包含了经过加载和编译的最终源文件,所以它可以直接在浏览器中运行。
hash、chunkhash、contenthash
理解了chunk的概念,相信上面表中chunkhash和hash的区别也很容易理解了;
- hash:是跟整个项目的构建相关,只要项目里有文件更改,整个项目构建的hash值都会更改,并且全部文件都共用相同的hash值。
- chunkhash:跟入口文件的构建有关,根据入口文件构建对应的chunk,生成每个chunk对应的hash;入口文件更改,对应chunk的hash值会更改。
- contenthash:跟文件内容本身相关,根据文件内容创建出唯一hash,也就是说文件内容更改,hash就更改。
模式
在webpack2和webpack3中我们需要手动加入插件来进行代码的压缩、环境变量的定义,还需要注意环境的判断,十分的繁琐;在webpack4中直接提供了模式这一配置,开箱即可用;如果忽略配置,webpack还会发出警告。
module.exports = {
mode: 'development/production'
}
开发模式是告诉webpack,我现在是开发状态,也就是打包出来的内容要对开发友好,便于代码调试以及实现浏览器实时更新。
生产模式不用对开发友好,只需要关注打包的性能和生成更小体积的bundle。看到这里用到了很多Plugin,不用慌,下面我们会一一解释他们的作用。
相信很多童鞋都曾有过疑问,为什么这边DefinePlugin
定义环境变量的时候要用JSON.stringify("production")
,直接用"production"
不是更简单吗?
我们首先来看下JSON.stringify("production")
生成了什么;运行结果是""production"
",注意这里,并不是你眼睛花了或者屏幕上有小黑点,结果确实比"production"
多嵌套了一层引号。
我们可以简单的把DefinePlugin
这个插件理解为将代码里的所有process.env.NODE_ENV
替换为字符串中的内容。假如我们在代码中有如下判断环境的代码:
// webpack.config.js
module.exports = {
plugins: [
new webpack.DefinePlugin({
"process.env.NODE_ENV": "production"
}),
]
}
// index.js
if (process.env.NODE_ENV === 'production') {
console.log('production');
}
这样生成出来的代码就会编译成这样:
//dist/bundle.js
//代码中并没有定义production变量
if (production === 'production') {
console.log('production');
}
但是我们代码中可能并没有定义production
变量,因此会导致代码直接报错,所以我们需要通过JSON.stringify
来包裹一层:
//webpack.config.js
module.exports = {
plugins: [
new webpack.DefinePlugin({
//"process.env.NODE_ENV": JSON.stringify("production")
//相当于
"process.env.NODE_ENV": '"production"'
}),
]
}
//dist/bundle.js
if ("production" === 'production') {
console.log('production');
}
生成HTML文件(html-webpack-plugin)
在上面的代码中我们发现都是手动来生成index.html,然后引入打包后的bundle文件,但是这样太过繁琐,而且如果生成的bundle文件引入了hash值,每次生成的文件名称不一样,因此我们需要一个自动生成html的插件;首先我们需要安装这个插件:yarn add html-webpack-plugin -D 或者 npm install html-webpack-plugin -D
使用:
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
// 其他代码
plugins: [
new HtmlWebpackPlugin({
// 模板文件
template: resolve('../public/index.html'),
// 生成的html名称
filename: 'index.html',
// icon
favicon: resolve('../public/logo.ico')
}),
]
}
webpack loader
loader 用于对模块的源代码进行转换。默认webpack只能识别commonjs代码,但是我们在代码中会引入比如vue、ts、less等文件,webpack就处理不过来了;loader拓展了webpack处理多种文件类型的能力,将这些文件转换成浏览器能够渲染的js、css。
module.rules
允许我们配置多个loader,能够很清晰的看出当前文件类型应用了哪些loader。
module.exports = {
module: {
rules: [
{ test: /\.css$/, use: 'css-loader' },
{ test: /\.ts$/, use: 'ts-loader' }
]
}
};
loader 特性
- loader 支持链式传递。能够对资源使用流水线(pipeline)。一组链式的 loader 将按照相反的顺序执行。loader 链中的第一个 loader 返回值给下一个 loader。在最后一个 loader,返回 webpack 所预期的 JavaScript。
- loader 可以是同步的,也可以是异步的。
- loader 运行在 Node.js 中,并且能够执行任何可能的操作。
- loader 接收查询参数。用于对 loader 传递配置。
- loader 也能够使用 options 对象进行配置。
- 除了使用 package.json 常见的 main 属性,还可以将普通的 npm 模块导出为 loader,做法是在 package.json 里定义一个 loader 字段。
- 插件(plugin)可以为 loader 带来更多特性。
- loader 能够产生额外的任意文件。
loader 通过(loader)预处理函数,为 JavaScript 生态系统提供了更多能力。 用户现在可以更加灵活地引入细粒度逻辑,例如压缩、打包、语言翻译和其他更多。
babel-loader
兼容低版本浏览器的痛相信很多童鞋都经历过,写完代码发现自己的js代码不能运行在IE10或者IE11上,然后尝试着引入各种polyfill;babel的出现给我们提供了便利,将高版本的ES6甚至ES7转为ES5;我们首先安装babel所需要的依赖:yarn add -D babel-loader @babel/core @babel/preset-env @babel/plugin-transform-runtime
由于babel-loader的转译速度很慢,在后面我们加入了时间插件后可以看到每个loader的耗时,babel-loader是最耗时间;因此我们要尽可能少的使用babel来转译文件,正则上使用$来进行精确匹配,通过exclude将node_modules
中的文件进行排除,include
将只匹配src
中的文件;可以看出来include的范围比exclude更缩小更精确,因此也是推荐使用include。
// 省略其他代码
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
include: [resolve('src')]
use: {
loader: 'babel-loader',
options: {
presets: [
['@babel/preset-env', { targets: "defaults" }]
],
plugins: ['@babel/plugin-proposal-class-properties']
}
}
}
]
}
file-loader 和 url-loader
file-loader
和url-loader
都是用来处理图片、字体图标等文件;url-loader
工作时分两种情况:当文件大小小于limit参数,url-loader
将文件转为base-64编码,用于减少http请求;当文件大小大于limit参数时,调用file-loader
进行处理;因此我们优先使用url-loader
。
module: {
rules: [
{
test: /\.(jpe?g|png|gif)$/i, //图片文件
use: [
{
loader: 'url-loader',
options: {
// 10K
limit: 1024,
//资源路径
outputPath: resolve('../dist/images')
},
}
],
exclude: /node_modules/
},
]
}
搭建webpack开发环境
在上面我们都是通过命令行打包生成 dist 文件,然后直接打开html或者通过static-server来查看页面的;但是开发中我们写完代码每次都来打包会严重影响开发的效率,我们期望的是写完代码后立即就能够看到页面的效果;webpack-dev-server
就很好的提供了一个简单的web服务器,能够实时重新加载。
webpack-dev-server
的用法和wepack
一样,只不过他会额外启动一个express
的服务器。我们在项目中webpack.config.dev.js
配置文件对开发环境进行一个配置:
module.exports = {
mode: 'development',
plugins: [
new Webpack.HotModuleReplacementPlugin()
],
devtool: 'cheap-module-eval-source-map',
devServer: {
// 端口
port: 3300,
// 启用模块热替换
hot: true,
// 自动打开浏览器
open: true,
// 设置代理
proxy:{
"/api/**":{
"target":"http://127.0.0.1:8075/",
"changeOrigin": true
}
}
}
}
通过命令行webpack-dev-server
来启动服务器,启动后我们发现根目录并没有生成任何文件,因为webpack
打包到了内存中,不生成文件的原因在于访问内存中的代码比访问文件中的代码更快。
我们在public/index.html
的页面上有时候会引用一些本地的静态文件,直接打开页面的会发现这些静态文件的引用失效了,我们可以修改server
的工作目录,同时指定多个静态资源的目录:
contentBase: [
path.join(__dirname, "public"),
path.join(__dirname, "assets")
]
热更新(Hot Module Replacemen简称HMR)是在对代码进行修改并保存之后,webpack对代码重新打包,并且将新的模块发送到浏览器端,浏览器通过新的模块替换老的模块,这样就能在不刷新浏览器的前提下实现页面的更新。
webpack plugins
上面介绍了DefinePlugin、HtmlWebpackPlugin等很多插件,我们发现这些插件都能够不同程度的影响着webpack的构建过程,下面还有一些常用的插件:
clean-webpack-pluginclean-webpack-plugin
用于在打包前清理上一次项目生成的bundle文件,它会根据output.path自动清理文件夹;这个插件在生产环境用的频率非常高,因为生产环境经常会通过hash生成很多bundle文件,如果不进行清理的话每次都会生成新的,导致文件夹非常庞大。
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
module.exports = {
plugins: [
new CleanWebpackPlugin(),
],
}
mini-css-extract-plugin
我们在使用webpack构建工具的时候,通过style-loader
,可以把解析出来的css通过js插入内部样式表的方式到页面中,mini-css-extract-plugin插件也是用来提取css到单独的文件的,该插件有个前提条件,只能用于webpack 4及以上的版本,所以如果使用的webpack版本低于4,,那还是用回extract-text-webpack-plugin插件。
const MiniCssExtractPlugin = require("mini-css-extract-plugin")
module.exports = {
// 省略其他代码
module: {
rules: [
{
test: /\.less$/,
use: [
{
loader: dev ? 'style-loader': MiniCssExtractPlugin.loader
},
{
loader: 'css-loader'
},
{
loader: 'less-loader'
}
]
}
]
},
plugins: [
new MiniCssExtractPlugin({
filename: "[name].[hash:8].css",
})
]
}
copy-webpack-plugin
我们在public/index.html中引入了静态资源,但是打包的时候webpack并不会帮我们拷贝到dist目录,因此copy-webpack-plugin就可以很好地帮我做拷贝的工作了。
const CopyWebpackPlugin = require('copy-webpack-plugin')
module.exports = {
plugins: [
new CleanWebpackPlugin(),
new CopyWebpackPlugin([{
from: path.resolve(__dirname, '../static'),
to: path.resolve(__dirname, '../dist/static')
}])
],
}
ProvidePlugin
ProvidePlugin
可以很快的帮我们加载想要引入的模块,而不用require
。一般我们加载jQuery
需要先把它import
:
import $ from 'jquery'
$('#layout').html('test')
但是我们在config中配置ProvidePlugin
插件后能够不用import,直接使用$
:
module.exports = {
plugins: [
new webpack.ProvidePlugin({
$: 'jquery',
jQuery: 'jquery'
}),
]
}
在项目中引入了太多模块并且没有require
会让人摸不着头脑,因此建议加载一些常见的比如jQuery、vue、lodash等。
loader和plugin的区别(面试中常遇见)
- 对于loader,它是一个转换器,将A文件进行编译形成B文件,这里操作的是文件,比如将A.scss转换为A.css,单纯的文件转换过程
- plugin是一个扩展器,它丰富了webpack本身,针对是loader结束后,webpack打包的整个过程,它并不直接操作文件,而是基于事件机制工作,会监听webpack打包过程中的某些节点,执行广泛的任务