跳到主要内容

Webpack

npm install webpack webpack-cli webpack-dev-server -D
npx webpack
npx webpack serve --open
// webpack.config.js
const path = require("path");
const htmlWebpackPlugin = require("html-webpack-plugin");

module.exports = {
mode: "development",
entry: {
main: './src/main.js',
lib: './src/lib.js'
},
output: {
path: path.join(__dirname, "./dist"),
filename: "js/[name].[contenthash].js",
clean: true,
chunkFilename: '[contenthash].js'
},
plugins: [
new htmlWebpackPlugin({
template: path.join(__dirname, "./public/index.html"),
}),
],
module: {
rules: [
{
test: /.css$/,
use: ['style-loader', 'css-loader']
},
{
test: /.txt$/,
type: 'asset/source'
},
{
test: /.jpg$/,
type: 'asset/resource',
generator: {
filename: 'images/[hash][ext]'
}
},
{
test: /.png$/,
type: 'asset/inline',
},
]
},
devServer: {
static: './dist'
},
devtool: "inline-source-map",
optimization: {
splitChunks: {
chunks: 'all'
},
runtimeChunk: 'single',
},
externals: {
lodash: '_'
},
resolve: {
extensions: [".js", ".ts"],
alias: {

}
},
};

mode

  • development,即开发模式。此时代码没有tree shaking、也没有经过压缩处理。
  • production,即生产模式,是配置的默认值。此时打包ES模块时默认开启tree shaking,代码经过压缩处理。

entry

webpack能够从一个入口模块出发,递归查找所有被依赖的模块,将其打包生成构建产物。通常来说一个入口文件对应一个构建产物js(称之为chunk),但是通过代码分割技术(见后续章节),一个入口文件是能够对应多个构建产物js(多个chunk,主要那个chunk被称为initial-chunk,其余的都被称为non-initial-chunk)的。

// 单文件入口
entry: './src/index.js',
entry: {
home: './src/index.js'
}

// 多文件入口
entry: {
home: './src/index.js',
test: './src/test.js'
},

output

用来指示构建产物的存放路径和文件名等信息

entry: {
home: './src/index.js',
test: './src/test.js'
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'js/[name].[contenthash].bundle.js', // initial-chunk的文件名
chunkFilename: '[contenthash].js', // non-initial-chunk的文件名
clean: true, // 每次构建清空构建目录,以前用clean-webpack-plugin实现
},

[name]

chunk名,如上述配置的hometest

[contenthash]

chunk内容的哈希值

loader

webpack只能理解js模块之间的引用关系,如果我们想要在js文件中引入其他类型的文件就需要使用对应的loader,简单来说loader起到一个转化的功能。

需要注意的是loader的执行顺序是从右往左,如['style-loader', 'css-loader']表示当被依赖的模块是css文件时,会先将css文件内容传给css-loader处理,处理后的结果再传给style-loader处理,最终处理的结果会被依赖该css文件的模块所使用。

style-loader

js模块中引入css文件的常见做法只直接import './style.css',实际上style-loader的作用是返回一段js代码,这段代码的作用是动态生成一个style标签,标签的内容是css文件内表达的样式。

css-loader

用来处理css文件,并会将处理后的内容交给style-loader来动态生成style标签从而注入样式。除此之外,还提供了css module的能力,允许我们通过import xx from './style.module.css'css文件中导入具体的内容(默认情况下只有.module.css的文件才能使用该功能,可通过options.modules: true来令所有css文件都能这样引用)

module.exports = {
module: {
rules: [
{
test: /\.css$/,
use: [
{ loader: 'style-loader' },
{
loader: 'css-loader',
options: {
modules: true,
},
},
],
},
],
},
};
/* style.module.css */
.Root {
background: 'pink'
}
import { Root } from './style.module.css'

function App() {
return <div className={Root}></div>
}

asset modules

webpack5通过asset modules内置了raw-loaderurl-loaderfile-loader的功能

type/resource

等同于file-loader

module.exports = {
module: {
rules: [
// webpack5
{
test: /\.png/,
type: 'asset/resource'
},

// webpack4 使用file-loader实现
{
test: /\.png$/,
use: [
{
loader: 'file-loader',
},
],
},
]
},
}
import mainImage from './images/main.png';

img.src = mainImage; // '/dist/151cfcfa1bd74779aadb.png'

默认情况下资源会存放在output.path根目录,我们可以使用output.assetModuleFilenameRule.generator调整资源的生成目录。

module.exports = {
output: {
path: path.resolve(__dirname, 'dist'),
assetModuleFilename: 'images/[hash][ext][query]', //
},
module: {
rules: [
{
test: /\.png/,
type: 'asset/resource'
},
{
test: /\.html/,
type: 'asset/resource',
generator: {
filename: 'static/[hash][ext][query]',
},
},
],
},
};

type/inline

等同于url-loader

module.exports = {
module: {
rules: [
// webpack5
{
test: /\.svg/,
type: 'asset/inline'
},

// webpack4 使用url-loader实现
{
test: /\.svg$/,
use: [
{
loader: 'url-loader',
options: {
limit: 8192, // 资源大小大于该值时自动换成file-loader处理
}
},
],
},
]
}
}
import svg from './images/default.svg';

el.style.background = `url(${svg})`; // url(data:image/svg+xml;base64,xxxxxx)

type

根据资源的大小自动选择type/resourcetype/inlineurl-loader其实也内置了file-loader,以前也是一样通过url-loader根据资源的大小选择不同的处理方式

type/source

等同于raw-loader

module.exports = {
module: {
rules: [
// webpack5
{
test: /\.txt/,
type: 'asset/source'
},

// webpack4 使用raw-loader实现
{
test: /\.txt$/,
use: [
{ loader: 'raw-loader' },
],
},
]
}
}
Hello world
import txt from './hello.txt'
console.log(txt) // hello world

plugins

插件,顾名思义,就是对webpack功能进行拓展。

plugins: [
new HtmlWebpackPlugin({}),
new webpack.HotModuleReplacementPlugin({}),
]

html-webpack-plugin

webpack本身的作用是构建出bundle.js,然后构建文件需要在我们的html中使用。

html-webpack-plugin插件的作用是每次使用webpack命令,都会自动在dist(指构建出口)生成一个html文件,这个html文件会自动引入我们的bundle.js

// npm i -D html-webpack-plugin
const HtmlWebpackPlugin = require('html-webpack-plugin');

const webpackConfig = {
plugins: [
new HtmlWebpackPlugin({
template: path.resolve(__dirname, 'public/index.html'),
}),
],
};
<!-- public/index.html 模板文件 -->
<body>
<div id="root"></div>
</body>

<!-- dist/index.html 自动生成的文件 -->
<body>
<div id="root"></div>
<script src="bundle.js"></script>
</body>

clean-webpack-plugin

通常每次使用webpack进行构建,都希望能先清除上次构建的产物

// npm i -D clean-webpack-plugin
const { CleanWebpackPlugin } = require('clean-webpack-plugin');

const webpackConfig = {
plugins: [
new CleanWebpackPlugin(),
],
};

resolve

extensions

resolve: {
extensions: [".js", ".mjs", ".cjs", ".jsx", ".tsx"]
}
import test from './app' // 检索各种后缀,如app.mjs、app.cjs

alias

导入模块时的别名。

const path = require('path');

module.exports = {
resolve: {
alias: {
Test: path.resolve(__dirname, 'src/test/'),
},
},
};
import Test from 'Test/index.js' // src/test/index.js

mainFields

Node#package.json

devtool

构建的时候生成sourceMap

module.exports = {
devtool: 'source-map'
};

source-map

在构建产物index.js同目录下生成index.js.map,同时index.js末尾会附上//# sourceMappingURL=index.js.map

function A() {}
//# sourceMappingURL=index.js.map

Inline-source-map

sourceMap通过内联的方式附在构建产物index.js的末尾

function A() {}
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2Zxxxxxxxxxx

eval-source-map

构建产物index.js内部实现变成通过eval执行模块对应的代码,并在eval的末尾内联sourceMap(热知识,eval可以在代码末尾内联sourceMap来方便eval执行出错时进行调试)

// index.js 伪代码
var __webpack_modules__ = {
138: () => {
eval(
"const test = __webpack_require__(4)\n\ntest()//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2Zxxxxxxxxxx\n//# sourceURL=webpack-internal:///138\n"
);
},
4: (module) => {
eval(
"module.exports = function test(a) {\n let arr = [];\n console.log(arr[4].age);\n return 'test'\n}//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2Zxxxxxxxxxx\n//# sourceURL=webpack-internal:///4\n"
);
},
},

另外使用eval-source-map构建的产物可以在浏览器sourcewebpack-internal://一栏看到每个模块的源码

devServer

npx webpack serve

publicPath

Imagine that the server is running under http://localhost:8080 and output.filename is set to bundle.js. By default the devServer.publicPath is '/', so your bundle is available as http://localhost:8080/bundle.js.

module.exports = {
//...
devServer: {
publicPath: 'http://localhost:8080/assets/',
},
};

The bundle will also be available as http://localhost:8080/assets/bundle.js.

contentBase

主要用来访问非构建生成的静态资源,默认值为当前工作目录,也就是说当前目录的所有文件都能通过开发服务器获取。

Content not from webpack is served from ~/workshop/repo

Hot Module Replacement

模块热替换功能应该只用在开发环境当中,可以通过npx webpack serve --hot来开启该功能,此时webpack会自动使用HotModuleReplacementPlugin内置插件。

对于CSS文件由于我们使用了style-loader,文件的改变会自动触发热更新;而对于JS文件我们需要额外加一些代码,如下代码当我们修改了child.js会进行模块热替换。

// parent.js
if (module.hot) { // 通过该守卫来避免生产环境报错
module.hot.accept('./child.js')
}

optimization

runtimeChunk

module.exports = {
optimization: {
runtimeChunk: 'single',
},
}

splitChunksPlugin

module.exports = {
optimization: {
splitChunks: {
chunks: 'all'
},
},
}

externals

import _ from 'lodash'
console.log(_)

假设我们的代码是这样的,在某些情况下我们可能希望webpack构建的时候不把lodash也打包进去。事实上这种场景是很常见的。

场景一:我们的HTML已经通过外链引用了lodash,所以构建产物自然不希望包括lodash,这样做的好处是我们能够在开发过程中通过代理来将外链的lodash替换成本地的lodash代码,方便开发调试。

场景二:我们正在开发一个插件,通过yarn add lodash -Plodash作为一个peer dependencies安装,这意味着对于该插件的使用者需要自行安装lodash,我们需要使用externals来把lodash从构建的产物中排除。

module.exports = {
externals: {
lodash: '_'
}
}

场景三:我们正在开发一个后端库,与前端库相比后端库其实并不需要将库所依赖的第三库模块(node_modules)和Node内置模块也打包,通常我们会用webpack-node-externals来排除这些模块

const nodeExternals = require('webpack-node-externals');

module.exports = {
externalsPresets: { node: true }, // webpack5,对于webpack4的target: node
externals: [nodeExternals()]
}

代码分割

常见的代码分割方式有以下几种

  1. 使用多入口而非单一入口构建
  2. 使用splitChunksPlugin把公共依赖或是第三方库(如lodashJquery)提取到一个单独的chunk
  3. 使用import()动态加载模块

原理

webpack可以打包ES模块和CommonJS模块。

webpack把每个文件模块都当成一个对象var module = { exports: {}}。并通过对文件模块的解析来给该对象赋予属性,如ES模块对应的形式如

// ES模块 a.js
export default function() {
console.log('111')
}
export function A() {
console.log('222')
}

// 打包后对应的对象
var module = {
exports: {
default: function() { console.log('111') }, // 严格来讲这里是getter
A: function() { console.log('222') }, // 同理,此处为了看起来简单
}
}

而由于CommonJS模块没有默认导出,所以对应的打包后对象也不存在default属性。

// CommonJS模块 b.js
module.exports.A = function() {
console.log('111');
}

module.exports.B = function() {
console.log('222');
}

// 打包后对应的对象
var module = {
exports: {
A: function() { console.log('111') },
B: function() { console.log('222') },
}
}

当我们在webpack导入模块时,require返回模块整体导出module.exportsimport * as xxx from也可以整体导入模块,或者是导入模块的不同导出接口,包括default接口。

至于如何分辨属于何种模块,则根据module.__esModule判断,这个属性是由__webpack_require__.r定义的。

动态加载

webpack中每个文件都是一个模块。

从一个entry文件开始打包所依赖的所有模块,可以得到一个包括一个thunkthunkGroup

如果有多个entry,那么打包之后得到的是多个thunkGroup,每个thunkGroup包括一个thunk

包括一个thunkthunkGroup听起来有点奇怪,什么时候包括多个thunk呢?通常是使用动态加载import()

// webpack.config.js
entry: './src/index.js'

// index.js
import('./test.js').then(() => {
ReactDOM.render(
<App />,
document.querySelector('#root')
)
})

通过webpack,我们的dist会生成两个js文件,或者说是两个main.js[id].js(这里的id是个随机数字)。

这里的/dist/main.js称为initial thunk/dist/[id].js称为non-initial thunk

其中initial thunk的名字可以在output.filename中指定;而non-initial thunk的名字可以在output.chunkFileName中指定,除此之外也可以使用Magic comment来指定,如:

// index.js
import(
/* webpackChunkName: "akara" */
'./test.js'
).then(() => {
ReactDOM.render(
<App />,
document.querySelector('#root')
)
})

这样我们得到的non-initial thunk文件名就是akara.js

现在,在我们的index.html引入main.js时,main.js会自动地加载akara.js文件。

构建后端代码

通常我们使用webpack打包前端项目,如果需要打包后端项目我们需要targetexternals配置项,因为后端项目我们希望内置模块和第三方模块不要被打包进bundle中。

const nodeExternals = require('webpack-node-externals')
module.exports = {
target: 'node', // 不打包path, fs等内置核心模块
externals: [nodeExternals()], // 不打包node_modules的第三方模块
}

构建第三方库

像上一小节打包得到的代码只能直接调用,没有导出的接口,如果我们想要打包Node模块可以使用output.libraryTarget配置项。

// webpack.config.js
output: {
library: {
type: 'umd',
}
},