学一点Webpack配置:基本配置
这两天朋友圈流行这么一张图:
多么形象的展示了前端学习的曲线图。真可谓是一言难尽呀,现在的前端真不好学,乱而杂。如果你要是再看看@Kamran Ahmed整理的 2017年 、 2018年 和 2019年 现代Web开发者要掌握的 Roadmap ,估计更会泪崩:
现状是如此,未来可能会更混乱,但我们不应该去抱怨,应该更应该保持一颗爱学习的心,继续往前行走。
学点Webpack配置方面的知识
Webpack 是构建工具中必不可少的一部分:
作为现代Web开发者就需要对Webpack有所了解,哪怕掌握的不够深入,略知皮毛也对我们自己的工作或学习都是有所帮助的。比如说吧,前段时间折腾React环境下的CSS Modules,就是因为自己对Webpack不了解,有些坑踩了无法立刻解决,就算借助互联网,解的也是知半解(而且现在技术更新太快,网上有些教程根本走不通,不踩不知道,一踩只有泪)。正因为这个原因,促使自己去了解Webpack更多的知识。接下来的内容是一些基础,主要会介绍怎么用Webpack来构建自己的开发环境,感兴趣的请继续往下阅读。
Webpack是什么
一直以来,在我自己的印象和理解中,都认为 Webpack是一个构建工具 。主要用来构建开发的工程体系。但从其官网来看,告诉我Webpack是一个模块Bundler(捆绑器):
那么,Webpack到底是一个构建工具(或者说一个构建系统)还是一个模块Bundler(捆绑器)呢?答案是:
Webpack既是一个构建系统,也是一个捆绑器!
Webpack不是先构建你的资源(Assets),然后再bundle你的模块,它把你的资源本身就当做是一个模块。这些模块可以被 导入 、 修改 和 操作 等,最后才被打包到你最后的bundle。
简单地说,Webpack其最核心的功能就是 解决模板之间的依赖,把各个模块按照特定的规则和顺序组织在一起,最终合并成一个JS文件(比如 bundle.js
) 。这个整个过程也常常被称为是 模块打包 。换句话说,Webpack是一个指令集合的配置文档,然后通过配置好的这些指令去驱动程序做一些指令要求要做的事情。而这些动作都是通过自己写的规则去做编译,而且通过JavaScript的引入( import
)语法让Webpack知道需要它帮忙编译什么东西(比如Pug、Sass等等)。所以我们始终会有一个入口文件(比如 index.js
)注入那些Preprocess,让那些Preprocess可以通过这些入口文件的JavaScript让Webpack去根据相关的配置指令编译它,然后打包到一个出口文件中,比如 bundles.js
。
为什么要用Webpack
一直以来,在开发Web页面或Web应用程序的时候,都习惯性的将不同资源放置在不同的文件目录之中,比如图片放置在 images
(或 img
)下,样式文件放置在 styles
(或 css
)中,脚本文件放在 js
和模板文件放置在 pages
中。一直以来,发布的时候都会一次性的将所有资源打包发布,不管这些资源用到了还是没用到(事实上很多时候自己都分不清楚哪资源被使用)。用一句话来描述就是: 依赖太复杂,太混乱,无法维护和有效跟踪 。比如哪个样式文件引用了 a.img
,哪个样式文件引用了 b.img
;另外页面到底是引用了 a.css
呢还是 b.css
呢?
而Webpack这样的工具却能很多好的解决它们之间的依赖关系,使其打包后的结果能运行在浏览器上。其目前的工作方式主要被分为两种:
.js
相比于Parcel、Rollup具有同等功能的工具而言,Webpack还具有其他的优势:
- Webpack支持多种模块标准 :这对于一些同时使用多种模块标准的工程非常有用,Webpack会帮我们处理好不同类型模块之间的依赖关系
- Webpack有完备的代码分割解决方案 :它可以分割打包后的资源,首屏只加载必要的部分,不太重要的功能放到后面动态地加载
- Webpack可以处理各种类型的资源 :除了JavaScript之外,Webpack还可以处理样式、模板、图片等资源。开发者要做的只是将之些资源导入,而无需关注其他
另外,Webpack还拥有一个强大的社区。这也是其受开发者青眯的原因之一。接下来,我们还是实际一点,动手来撸码。
从零开始构建你自己的开发环境
为了更好的理解Webpack能帮我们做什么,我打算从零开始构建一个属于自己的开发环境。可能在后面的内容中会涉及到很多关键词,比如Webpack、loaders、Babel、sourcemaps、React、TypeScript,CSS Modules等等。接下来一步一步的学习中会了解到这些单词和相关技术。
后面会一步一步的带大家如何使用Webpack配置适合自己的开发环境,会涉及到一些相关技术,但不会深入到具体技术细节中。
在写这篇文章所具备的环境是:Node是 v10.9.0
,NPM是 v6.9.2
,Webpack是 v4.34.0
,React是 v16.8.6
,TypeScript是 v3.4.4
,Sass是 v.6.9.0
,PostCSS是 v.6.9.0
等。接下来的内容会以配置React + TypeScript + CSS Modules + PostCSS为主线,从零开始一个项目。另外,接下来的内容会以不同分支的形式将 代码放置在Github上 。感兴趣的可以直接将仓库克隆下来,切换到对应步骤的分支,查看代码。
Step01:初始化项目
请将Git分支切换到 step1
分支查看代码 。
首先在你的本地创建一个项目,比如我这里创建了一个 webpack-sample
项目:
⇒ mkdir webpack-sample && cd webpack-sample
进入到新创建的项目目录下,执行 npm init
或者 npm init -y
命令来初始化项目,执行完该命令之后,在你的命令终端会看到类似下图这样的命令询问,你可以根据你自己的需要去输入你想要的内容,或者一路 Enter
键执行下去:
此时你的项目根目录下会增加一些文件和文件夹:
|--webpack-sample/ |----node_modules/ |----package.json |----package-lock.json
其中 package.json
文件里将包含一些项目信息:
注意,这个文件随着后面的步骤完成,会增加更多的内容。
而 package-lock.json
文件是当 node_modules/
或 package.json
发生变化时自动生成的文件,它的主要功能是 确定当前安装的包的依赖,以便后续重新安装的时候生成相同的依赖,而忽略项目开发过程中有些依赖已经发生的更新 。
在Step01中,我们对 package.json
文件只做一个修改,删除 "main": "index.js"
入口,并添加 "private":true
选项,以便确保安装包是私有的,这样可以防止意外发布你的代码。
Step02:安装Webpack和初始配置Webpack
请将分支切换到 step2
查看代码 。
在这一步,先来安装Webpack。执行下面的命令安装Webpack配置所需要的包:
⇒ npm i webpack webpack-cli webpack-dev-server -D
此时打开 package.json
文件,你会发现在文件中有一个新增项 devDependencies
:
{ // 其他项信息在这省略,详细请查看该文件 "devDependencies": { "webpack": "^4.35.0", "webpack-cli": "^3.3.5", "webpack-dev-server": "^3.7.2" } }
注意,在命令终端使用 npm i
安装依赖关系时,如果带后缀 -D
(或 --save-dev
) 安装的包会记录在 "devDependencies"
下;如果使用 --save
后缀(我们后面会用到)安装的包会记录在 "dependencies"
下。两者的区别是:
-
"devDependencies"
:dev
开发时的依赖包 -
dependencies
:程序运行时的依赖包
为了验证Webpack是否能正常工作,这个时候我们需要创建一些新的文件。在 webpack-sample
根目录下创建 /src
目录,并且在该目录下创建一个 index.js
文件:
⇒ mkdir src && cd src && touch index.js
执行完上面的命令,你会发现你的项目目录结构变成下图这样:
我们在新创建的 /src/index.js
文件下添加一行最简单的JavaScript代码:
console.log("Hello, Webpack!(^_^)~~")
保存之后回到命令终端,第一次执行有关于Webpack相关的命令:
⇒ npx webpack src/index.js --output dist/bundle.js
执行完上面的命令后,如果看到下图这样的结果,那么要恭喜你,Webpack的安装已经成功,你可以在你的命令终端执行有关于Webpack相关的命令:
回到项目中,会发现项目根目下自动创建了一个 /dist
目录,而且该目录下包含了一个 bundle.js
文件:
执行完上面的命令之后,可以看到有相关的警告信息。那是因为 Webpack4增加了 mode
属性 ,用来表示不同的环境。 mode
模式具有 development
, production
和 none
三个值,其默认值是 production
。也就是说,在执行上面的命令的时候,我们可以带上相应的 mode
属性的值,比如说,设置 none
来禁用任何默认行为:
⇒ npx webpack src/index.js --output dist/bundle.js --mode none
执行到这里,只知道我们可以运行Webpack相关命令。并不知道 /src/index.js
的代码是否打包到 /dist/bundle.js
中。为此,我们可以在 /dist
目录下创建一个 index.html
⇒ cd dist && touch index.html
并且将生成出来的 bundle.js
引入到新创建的 index.html
:
Hello Webpack (^_^) ~~~
在浏览器中打开 /dist/index.html
,或者在命令行中执行:
⇒ npm i -g http-server ⇒ http-server dist
http-server
是一个启动服务器的 npm
包,执行上面的命令之后,就可以在浏览器中访问 http://127.0.0.1:8080/
(访问的 /dist/index.html
),在浏览器的 console.log
控制台中,可以看到 src/index.js
的脚本输出的值:
上面的过程足以验证,你的Webpack能正常的工作了。
不过,当你需要构建的东西越复杂,需要的标志就会越多。在某种程度上说,就会变得难以控制。这个时候我们就需要一个文件来管理这些配置。接下来我们需要创建一个 webpack.config.js
这样的一个文件,用来配置Webpack要做的事情。注意,这个文件是一个 node.js
文件,所以你可以在任何节点文件中执行任何你能够执行的操作。你也可以写成 json
文件,但是node文件更强大一些。
首先们先创建Webpack的配置文件,在 webpack-sample
根目录下创建一个 /build
目录,然后在该目录下添加一个名为 webpack.config.js
文件:
⇒ mkdir build && cd build && touch webpack.config.js
执行完上面的命令之后,你会发现你的项目文件目录结构变成下面这样了:
这个时候,新创建的 webpack.config.js
文件里面是一片空白,它就是Webpack的配置文件,将会导出一个对象的JavaScript文件。我们需要在这个文件中添加一些配置:
var webpack = require('webpack'); var path = require('path'); var DIST_PATH = path.resolve(__dirname, '../dist'); // 声明/dist的路径 module.exports = { // 入口JS路径 // 指示Webpack应该使用哪个模块,来作为构建其内部依赖图的开始 entry: path.resolve(__dirname,'../src/index.js'), // 编译输出的JS入路径 // 告诉Webpack在哪里输出它所创建的bundle,以及如何命名这些文件 output: { path: DIST_PATH, // 创建的bundle生成到哪里 filename: 'bundle.js', // 创建的bundle的名称 }, // 模块解析 module: { }, // 插件 plugins: [ ], // 开发服务器 devServer: { } }
Webpack配置是标准的 Node.js CommonJS模块 ,它通过 require
来引入其他模块,通过 module.exports
导出模块,由Webpack根据对象定义属性进行解析。
上面很简单,到目前为止只通过 entry
设置了入口起点 ,然后通过 output
配置了打包文件输出的目的地和方式 。你可能也发现了,在配置文件中还有 module
、 plugins
和 devServer
没有添加任何东西。不需要太急,后面会一步一步带着大家把这里的内容补全的,而且随着配置的东西越来越多,整个 webpack.config.js
也会更变越复杂。
完成 webpack.config.js
的基础配置之后,回到 package.json
文件,并在 "scripts"
下添加 "build": "webpack --config ./build/webpack.config.js"
:
// package.json { // ... "scripts": { "build": "webpack --config ./build/webpack.config.js", "test": "echo \"Error: no test specified\" && exit 1" }, }
这样做的,为让我们直接在命令终端执行相关的命令就可以实现相应的功能。比如上面配置的 build
,在命令终端执行:
⇒ npm run build
上面的命令执行的效果前面提到的 npx webpack src/index.js --output dist/bundle.js --mode none
等同。同样有警告信息,主要是 mode
的配置没有添加。在上面的配置中添加:
{ "scripts": { "build": "webpack --config ./build/webpack.config.js --mode production", "test": "echo \"Error: no test specified\" && exit 1" }, }
再次执行 npm run build
,不会再有警告信息。你可以试着修改 /src/index.js
的代码:
alert(`Hello, Webpack! Let's Go`);
重新编译之后,打开 /dist/index.html
你会发现浏览器会弹出 alert()
框:
为了开发方便,不可能通过 http-server
来启用服务。我们可以把这部分事件放到开发服务器中来做,对应的就是 devServer
,所以我们接着在 webpack.config.js
中添加 devServer
相关的配置:
// webpack.config.js // 开发服务器 devServer: { hot: true, // 热更新,无需手动刷新 contentBase: DIST_PATH, // host: '0.0.0.0', // host地址 port: 8080, // 服务器端口 historyApiFallback: true, // 该选项的作用所用404都连接到index.html proxy: { "/api": "http://localhost:3000" // 代理到后端的服务地址,会拦截所有以api开头的请求地址 } }
有关于 devServer
更详细的配置参数描述, 可以查阅读Webpack官网相关文档 。
和 build
类似,需要在 package.json
的 scripts
中添加相关的命令:
// package.json "scripts": { "build": "webpack --config ./build/webpack.config.js --mode production", "dev": "webpack-dev-server --config ./build/webpack.config.js --mode development --open", "test": "echo \"Error: no test specified\" && exit 1" },
保存所有文件,在命令行中执行 npm run dev
就可以启动服务器:
你可以验证一下,修改 /src/index.js
:
document.addEventListener('DOMContentLoaded', () => { const h1Ele = document.createElement('h1') document.body.append(h1Ele); h1Ele.innerText = 'Hello Webpack (^_^)' h1Ele.style.color = '#f46'; })
保存该文件之后,浏览器会立刻刷新,你将看到修改之后的变化:
Step03: 优化Webpack配置
请将分支切换到 step3
查看代码 。
在 Step02
中,开发和生产环境相关的配置都集成在 webpack.config.js
一个文件中。为了更好的维护代码,在 Step03
中做一些优化。把 webpack.config.js
拆分成三个部分:
- 公共配置 :把开发和生产环境需要的配置都集中到公共配置文件中,即
webpack.common.js
- 开发环境配置 :把开发环境需要的相关配置放置到
webpack.dev.js
- 生产环境配置 :把生产环境需要的相关配置放置到
webpack.prod.js
先在 /build
目录下创建上面提到的三个配置文件。在命令终端执行下面的命令即可:
⇒ cd build && touch webpack.common.js webpack.dev.js webpack.prod.js
这个时候,整个项目目录结构变成下图这样:
从 Step02
中遗留下来的 webpack.config.js
文件将会从 /build
目录中移除。
为了更好的管理和维护这三个文件,需要安装一个 webpack-merge
插件 :
⇒ npm i webpack-merge -D
执行完上面的命令之后, package.json
文件中的 devDependencies
会增加 webpack-merge
相关的配置:
// package.json { //... 省略的信息请查看原文件 "devDependencies": { "webpack": "^4.35.0", "webpack-cli": "^3.3.5", "webpack-dev-server": "^3.7.2", "webpack-merge": "^4.2.1" } }
接下来分别给 webpack.common.js
、 webpack.dev.js
和 webpack.prod.js
文件添加相关的配置:
Webpack公共配置
在公共配置文件 webpack.common.js
文件中添加相应的配置:
const webpack = require('webpack'); const path = require('path'); const DIST_PATH = path.resolve(__dirname, '../dist/'); // 声明/dist的路径 module.exports = { // 入口JS路径 // 指示Webpack应该使用哪个模块,来作为构建其内部依赖图的开始 entry: path.resolve(__dirname,'../src/index.js'), // 编译输出的JS入路径 // 告诉Webpack在哪里输出它所创建的bundle,以及如何命名这些文件 output: { path: DIST_PATH, // 创建的bundle生成到哪里 filename: 'bundle.js', // 创建的bundle的名称 }, // 模块解析 module: { }, // 插件 plugins: [ ] }
Webpack开发环境配置
接着给Webpack开发环境配置文件 webpack.dev.js
添加下面的相关配置:
const webpack = require('webpack'); const path = require('path'); const merge = require('webpack-merge'); const commonConfig = require('./webpack.common.js'); const DIST_PATH = path.resolve(__dirname, '../dist/'); // 声明/dist的路径 module.exports = merge(commonConfig, { mode: 'development', // 设置webpack mode的模式 // 开发环境下需要的相关插件配置 plugins: [ ], // 开发服务器 devServer: { hot: true, // 热更新,无需手动刷新 contentBase: DIST_PATH, // host: '0.0.0.0', // host地址 port: 8080, // 服务器端口 historyApiFallback: true, // 该选项的作用所用404都连接到index.html proxy: { "/api": "http://localhost:3000" // 代理到后端的服务地址,会拦截所有以api开头的请求地址 } } })
Webpack生产环境配置
继续给Webpack生产环境配置文件 webpack.prod.js
添加相关配置:
const webpack = require('webpack'); const path = require('path'); const merge = require('webpack-merge'); const commonConfig = require('./webpack.common.js'); module.exports = merge(commonConfig, { mode: 'production', // 设置Webpack的mode模式 // 生产环境下需要的相关插件配置 plugins: [ ], })
上面的配置信息只是将 Step02
中 webpack.config.js
分成三个文件来配置,随着后续添加相应的配置信息,那么这三个文件中的配置信息会越来越多,也会越来越复杂。
修改完Webpack的配置之后,对应的 package.json
中的 scripts
中的信息也要做相应的调整:
// package.json { // ... 其他配置信息请查看原文件 "scripts": { "build": "webpack --config ./build/webpack.prod.js --mode production", "dev": "webpack-dev-server --config ./build/webpack.dev.js --mode development --open", "test": "echo \"Error: no test specified\" && exit 1" }, }
这个时候重新在命令终端执行
// 执行build命令,重新打包 ⇒ npm run build // 执行dev命令 ⇒ npm run dev
这仅仅是最基础部分的优化,因为我们的配置还是最简单的,后续我们添加了别的配置之后,也会在相应的步骤做相应的优化。
Step04: 配置React开发环境
请将分支切换到 step4
查看代码 。
经过前面三步,我们完成了Webpack的基本配置,知道文件入口,出口,打包以及开发,生产等环境。接下来,我们来给工程配置React相关的环境。
React的环境需要先安装 react
和 react-dom
。所以先在命令终端中执行下面的命令:
⇒ npm i react react-dom --save
执行完上面的命令之后,在 package.json
文件中的 dependencies
增加了 ract
和 react-dom
相应的信息:
// package.json { "dependencies": { "react": "^16.8.6", "react-dom": "^16.8.6" }, }
为了验证React相关的环境是否能正常工作,将 /src/index.js
中的内容做一些修改:
// /src/index.js import React from 'react'; import ReactDOM from 'react-dom'; import App from './components/App' ReactDOM.render(, document.getElementById('root'))
在这个 index.js
中引用了 App
组件。所以我们在 /src
目录下新增 components/
目录,并在该目录下新增 App.js
:
// src/components/App.js import React from 'react'; export default class App extends React.Component { render() { return (Hello Webpack and React! (^_^)
) } }
另外在 /src/
新增一个模板文件 index.html
:
Hello Webpack (^_^) ~~~
这个时候你在命令终端不管是执行 npm run build
还是 npm run dev
都无法正常运行,会报错:
首先我要告诉你的是 Step04
这一步的操作并没有任何问题,主要是在编译的过程中缺少必要的东西。那就是Babel相关的配置。接下来的 Step05
将会添加Babel相关的配置。
Step05:添加Babel相关的配置
请将分支切换到 step5
查看代码 。
在 Step04
中会失败主要是因为Webpack只识别JavaScript文件,而且只能编译ES5。实际上ES6(甚至后面要说的JSX),Webpack它根本不认识。那么要解决这个问题,就需要借助Babel来处理。先安装需要的插件:
⇒ npm i babel-loader @babel/core @babel/preset-env @babel/plugin-transform-runtime @babel/plugin-transform-modules-commonjs @babel/preset-react -D ⇒ npm i @babel/runtime --save
执行完上面的命令之后, package.json
文件在 dependencies
和 devDependencies
添加了新的配置信息:
// package.json { // ...省略的信息可以查看原文件 "dependencies": { "@babel/runtime": "^7.4.5", "react": "^16.8.6", "react-dom": "^16.8.6" }, "devDependencies": { "@babel/core": "^7.4.5", "@babel/plugin-transform-modules-commonjs": "^7.4.4", "@babel/plugin-transform-runtime": "^7.4.4", "@babel/preset-env": "^7.4.5", "@babel/preset-react": "^7.0.0", "babel-loader": "^8.0.6", "webpack": "^4.35.0", "webpack-cli": "^3.3.5", "webpack-dev-server": "^3.7.2", "webpack-merge": "^4.2.1" } }
接着在 webpack-sample
根目录下创建 .babelrc
文件来配置Babel:
{ "presets": [ [ "@babel/preset-env", { "targets": { "browsers": [ "> 1%", "last 5 versions", "ie >= 8" ] } } ], "@babel/preset-react" ], "plugins": [ "@babel/plugin-transform-runtime", "@babel/plugin-transform-modules-commonjs" ] }
有关于Babel更详细的配置 可以点击这里阅读 ,这里不再做过多的阐述。
最后在 webpack.common.js
配置文件中的 module
中添加 rules
来处理 .js
和 .jsx
文件,这也是我们添加的第一个有关于Webpack的Loader相关的东西:
// webpack.common.js module.exports = { // ... 省略的信息查看原文件代码 // 模块解析 module: { rules: [ { test: /\.(js|jsx)$/, exclude: /node_modules/, use: { loader: "babel-loader" } } ] }, }
这个时候执行 npm run build
可以正常的执行,但是执行 npm run dev
还是会报错:
执行 npm run dev
不成功是因为我们的模板文件没有自动插入到 /dist
目录下。为了让 /src/
目录下的模板文件 index.html
能自动编译到 /dist
目录下,并且所有的 .js
引用能自动插入到 index.html
中。我们需要使用Webpack的两个插件:
- 生成
index.html
文件,并且自动插入到/d