本质上,webpack 是一个用于现代 JavaScript 应用程序的 静态模块打包工具。
首先介绍一下前端打包和构建的概念。
前端打包,从字面意思,是将多个文件合并成一个文件。
因为前端模块化开发,不同模块放在不同的文件中,而发布运行时候,为了减少网络请求,应该将文件合并在一起。webpack做的就是这个工作。
当然前端代码开发完成将要发布时候,不仅仅打包就够了,还有很多工作要做:
等等
做这些工作,将源代码转换成用户可以使用的文件(html,js,css,png等),即目标文件的过程,就是构建。打包也属于构建工作中的一部分。
webpack本质是一个模块打包机,但它结合loader、插件的能力可以完成构建工作。并可以完成很多个性化的构建工作。
学习webpack要动手操作,根据教程和文档,尝试每个配置,执行打包查看打包结果,观察配置对打包结果的影响,并思考配置在实际场景中的应用。
webpack提供了CLI的接口和node接口,这里我们主要介绍CLI接口。
我们使用webpack,对源代码和其他资源打包构建。
每个项目都会有入口模块,是项目初始开始运行的模块,入口文件会依赖其他模块,模块还可能依赖其他模块,这样就形成了一个树状的结构。我们希望将项目打包构建然后生成的资源放到指定文件夹下。我们怎么使用webpack实现这一目标呢?
我们会将webpack引入项目中,然后告诉webpack我们的入口文件,webpack根据入口文件构建一个依赖树,然后遍历这个依赖树,并在遍历过程中解析,然后将模块进行转换,然后打包输出到指定文件夹下,打包的结果我们称为“bundle”。
我们通常会写好webpack配置文件(在配置文件中我们自己定义项目入口、输出的目录、各种类型文件的解析方式等等),然后输入webpack命令(可能加一些参数),webpack就会根据配置进行打包工作了。
npm init -y
npm install webpack webpack-cli --save-dev
创建目录和文件
.
├── node_modules
├── package-lock.json
├── package.json
├── src
│ ├── index.js
│ └── log.js
在package.json中加入脚本"build": “webpack”。
目前没有创建webpack配置文件,webpack会按照默认方式打包。
入口默认在src中,打包后的结果默认输出到dist/main.js中。
// index.js
import log from './log';
const name = 'webpack';
log(name);
// log.js
export default (...args) => console.log(...args);
在项目目录下运行命令npm run build
我们看到在项目中多了一个"dist"目录,其中有main.js文件。
// main.js
(()=>{"use strict";((...c)=>{console.log(...c)})("webpack")})();
观察main.js中的内容我们可以发现,代码进行了打包,把源代码中的两个文件打包成了一个文件,并且进行了压缩(这时候还没有进行代码转换,还保留箭头函数语法,这时因为我们还没配置loader,loader后面会介绍)。
指定webpack配置打包,可以通过webpack --config <配置文件名>来指定webpack配置进行打包,如果不加config参数,默认项目根目录的webpack.config.js文件作为配置文件。
我们在项目中创建webpack.config.js文件,并写下如下的配置
// webpack.config.js
const path = require('path');
module.exports = {
// 入口文件
entry: './src/index',
// 出口配置
output: {
// path需要是一个绝对路径
path: path.resolve(__dirname, 'output'),
filename: '[name].[hash].js'
}
};
这个配置指定入口是"src/index.js",打包结果输出到"output"目录中,并且输出的文件名字格式为’[name].[hash].js’,其中"[name]","[hash]“是占位符,会在打包后替换成相应地实际值。”[name]“是bundle名,在entry中指定,默认是"main”。"[hash]"是hash值。
然后我们更改npm scripts中的"build"为"webpack --config webpack.config.js"。
"scripts": {
"test": "echo "Error: no test specified" && exit 1",
"build": "webpack --config webpack-config.js"
}
config参数用来指定配置文件,如果不传这个参数,webpack打包时候默认会找到webpack.config.js,如果找不到这个文件就用默认的配置进行打包。所以其实这里我们可以省略这个–config参数。
然后执行npm run build,然后我们会看到项目中增加了output目录,其中有一个"main.35844ec757242b562acf.js"文件。文件内容和上面的默认配置打包一样。
对于打包工作,我们称待打包的文件为“入口”,打包后的产物成为“出口”。从入口和出口的关系角度,打包可能有以下几个场景:
每个入口对应且只能对应一个出口,一个出口可能对应一个入口,也可能对应多个入口。
对于一个入口一个出口情形,entry配置为一个字符串,指定入口路径即可,正如上面例子中看到的。这种配置默认bundle名字是"main"。
这两种配置是等价的
module.exports = {
entry: './src/index.js,
};
module.exports = {
entry: {
main: './src/index.js',
}
};
对于多个入口的情况,需要配置一个数组。
module.exports = {
entry: ['./src/a.js', './src/b.js'],
};
// 等价于
module.exports = {
entry: {
main: ['./src/a.js', './src/b.js'],
}
};
对于多个入口,对个出口的情况,需要给entry配置为对象,指定不同bundle的入口
module.exports = {
entry: {
pageOne: './src/pageOne/index.js',
pageTwo: './src/pageTwo/index.js',
pageThree: './src/pageThree/index.js',
}
};
入口可以分为同步入口和异步入口。同步入口打包后的产物在页面初始化时候就会被加载,异步入口在打包后,在需要的时候才会被加载。
异步入口的加载由打包时候打进去的运行时代码控制。
// index.js
const name = 'webpack';
setTimeout(() => {
// 模块有50%概率加载
if (Math.random() > 0.5) {
return;
}
const logPromise = import('./log');
logPromise.then(res => {
const log = res.default;
log(name);
});
}, 3000);
// log.js
export default (...args) => console.log(...args);
output配置有3个主要的字段
path和filename在上面例子中已经说明。
通常发布项目后会对静态资源进行cdn分发,因此需要指定引用路径,webpack支持通过output.publicPath配置指定引用路径。
loader 用于对模块的源代码进行转换
更准确地说,loader用于对指定类型的文件进行处理。loader就是对指定类型的文件进行处理一个工具。
在项目中,除了js之外,还有样式文件(less、stylus、sass等预编译语法),图片、字体图标等,每种类型的文件的处理方式都不同,都需要配置对应的loader进行处理。
当我们配置好loader后,webpack工作时候,遇见指定类型的文件,就使用对应的loader对文件进行处理。
对于一种文件,可以通过数组配置多个loader,webpack处理文件时候,会以从数组后面往前面(或者说从右往左)的顺序来调用loader。
以babel-loader处理js文件为例。
babel-loader
const path = require('path');
module.exports = {
entry: './src/index',
output: {
// path需要是一个绝对路径
path: path.resolve(__dirname, 'output'),
filename: '[name].[hash].js',
},
module: {
rules: [
{
test: /.js/,
use: {
loader: 'babel-loader',
options: {
"presets": [
"@babel/preset-env"
]
}
}
}
]
}
};
Babel 是一个 JavaScript 编译器
最常见的是ES6转ES5,JSX转换等。
Babel 是一个工具链,主要用于将采用 ECMAScript 2015+ 语法编写的代码转换为向后兼容的 JavaScript 语法,以便能够运行在当前和旧版本的浏览器或其他环境中\
babel-loader是webpack的一个loader,它调用babel对js文件进行处理。
@babel/core模块负责核心的源码解析与输出工作。
bable根据配置文件进行代码的处理。
配置中有两个重要概念:预设和插件。babel本身并不能进行代码转换,它依赖一个个的插件进行代码转换工作。每个插件负责一个语法特性,比如箭头函数使用"@babel/plugin-transform-arrow-functions
"插件进行转换。
预设是一系列插件集合,比如"@babel/preset-env"这个预设是最常用的预设,用来转换ES2015、2016、2017语法。
再比如"@babel/preset-react"用来解析JSX的语法。
可以在babel-loader中配置,也可以在.babelrc配置文件中配置。推荐后者。
处理某个类型文件时候,可以使用多个loader,这时候需要配置use字段为数组。比如
module.exports = {
module: {
rules: [
{
test: /.css$/,
use: [
// [style-loader](/loaders/style-loader)
{ loader: 'style-loader' },
// [css-loader](/loaders/css-loader)
{
loader: 'css-loader',
options: {
modules: true
}
},
// [sass-loader](/loaders/sass-loader)
{ loader: 'sass-loader' }
]
}
]
}
};
常用loader:
常用loader文档
loader 用于转换某些类型的模块,而插件则可以用于执行范围更广的任务。包括:打包优化,资源管理,注入环境变量。
我们先看一个常用的插件:html-webpack-plugin
html-webpack-plugin可以在打包时候生成一个html入口页面,并且在页面中自动生成标签引入打包的js文件。可以通过template字段指定页面的模板,不指定的话,插件会使用默认的模板。
webpack 测试
hello world
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
entry: './src/index',
output: {
// path需要是一个绝对路径
path: path.resolve(__dirname, 'output'),
filename: '[name].[hash].js'
},
module: {
rules: [
{
test: /.js/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
}
}
]
},
plugins: [
new HtmlWebpackPlugin({
template: path.resolve(__dirname, 'src/index.html')
})
]
};
打包结果如下
// output/index.html
webpack 测试
hello world
常用的插件
常用plugin文档
mode表示打包时候的模式。有production、development、none三个取值。production和development模式对应不同的优化,比如production模式会对代码进行压缩。none没有优化。
生产环境指项目开发完成发布交付用户使用的环境,开发环境指开发者本地调试环境。
分包加载的目的有两个:
是减少项目迭代后用户更新资源的请求资源大小
让大的文件分成小片,并行加载,可以加快加载速度
需要注意的是:
不能并行太多,否则超过浏览器限制就不会并行了
切片尽可能小,这样可以加快加载速度
通用性好的尽可能切成单独的片段,有助于缓存,迭代后减少需要加载的文件数量。
使用webpack对项目进行构建,会将源代码进行转换,那么当代码在浏览器中运行报错时候,错误会定位到转换后的代码相应的位置。而开发者希望报错能定位到源代码的相应位置,这样就更方便调试。
那么就需要源代码和转换后代码的信息,浏览器拿到这个信息之后就可以在报错时候定位到源代码的相应位置。
所以这里有两个关键点,一个是我们需要告诉浏览器源代码和转换后的代码的关系,另外浏览器也需要支持根据这个关系从转换后代码映射到源码的对应位置。
这种将转换后代码和源码位置做映射的技术成为sourceMap
chrome可以支持sourceMap,webpack打包也支持生成sourceMap映射文件。
webpack可以配置sourceMap,在devTools中进行配置。
最佳实践是生产环境使用"source-map",开发环境用"cheap-module-source-map"。
有时候我们希望使用script标签方式引入外部依赖,而不是打包到bundle中,这时候就需要用到externals配置。
这样做有一个好处,可以避免每次都对一些第三方包进行打包,提升构建性能。
官网示例
webpack-dev-server是通常搭配webpack一起使用的工具,它可以让我们的调试非常方便。
它提供了包括不限于以下能力
安装:
npm install --save-dev webpack-dev-server
配置,增加npm scripts,启动webpack-dev-server
"scripts": {
"build": "webpack --config webpack-config.js",
"start": "webpack serve --open --config webpack.config.js"
}
如果我们的应用初始化需要一系列过程,请求接口、处理数据并渲染界面,但是我们修改代码时候只改了一个界面的文本话术,这时候我们保存代码,不希望调试的页面reload重新执行一遍整个初始化流程,而是修改的代码的部分更新界面即可。wds的模块热替换就可以帮助我们实现这个功能。
模块热替换的大概原理是,当我们使用wds打包并启动服务,会在打包的代码中自动加入wds运行时代码,运行时代码用于和wds服务建立一个websocket连接,这个连接用于后续的模块热替换的通信。
当我们保存代码后,wds监听本地文件系统更新,会将改动作为补丁通过websocket发送给浏览器端,浏览器端拿到补丁之后不做reload操作,而是对界面进行相应地更新。
模块热替换首先需要WDS支持,将运行时代码打包进去,并且启动WDS,监听文件系统并传输补丁等一系列操作。
还需要我们的代码在收到补丁时候做相应地处理。
WDS默认启动模块热替换功能,对于常见框架,已经有实现了模块热替换功能的工具,我们只需要使用工具就可以实现HMR的能力,对于样式,style-loader本身实现了HMR,因此开发环境使用style-loader即可。
React实现模块热替换有两个方案:
react-refresh-webpack-plugin
react-hot-loader
其中react-hot-loader代码侵入性较强,推荐用react-refresh-webpack-plugin。
当项目规模较大时候,目录层级比较深,对模块的引用的书写比较麻烦,例如如下目录结构。
src
├── common
│ └── util
│ └── time.js
└── component
└── button
└── DateButton
└── index.js
如果在src/component/button/DateButton/index.js中依赖src/common/util/time.js模块,需要这样引用
// src/component/button/DateButton/index.js
import from '../../../common/util/time';
这时候可以用resolve.alias配置目录别名。
配置了别名后,使用别名引用模块会简化很多。通常都将src配置为"~“或者”@"。
const path = require('path');
module.exports = {
// other config
resolve: {
alias: {
'~': path.join(__dirname, './src'),
'@': path.join(__dirname, './src')
}
}
};
这样我们依赖src目录下面某个子目录中的模块时候可以这样引用
import store from '~/common/util/time';
使用webpack打包大型项目时候,往往需要做性能优化,包括构建时性能优化、运行时性能优化、加载时性能优化、优化开发体验。这个在webpack进阶使用是很常见的。
关于webpack性能调优
在实际项目中,我们可以使用基于webpack的脚手架来创建项目而并不需要手动搭建,但是还是可能根据自己项目特定需求修改webpack配置,因此掌握webpack是必要的。
React最流行的脚手架是官方维护的create-react-app,默认是隐藏的配置文件,可以通过npm run eject命令将配置文件弹出(注意该操作不可逆),然后修改配置文件。
Vue脚手架vue-cli支持在vue.config.js修改webpack配置。
使用webpack搭建React项目实现以下功能,并能够让打包的项目在启动的服务(可以使用node 的anywhere模块,mac可以自带的python的python -m SimpleHTTPServer)中运行起来。