webpack 是一个现代 JavaScript 应用程序的静态模块打包器,已经成为前端开发不可获取的工具。特别是在开发大型项目时,项目太大,文件过多导致难以维护,或者是优化网络请求时,webpack 都是不可获取的利器。但是 webpack 配置并没有那么容易,webpack 配置项繁多,繁多的背后是配置的灵活性。许多的框架都是由 webpack 搭建而成,因此学会使用 webpack 可以让自己更好的理解脚手架搭建过程,甚至自己写一个灵活高效的脚手架工具。
从最基础的开始,使用的 webpack 版本是^4.39.2
,搭建时会用到以下技术:
- 从单页面到多页面
- 代码切片
- 热更新
- 热替换
- CSS 分离
- HTML 模板
- babel 的使用
- 支持 img、sass、jsx、css、typescript 等工具
- webpack 一些插件的使用
- postcss 的简单配置
- 不同开发环境的配置
- 配置 react 开发环境
首先,配置 webpack,大致的骨架是这样的,这是最基本的配置内容:
代码语言:javascript复制{
entry: "", // 入口配置
mode: 'development', // 环境配置
output: {}, // 打包输出配置
module: {}, // loader 配置项
plugins: [], // 插件配置项
devServer: {}, // 服务器配置
}
那么就开始一一进行配置。
安装 webpack,设置程序打包命令
首先是安装:yarn add webpack --dev
或者 npm install webpack --save-dev
或者 yarn add webpack -D
npm i webpack -D
在开发环境安装。
安装完时候,来到 package.json 文件,在 scripts
项中写入一下命令:
"build": "webpack"
当运行命令 npm run build
后,webpack 默认会在项目根目录下查找一个叫 webpack.config.js
的文件,然后进行打包。当文件名不想叫这个或者不想在根目录下创建这个文件时,可以在后面的 --config
字段之后写上文件的所在路径。例如:
"build" "webpack --config config/webpack.config.dev.js"
这样,当运行时,webpack 就从 项目根路径/config/webpack.config.dev.js
这个路径查找配置文件。
在运行命令时,可能会提醒安装
webpack-cli
输入yes
即可。
entry 入口配置(必须的)
entry 大致有四种写法,分别是字符串的形式、数组形式、函数形式和对象形式。代表的含义分别是:
形式 | 含义 | 举例 |
---|---|---|
字符串的形式 | 这种表示单个入口 | 例如:entry: "path(__dirname,"../src/index.js")" |
数组形式 | 这种也是表示单个入口 | 例如:entry: "["./01.js","./index.js"]" |
函数形式 | 可以是单入口也可以是多入口 | 该函数应该返回一个字符路径、数组或对象作为打包入口 |
对象形式 | 这种表示多个入口 | 例如:entry: {app: './src/app.js',vendors: './src/vendors.js'} |
代码语言:javascript复制注意!:第一种和第二种都表示单入口,但含义不同。使用数组的作用是将多个资源预先 合并,在打包的时候, webpack 会将数组的最后一项作为实际的入口路径。
modules.exports = {
entry: ["babel-polyfill","./src/index.js"]
}
就相当于:
代码语言:javascript复制// webpack.config.js
modules.exports = {
entry: "./src/index.js"
}
// 在打包后的文件中,会包含数组中所有的路径对应的文件
import "babel-polyfill"; // 以及 ./src/index.js 文件
output 配置
output 配置项很多,有两个是必须的:
path
指定文件输出时的文件夹(不存在时会自动创建);filename
指定文件输出时文件的名字;
单页面
代码语言:javascript复制{
entry: path.join(__dirname,"../src/index.js"), // 入口配置
output: {
path: path.join(__dirname,"../build"),
filename: "index.js"
}, // 打包输出配置
}
运行后,就会在 build 文件夹下创建一个 index.js 的打包文件。
多页面
代码语言:javascript复制{
entry: {
index: path.join(__dirname,"../src/index.js"),
demo: path.join(__dirname,'../src/demo.js')
}, // 入口配置
output: {
path: path.join(__dirname,"../build"),
filename: "[name].js"
}, // 打包输出配置
}
运行后,在 build 文件夹下就会多出两个文件。注意 output 中 filename 的文件名 —— [name]
这个 name 对应的就是 entry
对象的键。当然,也可以为 filename 指定别的字段,但也是要用 []
包裹。可以指定的字段有:
[name]
当前 chunk 的名字[hash]
此次打包所有资源生成的 hash[id]
指代当前 chunk 的 id[chunkhash]
指代当前 chunk 内容的 hash[query]
模块的 query,例如,文件名 ? 后面的字符串
表中
hash
和chunkhash
的作用:当下一次请求时,请求到的资源会被立刻下载新的版本,而不会用本地缓存。query
也有类似的效果,只是需要人为指定。
publicPath
publicPath
是 output 中的一个配置项,不是必须的。但是它是一个非常重要的配置项。
与 path
属性不同,publicPath
用来指定资源的请求位置,而 path
是用来指定资源的输出位置(打包后文件的所在路径)。在 HTML 页面中,我们可能会通过 <script>
标签来加载 JS 代码,标签中的 src
路径就是一个请求路径(不光是 HTML 中的 JS 文件,也可能是 CSS 中的图片、字体等资源、HTML 中的图片、CSS 文件等)。publicPath 的作用就是指定这部分间接资源的请求位置。
publicPath
有三种形式,分别是:
- 与 HTML 相关,在请求这些资源时会以当前页面 HTML 所在路径加上相对路径,构成实际请求的 URL。
- Host 相关,若 publicPath 的值以 ‘/’开始,则代表此时
publicPath
是以当前页面的 hostname 为基础路径的。 - CDN 相关,上面两种都是相对路径,而这个是 绝对路径。如:publicPath 为:"https://www.example.com/",代表当前路径是 CDN 相关。
举个例子,当使用第一种形式时,当我们使用
html-webpack-plugin
插件动态生成一个 HTML,并打包到 build 文件夹后,JS 文件(指定的 entry)会自动插入到 HTML 中。当我们指定publicPath: '/'
,后就会变成:
当没有指定 publicPath 时,默认是 ""
,即:
而如果是 "/static" 时,HTML 引入的资源路径前都将有一个 "/static"。这个路径是相对于项目根路径的。
html-webpack-plugin 插件
这是一个很实用的插件,在上面的例子中,都没有提到 html,而这个插件可以动态生成 html。下载: npm install html-webpack-plugin -D
或者 yarn add npm install html-webpack-plugin -D
。配置:
const HtmlWebpackPlugin = require('html-webpack-plugin');
{
plugins: [
new HtmlWebpackPlugin({
// 以下都是可选的配置项:
title: "hello world!", // html 的 title 标签内容
// html 模板路径
template: path.join(__dirname,'../public/index.html'),
// title favicon 路径
favicon: path.join(__dirname,'../public/favicon.ico'),
// 指定引入的 js 代码 插入到哪里(默认是body最底部,即:true)
// 也可以指定字符串:"body" 或 "head"
inject: false,
// 指定 打包输出后,文件的名字(不指定的话还是原来的名字)
filename: "hello.html",
// 还有许多配置,这是常用的几个
})
]
}
多个 HTML 页面的配置
有时候,想要配置多页面应用,这时就要多实例化几个这个插件。这时就可能会用到别的一个配置属性 —— chunks
{
entry: {
index: path.join(__dirname,"../src/index.js"),
demo: path.join(__dirname,'../src/demo.js')
}, // 入口配置
output: {
path: path.join(__dirname,"../build"),
filename: "[name].js"
}, // 打包输出配置
plugins: [
new HtmlWebpackPlugin({
chunks: ['index'], // 指定 chunk 的名字(一般就是 entry 对象的键)
// html 模板路径
template: path.join(__dirname,'../public/index.html'),
favicon: path.join(__dirname,'../public/favicon.ico'),
inject: "body",
filename: "index.html",
}),
new HtmlWebpackPlugin({
chunks: ['demo'],
// html 模板路径
template: path.join(__dirname, '../public/demo.html'),
favicon: path.join(__dirname, '../public/favicon.ico'),
inject: "body",
filename: "demo.html",
}),
], // 插件配置项
}
mode 环境变量
mode 的选项一般是这两者其一:development
或 production
,即:开发模式或生产模式。生产模式的代码一般是压缩过的。
单纯的指定 mode 值,可能不能满足我们的需要,这时可以使用另一种办法来设置 mode 值。就是在 package.json 文件的 'scripts' 命令中传入参数。
// ....
"scripts": {
"build": "cross-env NODE_ENV='development' webpack --config config/webpack.config.dev.js"
}
// ....
可以注意到,在之前的命令中,我们在前面又添加了一部分内容:cross-env NODE_ENV='development
这是给 Node.js 的全局变量 process
的 env 属性传入了一个值,前面 cross-env
是一个 npm 包,主要为了解决在 Windows 系统下不支持传值命令。
这样,在 webpack 配置文件中,就可以接收到这个值:
var mode = process.env.NODE_ENV; // development
这样就可以根据传入的值,来对配置文件作进一步的改进:
代码语言:javascript复制const mode = process.env.NODE_ENV;
const isDev = mode === 'development';
const config = {
// 公共配置项,比如 loader、mode、entry 和 output 中相同的配置项
mode: mode,
entry: "xxx",
// 等等
}
if(isDve){
// 是开发模式时的配置,比如:
config.devServer = {
// 对 webpack-dev-server 的配置
}
}else{
// 是生产模式时的配置
}
// 最后导出:
module.exports = config;
module 配置
这一部分比较多,主要是配置各种 loader,比如 css-loader
,babel-loader
,sass-loader
等等。而这些配置存在于 module.rules 这个配置项中。所有的 loader 都是需要安装的。通过 npm install xxx-loader
或 yarn add xxx-loader
的形式进行安装。
style-loader
和 css-loader
两者有很大不同,css-loader
的作用仅仅是处理 CSS 的各种加载语法,例如 @import
和 url()
等。而 style-loader
才是真正让样式起作用的 loader。因此这两个一般配合使用:
{
module: {
rules: [
{
test: /.css$/,
use: ['style-loader','css-loader']
}
]
}
}
还需要注意的是:webpack 打包时是按数组从后往前的顺序将资源交给 loader 处理的,因此要把最后生效的放在前面。
loader options
有时候使用一个 loader 时,可能要对它进行一些配置,例如 babel-loader
babel 的一些配置就可以写在 options 里,当然也可以建一个 .babelrc
文件进行配置。当一个 loader 需要配置时,它就不能在 use 属性里是个单纯的字符串了,而是一个对象。
{
module: {
rules: [
{
test: /.css$/,
use: ['style-loader',{
// 对 css-loader 配置时,是个对象
loader: 'css-loader',
options: {
// css-loader 的配置项
}
}]
}
]
}
}
需要注意的是,css 中还不能书写背景图片路径(例如:
background: url()
)。不然会报错。因为加载的不是样式,而是图片,在 webpack 中,想要加载图片,还需要使用file-loader
,之后会介绍。
sass-loader
和 less-loader
sass
和 less
是 CSS 的预处理器,需要安装。而且最终会编译成 CSS,因此我们还需要 style-loader
和 css-loader
。而且还要安装编译 Sass 的包:node-sass
(不然会报错)。
module: {
rules: [
{
test: /.(sass|scss)$/,
use: ["style-loader", "css-loader","sass-loader"]
},
{
test: /.less$/,
use: ["style-loader", "css-loader", "less-loader"]
}
]
}
html-loader
有了这个 loader,我们可以将一个 html 文件通过 JS 加载进来。比如这样:
代码语言:javascript复制rules: [
{
test: /.html$/,
use: 'html-loader'
}
]
// loader.html
<h1>Hello World!</h1>
// index.js
import html from './loader.html';
document.write(html);
file-loader
和 url-loader
file-loader
用于打包文件类型的资源。比如 CSS 的背景图片和字体、HTML 的 img 标签中的 src 路径等。
rules: [
{
test: /.(png|jpg|gif)$/,
use: "file-loader",
}
]
这样就可以对 png、jpg、gif 类型的图片文件进行打包,而且可以在 JS 中加载图片。
file-loader 中的 options
主要有两个配置项:
name
,指定打包后文件的名字,默认是 hash 值加上文件后缀。也可以制定成:[name].[ext]
表示原来的名字和文件后缀。publicPath
这里的 publicPath 与 output 中的 publicPath 一样,在这里指定后,会覆盖原有的 output.publicPath。比如:
rules: [
{
test: /.(png|jpg|gif)/,
use: {
loader: "file-loader",
options: {
name: '[name].[ext]',
publicPath: "",
}
}
}
]
url-loader
与 file-loader
作用类似,唯一的不同是:url-loader
可以设置一个文件大小的阈(yù)值。当大于该阈值时与 file-loader 一样返回 publicPath,而小于阈值时则返回文件的 base64
形式编码。比如:
{
test: /.(png|jpg|gif)$/,
use: {
loader: "url-loader",
options: {
// 当文件小于这个值时,使用 base64 编码形式
// 大于该值时,使用 publicPath
// 这个属性在 file-loader 中是没有的。
limit: 10240,
name: '[name].[ext]'
}
}
}
ts-loader
使用 ts-loader
可以让我们使用 typescript 来编写 js 代码。安装该 loader 后,还要安装 typescript。
yarn add ts-loader typescript
代码语言:javascript复制rules: [
{
test: /.ts$/,
use: "ts-loader",
}
]
babel-loader
babel-loader 很重要,使用 babel-loader 可以让我们写的 JS 代码更加兼容浏览器环境。配置 babel-loader 时需要下载好几个其他的包。yarn add babel-loader @babel/core @babel/preset-env -D
。这三个是最核心的模块。主要作用如下:
babel-loader
它是 babel 与 webpack 协同工作的模块;@babel/core
babel 编译器的核心模块;@babel/preset-env
它是官方推荐的预置器,可根据用户设置的目标环境自动添加所需的插件和补丁来编译 ES6 代码。 具体配置如下:
rules: [
{
test: /.js$/,
// 不要编译 node_modules 下面的代码
exclude: path.join(__dirname,'../node_modules'),
use: {
loader: "babel-loader",
options: {
// 当为 true 时,会启动缓存机制,
// 在重复打包未改变过的模块时防止二次编译
// 这样做可以加快打包速度
"cacheDirectory": true,
}
}
}
]
对于 options 其它部分,可以在项目根目录下新建一个 .babelrc
文件。.babelrc 文件相当于一个 json 文件。它的配置项大概是这样的:
{
"presets": [],
"plugins": [],
}
比如要配置的一个内容:
代码语言:javascript复制{
"presets": [
["@babel/env", // 每一个 preset 就是数组的每一项
// 当有的 preset 需要配置时,这一项将也是一个数组
// 数组的第一项是 preset 名称,第二项是该 preset 的配置内容,是一个对象
{ // @babel/preset-env 会将 ES6 module 转成 CommonJS 的形式
// 将 mudules 设置成 false,可以禁止模块语句的转化
// 而将 ES6 module 的语法交给 webpack 本身处理
"mudules": false,
// targets 可以指定兼容的各个环境的最低版本
"targets": {
"edge": "17",
"firefox": "60",
"chrome": "67",
"safari": "11.1"
}
}
]
],
"plugins": []
}
env 的 targets 属性,可以配置的环境名称有:
chrome
,opera
,edge
,firefox
,safari
,ie
,ios
,android
,node
,electron
。当然 targets 的值也可以是一个字符串,例如:"targets": "> 0.25%, not dead"
表示仅包含浏览器具有> 0.25%市场份额的用户所需的 polyfill 和代码转换。
处理 react jsx 语法:@babel/preset-react
下载: yarn add @babel/preset-react -D
。当然,如果想使用 react,也要下载。在 .babelrc
的 presets 项中添加一个 preset:
{
"presets": [
["@babel/env",
{
"modules": false,
"targets": {
"ie": 9
}
}
],
"@babel/react"
]
}
这个时候就可以愉快的使用 react 了!
处理 .jsx
的文件
用 react 写的文件不光可以使用 .js
后缀,也可以使用 .jsx
文件后缀。但想要使用,这需要配置,不然会报错。来到 webpack 配置文件,添加一个 loader 项:
{
test: /.jsx$/,
use: "babel-loader",
}
当然,也可以与 js 配置写在一起:
代码语言:javascript复制test: /.(js|jsx)$/,
use: {
// ...
}
postcss-loader
下载:npm install postcss-loader
配置:
// 不需要再次创建新的 loader 对象,应该在之前的 style-loader css-loader 之后直接添加 postcss-loader 即可
{
test: /.css$/,
// 顺序很重要
use: ['style-loader','css-loader','postcss-loader'],
}
配置 PostCSS
这里需要创建一个文件 —— postcss.config.js
在项目根目录下。
自动添加后缀 —— autoprefixer
代码语言:javascript复制const autoprefixer = require('autoprefixer');
module.exports = {
plugins: [
autoprefixer({
// 需要支持的特性(这里添加了 grid 布局)
grid: true,
// 浏览器兼容
overrideBrowserList: [
'>1%', // 浏览器份额 大于 1% 的。
'last 3 versions', // 兼容最后三个版本
'android 4.2',
'ie 8'
],
})
]
};
postcss-preset-env
插件
这个插件可以让我们在应用中使用最新的 CSS 语法特性。同样需要下载: yarn add postcss-preset-env
。使用:
// postcss.config.js
const autoprefixer = require('autoprefixer');
const postcssPresetEnv = require('postcss-preset-env');
module.exports = {
plugins: [
autoprefixer({
grid: true,
overrideBrowserList: [
'>1%',
'last 3 versions',
'android 4.2',
'ie 8'
],
}),
postcssPresetEnv({
state: 3,
features: {
'color-mod-function': {
unresolved: 'warn'
},
browsers: 'last 2 versions'
}
})
]
};
resolve 配置项
这是一个可选的配置项,配置 resolve
用来设置模块如何被解析。几个常见的配置项:
1. resolve.alias
这个属性是给路径添加别名的,当使用 import
或者 require
去引用别的模块时,文件路径可能会比较长,这个时候就可以使用 alias
来简化路径。也可以在给定对象的键后的末尾添加 $,以表示精准匹配。比如:
// 在 webpack 中配置 resolve.alias
module.exports = {
// ....
resolve: {
alias: {
xyz$: path.resolve(__dirname, 'path/to/file.js'),
xyz: path.resolve(__dirname, 'path/to/file.js')
}
}
// ....
}
// 引用:index.js
import App1 from 'xyz'; // 精准匹配,会解析到 path/to/file.js 中的 js 文件
import App2 from 'xyz/index.js'; // 非精准匹配,匹配 path/to/file.js/index.js 中的内容
resolve.extensions
这个配置项设置后会自动解析确定的扩展。默认值为 extensions: ['.wasm', '.mjs', '.js', '.json']
。还可以做更改,比如 添加 jsx 文件:
{
resolve: {
extensions: ['.wasm', '.mjs', '.js', '.json','.jsx']
}
}
resolve 配置项还有许多,上面两个应该是比较常用的。其他的可以参看官网:webpack 中文文档[1] 或 webpack 英文文档[2]
最后
下一节还是 webpack 的配置,之后将要配置 devServer
、配置优化和生产环境的配置。最后再说一下 create-react-app 脚手架的改造。
参考资料
[1]
webpack中文文档: https://webpack.docschina.org/configuration/resolve/
[2]
webpack英文文档: https://webpack.js.org/configuration/resolve/