webpack 是什么?我们为什么要用它?
首先贴出官方解释:
本质上,webpack 是一个现代 JavaScript 应用程序的静态模块打包器(module bundler)。当 webpack 处理应用程序时,它会递归地构建一个依赖关系图(dependency graph),其中包含应用程序需要的每个模块,然后将所有这些模块打包成一个或多个 bundle。
通俗的讲,webpack 是一个代码加工机器,我们把自己的代码丢给他,它经过加工之后再还给我们。
以上所说的加工,包括但不限于以下几点:
- 代码打包压缩
- 代码编译(ES6转ES5、ts 转 js 、less\sass 转 css 等)
- 代码优化,提取公共模块
webpack 还可以为我们提供前端静态服务,极大地方便了我们日常的开发调试。
综上,我们之所以使用 webpack ,主要是为了提高开发效率,优化自身代码。
webpack 核心概念
当我们把自身代码交给 webpack 加工时,必然会需要一个入口,而加工过后的代码需要还给我们,肯定也会需要出口,而在加工的过程中 webpack 需要进行一系列的工序。如同面粉变成面包,不只是烘烤这么简单。
由上,我们来理解 webpack 核心的四个概念:
- entry : 文件入口
- output : 文件出口
- module : 工序一 (通过配置,对不同的文件类型做处理,完成模块代码的转换)
- plugins : 工序二 (通过插件,来获取更加强大的加工处理能力,完成更复杂的构建任务)
到这里,我们便可以写出一份简单的配置文件骨架:
module.exports = {
entry: {
··· 文件入口
},
output: {
··· 文件出口
},
module: {
rules: [
··· 一系列的 loader
],
},
plugins: [
··· 一系列的插件
]
}
webpack 的基本配置使用
在 webpack 4.0以上版本中为我们提供了 --mode
这个命令配置项,mode 分为 development 和 production 两个选项,默认为production。它为我们提供了一些简单的基本配置,对于简单的项目,我们不再需要配置文件,mode 默认入口文件为 src 文件夹 下的 index.js,出口为 dist 文件夹,打包时,我们只需要选择 mode 的参数即可。
webpack --mode development/production
在 mode 的默认配置中,已经为我们处理了一些常见的用法。但是,这些默认配置,无法处理非 js 文件内容。
下面这段话来自知乎:
development默认值会给你最好的开发体验,它注重:
浏览器调试工具
快速开发周期中的快速增量编译
在运行过程中提供有效的错误信息
而production默认值会给你提供一系列有效的默认值以便部署你的应用,它注重:
小的输出体积
运行快速的代码
忽略仅在开发时需要的代码
不暴露源码和文件路径
易于使用的输出产物
实际上,就目前看来,在项目开发中,我们仍需配置自己的配置文件。
在日常的开发中,我们一般需要 webpack 为我们解决下面这些问题:
- 构建我们发布需要的 HTML、CSS、JS 文件
- 使用 CSS 预处理器来编写样式
- 处理和压缩图片
- 使用 Babel 来支持 ES 新特性
- 本地提供静态服务以方便开发调试
根据以上需求,我们可以这样配置自己 webpack.config.js :
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const ExtractTextPlugin = require('extract-text-webpack-plugin'); // 注意版本号 webpack 4 以上版本请下载 @next 版本
const path = require('path');
module.exports = {
entry: {
index: './src/index.js',
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].min.js'
},
module:{
rules: [
{
test: /\.jsx?$/,// 匹配文件路径的正则表达式,通常我们都是匹配文件类型后缀
exclude: /(node_modules|bower_components)/, // 过滤掉不需要处理的文件
use: { loader: 'babel-loader' } // 指定使用的 loader
},
{
test: /\.less/,
use: ExtractTextPlugin.extract({ // 使用插件抽离 css ,生成单独的 css 文件
fallback: 'style-loader',
use: ['css-loader', 'less-loader']
})
},
{
test: /\.html$/,
use: [
{
loader: 'html-loader',
options: { // 压缩 html
minimize: true
}
}
]
},
{
test: /\.(png|jpg|gif)$/,
use: [
{
loader: 'file-loader',
options: {
// 自定义配置,图片压缩、处理等
}
}
]
}
]
},
// 提供静态服务
devServer:{
port: 9999,
headers: { // 添加头部信息
"X-Custom-Foo": "bar"
},
proxy: { // 请求代理
"/api": {
target: "http://localhost:3000"
}
}
},
plugins: [
// 每次打包前清除 dist 下的文件
new CleanWebpackPlugin('dist'),
// 提取样式,生成单独文件
new ExtractTextPlugin("styles.css"),
// 生成新的 html 文件
new HtmlWebpackPlugin({
filename: 'index.html', // 如果文件名不是 index , 开发时要在 url 处添加文件名
template: path.resolve(__dirname + '/src/index.html'), // 注意路径,
})
]
}
package.json (请注意版本号,因为版本更新会造成不可预期的错误)
{
"name": "webpack-share",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"dev": "webpack-dev-server --open --inline --mode development --progress",
"build": "webpack --mode production --progress"
},
"author": "",
"license": "ISC",
"devDependencies": {
"babel": "^6.23.0",
"babel-core": "^6.26.0",
"babel-loader": "^7.1.4",
"babel-preset-env": "^1.6.1",
"clean-webpack-plugin": "^0.1.19",
"css-loader": "^0.28.11",
"extract-text-webpack-plugin": "^3.0.2",
"file-loader": "^1.1.11",
"html-loader": "^0.5.5",
"html-webpack-plugin": "^3.2.0",
"less": "^3.0.1",
"less-loader": "^4.1.0",
"path": "^0.12.7",
"style-loader": "^0.20.3",
"webpack": "^4.5.0",
"webpack-cli": "^2.0.14",
"webpack-dev-server": "^3.1.2"
}
}
这样,一个简单的前端开发环境就已经配置好了,这份配置文件解决了之前我们所提出的问题:
1、构建我们发布需要的 HTML、CSS、JS 文件
- 使用 html-loader 、 html-webpack-plugin 处理 html 文件,引入处理后的 css , js 文件,使用 extract-text-webpack-plugin 抽离单独的 css 文件(注意版本号,在 webpack 4 中需要下载 @next 版本)
2、使用 CSS 预处理器来编写样式
- 使用 less-loader、 css-loader 、style-loader 处理样式文件
3、处理和压缩图片
- 使用 file-loader 处理图片资源
4、使用 Babel 来支持 ES 新特性
- 使用 babel-loader 对 js 编译
5、本地提供静态服务以方便开发调试
- 使用 devServer 提供静态服务,配置请求代理,避免产生跨域安全问题
如果在项目中引入 react ,我们要做一下简单修改:
{
test: /\.jsx?$/,// 匹配文件路径的正则表达式,通常我们都是匹配文件类型后缀
exclude: /(node_modules|bower_components)/, // 过滤掉不需要处理的文件
use: { // 指定使用的 loader
loader: 'babel-loader',
options: {
// presets: ['es2015','react'] webpack 3
presets: ['@babel/preset-react','@babel/preset-es2015'] // webpack 4
}
}
},
package.json :
"@babel/preset-es2015": "^7.0.0-beta.44",
"@babel/preset-react": "^7.0.0-beta.44",
如果要引入 antd , 在 less-loader 中还需要做一下修改:
{
test: /\.less/,
use: ExtractTextPlugin.extract({ // 使用插件抽离 css ,生成单独的 css 文件
fallback: 'style-loader',
use: [
{
loader: 'css-loader'
},
{
loader: 'less-loader',
options: { // 引入 js
javascriptEnabled: true
}
}
]
})
}
一些常用的其他配置
resolve
这里只做简单介绍,更多详情,请看官网
在 webpack 中,和模块路径解析相关的配置都在 resolve 字段下,我们可以通过配置 resolve 来提高自身开发的体验,例如常用的 alias 别名设置,简化了我们引入文件的路径。 首先我们来了解一下在 webpack 中的解析规则:
- 解析相对路径
1、如果是文件,直接加载
2、如果是文件夹,则查找文件夹下 package.json 文件
- 找到 package.json: 则一般情况会对照 main 属性,获取文件路径来解析,如果找不到 main 字段,一般情况话会寻找文件夹下的 index.js
- 找不到 package.json : 会寻找 index.js
- 解析绝对路径
直接查找文件
- 解析模块
由下自上查找 node_modules 中的模块
alias 别名
alias 可以为一段路径创建一个别名,使一些较为常用的冗长路径得到简化:
题外话:
path.join([path1][, path2][, ...])用于连接路径。该方法的主要用途在于,会正确使用当前系统的路径分隔符,Unix系统是/,Windows系统是\。
path.resolve([from ...], to) 将 to 参数解析为绝对路径。
resolve: {
alias: {
Js: path.resolve(__dirname + '/src/js'),
Less: path.resolve(__dirname, './src/css'), // 模糊匹配: 引用时,只要匹配到 Less 都会被替换
Css$: path.resolve(__dirname, './src/css/index.css') // 精确匹配:引用时,只能 improt 'Css'
}
},
这样,我们在引入 css 和 js 时,只需要在别名下查找相应文件就好:
// 设置别名前
import '../css/a.less';
// 设置别名后
import 'Less/a.less';
improt 'Css'
extensions 自动解析确定的扩展
resolve: {
// 默认配置
// extensions: [".js", ".json"]
extensions: ['.js','.jsx','.less','.css']
},
设置补全扩展名数组,引用文件时,可以不加后缀,wepack 会从数组中自动补全。
resolve 其他配置
resolve: {
// 当目录下有 package.json 文件时,默认查找字段
// 配置 target === "web" 或者 target === "webworker" 时 mainFields 默认值是:
mainFields: ['browser', 'module', 'main'],
// target 的值为其他时,mainFields 默认值为:
mainFields: ["module", "main"],
// 当目录下没有 package.json 文件时,默认查找文件
mainFiles: ["index"],
modules: [
path.resolve(__dirname, 'my_modules'), //告诉 webpack 解析模块时应该搜索的目录,可以自定义一些自己的模块路径。
'node_modules',
]
}
devtool
devtool: 'source-map',
此选项控制是否生成,以及如何生成 source map。在开发环境时使用,便于准确定位代码位置。
webpack-dev-serve
devServer:{
contentBase: path.resolve(__dirname, "dist"), // 未经 webpack 处理的静态文件访问路径
port: 9999,
publicPath: '/', // 确定应该从哪里提供 bundle,并且此选项优先。建议将 devServer.publicPath 和 output.publicPath 的值保持一致。
overlay:{ //当有编译错误或者警告的时候显示一个全屏 overlay
errors:true,
warnings:true,
},
headers: { // 添加头部信息
"X-Custom-Foo": "bar"
},
proxy: { // 请求代理
"/api": {
target: "http://localhost:3000",
}
},
},
开发环境和生产环境
基于开发环境和生产环境的需求差异,我们一般需要使用两套不同的配置文件。
首先,我们来看一下开发环境和生产环境环境需求点的异同:
相同点
1、共同的入口
2、共同的代码处理
3、同样的解析配置
3、共同的出口
不同点
开发环境
1、模块热更新 // 提高开发效率
2、接口代理 // 方便接口调用
3、devtool // 准确定位代码位置
生产环境
1、提取公共代码
2、压缩混淆
3、去除无用代码
4、文件压缩 // 减小代码体积
我们发现,在 wenpack 4 中,mode 的两个参数已经能很好地解决我们大部分的需求,但是仍有些个性配置需要我们手动创建。我们需要根据不同的业务需求对配置文件进行拆分,然后利用 webpack-merge 对不同的配置文件进行合并。
通常情况下,我们会拆分出三个配置文件:
webpack.base.conf.js // 公共配置
webpack.prod.conf.js // 生产环境配置
webpack.dev.conf.js // 开发环境配置
你可以把两份差异配置引入公共配置,然后判断传入参数来决定使用那份配置:
webpack-dev-server --config ./webpack.base.conf.js --mode development/production 或 --env development/production
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const ExtractTextPlugin = require('extract-text-webpack-plugin'); // 注意版本号 webpack 4 以上版本请下载 @next 版本
const merge = require('webpack-merge')
const webpack = require('webpack');
const path = require('path');
const prodConf = require('./webpack.prod.conf');
const devConf = require('./webpack.dev.conf');
module.exports = (env, argv) => {
console.log(env,'==================', argv.mode)
const baseConf = {
entry: {
index: path.resolve(__dirname, '../src/index.js'),
},
output: {
path: path.resolve(__dirname, '../dist'),
filename: 'js/[name].min.js'
},
····
}
const config = env === 'dev' ? devConf : prodConf;
return merge(baseConf,devConf)
}
也可以将共同配置分别引入差异配置中,在启动命令上指定所使用的配置文件:
webpack-dev-server --config 配置文件
优化
略