webpack 配置完全指南
概念
来看一下官网对 webpack 的定义:
本质上,webpack 是一个现代 JavaScript 应用程序的静态模块打包器(module bundler)。当 webpack 处理应用程序时,它会递归地构建一个依赖关系图(dependency graph),其中包含应用程序需要的每个模块,然后将所有这些模块打包成一个或多个 bundle。
首先 webpack 是一个静态模块打包器,所谓的静态模块,包括脚本、样式表和图片等等;webpack 打包时首先遍历所有的静态资源,根据资源的引用,构建出一个依赖关系图,然后再将模块划分,打包出一个或多个 bundle。再次白 piao 一下官网的图,生动的描述了这个过程:
提到 webpack,就不得不提 webpack 的四个核心概念
入口(entry):指示 webpack 应该使用哪个模块,来作为构建其内部依赖图的开始
输出(output):在哪里输出它所创建的 bundles
loader:让 webpack 能够去处理那些非 JavaScript 文件
插件(plugins):用于执行范围更广的任务
你的第一个打包器
我们首先在全局安装 webpack:
webpack 可以不使用配置文件,直接通过命令行构建,用法如下:
这里的 entry 和 output 就对应了上述概念中的入口和输入,我们来新建一个入口文件:
有了入口文件我们还需要通过命令行定义一下输入路径 dist/bundle.js:
这样 webpack 就会在 dist 目录生成打包后的文件。
我们也可以在项目目录新建一个 html 引入打包后的 bundle.js 文件查看效果。
配置文件
命令行的打包方式仅限于简单的项目,如果我们的项目较为复杂,有多个入口,我们不可能每次打包都把入口记下来;因此一般项目中都使用配置文件来进行打包;配置文件的命令方式如下:
配置文件默认的名称就是webpack.config.js
,一个项目中经常会有多套配置文件,我们可以针对不同环境配置不同的文件,通过--config
来进行切换:
多种配置类型
config 配置文件通过module.exports
导出一个配置对象:
除了导出为对象,还可以导出为一个函数,函数中会带入命令行中传入的环境变量等参数,这样可以更方便的对环境变量进行配置;比如我们在打包线上正式环境和线上开发环境可以通过env
进行区分:
另外还可以导出为一个 Promise,用于异步加载配置,比如可以动态加载入口文件:
入口
正如在上面提到的,入口是整个依赖关系的起点入口;我们常用的单入口配置是一个页面的入口:
它是下面的简写:
但是我们一个页面可能不止一个模块,因此需要将多个依赖文件一起注入,这时就需要用到数组了,代码在 demo2 中:
有时候我们一个项目可能有不止一个页面,需要将多个页面分开打包,entry 支持传入对象的形式,代码在 demo3 中:
这样 webpack 就会构建三个不同的依赖关系。
输出
output
选项用来控制 webpack 如何输入编译后的文件模块;虽然可以有多个 entry,但是只能配置一个output
:
这里我们配置了一个单入口,输出也就是 bundle.js;但是如果存在多入口的模式就行不通了,webpack 会提示Conflict: Multiple chunks emit assets to the same filename
,即多个文件资源有相同的文件名称;webpack 提供了占位符
来确保每一个输出的文件都有唯一的名称:
这样 webpack 打包出来的文件就会按照入口文件的名称来进行分别打包生成三个不同的 bundle 文件;还有以下不同的占位符字符串:
在这里引入 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 文件包含了经过加载和编译的最终源文件,所以它可以直接在浏览器中运行。
我们通过下面一张图更深入的理解这三个概念:
总结:
module,chunk 和 bundle 其实就是同一份逻辑代码在不同转换场景下的取了三个名字:我们直接写出来的是 module,webpack 处理时是 chunk,最后生成浏览器可以直接运行的 bundle。
hash、chunkhash、contenthash
理解了 chunk 的概念,相信上面表中 chunkhash 和 hash 的区别也很容易理解了;
hash:是跟整个项目的构建相关,只要项目里有文件更改,整个项目构建的 hash 值都会更改,并且全部文件都共用相同的 hash 值。
chunkhash:跟入口文件的构建有关,根据入口文件构建对应的 chunk,生成每个 chunk 对应的 hash;入口文件更改,对应 chunk 的 hash 值会更改。
contenthash:跟文件内容本身相关,根据文件内容创建出唯一 hash,也就是说文件内容更改,hash 就更改。
模式
在 webpack2 和 webpack3 中我们需要手动加入插件来进行代码的压缩、环境变量的定义,还需要注意环境的判断,十分的繁琐;在 webpack4 中直接提供了模式这一配置,开箱即可用;如果忽略配置,webpack 还会发出警告。
开发模式是告诉 webpack,我现在是开发状态,也就是打包出来的内容要对开发友好,便于代码调试以及实现浏览器实时更新。
生产模式不用对开发友好,只需要关注打包的性能和生成更小体积的 bundle。看到这里用到了很多 Plugin,不用慌,下面我们会一一解释他们的作用。
相信很多童鞋都曾有过疑问,为什么这边 DefinePlugin 定义环境变量的时候要用JSON.stringify("production")
,直接用"production"
不是更简单吗?
我们首先来看下JSON.stringify("production")
生成了什么;运行结果是""production""
,注意这里,并不是你眼睛花了或者屏幕上有小黑点,结果确实比"production"
多嵌套了一层引号。
我们可以简单的把 DefinePlugin 这个插件理解为将代码里的所有process.env.NODE_ENV
替换为字符串中的内容
。假如我们在代码中有如下判断环境的代码:
这样生成出来的代码就会编译成这样:
但是我们代码中可能并没有定义production
变量,因此会导致代码直接报错,所以我们需要通过 JSON.stringify 来包裹一层:
这样编译出来的代码就没有问题了。
自动生成页面
在上面的代码中我们发现都是手动来生成 index.html,然后引入打包后的 bundle 文件,但是这样太过繁琐,而且如果生成的 bundle 文件引入了 hash 值,每次生成的文件名称不一样,因此我们需要一个自动生成 html 的插件;首先我们需要安装这个插件:
在 demo3 中,我们生成了三个不同的 bundle.js,我们希望在三个不同的页面能分别引入这三个文件,如下修改 config 文件:
我们以 index.html 作为模板文件,生成 home、list、detail 三个不同的页面,并且通过 chunks 分别引入不同的 bundle;如果这里不写 chunks,每个页面就会引入所有生成出来的 bundle。
html-webpack-plugin 还支持以下字段:
上面设置 title 后需要在模板文件中设置模板字符串:
loader
loader 用于对模块 module 的源码进行转换,默认 webpack 只能识别 commonjs 代码,但是我们在代码中会引入比如 vue、ts、less 等文件,webpack 就处理不过来了;loader 拓展了 webpack 处理多种文件类型的能力,将这些文件转换成浏览器能够渲染的 js、css。
module.rules
允许我们配置多个 loader,能够很清晰的看出当前文件类型应用了哪些 loader,loader 的代码均在 demo4 中。
我们可以看到 rules 属性值是一个数组,每个数组对象表示了不同的匹配规则;test 属性时一个正则表达式,匹配不同的文件后缀;use 表示匹配了这个文件后调用什么 loader 来处理,当有多个 loader 的时候,use 就需要用到数组。
多个 loader 支持链式传递,能够对资源进行流水线处理,上一个 loader 处理的返回值传递给下一个 loader;loader 处理有一个优先级,从右到左,从下到上;在上面 demo 中对 css 的处理就遵从了这个优先级,css-loader 先处理,处理好了再给 style-loader;因此我们写 loader 的时候也要注意前后顺序。
css-loader 和 style-loader
css-loader 和 style-loader 从名称看起来功能很相似,然而两者的功能有着很大的区别,但是他们经常会成对使用;安装方法:
css-loader 用来解释 @import 和 url();style-loader 用来将 css-loader 生成的样式表通过<style>标签
,插入到页面中去。
然后在入口文件中将 index.css 引入,就能看到打包的效果,页面中插入了三个 style 标签,代码在 demo4:
sass-loader 和 less-loader
这两个 loader 看名字大家也能猜到了,就是用来处理 sass 和 less 样式的。安装方法:
在 config 中进行配置,代码在 demo4:
postcss-loader
都 0202 年了,小伙伴肯定不想一个一个的手动添加-moz、-ms、-webkit 等浏览器私有前缀;postcss 提供了很多对样式的扩展功能;啥都不说,先安装起来:
老规矩,还是在 config 中进行配置:
正当我们兴冲冲的打包看效果时,发现样式还是老样子,并没有什么改变。
这是因为 postcss 主要功能只有两个:第一就是把 css 解析成 JS 可以操作的抽象语法树 AST,第二就是调用插件来处理 AST 并得到结果;所以 postcss 一般都是通过插件来处理 css,并不会直接处理,所以我们需要先安装一些插件:
在项目根目录新建一个.browserslistrc
文件。
我们将 postcss 的配置单独提取到项目根目录下的postcss.config.js
:
有了autoprefixer
插件,我们打包后的 css 就自动加上了前缀。
babel-loader
兼容低版本浏览器的痛相信很多童鞋都经历过,写完代码发现自己的 js 代码不能运行在 IE10 或者 IE11 上,然后尝试着引入各种 polyfill;babel 的出现给我们提供了便利,将高版本的 ES6 甚至 ES7 转为 ES5;我们首先安装 babel 所需要的依赖:
然后在 config 添加 loader 对 js 进行处理:
同样的,我们把 babel 的配置提取到根目录,新建一个.babelrc
文件:
我们可以在 index.js 中尝试写一些 es6 的语法,看到代码会被转译成 es5,代码在 demo4 中。由于 babel-loader 的转译速度很慢,在后面我们加入了时间插件后可以看到每个 loader 的耗时,babel-loader 是最耗时间;因此我们要尽可能少的使用 babel 来转译文件,我们对 config 进行改进,
正则上使用$
来进行精确匹配,通过 exclude 将 node_modules 中的文件进行排除,include 将只匹配 src 中的文件;可以看出来 include 的范围比 exclude 更缩小更精确,因此也是推荐使用 include。
file-loader 和 url-loader
file-loader 和 url-loader 都是用来处理图片、字体图标等文件;url-loader 工作时分两种情况:当文件大小小于 limit 参数,url-loader 将文件转为 base-64 编码,用于减少 http 请求;当文件大小大于 limit 参数时,调用 file-loader 进行处理;因此我们优先使用 url-loader,首先还是进行安装,安装 url-loader 之前还需要把 file-loader 先安装:
接下来还是修改 config:
我们在 css 中给 body 添加一个小于 10k 的居中背景图片:
打包后查看 body 的样式可以发现图片已经被替换成 base64 格式的 url 了,代码在 demo4。
html-withimg-loader
如果我们在页面上引用一个图片,会发现打包后的 html 还是引用了 src 目录下的图片,这样明显是错误的,因此我们还需要一个插件对 html 引用的图片进行处理:
老样子还是在 config 中对 html 进行配置:
然鹅,打开页面发现却是这样的:
这是因为在 url-loader 中把每个图片作为一个模块来处理了,我们还需要去 url-loader 中修改:
这样我们在页面上的图片引用也被修改了,代码在 demo4 中。
注
html-withimg-loader 会导致 html-webpack-plugin 插件注入 title 的模板字符串<%= htmlWebpackPlugin.options.title %>
失效,原封不动的展示在页面上;因此,如果我们想保留两者的功能需要在配置 config 中把 html-withimg-loader 删除并且通过下面的方式来引用图片:
vue-loader
最后说一下一个比较特殊的 vue-loader,看名字就知道是用来处理 vue 文件的。
我们首先来创建一个 vue 文件,具体代码在 demo5 中:
然后在 webpack 的入口文件中引用它:
不过 vue-loader 和其他 loader 不太一样,除了将它和.vue
文件绑定之外,还需要引入它的一个插件:
这样我们就能愉快的在代码中写 vue 了。
搭建开发环境
在上面的 demo 中我们都是通过命令行打包生成 dist 文件,然后直接打开 html 或者通过static-server
来查看页面的;但是开发中我们写完代码每次都来打包会严重影响开发的效率,我们期望的是写完代码后立即就能够看到页面的效果;webpack-dev-server 就很好的提供了一个简单的 web 服务器,能够实时重新加载。
首先在我们的项目中安装依赖:
webpack-dev-server 的用法和 wepack 一样,只不过他会额外启动一个 express 的服务器。我们在项目中新建一个webpack.dev.config.js
配置文件,单独对开发环境进行一个配置,相关代码在 demo6 中:
通过命令行webpack-dev-server
来启动服务器,启动后我们发现根目录并没有生成任何文件,因为 webpack 打包到了内存中,不生成文件的原因在于访问内存中的代码比访问文件中的代码更快。
我们在 public/index.html 的页面上有时候会引用一些本地的静态文件,直接打开页面的会发现这些静态文件的引用失效了,我们可以修改 server 的工作目录,同时指定多个静态资源的目录:
热更新(Hot Module Replacemen 简称 HMR)是在对代码进行修改并保存之后,webpack 对代码重新打包,并且将新的模块发送到浏览器端,浏览器通过新的模块替换老的模块,这样就能在不刷新浏览器的前提下实现页面的更新。
可以看出浏览器和 webpack-dev-server 之间通过一个 websock 进行连接,初始化的时候 client 端保存了一个打包后的 hash 值;每次更新时 server 监听文件改动,生成一个最新的 hash 值再次通过 websocket 推送给 client 端,client 端对比两次 hash 值后向服务器发起请求返回更新后的模块文件进行替换。
我们点击源码旁的行数看一下编译后的源码是什么样的:
发现跟我们的源码差距还是挺大的,本来是一个简单 add 函数,通过 webpack 的模块封装,已经很难理解原来代码的含义了,因此,我们需要将编译后的代码映射回源码;devtool 中不同的配置有不同的效果和速度,综合性能和品质后,我们一般在开发环境使用cheap-module-eval-source-map
,在生产环境使用source-map
。
其他各模式的对比:
plugins
在上面我们也介绍了 DefinePlugin、HtmlWebpackPlugin 等很多插件,我们发现这些插件都能够不同程度的影响着 webpack 的构建过程,下面还有一些常用的插件,plugins 相关代码在 demo7 中。
clean-webpack-plugin
clean-webpack-plugin 用于在打包前清理上一次项目生成的 bundle 文件,它会根据 output.path 自动清理文件夹;这个插件在生产环境用的频率非常高,因为生产环境经常会通过 hash 生成很多 bundle 文件,如果不进行清理的话每次都会生成新的,导致文件夹非常庞大;这个插件安装使用非常方便:
安装后我们在 config 中配置一下就可以了:
mini-css-extract-plugin
我们之前的样式都是通过 style-loader 插入到页面中去,但是生产环境需要单独抽离样式文件,mini-css-extract-plugin 就可以帮我从 js 中剥离样式:
我们在开发环境使用 style-loader,生产环境使用 mini-css-extract-plugin:
引入 loader 后,我们还需要配置 plugin,提取的 css 同样支持output.filename
中的占位符字符串。
optimize-css-assets-webpack-plugin
我们可以发现虽然配置了production
模式,打包出来的 js 压缩了,但是打包出来的 css 确没有压缩;在生产环境我们需要对 css 进行一下压缩:
然后也是引入插件:
copy-webpack-plugin
和 demo6 中一样,我们在 public/index.html 中引入了静态资源,但是打包的时候 webpack 并不会帮我们拷贝到 dist 目录,因此 copy-webpack-plugin 就可以很好地帮我做拷贝的工作了
在 config 中配置我们需要拷贝的源路径和目标路径:
ProvidePlugin
ProvidePlugin 可以很快的帮我们加载想要引入的模块,而不用 require。一般我们加载 jQuery 需要先把它 import:
但是我们在 config 中配置 ProvidePlugin 插件后能够不用 import,直接使用$
:
但是如果在项目中引入了太多模块并且没有 require 会让人摸不着头脑,因此建议加载一些常见的比如 jQuery、vue、lodash 等。
loader 和 plugin 的区别
介绍了这么多 loader 和 plugin,我们来回顾一下他们两者的区别:
loader:由于 webpack 只能识别 js,loader 相当于翻译官的角色,帮助 webpack 对其他类型的资源进行转译的预处理工作。plugins:plugins 扩展了 webpack 的功能,在 webpack 运行时会广播很多事件,plugin 可以监听这些事件,然后通过 webpack 提供的 API 来改变输出结果。
评论