一、webpack解析代码模块路径
在 webpack 支持的前端代码模块化中,我们可以使用类似 import * as m from ‘./index.js’ 来引用代码模块 index.js。
引用第三方类库则是像这样:import React from ‘react’。webpack 构建的时候,会解析依赖后,然后再去加载依赖的模块文件,那么 webpack 如何将上述编写的 ./index.js 或 react 解析成对应的模块文件路径呢?
webpack 中有一个很关键的模块 enhanced-resolve 就是处理依赖模块路径的解析的,这个模块可以说是 Node.js 那一套模块路径解析的增强版本,有很多可以自定义的解析配置。
模块解析规则
简单整理一下基本的模块解析规则,以便更好地理解后续 webpack 的一些配置会产生的影响。
- 解析相对路径
- 查找相对当前模块的路径下是否有对应文件或文件夹
- 是文件则直接加载
- 是文件夹则继续查找文件夹下的 package.json 文件
- 有 package.json 文件则按照文件中 main 字段的文件名来查找文件
- 无 package.json 或者无 main 字段则查找 index.js 文件
- 解析模块名
查找当前文件目录下,父级目录及以上目录下的 node_modules 文件夹,看是 否有对应名称的模块 - 解析绝对路径(不建议使用)
直接查找对应路径的文件
在 webpack 配置中,和模块路径解析相关的配置都在 resolve 字段下:
1 | module.exports = { |
常用配置
- resolve.alias
假设我们有个 utils 模块极其常用,经常编写相对路径很麻烦,希望可以直接 import ‘utils’ 来引用,那么我们可以配置某个模块的别名,如:
1 | alias: { |
上述的配置是模糊匹配,意味着只要模块路径中携带了 utils 就可以被替换掉,如:
1 | import 'utils/query.js' // 等同于 import '[项目绝对路径]/src/utils/query.js' |
如果需要进行精确匹配可以使用:
1 | alias: { |
- resolve.extensions
1 | extensions: ['.wasm', '.mjs', '.js', '.json', '.jsx', '.css'], |
这个配置可以定义在进行模块路径解析时,webpack 会尝试帮你补全那些后缀名来进行查找,例如有了上述的配置,当你在 src/utils/ 目录下有一个 common.js 文件时,就可以这样来引用:
1 | import * as common from './src/utils/common' |
webpack 会尝试给你依赖的路径添加上 extensions 字段所配置的后缀,然后进行依赖路径查找,所以可以命中 src/utils/common.js 文件。
- resolve.modules
对于直接声明依赖名的模块(如 react ),webpack 会类似 Node.js 一样进行路径搜索,搜索 node_modules 目录,这个目录就是使用 resolve.modules 字段进行配置的,默认就是:
1 | resolve: { |
通常情况下,不会调整这个配置,但是如果可以确定项目内所有的第三方依赖模块都是在项目根目录下的 node_modules 中的话,那么可以在 node_modules 之前配置一个确定的绝对路径:
1 | resolve: { |
- resolve.mainFields
package.json 文件则按照文件中 main 字段的文件名来查找文件
之前有提到这么一句话,其实确切的情况并不是这样的,webpack 的 resolve.mainFields 配置可以进行调整。当引用的是一个模块或者一个目录时,会使用 package.json 文件的哪一个字段下指定的文件,默认的配置是这样的:
1 | resolve: { |
因为通常情况下,模块的 package 都不会声明 browser 或 module 字段,所以便是使用 main 了。
在 NPM packages 中,会有些 package 提供了两个实现,分别给浏览器和 Node.js 两个不同的运行时使用,这个时候就需要区分不同的实现入口在哪里。如果你有留意一些社区开源模块的 package.json 的话,你也许会发现 browser 或者 module 等字段的声明。
- resolve.mainFiles
当目录下没有 package.json 文件时,想要默认使用目录下的 index.js 这个文件,其实这个也是可以配置的,使用 resolve.mainFiles 字段,默认配置是:
1 | resolve: { |
通常情况下我们也无须修改这个配置,index.js 基本就是约定俗成的了。
- resolve.resolveLoader
这个字段 resolve.resolveLoader 用于配置解析 loader 时的 resolve 配置,原本 resolve 的配置项在这个字段下基本都有。我们看下默认的配置:
1 | resolve: { |
这里提供的配置相对少用,我们一般遵从标准的使用方式,使用默认配置,然后把 loader 安装在项目根路径下的 node_modules 下就可以了。
二、配置loader
loader匹配规则
当我们需要配置 loader 时,都是在 module.rules 中添加新的配置项,在该字段中,每一项被视为一条匹配使用 loader 的规则。
1 | module.exports = { |
loader 的匹配规则中有两个最关键的因素:一个是匹配条件,一个是匹配规则后的应用。
匹配条件通常都使用请求资源文件的绝对路径来进行匹配,在官方文档中称为 resource,除此之外还有比较少用到的 issuer,则是声明依赖请求的源文件的绝对路径。
上述代码中的 test 和 include 都用于匹配 resource 路径,是 resource.test 和 resource.include 的简写,你也可以这么配置:
1 | module.exports = { |
issuer 规则匹配的场景比较少见,可以用它来尝试约束某些类型的文件中只能引用某些类型的文件。
当规则的条件匹配时,便会使用对应的 loader 配置,如上述例子中的 babel-loader。
规则条件配置
大多数情况下,配置 loader 的匹配条件时,只要使用 test 字段就好了,很多时候都只需要匹配文件后缀名来决定使用什么 loader,但也不排除在某些特殊场景下,我们需要配置比较复杂的匹配条件。webpack 的规则提供了多种配置形式:
- { test: … } 匹配特定条件
- { include: … } 匹配特定路径
- { exclude: … } 排除特定路径
- { and: […] }必须匹配数组中所有条件
- { not: […] } 排除匹配数组中所有条件
- { or: […] } 匹配数组中任意一个条件
上述的所谓条件的值可以是:
- 字符串:必须以提供的字符串开始,所以是字符串的话,这里我们需要提供绝对路径
- 正则表达式:调用正则的 test 方法来判断匹配
- 函数:(path) => boolean,返回 true 表示匹配
- 数组:至少包含一个条件的数组
- 对象:匹配所有属性值的条件
1 | rules: [ |
module type
webpack 4.x 版本强化了 module type,即模块类型的概念,相当于 webpack 内置一个更加底层的文件类型处理,暂时只有 JS 相关的支持,后续会再添加 HTML 和 CSS 等类型。不同的模块类型类似于配置了不同的 loader,webpack 会有针对性地进行处理,现阶段实现了以下 5 种模块类型。
- javascript/auto:即 webpack 3 默认的类型,支持现有的各种 JS 代码模块类型 —— CommonJS、AMD、ESM
- javascript/esm:ECMAScript modules,其他模块系统,例如 CommonJS 或者 AMD 等不支持,是 .mjs 文件的默认类型
- javascript/dynamic:CommonJS 和 AMD,排除 ESM
- javascript/json:JSON 格式数据,require 或者 import 都可以引入,是 .json 文件的默认类型
- webassembly/experimental:WebAssembly modules,当前还处于试验阶段,是 .wasm 文件的默认类型
如果不希望使用默认的类型的话,在确定好匹配规则条件时,我们可以使用 type 字段来指定模块类型,例如把所有的 JS 代码文件都设置为强制使用 ESM 类型:
1 | { |
上述做法是可以帮助你规范整个项目的模块系统,但是如果遗留太多不同类型的模块代码时,还是直接使用默认的 javascript/auto。
使用loader配置
在当前版本的 webpack 中,module.rules 的匹配规则最重要的还是用于配置 loader,我们可以使用 use 字段:
1 | rules: [ |
use 字段可以是一个数组,也可以是一个字符串或者表示 loader 的对象。如果只需要一个 loader,也可以这样:
1 | use: { loader: 'babel-loader', options: { ... } } |
loader应用顺序
,一个匹配规则中可以配置使用多个 loader,即一个模块文件可以经过多个 loader 的转换处理,在一个rule中进行的执行顺序是从右往左。
如果多个 rule 匹配了同一个模块文件,loader 的应用顺序又是怎样的呢?
1 | rules: [ |
eslint-loader 要检查的是人工编写的代码,如果在 babel-loader 之后使用,那么检查的是 Babel 转换后的代码,所以必须在 babel-loader 处理之前使用。
这样无法法保证 eslint-loader 在 babel-loader 应用前执行。webpack 在 rules 中提供了一个 enforce 的字段来配置当前 rule 的 loader 类型,没配置的话是普通类型,我们可以配置 pre 或 post,分别对应前置类型或后置类型的 loader。
还有一种行内 loader,即我们在应用代码中引用依赖时直接声明使用的 loader,如 const json = require(‘json-loader!./file.json’) 这种,不建议在应用开发中使用这种loader
所有的 loader 按照前置 -> 行内 -> 普通 -> 后置的顺序执行。所以当我们要确保 eslint-loader 在 babel-loader 之前执行时,可以如下添加 enforce 配置:
1 | { |
通常建议把要应用的同一类型 loader 都写在同一个匹配规则中,这样更好维护和控制。
使用noParse
在 webpack 中,我们需要使用的 loader 是在 module.rules 下配置的,webpack 配置中的 module 用于控制如何处理项目中不同类型的模块。
除了 module.rules 字段用于配置 loader 之外,还有一个 module.noParse 字段,可以用于配置哪些模块文件的内容不需要进行解析。对于一些不需要解析依赖(即无依赖) 的第三方大型类库等,可以通过这个字段来配置,以提高整体的构建速度。
使用 noParse 进行忽略的模块文件中不能使用 import、require、define 等导入机制。
1 | module.exports = { |
noParse 从某种程度上说是个优化配置项,日常也可以不去使用。
webpack 的 loader 相关配置都在 module.rules 字段下,我们需要通过 test、include、exclude 等配置好应用 loader 的条件规则,然后使用 use 来指定需要用到的 loader,配置应用的 loader 时还需要注意一下 loader 的执行顺序。
三、使用plugin
webpack 中的 plugin 大多都提供额外的能力,它们在 webpack 中的配置都只是把插件实例添加到 plugins 字段的数组中。不过由于需要提供不同的功能,不同的插件本身的配置比较多样化。
常用的插件:
- DefinePlugin
- copy-webpack-plugin
- extract-text-webpack-plugin/mini-css-extract-plugin
- ProvidePlugin
- IgnorePlugin
前面4个上基础篇已经讲过,所以从第4个开始记录:
DefinePlugin
DefinePlugin 是 webpack 内置的插件,可以使用 webpack.DefinePlugin 直接获取。
这个插件用于创建一些在编译时可以配置的全局常量,这些常量的值我们可以在 webpack 的配置中去指定,例如:
1 | module.exports = { |
有了上面的配置,就可以在应用代码文件中,访问配置好的变量了,如:
1 | console.log("Running App version " + VERSION); |
简述整个配置规则:
- 如果配置的值是字符串,那么整个字符串会被当成代码片段来执行,其结果作为最终变量的值,如上面的 “1+1”,最后的结果是 2
- 如果配置的值不是字符串,也不是一个对象字面量,那么该值会被转为一个字符串,如 true,最后的结果是 ‘true’
- 如果配置的是一个对象字面量,那么该对象的所有 key 会以同样的方式去定义
社区中关于 DefinePlugin 使用得最多的方式是定义环境变量,例如 PRODUCTION = true 或者 DEV = true 等。部分类库在开发环境时依赖这样的环境变量来给予开发者更多的开发调试反馈,例如 react 等。
建议使用 process.env.NODE_ENV: … 的方式来定义 process.env.NODE_ENV,而不是使用 process: { env: { NODE_ENV: … } } 的方式,因为这样会覆盖掉 process 这个对象,可能会对其他代码造成影响。
copy-webpack-plugin
从名字可以猜到这个插件是用来复制文件的。
我们一般会把开发的所有源码和资源文件放在 src/ 目录下,构建的时候产出一个 build/ 目录,通常会直接拿 build 中的所有文件来发布。有些文件没经过 webpack 处理,但是我们希望它们也能出现在 build 目录下,这时就可以使用 CopyWebpackPlugin 来处理了。
1 | const CopyWebpackPlugin = require('copy-webpack-plugin') |
extract-text-webpack-plugin/mini-css-extract-plugin
用它们来把依赖的 CSS 分离出来成为单独的文件。
- 如果当前项目是webpack3.x版本,使用extract-text-webpack-plugin;
- 如果当前项目是webpack4.x版本(但已有extract-text-webpack-plugin配置),可以继续用extract-text-webpack-plugin,但必须用对应的beta版本,且这个beta版本不支持生成hash;
- 如果当前项目是webpack4.x版本且是新项目,使用mini-css-extract-plugin。
1 | yarn add extract-text-webpack-plugin@next -D |
1 | // const ExtractTextPlugin = require('extract-text-webpack-plugin'); |
在 webpack 中,loader 和 plugin 的区分是很清楚的,针对文件模块转换要做的使用 loader,而其他干涉构建内容的可以使用 plugin。 ExtractTextWebpackPlugin 和 MiniCssExtractPlugin既提供了 plugin,也提供了 extract 方法来获取对应需要的 loader。
ProvidePlugin
ProvidePlugin 也是一个 webpack 内置的插件,我们可以直接使用 webpack.ProvidePlugin 来获取。
该组件用于引用某些模块作为应用运行时的变量,从而不必每次都用 require 或者 import,其用法相对简单:
1 | new webpack.ProvidePlugin({ |
在你的代码中,当 identifier 被当作未赋值的变量时,module 就会被自动加载了,而 identifier 这个变量即 module 对外暴露的内容。
注意,如果是 ES6 的 default export,那么你需要指定模块的 default 属性:identifier: [‘module’, ‘default’],
IgnorePlugin
IgnorePlugin 和 ProvidePlugin 一样,也是一个 webpack 内置的插件,可以直接使用 webpack.IgnorePlugin 来获取。
这个插件用于忽略某些特定的模块,让 webpack 不把这些指定的模块打包进去。例如我们使用 moment.js,直接引用后,里边有大量的 i18n 的代码,导致最后打包出来的文件比较大,而实际场景并不需要这些 i18n 的代码,这时我们可以使用 IgnorePlugin 来忽略掉这些代码文件,配置如下:
1 | module.exports = { |
IgnorePlugin 配置的参数有两个,第一个是匹配引入模块路径的正则表达式,第二个是匹配模块的对应上下文,即所在目录名。
四、更好地使用webpack-dev-server
在构建代码并部署到生产环境之前,我们需要一个本地环境,用于运行我们开发的代码。这个环境相当于提供了一个简单的服务器,用于访问 webpack 构建好的静态文件,我们日常开发时可以使用它来调试前端代码。
webpack-dev-server 是 webpack 官方提供的一个工具,可以基于当前的 webpack 构建配置快速启动一个静态服务。当 mode 为 development 时,会具备 hot reload 的功能,即当源码文件变化时,会即时更新当前页面,以便你看到最新的效果。
基础使用
建议把 webpack-dev-server 作为开发依赖安装
1 | yarn add webpack-dev-server -D |
package 中的 scripts 配置:
1 | { |
1 | yarn start |
webpack-dev-server 默认使用 8080 端口,如果你使用了 html-webpack-plugin 来构建 HTML 文件,并且有一个 index.html 的构建结果,那么直接访问 http://localhost:8080/ 就可以看到 index.html 页面了。如果没有 HTML 文件的话,那么 webpack-dev-server 会生成一个展示静态资源列表的页面。
详细配置
在 webpack 的配置中,可以通过 devServer 字段来配置 webpack-dev-server,如端口设置、启动 gzip 压缩等,几个常用的配置如下:
- public
public 字段用于指定静态服务的域名,默认是 http://localhost:8080/ ,当你使用 Nginx 来做反向代理时,应该就需要使用该配置来指定 Nginx 配置使用的服务域名。
- port
用于指定静态服务的端口,如上,默认是 8080,通常情况下都不需要改动
- publicPath
用于指定构建好的静态文件在浏览器中用什么路径去访问,默认是 /,例如,对于一个构建好的文件 bundle.js,完整的访问路径是 http://localhost:8080/bundle.js, 如果配置了 publicPath: ‘assets/‘,那么上述 bundle.js 的完整访问路径就是 http://localhost:8080/assets/bundle.js。
可以使用整个 URL 来作为 publicPath 的值,如 publicPath: ‘http://localhost:8080/assets/'。 如果使用了 HMR,那么要设置 publicPath 就必须使用完整的 URL。
建议将 devServer.publicPath 和 output.publicPath 的值保持一致。
- proxy
用于配置 webpack-dev-server 将特定 URL 的请求代理到另外一台服务器上。该功能是使用 http-proxy-middleware 来实现的,当你有单独的后端开发服务器用于请求 API 时,这个配置非常有用。例如:
1 | proxy: { |
- contentBase
用于配置提供额外静态文件内容的目录,之前提到的 publicPath 是配置构建好的结果以什么样的路径去访问,而 contentBase 是配置额外的静态文件内容的访问路径,即那些不经过 webpack 构建,但是需要在 webpack-dev-server 中提供访问的静态资源(如部分图片等)。推荐使用绝对路径:
1 | // 使用当前目录下的 public |
publicPath 的优先级高于 contentBase。
- before
- after
before 和 after 配置用于在 webpack-dev-server 定义额外的中间件,如
1 | before(app){ |
before 在 webpack-dev-server 静态资源中间件处理之前,可以用于拦截部分请求返回特定内容,或者实现简单的数据 mock。
after 在 webpack-dev-server 静态资源中间件处理之后,比较少用到,可以用于打印日志或者做一些额外处理。
webpack-dev-middleware
中间件就是在 Express 之类的 Web 框架中实现各种各样功能(如静态文件访问)的这一部分函数。多个中间件可以一起协同构建起一个完整的 Web 服务器。
webpack-dev-middleware 就是在 Express 中提供 webpack-dev-server 静态服务能力的一个中间件,我们可以很轻松地将其集成到现有的 Express 代码中去,就像添加一个 Express 中间件那么简单。
1 | yarn add webpack-dev-middleware -D |
接着创建一个 Node.js 服务的脚本文件,如 app.js:
1 | const webpack = require('webpack') |
运行:
1 | node app.js # 使用刚才创建的 app.js 文件 |
使用 webpack-dev-server 的好处是相对简单,直接安装依赖后执行命令即可,而使用 webpack-dev-middleware 的好处是可以在既有的 Express 代码基础上快速添加 webpack-dev-server 的功能,同时利用 Express 来根据需要添加更多的功能,如 mock 服务、代理 API 请求等。
其实 webpack-dev-server 也是基于 Express 开发的,前面提及的 webpack-dev-server 中 before 或 after 的配置字段,也可以用于编写特定的中间件来根据需要添加额外的功能。
实现简单的mock服务
webpack-dev-server 的 before 或 proxy 配置,又或者是 webpack-dev-middleware 结合 Express,都可以帮助我们来实现简单的 mock 服务。
主要的需求是当浏览器请求某一个特定的路径时(如 /some/path ),可以访问我们想要的数据内容。
先基于 Express app 实现一个简单 mock 功能的方法:
1 | module.export = function mock(app) { |
然后应用到配置中的 before 字段:
1 | const mock = require('./mock') |
这样的 mock 函数照样可以应用到 Express 中去,提供与 webpack-dev-middleware 同样的功能。
由于 app.get(‘’, (req, res) => { … }) 的 callback 可以拿到 req 请求对象,其实可以根据请求参数来改变返回的结果,即通过参数来模拟多种场景的返回数据来协助测试多种场景下的代码应用。
和后端开发进行联调时,亦可使用 proxy 代理到对应联调使用的机器上,从而可以使用本地前端代码的开发环境来进行联调。